IpqcInspectionMdpSyncService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. namespace Admin.NET.Plugin.AiDOP.Manufacturing;
  2. /// <summary>
  3. /// S6 过程检验单(IPQC)数据中台只读同步转换服务(DOP 内部,独立于 S6MdpSyncTransformService 的 KPI 管线)。
  4. ///
  5. /// 源:aidopdev.qms_gcjyd(过程检验单头,归口自旧系统 dopdemo,31 列)。
  6. /// 链路:qms_gcjyd 只读 → mdp_stg_ipqc_inspection(raw_data JSON) → mdp_std_ipqc_inspection(typed)。
  7. ///
  8. /// 归口说明(CTO 拍板方案 A):
  9. /// qms_gcjyd 原仅存于旧系统 dopdemo(0 行)、aidopdev 无此表,且 dopdemo 非运行时连接。
  10. /// 本服务 EnsureTablesAsync 在 aidopdev 建同构空表 qms_gcjyd(IF NOT EXISTS),
  11. /// 作为归口源;后续由上游一次性抽数填充,届时无需改代码即随刷新流通。
  12. ///
  13. /// 约束:
  14. /// - 只读 qms_gcjyd,仅写 mdp_stg_*/mdp_std_*/run_log;绝不 INSERT/UPDATE/DELETE 业务源单据表。
  15. /// - qms_gcjyd 无 tenant/org/factory 列 → 标准层按常量 tenant_id=0, factory_id=1 落库。
  16. /// - 本批只读列表:只做头表转换,不碰明细 qms_gcjydzb(留待 DETAIL 批)。
  17. /// - result_judgement(jgpd)为字典码,原样透出,语义解码后置。
  18. /// - 源为 0 行时转换成功完成、处理数为 0,不报错。
  19. /// </summary>
  20. public class IpqcInspectionMdpSyncService : ITransient
  21. {
  22. private const string JobCode = "S6_IPQC_INSPECTION_MDP_SYNC";
  23. private readonly ISqlSugarClient _db;
  24. public IpqcInspectionMdpSyncService(ISqlSugarClient db)
  25. {
  26. _db = db;
  27. }
  28. /// <summary>
  29. /// 全量同步转换:贴源(头) → 标准化(头)。
  30. /// </summary>
  31. public async Task<IpqcInspectionMdpSyncResult> RunFullAsync(CancellationToken cancellationToken = default, string triggerType = "AUTO")
  32. {
  33. cancellationToken.ThrowIfCancellationRequested();
  34. await EnsureTablesAsync();
  35. var now = DateTime.Now;
  36. var batchId = $"S6_IPQC_INSP_FULL_{now:yyyyMMddHHmmss}";
  37. var runLogId = await InsertRunLogAsync(batchId, now, triggerType);
  38. var result = new IpqcInspectionMdpSyncResult { BatchId = batchId, RunLogId = runLogId };
  39. try
  40. {
  41. result.HeadStageRows = await SyncHeadStagingAsync(batchId, now);
  42. result.HeadStandardRows = await TransformHeadStandardAsync(batchId, now);
  43. await MarkRunSuccessAsync(runLogId, now, result);
  44. return result;
  45. }
  46. catch (Exception ex)
  47. {
  48. await MarkRunFailedAsync(runLogId, now, ex.Message);
  49. throw;
  50. }
  51. }
  52. /// <summary>防御式建表(幂等):归口源空表 qms_gcjyd + 贴源层 + 标准层。</summary>
  53. private async Task EnsureTablesAsync()
  54. {
  55. // 归口源表(方案 A):同构自旧系统 dopdemo.qms_gcjyd 头表,空表,待上游抽数填充。
  56. // id 不设 AUTO_INCREMENT:抽数将携带旧系统主键。
  57. await _db.Ado.ExecuteCommandAsync(
  58. """
  59. CREATE TABLE IF NOT EXISTS qms_gcjyd (
  60. id BIGINT NOT NULL PRIMARY KEY COMMENT '主表id',
  61. djbh VARCHAR(255) NULL COMMENT '单据编号',
  62. cplx VARCHAR(255) NULL COMMENT '产品型号',
  63. scph VARCHAR(255) NULL COMMENT '生产批号',
  64. jyrq VARCHAR(255) NULL COMMENT '检验日期',
  65. lydjbh VARCHAR(255) NULL COMMENT '来源单据编号',
  66. jgpd BIGINT NULL COMMENT '结果判定',
  67. jyhgsl DECIMAL(10,0) NULL COMMENT '检验合格数量',
  68. jybhgsl DECIMAL(10,0) NULL COMMENT '检验不合格数量',
  69. fj TEXT NULL COMMENT '附件',
  70. bz TEXT NULL COMMENT '备注',
  71. jyr VARCHAR(255) NULL COMMENT '检验人',
  72. gxbm VARCHAR(255) NULL COMMENT '工序编码',
  73. gxmc VARCHAR(255) NULL COMMENT '工序名称',
  74. sczyry VARCHAR(255) NULL COMMENT '生产作业人员',
  75. ybl DECIMAL(10,0) NULL COMMENT '样本量',
  76. bdbh VARCHAR(255) NULL COMMENT '表单编号',
  77. bbh VARCHAR(255) NULL COMMENT '版本号',
  78. sxrq DATETIME NULL COMMENT '生效日期',
  79. wlbm VARCHAR(255) NULL COMMENT '物料编码',
  80. wlmc VARCHAR(255) NULL COMMENT '物料名称',
  81. lysl DECIMAL(10,0) NULL COMMENT '留样数量',
  82. phsl DECIMAL(10,0) NULL COMMENT '破坏数量',
  83. jygfid VARCHAR(255) NULL COMMENT '检验规范',
  84. jygfbbm VARCHAR(255) NULL COMMENT '没有用',
  85. jgbb VARCHAR(255) NULL COMMENT '检规版本',
  86. jgbh VARCHAR(255) NULL COMMENT '检规编号',
  87. title VARCHAR(255) NULL,
  88. jyr1 VARCHAR(255) NULL,
  89. ChangeStatus INT NULL,
  90. status VARCHAR(255) NULL COMMENT '检验状态'
  91. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='过程检验单(头) 归口自旧系统dopdemo,空表待上游抽数'
  92. """);
  93. await _db.Ado.ExecuteCommandAsync(
  94. """
  95. CREATE TABLE IF NOT EXISTS mdp_stg_ipqc_inspection (
  96. id BIGINT AUTO_INCREMENT PRIMARY KEY,
  97. tenant_id BIGINT NOT NULL DEFAULT 0,
  98. factory_id BIGINT NULL DEFAULT 1,
  99. source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
  100. source_table VARCHAR(100) NOT NULL,
  101. source_row_id VARCHAR(100) NOT NULL,
  102. source_biz_key VARCHAR(200) NULL,
  103. sync_batch_id VARCHAR(100) NOT NULL,
  104. sync_time DATETIME NOT NULL,
  105. process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  106. raw_data JSON NOT NULL,
  107. update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  108. UNIQUE KEY uk_mdp_stg_ipqc_insp (tenant_id, source_table, source_row_id)
  109. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S6过程检验单头贴源层'
  110. """);
  111. await _db.Ado.ExecuteCommandAsync(
  112. """
  113. CREATE TABLE IF NOT EXISTS mdp_std_ipqc_inspection (
  114. id BIGINT AUTO_INCREMENT PRIMARY KEY,
  115. tenant_id BIGINT NOT NULL DEFAULT 0,
  116. factory_id BIGINT NULL DEFAULT 1,
  117. source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
  118. bill_no VARCHAR(64) NOT NULL,
  119. product_model VARCHAR(255) NULL,
  120. production_batch_no VARCHAR(255) NULL,
  121. production_work_order VARCHAR(255) NULL,
  122. result_judgement VARCHAR(32) NULL,
  123. attachment TEXT NULL,
  124. remark TEXT NULL,
  125. inspector VARCHAR(255) NULL,
  126. process_code VARCHAR(255) NULL,
  127. process_name VARCHAR(255) NULL,
  128. production_person VARCHAR(255) NULL,
  129. sample_qty DECIMAL(18,6) NULL,
  130. form_no VARCHAR(255) NULL,
  131. version_no VARCHAR(255) NULL,
  132. effective_date DATETIME NULL,
  133. material_code VARCHAR(255) NULL,
  134. material_name VARCHAR(255) NULL,
  135. inspec_standard_version VARCHAR(255) NULL,
  136. inspec_standard_code VARCHAR(255) NULL,
  137. inspection_status VARCHAR(255) NULL,
  138. source_row_id VARCHAR(100) NOT NULL,
  139. source_biz_key VARCHAR(200) NULL,
  140. sync_batch_id VARCHAR(100) NOT NULL,
  141. sync_time DATETIME NOT NULL,
  142. update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  143. UNIQUE KEY uk_mdp_std_ipqc_insp (tenant_id, source_row_id),
  144. KEY idx_mdp_std_ipqc_insp_bill (tenant_id, bill_no),
  145. KEY idx_mdp_std_ipqc_insp_effdate (tenant_id, effective_date)
  146. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S6过程检验单头标准层'
  147. """);
  148. }
  149. /// <summary>贴源头:qms_gcjyd -> mdp_stg_ipqc_inspection(raw_data JSON 快照)。返回源头行数。</summary>
  150. private async Task<int> SyncHeadStagingAsync(string batchId, DateTime now)
  151. {
  152. var rows = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_gcjyd");
  153. await _db.Ado.ExecuteCommandAsync(
  154. """
  155. INSERT INTO mdp_stg_ipqc_inspection
  156. (tenant_id, factory_id, source_system, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time, process_status, raw_data)
  157. SELECT
  158. 0, 1, 'AIDOP', 'qms_gcjyd',
  159. CAST(m.id AS CHAR), CAST(m.djbh AS CHAR), @BatchId, @Now, 'PENDING',
  160. JSON_OBJECT(
  161. 'id', m.id, 'djbh', m.djbh, 'cplx', m.cplx, 'scph', m.scph, 'lydjbh', m.lydjbh,
  162. 'jgpd', m.jgpd, 'fj', CAST(m.fj AS CHAR), 'bz', CAST(m.bz AS CHAR), 'jyr', m.jyr,
  163. 'gxbm', m.gxbm, 'gxmc', m.gxmc, 'sczyry', m.sczyry, 'ybl', m.ybl,
  164. 'bdbh', m.bdbh, 'bbh', m.bbh, 'sxrq', m.sxrq, 'wlbm', m.wlbm, 'wlmc', m.wlmc,
  165. 'jgbb', m.jgbb, 'jgbh', m.jgbh, 'status', m.status
  166. )
  167. FROM qms_gcjyd m
  168. ON DUPLICATE KEY UPDATE
  169. source_biz_key=VALUES(source_biz_key), sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time),
  170. process_status=VALUES(process_status), raw_data=VALUES(raw_data), update_time=CURRENT_TIMESTAMP
  171. """,
  172. new SugarParameter("@BatchId", batchId),
  173. new SugarParameter("@Now", now));
  174. return rows;
  175. }
  176. /// <summary>标准化头:qms_gcjyd -> mdp_std_ipqc_inspection(typed)。返回处理头行数。</summary>
  177. private async Task<int> TransformHeadStandardAsync(string batchId, DateTime now)
  178. {
  179. var rows = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_gcjyd");
  180. await _db.Ado.ExecuteCommandAsync(
  181. """
  182. INSERT INTO mdp_std_ipqc_inspection
  183. (tenant_id, factory_id, source_system, bill_no, product_model, production_batch_no, production_work_order,
  184. result_judgement, attachment, remark, inspector, process_code, process_name, production_person,
  185. sample_qty, form_no, version_no, effective_date, material_code, material_name,
  186. inspec_standard_version, inspec_standard_code, inspection_status,
  187. source_row_id, source_biz_key, sync_batch_id, sync_time)
  188. SELECT
  189. 0, 1, 'AIDOP', IFNULL(m.djbh, CAST(m.id AS CHAR)), m.cplx, m.scph, m.lydjbh,
  190. CAST(m.jgpd AS CHAR), CAST(m.fj AS CHAR), CAST(m.bz AS CHAR), m.jyr, m.gxbm, m.gxmc, m.sczyry,
  191. m.ybl, m.bdbh, m.bbh, m.sxrq, m.wlbm, m.wlmc,
  192. m.jgbb, m.jgbh, m.status,
  193. CAST(m.id AS CHAR), CAST(m.djbh AS CHAR), @BatchId, @Now
  194. FROM qms_gcjyd m
  195. ON DUPLICATE KEY UPDATE
  196. bill_no=VALUES(bill_no), product_model=VALUES(product_model), production_batch_no=VALUES(production_batch_no),
  197. production_work_order=VALUES(production_work_order), result_judgement=VALUES(result_judgement),
  198. attachment=VALUES(attachment), remark=VALUES(remark), inspector=VALUES(inspector),
  199. process_code=VALUES(process_code), process_name=VALUES(process_name), production_person=VALUES(production_person),
  200. sample_qty=VALUES(sample_qty), form_no=VALUES(form_no), version_no=VALUES(version_no),
  201. effective_date=VALUES(effective_date), material_code=VALUES(material_code), material_name=VALUES(material_name),
  202. inspec_standard_version=VALUES(inspec_standard_version), inspec_standard_code=VALUES(inspec_standard_code),
  203. inspection_status=VALUES(inspection_status),
  204. sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP
  205. """,
  206. new SugarParameter("@BatchId", batchId),
  207. new SugarParameter("@Now", now));
  208. return rows;
  209. }
  210. private async Task<long> InsertRunLogAsync(string batchId, DateTime startedAt, string triggerType)
  211. {
  212. await _db.Ado.ExecuteCommandAsync(
  213. """
  214. INSERT INTO mdp_transform_run_log
  215. (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time)
  216. VALUES (0, @JobCode, 'S6过程检验单MDP同步与标准化转换', @TriggerType, @BatchId, 'RUNNING', @StartTime)
  217. """,
  218. new SugarParameter("@JobCode", JobCode),
  219. new SugarParameter("@TriggerType", NormalizeTriggerType(triggerType)),
  220. new SugarParameter("@BatchId", batchId),
  221. new SugarParameter("@StartTime", startedAt));
  222. return await _db.Ado.GetLongAsync(
  223. "SELECT id FROM mdp_transform_run_log WHERE batch_id=@BatchId ORDER BY id DESC LIMIT 1",
  224. new List<SugarParameter> { new("@BatchId", batchId) });
  225. }
  226. private async Task MarkRunSuccessAsync(long runLogId, DateTime startedAt, IpqcInspectionMdpSyncResult result)
  227. {
  228. var finishedAt = DateTime.Now;
  229. await _db.Ado.ExecuteCommandAsync(
  230. """
  231. UPDATE mdp_transform_run_log
  232. SET status='SUCCESS', end_time=@EndTime, duration_ms=@DurationMs,
  233. stage_rows=@StageRows, standard_rows=@StandardRows, dwd_rows=0, update_time=CURRENT_TIMESTAMP
  234. WHERE id=@Id
  235. """,
  236. new SugarParameter("@EndTime", finishedAt),
  237. new SugarParameter("@DurationMs", (int)(finishedAt - startedAt).TotalMilliseconds),
  238. new SugarParameter("@StageRows", result.HeadStageRows),
  239. new SugarParameter("@StandardRows", result.HeadStandardRows),
  240. new SugarParameter("@Id", runLogId));
  241. }
  242. private async Task MarkRunFailedAsync(long runLogId, DateTime startedAt, string message)
  243. {
  244. var finishedAt = DateTime.Now;
  245. await _db.Ado.ExecuteCommandAsync(
  246. """
  247. UPDATE mdp_transform_run_log
  248. SET status='FAILED', end_time=@EndTime, duration_ms=@DurationMs,
  249. error_message=@ErrorMessage, update_time=CURRENT_TIMESTAMP
  250. WHERE id=@Id
  251. """,
  252. new SugarParameter("@EndTime", finishedAt),
  253. new SugarParameter("@DurationMs", (int)(finishedAt - startedAt).TotalMilliseconds),
  254. new SugarParameter("@ErrorMessage", message.Length > 2000 ? message[..2000] : message),
  255. new SugarParameter("@Id", runLogId));
  256. }
  257. private static string NormalizeTriggerType(string? triggerType)
  258. => string.IsNullOrWhiteSpace(triggerType) ? "AUTO" : triggerType.Trim().ToUpperInvariant();
  259. }
  260. /// <summary>过程检验单 MDP 同步转换结果。</summary>
  261. public sealed class IpqcInspectionMdpSyncResult
  262. {
  263. public long RunLogId { get; set; }
  264. public string BatchId { get; set; } = string.Empty;
  265. public int HeadStageRows { get; set; }
  266. public int HeadStandardRows { get; set; }
  267. }