S1MdpSyncTransformService.cs 123 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924
  1. using Admin.NET.Plugin.AiDOP.DataPlatform;
  2. using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
  3. using Admin.NET.Plugin.AiDOP.DataPlatform.MdpRebuild;
  4. using Admin.NET.Plugin.AiDOP.DataPlatform.S1Refresh;
  5. using Admin.NET.Plugin.AiDOP.SmartOps;
  6. namespace Admin.NET.Plugin.AiDOP.Order;
  7. /// <summary>
  8. /// S1 首批 MDP 同步和标准化转换服务。
  9. /// Phase2:源→stg 经统一执行器;std/DWD/KPI 仍读 stg。
  10. /// </summary>
  11. public class S1MdpSyncTransformService : ITransient
  12. {
  13. private const string JobCode = "S1_MDP_SYNC_TRANSFORM";
  14. private readonly ISqlSugarClient _db;
  15. private readonly TransformRunLogFinalizer _runLogFinalizer;
  16. private readonly SmartOpsKpiAtomicBuildService _atomicBuild;
  17. private readonly MdpModuleStagingPuller _stagingPuller;
  18. private readonly IKpiTargetResolver _kpiTargetResolver;
  19. private readonly IS1MdpFullRunLock _fullRunLock;
  20. private readonly S9CompositeKpiWriter _s9Composite;
  21. private readonly MdpNeutralSourceGate _neutralGate;
  22. private readonly MdpTenantStdSourceResolver _stdSource;
  23. private readonly NativeNeutralProjectionService _nativeProjection;
  24. public S1MdpSyncTransformService(
  25. ISqlSugarClient db,
  26. SmartOpsKpiAtomicBuildService atomicBuild,
  27. MdpModuleStagingPuller stagingPuller,
  28. IKpiTargetResolver kpiTargetResolver,
  29. IS1MdpFullRunLock fullRunLock,
  30. TransformRunLogFinalizer runLogFinalizer,
  31. S9CompositeKpiWriter s9Composite,
  32. MdpNeutralSourceGate neutralGate,
  33. MdpTenantStdSourceResolver stdSource,
  34. NativeNeutralProjectionService nativeProjection)
  35. {
  36. _db = db;
  37. _runLogFinalizer = runLogFinalizer;
  38. _atomicBuild = atomicBuild;
  39. _stagingPuller = stagingPuller;
  40. _kpiTargetResolver = kpiTargetResolver;
  41. _fullRunLock = fullRunLock;
  42. _s9Composite = s9Composite;
  43. _neutralGate = neutralGate;
  44. _stdSource = stdSource;
  45. _nativeProjection = nativeProjection;
  46. }
  47. public async Task<S1MdpSyncTransformResult> RunFullAsync(
  48. S1MdpRunScope scope,
  49. CancellationToken cancellationToken = default,
  50. string triggerType = "AUTO",
  51. long? rebuildJobId = null,
  52. Func<S1MdpProgressUpdate, Task>? reportProgress = null)
  53. {
  54. scope = S1MdpRunScope.Create(scope.TenantId, scope.FactoryId);
  55. cancellationToken.ThrowIfCancellationRequested();
  56. var holderId = $"{triggerType}:{scope.ScopeKey}:{Environment.MachineName}:{Guid.NewGuid():N}";
  57. var lease = await _fullRunLock.TryAcquireAsync(scope.LockName, holderId, rebuildJobId, cancellationToken);
  58. if (lease == null)
  59. throw new S1MdpAlreadyRunningException();
  60. await using (lease)
  61. {
  62. using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  63. var heartbeat = KeepLeaseAliveAsync(lease, heartbeatCts.Token);
  64. var now = DateTime.Now;
  65. var batchId = $"S1_MDP_FULL_{scope.TenantId}_{scope.FactoryId}_{now:yyyyMMddHHmmss}";
  66. var runLogId = await InsertTransformRunLogAsync(scope, batchId, now, triggerType);
  67. var result = new S1MdpSyncTransformResult { BatchId = batchId, RunLogId = runLogId };
  68. try
  69. {
  70. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Preparing, 0, 5, "准备运行环境"));
  71. await EnsureS1RuntimeObjectsAsync();
  72. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Staging, 1, 10, "正在拉取源数据"));
  73. result.StageRows = await SyncStagingAsync(scope, batchId, now, cancellationToken, triggerType);
  74. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Staging, 1, 35, "拉取源数据完成", result.StageRows, S1MdpRebuildStage.Staging));
  75. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Standard, 2, 35, "正在标准化数据"));
  76. result.StandardRows = await TransformStandardAsync(scope, batchId, now, cancellationToken);
  77. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Standard, 2, 50, "标准化数据完成", result.StandardRows, S1MdpRebuildStage.Standard));
  78. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Dwd, 3, 50, "正在生成 DWD 明细"));
  79. result.DwdRows = await BuildDwdAsync(scope, batchId, now, result, cancellationToken);
  80. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Dwd, 3, 65, "生成 DWD 明细完成", result.DwdRows, S1MdpRebuildStage.Dwd));
  81. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Kpi, 4, 65, "正在重算 KPI"));
  82. result.KpiRows = await BuildS1KpiValuesAsync(scope, batchId, now, cancellationToken);
  83. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Kpi, 4, 80, "重算 KPI 完成", result.KpiRows, S1MdpRebuildStage.Kpi));
  84. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Atomic, 5, 82, "正在重建原子数据"));
  85. result.AtomicRows = await _atomicBuild.BuildOrderDeliveryDomainForAllDatesAsync(
  86. scope.TenantId, scope.FactoryId, batchId, cancellationToken);
  87. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Atomic, 5, 98, "重建原子数据完成", result.AtomicRows, S1MdpRebuildStage.Atomic));
  88. await ReportAsync(reportProgress, new S1MdpProgressUpdate(S1MdpRebuildStage.Finalizing, 5, 99, "正在收尾"));
  89. await _nativeProjection.ProjectAsync(scope.TenantId, batchId, cancellationToken);
  90. result.KpiRows += await _s9Composite.WriteRecentAsync(scope.TenantId, scope.FactoryId, now, cancellationToken);
  91. await MarkTransformRunSuccessAsync(runLogId, now, result);
  92. return result;
  93. }
  94. catch (Exception ex)
  95. {
  96. // 管理员取消由外层重算服务收口为 ABORTED,这里不能先写成 FAILED。
  97. if (ex is ModuleRebuildCancelledException)
  98. throw;
  99. var summary = ex is OperationCanceledException
  100. ? "HTTP 请求已取消或服务中断"
  101. : ex.Message;
  102. // 宿主关停不是转换失败:交给 finally 收口为 ABORTED,不污染 FAILED 语义。
  103. // 上面那句 summary 之所以把两种取消写在一起,正是因为 token 分不开它们;
  104. // 判据只能是 ApplicationStopping。
  105. if (!_runLogFinalizer.IsHostStopping)
  106. await MarkTransformRunFailedAsync(runLogId, now, summary);
  107. throw;
  108. }
  109. finally
  110. {
  111. await _runLogFinalizer.FinalizeIfHostStoppingAsync(runLogId, now);
  112. heartbeatCts.Cancel();
  113. try { await heartbeat; } catch (OperationCanceledException) { }
  114. }
  115. }
  116. }
  117. private static async Task ReportAsync(Func<S1MdpProgressUpdate, Task>? report, S1MdpProgressUpdate update)
  118. {
  119. if (report == null) return;
  120. await report(update);
  121. }
  122. /// <summary>贴源已由 FILE 导入写入后,只跑标准层/DWD/KPI,不再 Pull。</summary>
  123. public async Task<S1MdpSyncTransformResult> RunPostStagingAsync(
  124. S1MdpRunScope scope,
  125. CancellationToken cancellationToken = default,
  126. string triggerType = "FILE_IMPORT")
  127. {
  128. scope = S1MdpRunScope.Create(scope.TenantId, scope.FactoryId);
  129. cancellationToken.ThrowIfCancellationRequested();
  130. var now = DateTime.Now;
  131. var batchId = $"S1_MDP_FILE_{scope.TenantId}_{scope.FactoryId}_{now:yyyyMMddHHmmss}";
  132. var runLogId = await InsertTransformRunLogAsync(scope, batchId, now, triggerType);
  133. var result = new S1MdpSyncTransformResult { BatchId = batchId, RunLogId = runLogId };
  134. try
  135. {
  136. await EnsureS1RuntimeObjectsAsync();
  137. result.StandardRows = await TransformStandardAsync(scope, batchId, now, cancellationToken);
  138. result.DwdRows = await BuildDwdAsync(scope, batchId, now, result, cancellationToken);
  139. result.KpiRows = await BuildS1KpiValuesAsync(scope, batchId, now, cancellationToken);
  140. result.AtomicRows = await _atomicBuild.BuildOrderDeliveryDomainForAllDatesAsync(
  141. scope.TenantId, scope.FactoryId, batchId, cancellationToken);
  142. await _nativeProjection.ProjectAsync(scope.TenantId, batchId, cancellationToken);
  143. result.KpiRows += await _s9Composite.WriteRecentAsync(scope.TenantId, scope.FactoryId, now, cancellationToken);
  144. await MarkTransformRunSuccessAsync(runLogId, now, result);
  145. return result;
  146. }
  147. catch (Exception ex)
  148. {
  149. if (ex is ModuleRebuildCancelledException)
  150. throw;
  151. // 宿主关停不是转换失败:交给 finally 收口为 ABORTED,不污染 FAILED 语义。
  152. if (!_runLogFinalizer.IsHostStopping)
  153. await MarkTransformRunFailedAsync(runLogId, now, ex.Message);
  154. throw;
  155. }
  156. finally
  157. {
  158. await _runLogFinalizer.FinalizeIfHostStoppingAsync(runLogId, now);
  159. }
  160. }
  161. private async Task EnsureS1RuntimeObjectsAsync()
  162. {
  163. await MdpSchemaAligner.ExecuteAsync(_db, MdpSchemaDefinition.Ddl("dwd_requirement_examine_detail"));
  164. await MdpSchemaAligner.ExecuteAsync(_db,
  165. """
  166. INSERT INTO mdp_entity
  167. (tenant_id, source_id, entity_code, entity_name, entity_type, source_table_name, target_table_name, sync_mode, batch_size, status, remark, create_time, update_time)
  168. SELECT 0, s.id, 'S1_REQUIREMENT_EXAMINE_RESULT', 'S1需求核验结果主表', 'TABLE',
  169. 'b_examine_result', 'mdp_stg_so', 'FULL', 5000, 1, '需求核验结果主表,进入 S1 贴源层', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
  170. FROM mdp_source s
  171. WHERE s.tenant_id=0 AND s.source_code='AIDOPDEV_MYSQL'
  172. LIMIT 1
  173. ON DUPLICATE KEY UPDATE
  174. source_id=VALUES(source_id), entity_name=VALUES(entity_name), source_table_name=VALUES(source_table_name),
  175. target_table_name=VALUES(target_table_name), sync_mode=VALUES(sync_mode), status=VALUES(status),
  176. remark=VALUES(remark), update_time=CURRENT_TIMESTAMP;
  177. """);
  178. await MdpSchemaAligner.ExecuteAsync(_db,
  179. """
  180. INSERT INTO mdp_entity
  181. (tenant_id, source_id, entity_code, entity_name, entity_type, source_table_name, target_table_name, sync_mode, batch_size, status, remark, create_time, update_time)
  182. SELECT 0, s.id, 'S1_REQUIREMENT_EXAMINE_DETAIL', 'S1需求核验BOM明细', 'TABLE',
  183. 'b_bom_child_examine', 'mdp_stg_so', 'FULL', 5000, 1, '需求核验BOM明细,进入 S1 贴源层和 DWD', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
  184. FROM mdp_source s
  185. WHERE s.tenant_id=0 AND s.source_code='AIDOPDEV_MYSQL'
  186. LIMIT 1
  187. ON DUPLICATE KEY UPDATE
  188. source_id=VALUES(source_id), entity_name=VALUES(entity_name), source_table_name=VALUES(source_table_name),
  189. target_table_name=VALUES(target_table_name), sync_mode=VALUES(sync_mode), status=VALUES(status),
  190. remark=VALUES(remark), update_time=CURRENT_TIMESTAMP;
  191. """);
  192. await MdpSchemaAligner.ExecuteAsync(_db,
  193. """
  194. INSERT INTO mdp_entity
  195. (tenant_id, source_id, entity_code, entity_name, entity_type, source_table_name, target_table_name, sync_mode, incr_column, batch_size, status, remark, create_time, update_time)
  196. SELECT 0, s.id, v.entity_code, v.entity_name, 'TABLE', v.source_table_name, 'mdp_stg_so', v.sync_mode, v.incr_column, 5000, 1, v.remark, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
  197. FROM mdp_source s
  198. JOIN (
  199. SELECT 'S1_PRODUCT_DESIGN' AS entity_code, 'S1产品设计主表' AS entity_name, 'ado_product_design' AS source_table_name, 'INCR' AS sync_mode, 'UpdateTime' AS incr_column, '产品设计主表,进入订单标准层' AS remark
  200. UNION ALL SELECT 'S1_PRODUCT_DESIGN_BOM', 'S1产品设计BOM', 'ado_product_design_bom', 'FULL', NULL, '产品设计BOM,表无时间戳列,只能全量抽取'
  201. UNION ALL SELECT 'S1_PRODUCT_DESIGN_ROUTING', 'S1产品设计工艺路线', 'ado_product_design_routing', 'FULL', NULL, '产品设计工艺路线,表无时间戳列,只能全量抽取'
  202. ) v
  203. WHERE s.tenant_id=0 AND s.source_code='AIDOPDEV_MYSQL'
  204. ON DUPLICATE KEY UPDATE
  205. source_id=VALUES(source_id), entity_name=VALUES(entity_name), source_table_name=VALUES(source_table_name),
  206. target_table_name=VALUES(target_table_name), sync_mode=VALUES(sync_mode), incr_column=VALUES(incr_column),
  207. status=VALUES(status), remark=VALUES(remark), update_time=CURRENT_TIMESTAMP;
  208. """);
  209. }
  210. /// <summary>Phase2/3:源→stg 走执行器(DB/API);遗留 SyncOneEntityAsync 不再调用。</summary>
  211. private async Task<int> SyncStagingAsync(S1MdpRunScope scope, string batchId, DateTime now, CancellationToken cancellationToken, string triggerType = "AUTO")
  212. {
  213. _ = now;
  214. var codes = S1MdpEntityConfig.All.Select(x => x.EntityCode);
  215. return await _stagingPuller.PullEntitiesAsync(
  216. codes, batchId, scope.TenantId, fullRefresh: ModuleRebuildTriggerType.ShouldPullFull(triggerType), taskCode: "S1_MDP_INBOUND",
  217. cancellationToken, scope.FactoryId, requireMatchingSourceTenant: true);
  218. }
  219. /// <summary>
  220. /// 双模式入站:仅抽 stg(可指定 DB 实体码或 *_API),再跑 std/DWD/KPI。
  221. /// </summary>
  222. public async Task<S1MdpSyncTransformResult> RunInboundAsync(
  223. S1MdpRunScope scope,
  224. IEnumerable<string>? entityCodes = null,
  225. bool fullRefresh = false,
  226. CancellationToken cancellationToken = default)
  227. {
  228. scope = S1MdpRunScope.Create(scope.TenantId, scope.FactoryId);
  229. cancellationToken.ThrowIfCancellationRequested();
  230. var now = DateTime.Now;
  231. var batchId = $"S1_MDP_IN_{scope.TenantId}_{scope.FactoryId}_{now:yyyyMMddHHmmss}";
  232. var runLogId = await InsertTransformRunLogAsync(scope, batchId, now, "INBOUND");
  233. var result = new S1MdpSyncTransformResult { BatchId = batchId, RunLogId = runLogId };
  234. try
  235. {
  236. await EnsureS1RuntimeObjectsAsync();
  237. var codes = entityCodes?.ToList() ?? S1MdpEntityConfig.All.Select(x => x.EntityCode).ToList();
  238. result.StageRows = await _stagingPuller.PullEntitiesAsync(
  239. codes, batchId, scope.TenantId, fullRefresh, "S1_MDP_INBOUND", cancellationToken,
  240. scope.FactoryId, requireMatchingSourceTenant: true);
  241. result.StandardRows = await TransformStandardAsync(scope, batchId, now, cancellationToken);
  242. result.DwdRows = await BuildDwdAsync(scope, batchId, now, result, cancellationToken);
  243. result.KpiRows = await BuildS1KpiValuesAsync(scope, batchId, now, cancellationToken);
  244. result.AtomicRows = await _atomicBuild.BuildOrderDeliveryDomainForAllDatesAsync(
  245. scope.TenantId, scope.FactoryId, batchId, cancellationToken);
  246. await _nativeProjection.ProjectAsync(scope.TenantId, batchId, cancellationToken);
  247. result.KpiRows += await _s9Composite.WriteRecentAsync(scope.TenantId, scope.FactoryId, now, cancellationToken);
  248. await MarkTransformRunSuccessAsync(runLogId, now, result);
  249. return result;
  250. }
  251. catch (Exception ex)
  252. {
  253. if (ex is ModuleRebuildCancelledException)
  254. throw;
  255. // 宿主关停不是转换失败:交给 finally 收口为 ABORTED,不污染 FAILED 语义。
  256. if (!_runLogFinalizer.IsHostStopping)
  257. await MarkTransformRunFailedAsync(runLogId, now, ex.Message);
  258. throw;
  259. }
  260. finally
  261. {
  262. await _runLogFinalizer.FinalizeIfHostStoppingAsync(runLogId, now);
  263. }
  264. }
  265. /// <summary>Phase3:已停用。源→stg 改由执行器路径;保留方法供紧急回滚对照,勿再调用。</summary>
  266. [Obsolete("Phase3: use MdpModuleStagingPuller / executors")]
  267. private async Task<int> SyncOneEntityAsync(S1MdpEntityConfig entity, string batchId, DateTime now, long tenantId)
  268. {
  269. if (tenantId <= 0) throw new InvalidOperationException("S1 同步日志必须指定有效 tenantId");
  270. var entityRow = await _db.Ado.SqlQuerySingleAsync<S1MdpEntityRow>(
  271. "SELECT id AS Id, entity_name AS EntityName FROM mdp_entity WHERE tenant_id=0 AND entity_code=@EntityCode LIMIT 1",
  272. new SugarParameter("@EntityCode", entity.EntityCode));
  273. if (entityRow == null) throw Oops.Oh($"未找到 MDP 实体配置:{entity.EntityCode}");
  274. var columns = await _db.Ado.SqlQueryAsync<S1ColumnRow>(
  275. """
  276. SELECT COLUMN_NAME AS ColumnName
  277. FROM information_schema.COLUMNS
  278. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=@TableName
  279. ORDER BY ORDINAL_POSITION
  280. """,
  281. new SugarParameter("@TableName", entity.SourceTable));
  282. if (columns.Count == 0) throw Oops.Oh($"未找到源表:{entity.SourceTable}");
  283. var names = columns.Select(u => u.ColumnName).ToList();
  284. var tenantExpr = BuildOptionalColumnExpr(names, "tenant_id", "0");
  285. var factoryExpr = BuildOptionalColumnExpr(names, "factory_id", "NULL");
  286. var companyExpr = BuildOptionalColumnExpr(names, "company_id", "NULL");
  287. var sourceRowExpr = names.Any(u => string.Equals(u, entity.SourceRowIdExpression, StringComparison.OrdinalIgnoreCase))
  288. ? $"s.`{FindColumn(names, entity.SourceRowIdExpression)}`"
  289. : entity.SourceRowIdExpression;
  290. var rawDataExpr = BuildJsonObjectExpression(names);
  291. var rowsRead = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM `{entity.SourceTable}`");
  292. var logId = await InsertSyncLogAsync(tenantId, entityRow.Id, entityRow.EntityName, batchId, rowsRead);
  293. var started = DateTime.Now;
  294. try
  295. {
  296. var affected = await MdpSchemaAligner.ExecuteAsync(_db,
  297. $"""
  298. INSERT INTO `{entity.TargetTable}`
  299. (tenant_id, factory_id, company_id, source_system, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time, process_status, raw_data)
  300. SELECT
  301. {tenantExpr},
  302. {factoryExpr},
  303. {companyExpr},
  304. COALESCE((SELECT r.source_system FROM mdp_source_table_registry r WHERE r.source_table = @SourceTable LIMIT 1), NULLIF('',''), ''),
  305. @SourceTable,
  306. CAST({sourceRowExpr} AS CHAR),
  307. CAST(COALESCE({entity.SourceBizKeyExpression}, CAST({sourceRowExpr} AS CHAR)) AS CHAR),
  308. @BatchId,
  309. @Now,
  310. 'PENDING',
  311. {rawDataExpr}
  312. FROM `{entity.SourceTable}` s
  313. ON DUPLICATE KEY UPDATE
  314. tenant_id=VALUES(tenant_id),
  315. factory_id=VALUES(factory_id),
  316. company_id=VALUES(company_id),
  317. source_row_id=VALUES(source_row_id),
  318. sync_batch_id=VALUES(sync_batch_id),
  319. sync_time=VALUES(sync_time),
  320. process_status=VALUES(process_status),
  321. raw_data=VALUES(raw_data),
  322. update_time=CURRENT_TIMESTAMP
  323. """,
  324. new SugarParameter("@SourceTable", entity.SourceTable),
  325. new SugarParameter("@BatchId", batchId),
  326. new SugarParameter("@Now", now));
  327. await MarkSyncLogSuccessAsync(logId, started, affected);
  328. return rowsRead;
  329. }
  330. catch (Exception ex)
  331. {
  332. await MarkSyncLogFailedAsync(logId, started, ex.Message);
  333. throw;
  334. }
  335. }
  336. private async Task<int> TransformStandardAsync(S1MdpRunScope scope, string batchId, DateTime now, CancellationToken cancellationToken)
  337. {
  338. using var db = _db.CopyNew();
  339. db.Ado.CommandTimeOut = Math.Max(db.Ado.CommandTimeOut, 180);
  340. var total = 0;
  341. var registeredSource = (await _stdSource.GetSourceAsync(scope.TenantId, "SO", cancellationToken))?.SourceSystem;
  342. foreach (var command in BuildStandardCommands(scope, batchId, now))
  343. {
  344. cancellationToken.ThrowIfCancellationRequested();
  345. var gateSource = string.IsNullOrWhiteSpace(registeredSource)
  346. ? InferNeutralSource(command.Sql)
  347. : registeredSource;
  348. if (!await _neutralGate.AllowsAsync(scope.TenantId, "SO", gateSource))
  349. continue;
  350. total += await db.Ado.ExecuteCommandAsync(command.Sql, command.Parameters);
  351. }
  352. return total;
  353. }
  354. /// <summary>S1 标准 INSERT:发运相关表走 165;其余本库业务走 AIDOP_NATIVE。</summary>
  355. private static string InferNeutralSource(string sql)
  356. {
  357. if (sql.Contains("mdp_std_ship_trans", StringComparison.OrdinalIgnoreCase)
  358. || sql.Contains("ShippingPlan", StringComparison.Ordinal)
  359. || sql.Contains("ASNBOL", StringComparison.Ordinal))
  360. return "DOPDEMORQ_SQLSERVER";
  361. return MdpSourceIdentity.Native;
  362. }
  363. private async Task<int> BuildDwdAsync(
  364. S1MdpRunScope scope, string batchId, DateTime now,
  365. S1MdpSyncTransformResult result, CancellationToken cancellationToken)
  366. {
  367. using var db = _db.CopyNew();
  368. db.Ado.CommandTimeOut = Math.Max(db.Ado.CommandTimeOut, 180);
  369. var total = 0;
  370. foreach (var command in BuildDwdCommands(scope, batchId, now))
  371. {
  372. cancellationToken.ThrowIfCancellationRequested();
  373. total += await db.Ado.ExecuteCommandAsync(command.Sql, command.Parameters);
  374. }
  375. // DWD 阶段的最后一步:把本批次发布为「当前快照」。
  376. // 必须在所有 DWD 写入完成之后,否则读者会看到一个还没灌完的批次被标成 current。
  377. // 不计入 total —— total 语义是「本轮构建的 DWD 行数」,翻牌影响的行数属于发布动作,不是新建行。
  378. cancellationToken.ThrowIfCancellationRequested();
  379. var publishedRows = await PublishCurrentSnapshotAsync(db, scope, batchId);
  380. // 发布证据随运行日志一起落盘(见 MarkTransformRunSuccessAsync)。
  381. // 走到这里就意味着两条 DWD 写入与发布语句都已成功;若中途抛错,
  382. // 调用方的 catch 会写 FAILED,本字段保持 null —— 失败方向是保守的。
  383. result.Publication = new S1MdpSnapshotPublication
  384. {
  385. Table = "dwd_requirement_examine_detail",
  386. BatchId = batchId,
  387. FactoryId = scope.FactoryId,
  388. CurrentRows = publishedRows
  389. };
  390. return total;
  391. }
  392. /// <summary>
  393. /// 把 <paramref name="batchId"/> 原子地发布为该 (tenant, factory) 作用域下的当前快照。
  394. ///
  395. /// 【为什么必须是「一条」语句】
  396. /// BuildDwdAsync 全程没有事务(用的是 _db.CopyNew() 上的裸 ExecuteCommandAsync,没有 BeginTran)。
  397. /// 因此任何「两条语句」的写法都会在两条之间留下一个对并发读者可见的错误窗口:
  398. /// · 先清后置(先把旧批次清 0,再把新批次置 1)→ 中间窗口「零个当前批次」,
  399. /// 读者拿到空结果,会被当成「本工单已齐套、无缺料」——缺料监控直接漏报。
  400. /// · 先置后清(先把新批次置 1,再把旧批次清 0)→ 中间窗口「两个当前批次」,
  401. /// 读者按 is_current_flag=1 取数会同时拿到新旧两批,缺料数量凭空翻倍。
  402. /// · 而且第二条语句一旦失败/进程被杀,上述错误窗口就不再是「瞬时」,而是永久残留。
  403. /// 单条 UPDATE 由 InnoDB 保证语句级原子性,不存在中间可见状态,也没有"第二条没跑成"的失败模式。
  404. ///
  405. /// 【为什么 factory_id 要 COALESCE 归一】
  406. /// 同一次 run(scope.FactoryId=1)产出的行,factory_id 既可能是 1 也可能是 NULL:
  407. /// 入库过滤用的是 COALESCE(NULLIF(d.factory_id,0),1)=@FactoryId,而落库值取的是
  408. /// COALESCE(NULLIF(d.factory_id,0), NULLIF(r.factory_id,0), NULLIF(so.factory_id,0)),三者全空时就是 NULL。
  409. /// 实测(aidopdev,只读):1019060 行里 296189 行 factory_id IS NULL,
  410. /// 且 3439 个批次里有 594 个批次「同时含 factory_id=1 和 factory_id IS NULL」。
  411. /// 若作用域谓词直接写 factory_id=@FactoryId:
  412. /// ① NULL 行永远不被 = 匹配(SQL 三值逻辑,NULL=1 得 NULL 而非 TRUE),旧批次的 NULL 行会一直挂着 is_current_flag=1;
  413. /// ② 新批次也只有一半的行被置 1。
  414. /// 结果就是任务里点名要避免的「两个当前批次」永久共存。
  415. /// 故必须用 COALESCE(NULLIF(factory_id,0),1) 归一 —— 这也是本仓库通用的 factory 作用域写法。
  416. /// </summary>
  417. /// <summary>
  418. /// 发布当前快照,并返回本批次发布后的当前行数。
  419. /// <para>返回 0 是<b>合法结果</b>——源侧本轮没有数据时,成功发布的就是一个空快照。
  420. /// 调用方把这个数字记进运行日志,下游才能把「正常的空」与「没发布成」区分开。</para>
  421. /// </summary>
  422. private static async Task<int> PublishCurrentSnapshotAsync(ISqlSugarClient db, S1MdpRunScope scope, string batchId)
  423. {
  424. await db.Ado.ExecuteCommandAsync(
  425. """
  426. UPDATE dwd_requirement_examine_detail
  427. SET is_current_flag = CASE WHEN calc_batch_id=@BatchId THEN 1 ELSE 0 END
  428. WHERE tenant_id=@TenantId
  429. AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  430. AND (calc_batch_id=@BatchId OR is_current_flag=1)
  431. """,
  432. new SugarParameter("@BatchId", batchId),
  433. new SugarParameter("@TenantId", scope.TenantId),
  434. new SugarParameter("@FactoryId", scope.FactoryId));
  435. // 复核发布结果而不是相信影响行数:ODKU 与 CASE 的影响行数都不等于「当前有几行」。
  436. return await db.Ado.GetIntAsync(
  437. """
  438. SELECT COUNT(*) FROM dwd_requirement_examine_detail
  439. WHERE tenant_id=@TenantId
  440. AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  441. AND calc_batch_id=@BatchId
  442. AND is_current_flag=1
  443. """,
  444. new SugarParameter("@BatchId", batchId),
  445. new SugarParameter("@TenantId", scope.TenantId),
  446. new SugarParameter("@FactoryId", scope.FactoryId));
  447. }
  448. private async Task<int> BuildS1KpiValuesAsync(S1MdpRunScope scope, string batchId, DateTime now, CancellationToken cancellationToken)
  449. {
  450. var affected = 0;
  451. for (var dayOffset = 13; dayOffset >= 0; dayOffset--)
  452. {
  453. var statDate = now.Date.AddDays(-dayOffset);
  454. var rows = await CalculateS1KpiValuesAsync(scope, batchId, statDate);
  455. foreach (var row in rows)
  456. {
  457. cancellationToken.ThrowIfCancellationRequested();
  458. if (row.TenantId != scope.TenantId || row.FactoryId != scope.FactoryId)
  459. continue;
  460. affected += await UpsertS1KpiValueAsync(row, statDate, now);
  461. }
  462. }
  463. return affected;
  464. }
  465. private async Task<List<S1KpiCalcRow>> CalculateS1KpiValuesAsync(S1MdpRunScope scope, string batchId, DateTime statDate)
  466. {
  467. return await _db.Ado.SqlQueryAsync<S1KpiCalcRow>(
  468. """
  469. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  470. 'S1_L1_001' AS MetricCode,
  471. ROUND(AVG(TIMESTAMPDIFF(HOUR, order_date, COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date)) / 24), 4) AS MetricValue
  472. FROM mdp_std_so
  473. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  474. AND order_date IS NOT NULL
  475. AND COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date) IS NOT NULL
  476. AND COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date) >= order_date
  477. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  478. UNION ALL
  479. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  480. 'S1_L1_002' AS MetricCode,
  481. ROUND(100 * SUM(CASE WHEN TIMESTAMPDIFF(HOUR, order_date, COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date)) <= 72 THEN 1 ELSE 0 END) / COUNT(1), 4) AS MetricValue
  482. FROM mdp_std_so
  483. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  484. AND order_date IS NOT NULL
  485. AND COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date) IS NOT NULL
  486. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  487. UNION ALL
  488. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  489. 'S1_L1_003' AS MetricCode,
  490. ROUND(COUNT(1) / GREATEST(COUNT(DISTINCT NULLIF(planner_no, '')), 1), 4) AS MetricValue
  491. FROM mdp_std_so
  492. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  493. AND order_date IS NOT NULL AND order_date <= @StatDate
  494. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  495. UNION ALL
  496. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  497. 'S1_L1_004' AS MetricCode,
  498. ROUND(AVG(GREATEST(IFNULL(remaining_qty, 0), 0)) / GREATEST(SUM(IFNULL(shipped_qty, 0)), 1) * 30, 4) AS MetricValue
  499. FROM dwd_ship_trans
  500. WHERE calc_batch_id=@BatchId
  501. AND tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  502. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  503. HAVING SUM(IFNULL(shipped_qty, 0)) > 0
  504. UNION ALL
  505. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  506. 'S1_L2_010' AS MetricCode,
  507. ROUND(AVG(TIMESTAMPDIFF(HOUR, order_date, COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date)) / 24), 4) AS MetricValue
  508. FROM mdp_std_so
  509. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  510. AND order_date IS NOT NULL
  511. AND COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date) IS NOT NULL
  512. AND COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date) >= order_date
  513. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  514. UNION ALL
  515. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  516. 'S1_L2_011' AS MetricCode,
  517. ROUND(100 * SUM(CASE WHEN TIMESTAMPDIFF(HOUR, order_date, COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date)) <= 72 THEN 1 ELSE 0 END) / COUNT(1), 4) AS MetricValue
  518. FROM mdp_std_so
  519. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  520. AND order_date IS NOT NULL
  521. AND COALESCE(promised_delivery_date, capacity_date, material_ready_date, plan_delivery_date) IS NOT NULL
  522. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  523. UNION ALL
  524. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  525. 'S1_L2_012' AS MetricCode,
  526. ROUND(COUNT(1) / GREATEST(COUNT(DISTINCT NULLIF(planner_no, '')), 1), 4) AS MetricValue
  527. FROM mdp_std_so
  528. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  529. AND order_date IS NOT NULL AND order_date <= @StatDate
  530. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  531. UNION ALL
  532. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  533. 'S1_L2_013' AS MetricCode,
  534. ROUND(100 * SUM(CASE WHEN planned_ship_qty > 0 THEN 1 ELSE 0 END) / COUNT(1), 4) AS MetricValue
  535. FROM dwd_ship_trans
  536. WHERE calc_batch_id=@BatchId
  537. AND tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  538. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  539. UNION ALL
  540. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  541. 'S1_L2_014' AS MetricCode,
  542. ROUND(100 * SUM(CASE WHEN shipped_qty >= planned_ship_qty AND planned_ship_qty > 0 THEN 1 ELSE 0 END)
  543. / GREATEST(SUM(CASE WHEN planned_ship_qty > 0 THEN 1 ELSE 0 END), 1), 4) AS MetricValue
  544. FROM dwd_ship_trans
  545. WHERE calc_batch_id=@BatchId
  546. AND tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  547. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  548. HAVING SUM(CASE WHEN planned_ship_qty > 0 THEN 1 ELSE 0 END) > 0
  549. UNION ALL
  550. SELECT tenant_id AS TenantId, COALESCE(NULLIF(factory_id, 0), 1) AS FactoryId,
  551. 'S1_L2_015' AS MetricCode,
  552. ROUND(100 * SUM(CASE WHEN linkage_status = 'LINKED' THEN 1 ELSE 0 END)
  553. / GREATEST(SUM(CASE WHEN planned_ship_qty > 0 THEN 1 ELSE 0 END), 1), 4) AS MetricValue
  554. FROM dwd_ship_trans
  555. WHERE calc_batch_id=@BatchId
  556. AND tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  557. GROUP BY tenant_id, COALESCE(NULLIF(factory_id, 0), 1)
  558. HAVING SUM(CASE WHEN planned_ship_qty > 0 THEN 1 ELSE 0 END) > 0
  559. UNION ALL
  560. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  561. 'S1_L2_001' AS MetricCode,
  562. ROUND(AVG(TIMESTAMPDIFF(HOUR, create_time, update_time) / 24), 4) AS MetricValue
  563. FROM (
  564. SELECT tenant_id,
  565. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f') AS create_time,
  566. COALESCE(
  567. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.UpdateTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  568. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f')
  569. ) AS update_time
  570. FROM mdp_stg_so
  571. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  572. AND source_table = 'ado_contract_review'
  573. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') NOT LIKE 'UAT-PROC-%'
  574. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') NOT LIKE 'UAT-DEL-%'
  575. ) t
  576. WHERE create_time IS NOT NULL AND update_time IS NOT NULL AND update_time >= create_time
  577. GROUP BY tenant_id
  578. UNION ALL
  579. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  580. 'S1_L2_002' AS MetricCode,
  581. ROUND(100 * SUM(CASE WHEN TIMESTAMPDIFF(HOUR, create_time, update_time) <= 72 THEN 1 ELSE 0 END) / COUNT(1), 4) AS MetricValue
  582. FROM (
  583. SELECT tenant_id,
  584. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f') AS create_time,
  585. COALESCE(
  586. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.UpdateTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  587. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f')
  588. ) AS update_time
  589. FROM mdp_stg_so
  590. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  591. AND source_table = 'ado_contract_review'
  592. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') NOT LIKE 'UAT-PROC-%'
  593. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') NOT LIKE 'UAT-DEL-%'
  594. ) t
  595. WHERE create_time IS NOT NULL AND update_time IS NOT NULL AND update_time >= create_time
  596. GROUP BY tenant_id
  597. UNION ALL
  598. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  599. 'S1_L2_003' AS MetricCode,
  600. ROUND(COUNT(1) / GREATEST(COUNT(DISTINCT NULLIF(owner_account, '')), 1), 4) AS MetricValue
  601. FROM (
  602. SELECT tenant_id,
  603. COALESCE(
  604. JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ResponsibleAccount')),
  605. JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateUser'))
  606. ) AS owner_account
  607. FROM mdp_stg_so
  608. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  609. AND source_table = 'ado_contract_review'
  610. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') NOT LIKE 'UAT-PROC-%'
  611. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') NOT LIKE 'UAT-DEL-%'
  612. ) t
  613. GROUP BY tenant_id
  614. UNION ALL
  615. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  616. 'S1_L2_007' AS MetricCode,
  617. ROUND(AVG(TIMESTAMPDIFF(HOUR,create_time,update_time)/24),4) AS MetricValue
  618. FROM (
  619. SELECT tenant_id,
  620. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f') AS create_time,
  621. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.UpdateTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f') AS update_time
  622. FROM mdp_stg_so
  623. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id,0),1)=@FactoryId
  624. AND source_table='ado_contract_review'
  625. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') LIKE 'UAT-PROC-%'
  626. ) t
  627. WHERE create_time IS NOT NULL AND update_time>=create_time
  628. GROUP BY tenant_id
  629. UNION ALL
  630. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  631. 'S1_L2_008' AS MetricCode,
  632. ROUND(100*SUM(CASE WHEN TIMESTAMPDIFF(HOUR,create_time,update_time)<=72 THEN 1 ELSE 0 END)
  633. / NULLIF(COUNT(*),0),4) AS MetricValue
  634. FROM (
  635. SELECT tenant_id,
  636. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f') AS create_time,
  637. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.UpdateTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f') AS update_time
  638. FROM mdp_stg_so
  639. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id,0),1)=@FactoryId
  640. AND source_table='ado_contract_review'
  641. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') LIKE 'UAT-PROC-%'
  642. ) t
  643. WHERE create_time IS NOT NULL AND update_time IS NOT NULL
  644. GROUP BY tenant_id
  645. UNION ALL
  646. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  647. 'S1_L2_009' AS MetricCode,
  648. ROUND(COUNT(*)/NULLIF(COUNT(DISTINCT NULLIF(owner_account,'')),0),4) AS MetricValue
  649. FROM (
  650. SELECT tenant_id,
  651. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ResponsibleAccount')),
  652. JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateUser'))) AS owner_account
  653. FROM mdp_stg_so
  654. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id,0),1)=@FactoryId
  655. AND source_table='ado_contract_review'
  656. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.BillNo')),'') LIKE 'UAT-PROC-%'
  657. ) t
  658. GROUP BY tenant_id
  659. UNION ALL
  660. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  661. 'S1_L2_004' AS MetricCode,
  662. ROUND(AVG(cycle_hours / 24), 4) AS MetricValue
  663. FROM (
  664. SELECT tenant_id,
  665. COALESCE(
  666. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DrawingDesignCycle')) REGEXP '^-?[0-9]+$'
  667. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DrawingDesignCycle')) AS DECIMAL(18,6)) END,
  668. TIMESTAMPDIFF(HOUR,
  669. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DrawingActualStart')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  670. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DrawingActualEnd')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f')
  671. )
  672. ) AS cycle_hours
  673. FROM mdp_stg_so
  674. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  675. AND source_table = 'ado_product_design'
  676. ) t
  677. WHERE cycle_hours IS NOT NULL AND cycle_hours >= 0
  678. GROUP BY tenant_id
  679. UNION ALL
  680. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  681. 'S1_L2_005' AS MetricCode,
  682. ROUND(100 * SUM(CASE WHEN actual_end <= plan_end THEN 1 ELSE 0 END) / COUNT(1), 4) AS MetricValue
  683. FROM (
  684. SELECT tenant_id,
  685. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DrawingPlanEnd')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f') AS plan_end,
  686. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DrawingActualEnd')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f') AS actual_end
  687. FROM mdp_stg_so
  688. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  689. AND source_table = 'ado_product_design'
  690. ) t
  691. WHERE plan_end IS NOT NULL AND actual_end IS NOT NULL
  692. GROUP BY tenant_id
  693. UNION ALL
  694. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  695. 'S1_L2_006' AS MetricCode,
  696. ROUND(COUNT(1) / GREATEST(COUNT(DISTINCT NULLIF(design_lead, '')), 1), 4) AS MetricValue
  697. FROM (
  698. SELECT tenant_id,
  699. COALESCE(
  700. JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DesignLeadAccount')),
  701. JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DesignLeadName')),
  702. JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CreateUser'))
  703. ) AS design_lead
  704. FROM mdp_stg_so
  705. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  706. AND source_table = 'ado_product_design'
  707. ) t
  708. GROUP BY tenant_id
  709. UNION ALL
  710. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  711. CASE stage_no
  712. WHEN 1 THEN 'S1_L3_001'
  713. WHEN 2 THEN 'S1_L3_002'
  714. WHEN 3 THEN 'S1_L3_003'
  715. WHEN 4 THEN 'S1_L3_004'
  716. WHEN 5 THEN 'S1_L3_005'
  717. END AS MetricCode,
  718. ROUND(AVG(cycle_hours), 4) AS MetricValue
  719. FROM (
  720. SELECT tenant_id,
  721. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo')) REGEXP '^-?[0-9]+$'
  722. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo')) AS SIGNED) END AS stage_no,
  723. COALESCE(
  724. TIMESTAMPDIFF(HOUR,
  725. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StartTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  726. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CompleteTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f')
  727. ),
  728. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
  729. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) AS DECIMAL(18,6)) * 24 END
  730. ) AS cycle_hours
  731. FROM mdp_stg_so
  732. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  733. AND source_table = 'ado_contract_review_flow'
  734. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-PROC-%'
  735. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-DEL-%'
  736. ) t
  737. WHERE stage_no BETWEEN 1 AND 5
  738. AND cycle_hours IS NOT NULL AND cycle_hours >= 0
  739. GROUP BY tenant_id, stage_no
  740. UNION ALL
  741. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  742. CASE stage_no
  743. WHEN 1 THEN 'S1_L3_101'
  744. WHEN 2 THEN 'S1_L3_102'
  745. WHEN 3 THEN 'S1_L3_103'
  746. WHEN 4 THEN 'S1_L3_104'
  747. WHEN 5 THEN 'S1_L3_105'
  748. END AS MetricCode,
  749. ROUND(AVG(cycle_hours), 4) AS MetricValue
  750. FROM (
  751. SELECT tenant_id,
  752. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo')) REGEXP '^-?[0-9]+$'
  753. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo')) AS SIGNED) END AS stage_no,
  754. COALESCE(
  755. TIMESTAMPDIFF(HOUR,
  756. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StartTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  757. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CompleteTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f')
  758. ),
  759. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
  760. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) AS DECIMAL(18,6)) * 24 END
  761. ) AS cycle_hours
  762. FROM mdp_stg_so
  763. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  764. AND source_table = 'ado_contract_review_flow'
  765. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-PROC-%'
  766. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-DEL-%'
  767. ) t
  768. WHERE stage_no BETWEEN 1 AND 5
  769. AND cycle_hours IS NOT NULL AND cycle_hours >= 0
  770. GROUP BY tenant_id, stage_no
  771. UNION ALL
  772. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  773. metric_code AS MetricCode,
  774. ROUND(AVG(cycle_hours), 4) AS MetricValue
  775. FROM (
  776. SELECT tenant_id,
  777. CASE COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DeptNo')), ''), JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.Department')))
  778. WHEN 'LAW' THEN 'S1_L4_001'
  779. WHEN 'PRE_SALES' THEN 'S1_L4_002'
  780. WHEN 'MPS' THEN 'S1_L4_003'
  781. WHEN 'TEST' THEN 'S1_L4_004'
  782. WHEN '法律事务部' THEN 'S1_L4_001'
  783. WHEN '技术售前组' THEN 'S1_L4_002'
  784. WHEN '综合主计划' THEN 'S1_L4_003'
  785. WHEN '试验站' THEN 'S1_L4_004'
  786. END AS metric_code,
  787. COALESCE(
  788. TIMESTAMPDIFF(HOUR,
  789. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StartTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  790. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CompleteTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f')
  791. ),
  792. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
  793. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) AS DECIMAL(18,6)) * 24 END
  794. ) AS cycle_hours
  795. FROM mdp_stg_so
  796. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  797. AND source_table = 'ado_contract_review_flow'
  798. AND JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo')) = '1'
  799. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-PROC-%'
  800. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-DEL-%'
  801. ) t
  802. WHERE metric_code IS NOT NULL
  803. AND cycle_hours IS NOT NULL AND cycle_hours >= 0
  804. GROUP BY tenant_id, metric_code
  805. UNION ALL
  806. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  807. metric_code AS MetricCode,
  808. ROUND(AVG(cycle_hours), 4) AS MetricValue
  809. FROM (
  810. SELECT tenant_id,
  811. CASE COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DeptNo')), ''), JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.Department')))
  812. WHEN 'LAW' THEN 'S1_L4_101'
  813. WHEN 'PRE_SALES' THEN 'S1_L4_102'
  814. WHEN 'MPS' THEN 'S1_L4_103'
  815. WHEN 'TEST' THEN 'S1_L4_104'
  816. WHEN '法律事务部' THEN 'S1_L4_101'
  817. WHEN '技术售前组' THEN 'S1_L4_102'
  818. WHEN '综合主计划' THEN 'S1_L4_103'
  819. WHEN '试验站' THEN 'S1_L4_104'
  820. END AS metric_code,
  821. COALESCE(
  822. TIMESTAMPDIFF(HOUR,
  823. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StartTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  824. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CompleteTime')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f')
  825. ),
  826. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
  827. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ActualDays')) AS DECIMAL(18,6)) * 24 END
  828. ) AS cycle_hours
  829. FROM mdp_stg_so
  830. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  831. AND source_table = 'ado_contract_review_flow'
  832. AND JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo')) = '1'
  833. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-PROC-%'
  834. AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') NOT LIKE 'UAT-DEL-%'
  835. ) t
  836. WHERE metric_code IS NOT NULL
  837. AND cycle_hours IS NOT NULL AND cycle_hours >= 0
  838. GROUP BY tenant_id, metric_code
  839. UNION ALL
  840. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  841. CONCAT('S1_L3_',branch_code,LPAD(stage_no,2,'0')) AS MetricCode,
  842. ROUND(AVG(cycle_hours),4) AS MetricValue
  843. FROM (
  844. SELECT tenant_id,
  845. CASE
  846. WHEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-PROC-%' THEN '3'
  847. WHEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-DEL-%' THEN '4'
  848. END AS branch_code,
  849. CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo')) AS SIGNED) AS stage_no,
  850. TIMESTAMPDIFF(
  851. HOUR,
  852. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StartTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f'),
  853. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CompleteTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f')
  854. ) AS cycle_hours
  855. FROM mdp_stg_so
  856. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id,0),1)=@FactoryId
  857. AND source_table='ado_contract_review_flow'
  858. AND (COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-PROC-%'
  859. OR COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-DEL-%')
  860. ) t
  861. WHERE branch_code IS NOT NULL AND stage_no BETWEEN 1 AND 5
  862. AND cycle_hours IS NOT NULL AND cycle_hours>=0
  863. GROUP BY tenant_id,branch_code,stage_no
  864. UNION ALL
  865. SELECT tenant_id AS TenantId, 1 AS FactoryId,
  866. CONCAT('S1_L4_',branch_code,dept_code) AS MetricCode,
  867. ROUND(AVG(cycle_hours),4) AS MetricValue
  868. FROM (
  869. SELECT tenant_id,
  870. CASE
  871. WHEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-PROC-%' THEN '3'
  872. WHEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-DEL-%' THEN '4'
  873. END AS branch_code,
  874. CASE COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.DeptNo')),''),
  875. JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.Department')))
  876. WHEN 'LAW' THEN '01' WHEN '法律事务部' THEN '01'
  877. WHEN 'PRE_SALES' THEN '02' WHEN '技术售前组' THEN '02'
  878. WHEN 'MPS' THEN '03' WHEN '综合主计划' THEN '03'
  879. WHEN 'TEST' THEN '04' WHEN '试验站' THEN '04'
  880. END AS dept_code,
  881. TIMESTAMPDIFF(
  882. HOUR,
  883. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StartTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f'),
  884. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.CompleteTime')),'null'),''),'T',' '),'%Y-%m-%d %H:%i:%s.%f')
  885. ) AS cycle_hours
  886. FROM mdp_stg_so
  887. WHERE tenant_id=@TenantId AND COALESCE(NULLIF(factory_id,0),1)=@FactoryId
  888. AND source_table='ado_contract_review_flow'
  889. AND JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.StageNo'))='1'
  890. AND (COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-PROC-%'
  891. OR COALESCE(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.ReviewBillNo')),'') LIKE 'UAT-DEL-%')
  892. ) t
  893. WHERE branch_code IS NOT NULL AND dept_code IS NOT NULL
  894. AND cycle_hours IS NOT NULL AND cycle_hours>=0
  895. GROUP BY tenant_id,branch_code,dept_code
  896. """,
  897. new SugarParameter("@BatchId", batchId),
  898. new SugarParameter("@StatDate", statDate),
  899. new SugarParameter("@TenantId", scope.TenantId),
  900. new SugarParameter("@FactoryId", scope.FactoryId));
  901. }
  902. private async Task<int> UpsertS1KpiValueAsync(S1KpiCalcRow row, DateTime statDate, DateTime now)
  903. {
  904. var meta = await _db.Ado.SqlQuerySingleAsync<S1KpiMetaRow>(
  905. """
  906. SELECT MetricLevel, Direction, YellowThreshold, RedThreshold
  907. FROM ado_smart_ops_kpi_master
  908. WHERE TenantId=@TenantId AND ModuleCode='S1' AND MetricCode=@MetricCode AND IsEnabled=1
  909. LIMIT 1
  910. """,
  911. new SugarParameter("@TenantId", row.TenantId),
  912. new SugarParameter("@MetricCode", row.MetricCode));
  913. if (meta == null || row.MetricValue == null)
  914. return 0;
  915. var table = ResolveKpiValueTable(meta.MetricLevel);
  916. var current = await _db.Ado.SqlQuerySingleAsync<S1KpiValueRow>(
  917. $"""
  918. SELECT id AS Id, metric_value AS MetricValue, target_value AS TargetValue
  919. FROM {table}
  920. WHERE tenant_id=@TenantId AND factory_id=@FactoryId AND module_code='S1'
  921. AND metric_code=@MetricCode AND biz_date=@BizDate AND is_deleted=0
  922. ORDER BY id
  923. LIMIT 1
  924. """,
  925. new SugarParameter("@TenantId", row.TenantId),
  926. new SugarParameter("@FactoryId", row.FactoryId),
  927. new SugarParameter("@MetricCode", row.MetricCode),
  928. new SugarParameter("@BizDate", statDate));
  929. var prior = await _db.Ado.SqlQuerySingleAsync<S1KpiValueRow>(
  930. $"""
  931. SELECT id AS Id, metric_value AS MetricValue, target_value AS TargetValue
  932. FROM {table}
  933. WHERE tenant_id=@TenantId AND factory_id=@FactoryId AND module_code='S1'
  934. AND metric_code=@MetricCode AND biz_date<@BizDate AND is_deleted=0
  935. ORDER BY biz_date DESC, id DESC
  936. LIMIT 1
  937. """,
  938. new SugarParameter("@TenantId", row.TenantId),
  939. new SugarParameter("@FactoryId", row.FactoryId),
  940. new SugarParameter("@MetricCode", row.MetricCode),
  941. new SugarParameter("@BizDate", statDate));
  942. var actual = Math.Round(row.MetricValue.Value, 4);
  943. var snap = await _kpiTargetResolver.ResolveAsync(row.TenantId, row.FactoryId, row.MetricCode, "S1", statDate);
  944. var target = snap.TargetValue;
  945. var status = AidopS4KpiMerge.AchievementLevel(actual, target, meta.Direction, meta.YellowThreshold, meta.RedThreshold);
  946. var trend = ResolveTrendFlag(actual, prior?.MetricValue);
  947. if (current != null)
  948. {
  949. return await MdpSchemaAligner.ExecuteAsync(_db,
  950. $"""
  951. UPDATE {table}
  952. SET {Admin.NET.Plugin.AiDOP.SmartOps.KpiResultWrite.FromMetricSet}metric_value=@MetricValue, target_value=@TargetValue, status_color=@StatusColor, trend_flag=@TrendFlag,
  953. target_config_id=@TargetConfigId, target_source=@TargetSource, target_resolved_at=@TargetResolvedAt,
  954. is_active=1, status='ACTIVE', calc_time=@CalcTime, update_time=@CalcTime
  955. WHERE tenant_id=@TenantId AND factory_id=@FactoryId AND module_code='S1'
  956. AND metric_code=@MetricCode AND biz_date=@BizDate AND is_deleted=0
  957. """,
  958. new SugarParameter("@MetricValue", actual),
  959. new SugarParameter("@TargetValue", KpiTargetSnapshotSql.ValueOrDbNull(snap)),
  960. new SugarParameter("@StatusColor", status),
  961. new SugarParameter("@TrendFlag", trend),
  962. new SugarParameter("@TargetConfigId", KpiTargetSnapshotSql.ConfigIdOrDbNull(snap)),
  963. new SugarParameter("@TargetSource", KpiTargetSnapshotSql.SourceOrDbNull(snap)),
  964. new SugarParameter("@TargetResolvedAt", snap.ResolvedAt),
  965. new SugarParameter("@CalcTime", now),
  966. new SugarParameter("@TenantId", row.TenantId),
  967. new SugarParameter("@FactoryId", row.FactoryId),
  968. new SugarParameter("@MetricCode", row.MetricCode),
  969. new SugarParameter("@BizDate", statDate));
  970. }
  971. var nextId = Yitter.IdGenerator.YitIdHelper.NextId();
  972. return await MdpSchemaAligner.ExecuteAsync(_db,
  973. $"""
  974. INSERT INTO {table}
  975. (id, tenant_id, factory_id, status, biz_date, create_time, update_time, is_deleted, is_active,
  976. module_code, metric_code, result_status, result_reason, metric_value, target_value, status_color, trend_flag, calc_time,
  977. target_config_id, target_source, target_resolved_at)
  978. VALUES
  979. (@Id, @TenantId, @FactoryId, 'ACTIVE', @BizDate, @CalcTime, @CalcTime, 0, 1,
  980. 'S1', @MetricCode, {Admin.NET.Plugin.AiDOP.SmartOps.KpiResultWrite.FromMetricVals}@MetricValue, @TargetValue, @StatusColor, @TrendFlag, @CalcTime,
  981. @TargetConfigId, @TargetSource, @TargetResolvedAt)
  982. """,
  983. new SugarParameter("@Id", nextId),
  984. new SugarParameter("@TenantId", row.TenantId),
  985. new SugarParameter("@FactoryId", row.FactoryId),
  986. new SugarParameter("@BizDate", statDate),
  987. new SugarParameter("@CalcTime", now),
  988. new SugarParameter("@MetricCode", row.MetricCode),
  989. new SugarParameter("@MetricValue", actual),
  990. new SugarParameter("@TargetValue", KpiTargetSnapshotSql.ValueOrDbNull(snap)),
  991. new SugarParameter("@StatusColor", status),
  992. new SugarParameter("@TrendFlag", trend),
  993. new SugarParameter("@TargetConfigId", KpiTargetSnapshotSql.ConfigIdOrDbNull(snap)),
  994. new SugarParameter("@TargetSource", KpiTargetSnapshotSql.SourceOrDbNull(snap)),
  995. new SugarParameter("@TargetResolvedAt", snap.ResolvedAt));
  996. }
  997. private IEnumerable<S1MdpSqlCommand> BuildStandardCommands(S1MdpRunScope scope, string batchId, DateTime now)
  998. {
  999. yield return Cmd(
  1000. """
  1001. INSERT INTO mdp_std_so
  1002. (tenant_id, factory_id, company_id, source_system, order_id, order_entry_id, order_no, order_line, order_type,
  1003. customer_id, customer_no, customer_name, customer_order_no, country, item_code, item_name, item_spec,
  1004. map_number, map_name, bom_number, unit, order_qty, delivered_notice_qty, delivered_qty, price, tax_price,
  1005. amount, total_amount, order_date, customer_request_date, plan_delivery_date, promised_delivery_date,
  1006. capacity_date, material_ready_date, planner_no, planner_name, order_status, review_status, review_stage,
  1007. flow_state, progress, urgent, closed, deleted_flag, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time)
  1008. SELECT
  1009. COALESCE(NULLIF(e.tenant_id, 0), NULLIF(h.tenant_id, 0),
  1010. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1011. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.tenant_id')) AS SIGNED) END),
  1012. COALESCE(NULLIF(NULLIF(e.factory_id, 0), e.tenant_id),
  1013. NULLIF(NULLIF(h.factory_id, 0), h.tenant_id)),
  1014. NULLIF(e.company_id, 0),
  1015. COALESCE(NULLIF(e.source_system,''), (SELECT r.source_system FROM mdp_source_table_registry r WHERE r.source_table = e.source_table LIMIT 1), ''),
  1016. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.Id')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.Id')) AS SIGNED) END,
  1017. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.Id')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.Id')) AS SIGNED) END,
  1018. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.bill_no')), JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.bill_no')), e.source_biz_key),
  1019. CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.entry_seq')), JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.Id'))) AS CHAR),
  1020. CAST(JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.order_type')) AS CHAR),
  1021. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.custom_id')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.custom_id')) AS SIGNED) END,
  1022. JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.custom_no')),
  1023. JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.custom_name')),
  1024. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.custom_order_bill_no')), JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.bill_from'))),
  1025. JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.country')),
  1026. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.item_number')),
  1027. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.item_name')),
  1028. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.specification')),
  1029. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.map_number')),
  1030. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.map_name')),
  1031. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.bom_number')),
  1032. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.unit')),
  1033. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.qty')) AS DECIMAL(18,6)) END,
  1034. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.deliver_notice_count')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.deliver_notice_count')) AS DECIMAL(18,6)) END,
  1035. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.deliver_count')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.deliver_count')) AS DECIMAL(18,6)) END,
  1036. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.price')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.price')) AS DECIMAL(18,6)) END,
  1037. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.tax_price')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.tax_price')) AS DECIMAL(18,6)) END,
  1038. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.amount')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.amount')) AS DECIMAL(18,6)) END,
  1039. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.total_amount')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.total_amount')) AS DECIMAL(18,6)) END,
  1040. NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.date')), 'null'), ''),
  1041. NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.rdate')), 'null'), ''),
  1042. NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.plan_date')), 'null'), ''),
  1043. NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.date')), 'null'), ''),
  1044. NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.sys_capacity_date')), 'null'), ''),
  1045. NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.sys_material_date')), 'null'), ''),
  1046. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.planner_no')),
  1047. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.planner_name')),
  1048. CASE WHEN JSON_EXTRACT(h.raw_data,'$.closed') IN (1, true) THEN 'CLOSED' ELSE 'OPEN' END,
  1049. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.FlowStatus')), JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.flowstate'))),
  1050. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.CurrentDept')), JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.CurrentStage'))),
  1051. JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.flowstate')),
  1052. JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.progress')),
  1053. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(COALESCE(e.raw_data, h.raw_data),'$.urgent')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(e.raw_data, h.raw_data),'$.urgent')) AS SIGNED) END,
  1054. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.closed')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.closed')) AS SIGNED) END,
  1055. COALESCE(CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(COALESCE(e.raw_data, h.raw_data),'$.IsDeleted')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(e.raw_data, h.raw_data),'$.IsDeleted')) AS SIGNED) END, 0),
  1056. e.source_table,
  1057. e.source_row_id,
  1058. e.source_biz_key,
  1059. @BatchId,
  1060. @Now
  1061. FROM mdp_stg_so e
  1062. LEFT JOIN mdp_stg_so h ON h.source_table='crm_seorder'
  1063. AND h.tenant_id = e.tenant_id
  1064. AND h.sync_batch_id = (SELECT lb.sync_batch_id FROM mdp_stg_so lb
  1065. WHERE lb.source_table='crm_seorder' AND lb.tenant_id=@TenantId
  1066. ORDER BY lb.sync_time DESC, lb.id DESC LIMIT 1)
  1067. AND JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.Id')) = JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.seorder_id'))
  1068. LEFT JOIN mdp_stg_so r ON r.source_table='ado_contract_review'
  1069. AND r.tenant_id = e.tenant_id
  1070. AND JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.BillNo')) = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.contract_no')), JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.bill_no')), JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.bill_no')))
  1071. WHERE e.source_table='crm_seorderentry'
  1072. AND e.tenant_id=@TenantId
  1073. AND COALESCE(NULLIF(e.factory_id, 0), 1)=@FactoryId
  1074. -- ── 只取「最新一次 sync_batch_id」,使 mdp_std_so 成为源的真实镜像 ────────────────
  1075. -- 贴源层是纯 upsert、从不淘汰:源侧硬删除的订单行会永远留在 mdp_stg_so 里,
  1076. -- 而本 INSERT 原先不带任何批次条件、每轮重读整张贴源表,于是把这些孤儿一并
  1077. -- 物化进标准层。实测租户 797403760988229:贴源 414 行,源侧实际只剩 28 行,
  1078. -- 其余 386 行中 361 行是源侧已删除的孤儿、25 行是 biz_key 分隔符从 ':' 改成 '#'
  1079. -- 之后被弃用的旧格式重复行(1.0.259 改的 biz_key_expr)。
  1080. --
  1081. -- 这些孤儿不是只影响 S8:mdp_std_so 同时驱动 dwd_ship_trans(每条孤儿凭空
  1082. -- 造出一条早已过期、必然判 DELAYED 的发运行)与 S1/S7/S9 的多项 KPI。
  1083. --
  1084. -- 该等价关系(不在最新批次 = 源侧已删除)成立的前提是该 entity 每轮都全量重读。
  1085. -- 本批已把 S1_SEORDER / S1_SEORDER_ENTRY 的 sync_mode 由 INCR 改为 FULL
  1086. -- (见 UpdateScripts/1.0.521.sql):MdpSyncWindowResolver 对 FULL 实体会强制
  1087. -- ctx.FullRefresh=true,从而在**任何**调用路径上都跳过增量水位(含
  1088. -- RunInboundAsync 与 MdpHotWatchService 这两条 fullRefresh=false 的路径)。
  1089. -- ⚠️ 若哪天把它们改回 INCREMENTAL,本过滤会把「未变更的存量行」误当孤儿丢掉,
  1090. -- 届时必须回到贴源层整体替换的正解,不能只改这里。
  1091. AND e.sync_batch_id = (SELECT lb.sync_batch_id FROM mdp_stg_so lb
  1092. WHERE lb.source_table='crm_seorderentry' AND lb.tenant_id=@TenantId
  1093. ORDER BY lb.sync_time DESC, lb.id DESC LIMIT 1)
  1094. AND IFNULL(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.bill_no')), JSON_UNQUOTE(JSON_EXTRACT(h.raw_data,'$.bill_no'))), '') <> ''
  1095. AND COALESCE(NULLIF(e.tenant_id, 0), NULLIF(h.tenant_id, 0),
  1096. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1097. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(e.raw_data,'$.tenant_id')) AS SIGNED) END) IS NOT NULL
  1098. ON DUPLICATE KEY UPDATE
  1099. factory_id=VALUES(factory_id), customer_no=VALUES(customer_no), customer_name=VALUES(customer_name), item_code=VALUES(item_code),
  1100. item_name=VALUES(item_name), item_spec=VALUES(item_spec), order_qty=VALUES(order_qty),
  1101. delivered_notice_qty=VALUES(delivered_notice_qty), delivered_qty=VALUES(delivered_qty),
  1102. plan_delivery_date=VALUES(plan_delivery_date), promised_delivery_date=VALUES(promised_delivery_date),
  1103. capacity_date=VALUES(capacity_date), material_ready_date=VALUES(material_ready_date),
  1104. order_status=VALUES(order_status), review_status=VALUES(review_status), review_stage=VALUES(review_stage),
  1105. flow_state=VALUES(flow_state), progress=VALUES(progress), closed=VALUES(closed), deleted_flag=VALUES(deleted_flag),
  1106. sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP
  1107. """, scope, batchId, now);
  1108. yield return Cmd(
  1109. """
  1110. INSERT INTO mdp_std_ship_trans
  1111. (tenant_id, factory_id, company_id, source_system, trans_type, plan_id, plan_no, plan_line, order_id, order_entry_id,
  1112. order_no, order_line, customer_no, customer_name, country, item_code, item_name, item_spec, qty, plan_qty,
  1113. weight, volume, order_date, plan_ship_date, shipping_site, shipping_address, consignee, telephone,
  1114. status, confirm_status, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time)
  1115. SELECT
  1116. COALESCE(NULLIF(d.tenant_id, 0),
  1117. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1118. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END),
  1119. NULLIF(d.factory_id, 0),
  1120. NULLIF(d.company_id, 0),
  1121. COALESCE(NULLIF(d.source_system,''), (SELECT r.source_system FROM mdp_source_table_registry r WHERE r.source_table = d.source_table LIMIT 1), ''),
  1122. 'SHIP_PLAN',
  1123. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.plan_id')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.plan_id')) AS SIGNED) END,
  1124. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.LotSerial')), CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.plan_id')) AS CHAR)),
  1125. CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.RecID')) AS CHAR),
  1126. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.seorder_id')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.seorder_id')) AS SIGNED) END,
  1127. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.sentry_id')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.sentry_id')) AS SIGNED) END,
  1128. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.bill_no')), JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.OrdNbr'))),
  1129. CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.sentry_id')) AS CHAR),
  1130. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.CustomNo')),
  1131. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.CustomName')),
  1132. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Country')),
  1133. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ItemNum')),
  1134. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ItemName')),
  1135. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Specification')),
  1136. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Qty')) AS DECIMAL(18,6)) END,
  1137. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Qty')) AS DECIMAL(18,6)) END,
  1138. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Weight')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Weight')) AS DECIMAL(18,6)) END,
  1139. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Volume')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Volume')) AS DECIMAL(18,6)) END,
  1140. NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.OrdDate')), 'null'), ''),
  1141. NULLIF(NULLIF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.ShippingDate')), JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.CreateTime'))), 'null'), ''),
  1142. JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.ShippingSite')),
  1143. JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.ShippingAddress')),
  1144. JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Consignee')),
  1145. JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Telephone')),
  1146. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Status')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Status'))),
  1147. CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.IsConfirm')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.IsConfirm'))) AS CHAR),
  1148. d.source_table,
  1149. d.source_row_id,
  1150. d.source_biz_key,
  1151. @BatchId,
  1152. @Now
  1153. FROM mdp_stg_ship_trans d
  1154. LEFT JOIN mdp_stg_ship_trans m ON m.source_table='ShippingPlan'
  1155. AND m.tenant_id = d.tenant_id
  1156. AND JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.RecID')) = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.plan_id'))
  1157. WHERE d.source_table='ShippingPlanDetail'
  1158. AND d.tenant_id=@TenantId
  1159. AND COALESCE(NULLIF(d.factory_id, 0), 1)=@FactoryId
  1160. AND IFNULL(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.bill_no')), JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.OrdNbr'))), '') <> ''
  1161. AND COALESCE(NULLIF(d.tenant_id, 0),
  1162. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1163. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END) IS NOT NULL
  1164. ON DUPLICATE KEY UPDATE
  1165. factory_id=VALUES(factory_id), plan_no=VALUES(plan_no), order_no=VALUES(order_no), customer_no=VALUES(customer_no), customer_name=VALUES(customer_name),
  1166. item_code=VALUES(item_code), item_name=VALUES(item_name), qty=VALUES(qty), plan_qty=VALUES(plan_qty),
  1167. plan_ship_date=VALUES(plan_ship_date), shipping_site=VALUES(shipping_site), shipping_address=VALUES(shipping_address),
  1168. status=VALUES(status), confirm_status=VALUES(confirm_status), sync_batch_id=VALUES(sync_batch_id),
  1169. sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP
  1170. """, scope, batchId, now);
  1171. yield return Cmd(
  1172. """
  1173. INSERT INTO mdp_std_ship_trans
  1174. (tenant_id, factory_id, source_system, trans_type, shipper_rec_id, shipper_no, shipper_line, order_no, order_line,
  1175. customer_no, item_code, item_name, qty_to_ship, picking_qty, real_qty, gross_weight, net_weight, volume,
  1176. plan_ship_date, actual_ship_date, site, status, confirm_status, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time)
  1177. SELECT
  1178. COALESCE(NULLIF(d.tenant_id, 0),
  1179. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1180. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END),
  1181. NULLIF(d.factory_id, 0),
  1182. COALESCE(NULLIF(d.source_system,''), (SELECT r.source_system FROM mdp_source_table_registry r WHERE r.source_table = d.source_table LIMIT 1), ''),
  1183. 'ASN_SHIPPER',
  1184. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ASNBOLShipperRecID')) REGEXP '^-?[0-9]+$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ASNBOLShipperRecID')) AS SIGNED) END,
  1185. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Id')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Id'))),
  1186. CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Line')) AS CHAR),
  1187. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.OrdNbr')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.OrdNbr'))),
  1188. CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.OrdLine')) AS CHAR),
  1189. JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.SoldTo')),
  1190. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ContainerItem')),
  1191. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Descr')),
  1192. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.QtyToShip')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.QtyToShip')) AS DECIMAL(18,6)) END,
  1193. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.PickingQty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.PickingQty')) AS DECIMAL(18,6)) END,
  1194. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.RealQty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.RealQty')) AS DECIMAL(18,6)) END,
  1195. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.GrossWeight')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.GrossWeight')) AS DECIMAL(18,6)) END,
  1196. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.NetWeight')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.NetWeight')) AS DECIMAL(18,6)) END,
  1197. CASE WHEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Volume')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Volume'))) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Volume')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Volume'))) AS DECIMAL(18,6)) END,
  1198. NULLIF(NULLIF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ShipDate')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.ShipDate'))), 'null'), ''),
  1199. NULLIF(NULLIF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ShipDate')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.ShipDate'))), 'null'), ''),
  1200. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Site')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Site'))),
  1201. COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Status')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Status'))),
  1202. CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.IsConfirm')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.IsConfirm'))) AS CHAR),
  1203. d.source_table,
  1204. d.source_row_id,
  1205. d.source_biz_key,
  1206. @BatchId,
  1207. @Now
  1208. FROM mdp_stg_ship_trans d
  1209. LEFT JOIN mdp_stg_ship_trans m ON m.source_table='ASNBOLShipperMaster'
  1210. AND m.tenant_id = d.tenant_id
  1211. AND JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.RecID')) = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ASNBOLShipperRecID'))
  1212. WHERE d.source_table='ASNBOLShipperDetail'
  1213. AND d.tenant_id=@TenantId
  1214. AND COALESCE(NULLIF(d.factory_id, 0), 1)=@FactoryId
  1215. AND IFNULL(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Id')), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.Id'))), '') <> ''
  1216. AND COALESCE(NULLIF(d.tenant_id, 0),
  1217. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1218. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END) IS NOT NULL
  1219. ON DUPLICATE KEY UPDATE
  1220. factory_id=VALUES(factory_id), shipper_no=VALUES(shipper_no), order_no=VALUES(order_no), order_line=VALUES(order_line),
  1221. customer_no=VALUES(customer_no), item_code=VALUES(item_code), item_name=VALUES(item_name),
  1222. qty_to_ship=VALUES(qty_to_ship), picking_qty=VALUES(picking_qty), real_qty=VALUES(real_qty),
  1223. actual_ship_date=VALUES(actual_ship_date), site=VALUES(site), status=VALUES(status),
  1224. confirm_status=VALUES(confirm_status), sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time),
  1225. update_time=CURRENT_TIMESTAMP
  1226. """, scope, batchId, now);
  1227. yield return Cmd(
  1228. """
  1229. INSERT INTO mdp_std_ship_trans
  1230. (tenant_id, factory_id, source_system, trans_type, order_no, customer_no, item_code, item_name, qty,
  1231. plan_ship_date, status, linkage_status, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time)
  1232. SELECT
  1233. COALESCE(NULLIF(d.tenant_id, 0),
  1234. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1235. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END),
  1236. NULLIF(d.factory_id, 0),
  1237. COALESCE(NULLIF(d.source_system,''), (SELECT r.source_system FROM mdp_source_table_registry r WHERE r.source_table = d.source_table LIMIT 1), ''),
  1238. 'LINKAGE_PLAN',
  1239. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.bill_no')),
  1240. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.custom_no')),
  1241. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.item_number')),
  1242. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Descr')),
  1243. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.qty')) AS DECIMAL(18,6)) END,
  1244. NULLIF(NULLIF(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.sys_capacity_date')), JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.fystarttime'))), 'null'), ''),
  1245. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.type')),
  1246. CASE
  1247. WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.isuse')) = '1' THEN 'LINKED'
  1248. ELSE 'INACTIVE'
  1249. END,
  1250. d.source_table,
  1251. d.source_row_id,
  1252. d.source_biz_key,
  1253. @BatchId,
  1254. @Now
  1255. FROM mdp_stg_ship_trans d
  1256. WHERE d.source_table='LinkagePlan'
  1257. AND d.tenant_id=@TenantId
  1258. AND COALESCE(NULLIF(d.factory_id, 0), 1)=@FactoryId
  1259. AND IFNULL(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.bill_no')), '') <> ''
  1260. AND COALESCE(NULLIF(d.tenant_id, 0),
  1261. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1262. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END) IS NOT NULL
  1263. ON DUPLICATE KEY UPDATE
  1264. factory_id=VALUES(factory_id), order_no=VALUES(order_no), customer_no=VALUES(customer_no), item_code=VALUES(item_code),
  1265. item_name=VALUES(item_name), qty=VALUES(qty), plan_ship_date=VALUES(plan_ship_date),
  1266. status=VALUES(status), linkage_status=VALUES(linkage_status), sync_batch_id=VALUES(sync_batch_id),
  1267. sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP
  1268. """, scope, batchId, now);
  1269. }
  1270. private IEnumerable<S1MdpSqlCommand> BuildDwdCommands(S1MdpRunScope scope, string batchId, DateTime now)
  1271. {
  1272. yield return Cmd(
  1273. """
  1274. INSERT INTO dwd_requirement_examine_detail
  1275. (tenant_id, factory_id, stat_date, row_id, parent_row_id, examine_id, order_entry_id, bill_no, morder_no,
  1276. num, item_number, item_name, bom_number, model, kitting_time, item_type, erp_cls_name, qty, wastage,
  1277. need_count, sqty, use_qty, self_lack_qty, lack_qty, mo_qty, make_qty, purchase_qty, purchase_occupy_qty,
  1278. satisfy_time, have_ic_subs, substitute_code, create_time, source_system, sync_batch_id, calc_batch_id, calc_time,
  1279. bom_level, material_role, is_current_flag)
  1280. SELECT
  1281. COALESCE(NULLIF(d.tenant_id, 0), NULLIF(r.tenant_id, 0), NULLIF(so.tenant_id, 0),
  1282. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1283. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END),
  1284. COALESCE(NULLIF(d.factory_id, 0), NULLIF(r.factory_id, 0), NULLIF(so.factory_id, 0)),
  1285. @StatDate,
  1286. CAST(d.source_row_id AS SIGNED),
  1287. CASE
  1288. WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.num')) = '1' THEN NULL
  1289. ELSE p.parent_row_id
  1290. END,
  1291. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.examine_id')) REGEXP '^-?[0-9]+$'
  1292. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.examine_id')) AS SIGNED) END,
  1293. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.sentry_id')) REGEXP '^-?[0-9]+$'
  1294. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.sentry_id')) AS SIGNED) END,
  1295. COALESCE(so.order_no, JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.bill_no'))),
  1296. JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.morder_no')),
  1297. CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.num')) AS CHAR),
  1298. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.item_number')),
  1299. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.item_name')),
  1300. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.bom_number')),
  1301. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.model')),
  1302. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.kitting_time')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  1303. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.type')) = '1' THEN '替代件' ELSE '标准件' END,
  1304. COALESCE(
  1305. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.erp_cls_name')),
  1306. CASE JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.erp_cls'))
  1307. WHEN '0' THEN '配置类'
  1308. WHEN '1' THEN '自制'
  1309. WHEN '2' THEN '委外加工'
  1310. WHEN '3' THEN '外购'
  1311. WHEN '4' THEN '虚拟件'
  1312. END
  1313. ),
  1314. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.qty')) AS DECIMAL(18,4)) END,
  1315. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.wastage')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.wastage')) AS DECIMAL(18,4)) END,
  1316. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.needCount')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.needCount')) AS DECIMAL(18,4)) END,
  1317. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.sqty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.sqty')) AS DECIMAL(18,4)) END,
  1318. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.use_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.use_qty')) AS DECIMAL(18,4)) END,
  1319. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.self_lack_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.self_lack_qty')) AS DECIMAL(18,4)) END,
  1320. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.lack_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.lack_qty')) AS DECIMAL(18,4)) END,
  1321. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.mo_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.mo_qty')) AS DECIMAL(18,4)) END,
  1322. CASE
  1323. WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.make_qty')) = '0' AND JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.erp_cls')) = '1'
  1324. THEN CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.lack_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.lack_qty')) AS DECIMAL(18,4)) END
  1325. WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.make_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$'
  1326. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.make_qty')) AS DECIMAL(18,4))
  1327. END,
  1328. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.purchase_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.purchase_qty')) AS DECIMAL(18,4)) END,
  1329. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.purchase_occupy_qty')) REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.purchase_occupy_qty')) AS DECIMAL(18,4)) END,
  1330. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.satisfy_time')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  1331. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.haveicsubs')) = '1' THEN '是' ELSE '否' END,
  1332. JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.substitute_code')),
  1333. STR_TO_DATE(REPLACE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.create_time')), 'null'), ''), 'T', ' '), '%Y-%m-%d %H:%i:%s.%f'),
  1334. COALESCE(NULLIF(d.source_system,''), (SELECT r.source_system FROM mdp_source_table_registry r WHERE r.source_table = d.source_table LIMIT 1), ''),
  1335. d.sync_batch_id,
  1336. @BatchId,
  1337. @Now,
  1338. -- bom_level:仅当 level 是纯数字时才物化,脏值留 NULL(不猜层级)。
  1339. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.level')) REGEXP '^[0-9]+$'
  1340. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.level')) AS SIGNED) END,
  1341. -- material_role:把「层级」翻译成「角色」,让下游(含 S8)永远不必知道 level 的存在。
  1342. -- 失败方向必须保守:只有 level 精确等于 '1' 才判成品根;NULL / 脏值 / 空串一律落 INPUT_MATERIAL,
  1343. -- 绝不产生 NULL、绝不把行丢掉——漏判一个投入料会漏报缺料,误判一个成品根只会少算一行。
  1344. -- (SQL 三值逻辑:level 为 NULL 时 `= '1'` 得 NULL,CASE 自然走 ELSE,符合上述保守方向。)
  1345. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.level')) = '1'
  1346. THEN 'FINISHED_GOOD_ROOT' ELSE 'INPUT_MATERIAL' END,
  1347. -- is_current_flag 一律先写 0,由本阶段最后一步的原子发布语句统一翻牌(见 PublishCurrentSnapshotAsync)。
  1348. 0
  1349. FROM mdp_stg_so d
  1350. INNER JOIN mdp_stg_so r ON r.source_table='b_examine_result'
  1351. AND r.tenant_id = d.tenant_id
  1352. AND r.source_row_id = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.examine_id'))
  1353. LEFT JOIN mdp_std_so so ON so.tenant_id = COALESCE(r.tenant_id, d.tenant_id)
  1354. AND COALESCE(NULLIF(so.factory_id, 0), 1)=@FactoryId
  1355. AND so.order_entry_id = CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.sentry_id')) REGEXP '^-?[0-9]+$'
  1356. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.sentry_id')) AS SIGNED) END
  1357. LEFT JOIN (
  1358. SELECT examine_id, MIN(source_row_id) AS parent_row_id
  1359. FROM (
  1360. SELECT JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.examine_id')) AS examine_id, CAST(source_row_id AS SIGNED) AS source_row_id
  1361. FROM mdp_stg_so
  1362. WHERE source_table='b_bom_child_examine'
  1363. AND tenant_id=@TenantId
  1364. AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  1365. AND JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.num')) = '1'
  1366. AND JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.is_use')) IN ('1', 'true', 'True', 'base64:type16:AQ==')
  1367. AND sync_batch_id = (SELECT lb.sync_batch_id FROM mdp_stg_so lb
  1368. WHERE lb.source_table='b_bom_child_examine' AND lb.tenant_id=@TenantId
  1369. ORDER BY lb.sync_time DESC, lb.id DESC LIMIT 1)
  1370. ) x
  1371. GROUP BY examine_id
  1372. ) p ON p.examine_id = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.examine_id'))
  1373. WHERE d.source_table='b_bom_child_examine'
  1374. AND d.tenant_id=@TenantId
  1375. AND COALESCE(NULLIF(d.factory_id, 0), 1)=@FactoryId
  1376. AND JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.is_use')) IN ('1', 'true', 'True', 'base64:type16:AQ==')
  1377. -- ── B5 表头软删过滤 ────────────────────────────────────────────────────────────
  1378. -- b_examine_result 是软删表:核验单被作废后仍留在源里,只把 IsDeleted 置真。
  1379. -- 之前这里完全没有版本过滤,等于把已作废核验单的缺料当成现行缺料输出。
  1380. -- 实测(aidopdev,只读):贴源 5969 行表头里 5851 行 IsDeleted 为真,仅 118 行存活
  1381. -- ——即旧 DWD 有约 98% 建立在已作废表头上。
  1382. -- bit(1) 列经贴源后有两种编码并存('1'/'0' 与 base64:type16:AQ==/AA==),两种都必须认。
  1383. -- 这里用「存活白名单」而不是「已删黑名单」,与上面 is_use 的写法保持同一方向;
  1384. -- 实测该列在贴源层恰好只有这 4 种取值、无 NULL、无其它编码,故白名单当前不会误杀任何行。
  1385. AND JSON_UNQUOTE(JSON_EXTRACT(r.raw_data,'$.IsDeleted')) IN ('0', 'false', 'False', 'base64:type16:AA==')
  1386. -- ── B6 FULL=镜像 的 DWD 侧权宜实现(STOPGAP)────────────────────────────────────
  1387. -- 正解应当是贴源层按 (tenant_id, factory_id, source_table) 整体替换,但贴源写入由
  1388. -- DataPlatform/Executors/MdpStagingWriter.cs + MdpDbPullExecutor.cs 通用实现,
  1389. -- 且 mdp_stg_so 的唯一键 uk_source_key=(source_system, source_table, source_biz_key)
  1390. -- 连 tenant_id 都不含 —— 改成整体替换必须同时改那两个文件和该唯一键,均不在本次可改文件范围内。
  1391. -- 故在 DWD 侧兜底:只取每个 (tenant_id, source_table) 的「最新一次 sync_batch_id」。
  1392. -- 该等价关系成立的前提是 sync_mode='FULL'(实测两个 entity 均为 FULL):
  1393. -- FULL 每轮重读整张源表,仍存在的行其 sync_batch_id 必被刷新到最新批次;
  1394. -- 留着旧 sync_batch_id 的行 = 上一轮全量里已经查不到 = 源侧已删除的孤儿。
  1395. -- 实测孤儿量很大:贴源 b_bom_child_examine 共 32105 行,最新批次只有 15272 行。
  1396. -- ⚠️ 若哪天该 entity 被改成 INCREMENTAL,本过滤会错杀未变更的存量行,届时必须回到贴源层正解。
  1397. AND d.sync_batch_id = (SELECT lb.sync_batch_id FROM mdp_stg_so lb
  1398. WHERE lb.source_table='b_bom_child_examine' AND lb.tenant_id=@TenantId
  1399. ORDER BY lb.sync_time DESC, lb.id DESC LIMIT 1)
  1400. AND r.sync_batch_id = (SELECT lb.sync_batch_id FROM mdp_stg_so lb
  1401. WHERE lb.source_table='b_examine_result' AND lb.tenant_id=@TenantId
  1402. ORDER BY lb.sync_time DESC, lb.id DESC LIMIT 1)
  1403. AND COALESCE(NULLIF(d.tenant_id, 0), NULLIF(r.tenant_id, 0), NULLIF(so.tenant_id, 0),
  1404. CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) REGEXP '^[0-9]+$'
  1405. THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.tenant_id')) AS SIGNED) END) IS NOT NULL
  1406. ON DUPLICATE KEY UPDATE
  1407. parent_row_id=VALUES(parent_row_id), order_entry_id=VALUES(order_entry_id), bill_no=VALUES(bill_no),
  1408. morder_no=VALUES(morder_no), num=VALUES(num), item_number=VALUES(item_number), item_name=VALUES(item_name),
  1409. bom_number=VALUES(bom_number), model=VALUES(model), kitting_time=VALUES(kitting_time),
  1410. item_type=VALUES(item_type), erp_cls_name=VALUES(erp_cls_name), qty=VALUES(qty), wastage=VALUES(wastage),
  1411. need_count=VALUES(need_count), sqty=VALUES(sqty), use_qty=VALUES(use_qty), self_lack_qty=VALUES(self_lack_qty),
  1412. lack_qty=VALUES(lack_qty), mo_qty=VALUES(mo_qty), make_qty=VALUES(make_qty), purchase_qty=VALUES(purchase_qty),
  1413. purchase_occupy_qty=VALUES(purchase_occupy_qty), satisfy_time=VALUES(satisfy_time), have_ic_subs=VALUES(have_ic_subs),
  1414. substitute_code=VALUES(substitute_code), create_time=VALUES(create_time), sync_batch_id=VALUES(sync_batch_id),
  1415. calc_time=VALUES(calc_time), update_time=CURRENT_TIMESTAMP,
  1416. bom_level=VALUES(bom_level), material_role=VALUES(material_role)
  1417. -- 故意不写 is_current_flag:该列的唯一写者是本阶段最后一步的原子发布语句。
  1418. -- 若在这里也回写,同批次重跑会把已发布状态打回 0,制造瞬时「无当前快照」窗口。
  1419. """, scope, batchId, now);
  1420. yield return Cmd(
  1421. """
  1422. INSERT INTO dwd_ship_trans
  1423. (tenant_id, factory_id, company_id, stat_date, order_id, order_entry_id, order_no, order_line, customer_no,
  1424. customer_name, country, item_code, item_name, item_spec, order_qty, planned_ship_qty, shipped_qty,
  1425. remaining_qty, order_date, customer_request_date, plan_delivery_date, promised_delivery_date,
  1426. plan_ship_date, actual_ship_date, review_status, order_status, delivery_status, linkage_status, risk_level,
  1427. source_system, source_table, source_row_id, source_biz_key, sync_batch_id, calc_batch_id, calc_time)
  1428. SELECT
  1429. so.tenant_id,
  1430. so.factory_id,
  1431. so.company_id,
  1432. @StatDate,
  1433. so.order_id,
  1434. so.order_entry_id,
  1435. so.order_no,
  1436. IFNULL(so.order_line, ''),
  1437. so.customer_no,
  1438. so.customer_name,
  1439. so.country,
  1440. IFNULL(so.item_code, ''),
  1441. so.item_name,
  1442. so.item_spec,
  1443. IFNULL(so.order_qty, 0),
  1444. IFNULL(p.plan_qty, 0),
  1445. IFNULL(a.real_qty, 0),
  1446. GREATEST(IFNULL(so.order_qty, 0) - IFNULL(a.real_qty, 0), 0),
  1447. so.order_date,
  1448. so.customer_request_date,
  1449. so.plan_delivery_date,
  1450. so.promised_delivery_date,
  1451. p.plan_ship_date,
  1452. a.actual_ship_date,
  1453. so.review_status,
  1454. so.order_status,
  1455. CASE
  1456. WHEN IFNULL(so.order_qty, 0) > 0 AND IFNULL(a.real_qty, 0) >= IFNULL(so.order_qty, 0) THEN 'COMPLETED'
  1457. WHEN COALESCE(p.plan_ship_date, so.promised_delivery_date, so.plan_delivery_date) < @Now THEN 'DELAYED'
  1458. WHEN IFNULL(p.plan_qty, 0) > 0 THEN 'PLANNED'
  1459. ELSE 'OPEN'
  1460. END,
  1461. l.linkage_status,
  1462. CASE
  1463. WHEN COALESCE(p.plan_ship_date, so.promised_delivery_date, so.plan_delivery_date) < @Now
  1464. AND IFNULL(a.real_qty, 0) < IFNULL(so.order_qty, 0) THEN 'HIGH'
  1465. WHEN IFNULL(a.real_qty, 0) < IFNULL(so.order_qty, 0) THEN 'MEDIUM'
  1466. ELSE 'LOW'
  1467. END,
  1468. COALESCE(NULLIF(so.source_system,''), (SELECT r.source_system FROM mdp_source_table_registry r WHERE r.source_table = so.source_table LIMIT 1), ''),
  1469. so.source_table,
  1470. so.source_row_id,
  1471. so.source_biz_key,
  1472. so.sync_batch_id,
  1473. @BatchId,
  1474. @Now
  1475. FROM mdp_std_so so
  1476. LEFT JOIN (
  1477. SELECT tenant_id, order_no, order_entry_id, IFNULL(order_line, '') AS order_line, IFNULL(item_code, '') AS item_code,
  1478. SUM(IFNULL(plan_qty, IFNULL(qty, 0))) AS plan_qty,
  1479. MIN(plan_ship_date) AS plan_ship_date
  1480. FROM mdp_std_ship_trans
  1481. WHERE trans_type='SHIP_PLAN'
  1482. AND tenant_id=@TenantId
  1483. AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  1484. GROUP BY tenant_id, order_no, order_entry_id, IFNULL(order_line, ''), IFNULL(item_code, '')
  1485. ) p ON so.tenant_id=p.tenant_id
  1486. AND so.order_no=p.order_no
  1487. AND IFNULL(so.item_code, '')=p.item_code
  1488. AND (
  1489. (p.order_entry_id IS NOT NULL AND so.order_entry_id=p.order_entry_id)
  1490. OR (p.order_entry_id IS NULL AND IFNULL(so.order_line, '')=p.order_line)
  1491. )
  1492. LEFT JOIN (
  1493. SELECT tenant_id, order_no, IFNULL(order_line, '') AS order_line, IFNULL(item_code, '') AS item_code,
  1494. SUM(IFNULL(real_qty, IFNULL(qty_to_ship, 0))) AS real_qty,
  1495. MAX(actual_ship_date) AS actual_ship_date
  1496. FROM mdp_std_ship_trans
  1497. WHERE trans_type='ASN_SHIPPER'
  1498. AND tenant_id=@TenantId
  1499. AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  1500. GROUP BY tenant_id, order_no, IFNULL(order_line, ''), IFNULL(item_code, '')
  1501. ) a ON so.tenant_id=a.tenant_id AND so.order_no=a.order_no AND IFNULL(so.order_line, '')=a.order_line AND IFNULL(so.item_code, '')=a.item_code
  1502. LEFT JOIN (
  1503. SELECT tenant_id, order_no, item_code, MAX(linkage_status) AS linkage_status
  1504. FROM mdp_std_ship_trans
  1505. WHERE IFNULL(linkage_status, '') <> ''
  1506. AND tenant_id=@TenantId
  1507. AND COALESCE(NULLIF(factory_id, 0), 1)=@FactoryId
  1508. GROUP BY tenant_id, order_no, item_code
  1509. ) l ON so.tenant_id=l.tenant_id AND so.order_no=l.order_no AND IFNULL(so.item_code, '')=IFNULL(l.item_code, '')
  1510. WHERE so.tenant_id=@TenantId
  1511. AND COALESCE(NULLIF(so.factory_id, 0), 1)=@FactoryId
  1512. AND IFNULL(so.order_no, '') <> ''
  1513. ON DUPLICATE KEY UPDATE
  1514. factory_id=VALUES(factory_id), customer_no=VALUES(customer_no), customer_name=VALUES(customer_name), item_name=VALUES(item_name),
  1515. order_qty=VALUES(order_qty), planned_ship_qty=VALUES(planned_ship_qty), shipped_qty=VALUES(shipped_qty),
  1516. remaining_qty=VALUES(remaining_qty), plan_ship_date=VALUES(plan_ship_date), actual_ship_date=VALUES(actual_ship_date),
  1517. review_status=VALUES(review_status), order_status=VALUES(order_status), delivery_status=VALUES(delivery_status),
  1518. linkage_status=VALUES(linkage_status), risk_level=VALUES(risk_level), sync_batch_id=VALUES(sync_batch_id),
  1519. calc_batch_id=VALUES(calc_batch_id), calc_time=VALUES(calc_time), update_time=CURRENT_TIMESTAMP
  1520. """, scope, batchId, now);
  1521. }
  1522. private async Task<long> InsertSyncLogAsync(long tenantId, long entityId, string entityName, string batchId, int rowsRead)
  1523. {
  1524. await MdpSchemaAligner.ExecuteAsync(_db,
  1525. """
  1526. INSERT INTO mdp_sync_log
  1527. (tenant_id, entity_id, source_code, entity_name, sync_batch_id, sync_type, trigger_type, sync_start, rows_read, status)
  1528. VALUES (@TenantId, @EntityId, 'AIDOPDEV_MYSQL', @EntityName, @BatchId, 'FULL', 'AUTO', NOW(), @RowsRead, 'RUNNING')
  1529. """,
  1530. new SugarParameter("@TenantId", tenantId),
  1531. new SugarParameter("@EntityId", entityId),
  1532. new SugarParameter("@EntityName", entityName),
  1533. new SugarParameter("@BatchId", batchId),
  1534. new SugarParameter("@RowsRead", rowsRead));
  1535. return await _db.Ado.GetLongAsync(
  1536. "SELECT id FROM mdp_sync_log WHERE sync_batch_id=@BatchId AND entity_id=@EntityId ORDER BY id DESC LIMIT 1",
  1537. new List<SugarParameter>
  1538. {
  1539. new("@BatchId", batchId),
  1540. new("@EntityId", entityId)
  1541. });
  1542. }
  1543. private async Task MarkSyncLogSuccessAsync(long logId, DateTime started, int affected)
  1544. {
  1545. await MdpSchemaAligner.ExecuteAsync(_db,
  1546. """
  1547. UPDATE mdp_sync_log
  1548. SET sync_end=NOW(), duration_ms=@DurationMs, rows_insert=@RowsInsert, rows_update=0, rows_skip=0, rows_error=0, status='SUCCESS'
  1549. WHERE id=@Id
  1550. """,
  1551. new SugarParameter("@DurationMs", (int)(DateTime.Now - started).TotalMilliseconds),
  1552. new SugarParameter("@RowsInsert", affected),
  1553. new SugarParameter("@Id", logId));
  1554. }
  1555. private async Task MarkSyncLogFailedAsync(long logId, DateTime started, string message)
  1556. {
  1557. try
  1558. {
  1559. await MdpSchemaAligner.ExecuteAsync(_db,
  1560. """
  1561. UPDATE mdp_sync_log
  1562. SET sync_end=NOW(), duration_ms=@DurationMs, rows_error=1, status='FAILED', error_msg=@ErrorMsg
  1563. WHERE id=@Id
  1564. """,
  1565. new SugarParameter("@DurationMs", (int)(DateTime.Now - started).TotalMilliseconds),
  1566. new SugarParameter("@ErrorMsg", Truncate(message, 1000)),
  1567. new SugarParameter("@Id", logId));
  1568. }
  1569. catch (Exception ex)
  1570. {
  1571. // 写库自身失败兜底:避免再抛掩盖原异常;遗留 RUNNING 行可由运维手动清理
  1572. Console.Error.WriteLine($"[S1MdpSyncTransform] MarkSyncLogFailed write failed (syncLogId={logId}): {ex.Message}");
  1573. }
  1574. }
  1575. private async Task<long> InsertTransformRunLogAsync(S1MdpRunScope scope, string batchId, DateTime startedAt, string triggerType)
  1576. {
  1577. await MdpSchemaAligner.ExecuteAsync(_db,
  1578. """
  1579. INSERT INTO mdp_transform_run_log
  1580. (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time)
  1581. VALUES (@TenantId, @JobCode, 'S1 MDP同步与标准化转换', @TriggerType, @BatchId, 'RUNNING', @StartTime)
  1582. """,
  1583. new SugarParameter("@TenantId", scope.TenantId),
  1584. new SugarParameter("@JobCode", JobCode),
  1585. new SugarParameter("@TriggerType", NormalizeTriggerType(triggerType)),
  1586. new SugarParameter("@BatchId", batchId),
  1587. new SugarParameter("@StartTime", startedAt));
  1588. return await _db.Ado.GetLongAsync(
  1589. "SELECT id FROM mdp_transform_run_log WHERE batch_id=@BatchId ORDER BY id DESC LIMIT 1",
  1590. new List<SugarParameter> { new("@BatchId", batchId) });
  1591. }
  1592. private async Task MarkTransformRunSuccessAsync(long runLogId, DateTime startedAt, S1MdpSyncTransformResult result)
  1593. {
  1594. var finishedAt = DateTime.Now;
  1595. await MdpSchemaAligner.ExecuteAsync(_db,
  1596. """
  1597. UPDATE mdp_transform_run_log
  1598. SET status='SUCCESS', end_time=@EndTime, duration_ms=@DurationMs,
  1599. stage_rows=@StageRows, standard_rows=@StandardRows, dwd_rows=@DwdRows,
  1600. summary_json=@SummaryJson, update_time=CURRENT_TIMESTAMP
  1601. WHERE id=@Id
  1602. """,
  1603. new SugarParameter("@EndTime", finishedAt),
  1604. new SugarParameter("@DurationMs", ToDurationMs(finishedAt - startedAt)),
  1605. new SugarParameter("@StageRows", result.StageRows),
  1606. new SugarParameter("@StandardRows", result.StandardRows),
  1607. new SugarParameter("@DwdRows", result.DwdRows),
  1608. new SugarParameter("@SummaryJson", BuildRunSummaryJson(result)),
  1609. new SugarParameter("@Id", runLogId));
  1610. }
  1611. private async Task MarkTransformRunFailedAsync(long runLogId, DateTime startedAt, string message)
  1612. {
  1613. try
  1614. {
  1615. var finishedAt = DateTime.Now;
  1616. var db = _db.CopyNew();
  1617. await db.Ado.ExecuteCommandAsync(
  1618. """
  1619. UPDATE mdp_transform_run_log
  1620. SET status='FAILED', end_time=@EndTime, duration_ms=@DurationMs,
  1621. error_message=@ErrorMessage, update_time=CURRENT_TIMESTAMP
  1622. WHERE id=@Id
  1623. """,
  1624. new SugarParameter("@EndTime", finishedAt),
  1625. new SugarParameter("@DurationMs", ToDurationMs(finishedAt - startedAt)),
  1626. new SugarParameter("@ErrorMessage", Truncate(message, 2000)),
  1627. new SugarParameter("@Id", runLogId));
  1628. }
  1629. catch (Exception ex)
  1630. {
  1631. // 写库自身失败兜底(典型场景:远端 MySQL 瞬断导致 MarkFailed 自身也连不上):
  1632. // 避免再抛二次异常掩盖原错;遗留 RUNNING 行可由运维手动清理。
  1633. Console.Error.WriteLine($"[S1MdpSyncTransform] MarkTransformRunFailed write failed (runLogId={runLogId}): {ex.Message}");
  1634. }
  1635. }
  1636. private static S1MdpSqlCommand Cmd(string sql, S1MdpRunScope scope, string batchId, DateTime now)
  1637. {
  1638. return new S1MdpSqlCommand(sql, new[]
  1639. {
  1640. new SugarParameter("@BatchId", batchId),
  1641. new SugarParameter("@Now", now),
  1642. new SugarParameter("@StatDate", now.Date),
  1643. new SugarParameter("@TenantId", scope.TenantId),
  1644. new SugarParameter("@FactoryId", scope.FactoryId)
  1645. });
  1646. }
  1647. private static string BuildJsonObjectExpression(IEnumerable<string> columns)
  1648. {
  1649. var parts = columns.SelectMany(c => new[] { $"'{c.Replace("'", "''")}'", $"s.`{c}`" });
  1650. return $"JSON_OBJECT({string.Join(",", parts)})";
  1651. }
  1652. private static string BuildOptionalColumnExpr(IReadOnlyCollection<string> columns, string expected, string fallback)
  1653. {
  1654. return columns.Any(u => string.Equals(u, expected, StringComparison.OrdinalIgnoreCase))
  1655. ? $"s.`{FindColumn(columns, expected)}`"
  1656. : fallback;
  1657. }
  1658. private static string FindColumn(IEnumerable<string> columns, string expected)
  1659. {
  1660. return columns.First(u => string.Equals(u, expected, StringComparison.OrdinalIgnoreCase));
  1661. }
  1662. private static async Task KeepLeaseAliveAsync(IS1MdpFullRunLease lease, CancellationToken ct)
  1663. {
  1664. while (!ct.IsCancellationRequested)
  1665. {
  1666. try
  1667. {
  1668. await Task.Delay(TimeSpan.FromSeconds(30), ct);
  1669. await lease.HeartbeatAsync(CancellationToken.None);
  1670. }
  1671. catch (OperationCanceledException)
  1672. {
  1673. return;
  1674. }
  1675. }
  1676. }
  1677. private static int ToDurationMs(TimeSpan elapsed)
  1678. {
  1679. var ms = elapsed.TotalMilliseconds;
  1680. if (double.IsNaN(ms) || ms <= 0) return 0;
  1681. return ms >= int.MaxValue ? int.MaxValue : (int)ms;
  1682. }
  1683. private static string NormalizeTriggerType(string? triggerType)
  1684. {
  1685. return string.IsNullOrWhiteSpace(triggerType) ? "AUTO" : triggerType.Trim().ToUpperInvariant();
  1686. }
  1687. /// <summary>
  1688. /// 运行摘要。改用 JsonSerializer 而非字符串插值:batchId 今天不可能含引号,
  1689. /// 但拼 JSON 是会被后来者继承的脆弱写法,且本次要加的 publish 是嵌套对象。
  1690. /// </summary>
  1691. private static string BuildRunSummaryJson(S1MdpSyncTransformResult result)
  1692. {
  1693. return System.Text.Json.JsonSerializer.Serialize(new
  1694. {
  1695. batchId = result.BatchId,
  1696. stageRows = result.StageRows,
  1697. standardRows = result.StandardRows,
  1698. dwdRows = result.DwdRows,
  1699. kpiRows = result.KpiRows,
  1700. publish = result.Publication is null
  1701. ? null
  1702. : new
  1703. {
  1704. table = result.Publication.Table,
  1705. batchId = result.Publication.BatchId,
  1706. factoryId = result.Publication.FactoryId,
  1707. currentRows = result.Publication.CurrentRows
  1708. }
  1709. });
  1710. }
  1711. private static string ResolveKpiValueTable(int metricLevel)
  1712. {
  1713. return metricLevel switch
  1714. {
  1715. 1 => "ado_s9_kpi_value_l1_day",
  1716. 2 => "ado_s9_kpi_value_l2_day",
  1717. 3 => "ado_s9_kpi_value_l3_day",
  1718. 4 => "ado_s9_kpi_value_l4_day",
  1719. _ => "ado_s9_kpi_value_l2_day"
  1720. };
  1721. }
  1722. private static decimal DefaultS1Target(string metricCode) => LegacyKpiCodeTargets.GetOrZero(metricCode);
  1723. private static string ResolveKpiStatus(decimal actual, decimal target, string? direction, decimal? yellowThreshold, decimal? redThreshold)
  1724. {
  1725. if (target <= 0) return "gray";
  1726. var ratio = actual / target * 100m;
  1727. if (string.Equals(direction, "lower_is_better", StringComparison.OrdinalIgnoreCase))
  1728. {
  1729. if (actual <= target) return "green";
  1730. if (ratio <= (yellowThreshold ?? 110m)) return "yellow";
  1731. return ratio >= (redThreshold ?? 120m) ? "red" : "yellow";
  1732. }
  1733. if (actual >= target) return "green";
  1734. if (ratio >= (yellowThreshold ?? 95m)) return "yellow";
  1735. return ratio <= (redThreshold ?? 80m) ? "red" : "yellow";
  1736. }
  1737. private static string ResolveTrendFlag(decimal actual, decimal? previous)
  1738. {
  1739. if (previous == null) return "flat";
  1740. if (actual > previous.Value) return "up";
  1741. if (actual < previous.Value) return "down";
  1742. return "flat";
  1743. }
  1744. private static string Truncate(string? raw, int maxLength)
  1745. {
  1746. if (string.IsNullOrEmpty(raw)) return string.Empty;
  1747. return raw.Length <= maxLength ? raw : raw[..maxLength];
  1748. }
  1749. private sealed class S1ColumnRow
  1750. {
  1751. public string ColumnName { get; set; } = string.Empty;
  1752. }
  1753. private sealed class S1MdpEntityRow
  1754. {
  1755. public long Id { get; set; }
  1756. public string EntityName { get; set; } = string.Empty;
  1757. }
  1758. private sealed class S1KpiCalcRow
  1759. {
  1760. public long TenantId { get; set; }
  1761. public long FactoryId { get; set; }
  1762. public string MetricCode { get; set; } = string.Empty;
  1763. public decimal? MetricValue { get; set; }
  1764. }
  1765. private sealed class S1KpiMetaRow
  1766. {
  1767. public int MetricLevel { get; set; }
  1768. public string Direction { get; set; } = "higher_is_better";
  1769. public decimal? YellowThreshold { get; set; }
  1770. public decimal? RedThreshold { get; set; }
  1771. }
  1772. private sealed class S1KpiValueRow
  1773. {
  1774. public long Id { get; set; }
  1775. public decimal? MetricValue { get; set; }
  1776. public decimal? TargetValue { get; set; }
  1777. }
  1778. }
  1779. public sealed class S1MdpSyncTransformResult
  1780. {
  1781. public long RunLogId { get; set; }
  1782. public string BatchId { get; set; } = string.Empty;
  1783. public int StageRows { get; set; }
  1784. public int StandardRows { get; set; }
  1785. public int DwdRows { get; set; }
  1786. public int KpiRows { get; set; }
  1787. public int AtomicRows { get; set; }
  1788. /// <summary>
  1789. /// 当前快照发布证据。为 null 表示本轮没走到发布这一步。
  1790. ///
  1791. /// <para><b>为什么需要它</b>:发布语句在本轮零行时命中 0 行,在库里不留任何痕迹,
  1792. /// 于是「源侧本轮确实没有数据」与「发布压根没跑成」在 DWD 里字面等同。
  1793. /// 下游的 Authority 健康判定因此只能保守地判 UNKNOWN、拦下恢复 ——
  1794. /// 哪怕这是一个完全正常的空快照。</para>
  1795. ///
  1796. /// <para>把发布结果记进运行日志,就把这个隐式事实变成了可观测事实:
  1797. /// 「本轮成功发布了批次 X,其中当前行数为 N(N 可以是 0)」。</para>
  1798. /// </summary>
  1799. public S1MdpSnapshotPublication? Publication { get; set; }
  1800. }
  1801. /// <summary>
  1802. /// 一次当前快照发布的结果。与 <c>status='SUCCESS'</c> 写在同一条 UPDATE 里,
  1803. /// 因此不存在「成功了但没有发布证据」的中间态。
  1804. /// </summary>
  1805. public sealed class S1MdpSnapshotPublication
  1806. {
  1807. /// <summary>被发布为当前快照的表。</summary>
  1808. public string Table { get; set; } = string.Empty;
  1809. /// <summary>本轮批次号。读者据此确认「当前快照」出自哪一次运行。</summary>
  1810. public string BatchId { get; set; } = string.Empty;
  1811. /// <summary>发布作用域的工厂号。运行日志表本身没有工厂列,只能由这里承载。</summary>
  1812. public long FactoryId { get; set; }
  1813. /// <summary>发布后该批次的当前行数。<b>0 是合法值</b>,表示成功发布了一个空快照。</summary>
  1814. public int CurrentRows { get; set; }
  1815. }
  1816. internal sealed record S1MdpSqlCommand(string Sql, SugarParameter[] Parameters);
  1817. internal sealed record S1MdpEntityConfig(
  1818. string EntityCode,
  1819. string SourceTable,
  1820. string TargetTable,
  1821. string SourceRowIdExpression,
  1822. string SourceBizKeyExpression)
  1823. {
  1824. public static readonly IReadOnlyList<S1MdpEntityConfig> All = new List<S1MdpEntityConfig>
  1825. {
  1826. new("S1_SEORDER", "crm_seorder", "mdp_stg_so", "Id", "COALESCE(s.`bill_no`, CAST(s.`Id` AS CHAR))"),
  1827. new("S1_SEORDER_ENTRY", "crm_seorderentry", "mdp_stg_so", "Id", "CONCAT(IFNULL(s.`bill_no`,''), ':', IFNULL(s.`entry_seq`, CAST(s.`Id` AS CHAR)))"),
  1828. new("S1_SEORDER_CHANGE", "crm_seorder_change", "mdp_stg_so", "Id", "CONCAT(IFNULL(s.`bill_no`,''), ':', CAST(s.`Id` AS CHAR))"),
  1829. new("S1_CONTRACT_REVIEW", "ado_contract_review", "mdp_stg_so", "RecID", "COALESCE(s.`BillNo`, CAST(s.`RecID` AS CHAR))"),
  1830. new("S1_CONTRACT_REVIEW_FLOW", "ado_contract_review_flow", "mdp_stg_so", "RecID", "CONCAT(IFNULL(s.`ReviewBillNo`,''), ':', IFNULL(s.`StageNo`,''), ':', CAST(s.`RecID` AS CHAR))"),
  1831. new("S1_PRODUCT_DESIGN", "ado_product_design", "mdp_stg_so", "Id", "COALESCE(s.`BillNo`, CAST(s.`Id` AS CHAR))"),
  1832. new("S1_PRODUCT_DESIGN_BOM", "ado_product_design_bom", "mdp_stg_so", "Id", "CONCAT(CAST(s.`ProductDesignId` AS CHAR), ':', CAST(s.`Id` AS CHAR))"),
  1833. new("S1_PRODUCT_DESIGN_ROUTING", "ado_product_design_routing", "mdp_stg_so", "Id", "CONCAT(CAST(s.`ProductDesignId` AS CHAR), ':', CAST(s.`Id` AS CHAR))"),
  1834. new("S1_REQUIREMENT_EXAMINE_RESULT", "b_examine_result", "mdp_stg_so", "Id", "CONCAT(IFNULL(s.`bill_no`,''), ':', IFNULL(s.`morder_no`,''), ':', CAST(s.`Id` AS CHAR))"),
  1835. new("S1_REQUIREMENT_EXAMINE_DETAIL", "b_bom_child_examine", "mdp_stg_so", "Id", "CONCAT(IFNULL(s.`examine_id`,''), ':', IFNULL(s.`item_number`,''), ':', CAST(s.`Id` AS CHAR))"),
  1836. new("S1_SHIPPING_PLAN", "ShippingPlan", "mdp_stg_ship_trans", "RecID", "COALESCE(s.`LotSerial`, CAST(s.`RecID` AS CHAR))"),
  1837. new("S1_SHIPPING_PLAN_DETAIL", "ShippingPlanDetail", "mdp_stg_ship_trans", "RecID", "CONCAT(IFNULL(s.`plan_id`,''), ':', IFNULL(s.`OrdNbr`,''), ':', CAST(s.`RecID` AS CHAR))"),
  1838. new("S1_ASN_SHIPPER_MASTER", "ASNBOLShipperMaster", "mdp_stg_ship_trans", "RecID", "COALESCE(s.`Id`, CONCAT(IFNULL(s.`OrdNbr`,''), ':', CAST(s.`RecID` AS CHAR)))"),
  1839. new("S1_ASN_SHIPPER_DETAIL", "ASNBOLShipperDetail", "mdp_stg_ship_trans", "RecID", "CONCAT(IFNULL(s.`Id`,''), ':', IFNULL(s.`Line`, CAST(s.`RecID` AS CHAR)))"),
  1840. new("S1_LINKAGE_PLAN", "LinkagePlan", "mdp_stg_ship_trans", "id", "CONCAT(IFNULL(s.`bill_no`,''), ':', IFNULL(s.`item_number`,''), ':', CAST(s.`id` AS CHAR))")
  1841. };
  1842. }