ShipTransNeutralProjection.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. using SqlSugar;
  2. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Wms;
  3. /// <summary>
  4. /// 165 ASN(mdp_stg_ship_trans / ASNBOLShipperDetail)是销售发运的权威来源。
  5. /// InvTransHist.iss-so 只进入对账表,不写 SALES_SHIP。
  6. /// </summary>
  7. public sealed class ShipTransNeutralProjection : ITransient
  8. {
  9. /// <summary>
  10. /// 贴源筛选。<c>mdp_stg_ship_trans</c> 一张表承载 ASN 明细与发运计划(LinkagePlan / ShippingPlanDetail),
  11. /// 只有 ASN 明细是发货实绩;计划行没有 ShipDate / QtyShipped,放进来会变成一批 0 数量的 SALES_SHIP。
  12. /// 同一张 ASN 还会因 Q13 的 <c>AIDOPDEV_MYSQL → AIDOP_NATIVE</c> 迁移在贴源留两份,
  13. /// 故聚合口径一律先按业务字段去重,不按贴源行数算。
  14. /// </summary>
  15. private const string AsnStagingFilter = """
  16. s.tenant_id=@tid
  17. AND s.source_table='ASNBOLShipperDetail'
  18. AND NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.ShipDate')),'null') IS NOT NULL
  19. """;
  20. /// <summary>ASN 发货时间。eff_date / trans_time / approved_time 同源,避免三处各写一遍。</summary>
  21. private const string ShipDateExpr = """
  22. STR_TO_DATE(SUBSTRING_INDEX(REPLACE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.ShipDate')),'null'),'T',' '),'.',1),'%Y-%m-%d %H:%i:%s')
  23. """;
  24. /// <summary>ASN 明细行号。165 推 OrdLine,历史样例里也出现过 Line,两者都取不到才退 0。</summary>
  25. private const string OrdLineExpr = """
  26. COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdLine')),'null'),
  27. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Line')),'null'), '0')
  28. """;
  29. private readonly ISqlSugarClient _db;
  30. private readonly MdpNeutralSourceGate _gate;
  31. public ShipTransNeutralProjection(ISqlSugarClient db, MdpNeutralSourceGate gate)
  32. {
  33. _db = db;
  34. _gate = gate;
  35. }
  36. public async Task<int> ProjectAsync(long tenantId, string? batchId, CancellationToken cancellationToken = default)
  37. {
  38. cancellationToken.ThrowIfCancellationRequested();
  39. if (!await _gate.AllowsAsync(tenantId, "INV_TRANS", "DOPDEMORQ_SQLSERVER", syncBatchId: batchId, cancellationToken: cancellationToken))
  40. return 0;
  41. await _db.Ado.ExecuteCommandAsync(
  42. """
  43. CREATE TABLE IF NOT EXISTS mdp_ship_recon_diff (
  44. id BIGINT AUTO_INCREMENT PRIMARY KEY,
  45. tenant_id BIGINT NOT NULL,
  46. source_system VARCHAR(50) NOT NULL,
  47. domain VARCHAR(50) NOT NULL,
  48. ord_nbr VARCHAR(120) NOT NULL,
  49. ord_line VARCHAR(50) NOT NULL,
  50. biz_date DATE NOT NULL,
  51. asn_qty DECIMAL(18,6) NOT NULL DEFAULT 0,
  52. inv_iss_so_qty DECIMAL(18,6) NOT NULL DEFAULT 0,
  53. diff_qty DECIMAL(18,6) NOT NULL,
  54. detected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  55. UNIQUE KEY uk_ship_recon (tenant_id, source_system, domain, ord_nbr, ord_line, biz_date)
  56. )
  57. """);
  58. var inserted = await _db.Ado.ExecuteCommandAsync(
  59. $"""
  60. INSERT INTO mdp_std_inv_trans
  61. (tenant_id, source_system, written_by, domain, src_rec_id, trans_type, src_trans_type_raw,
  62. biz_doc_type, src_biz_doc_type_raw, approved_flag, void_flag, summary_flag,
  63. item_num, lot_serial, location, dimension1, dimension2, site, qty_change, doc_qty,
  64. eff_date, trans_time, approved_time, ord_nbr, ref_task_no, shipper_num, create_user,
  65. history_from, as_of, sync_batch_id)
  66. SELECT
  67. @tid, 'DOPDEMORQ_SQLSERVER', 'DB_SYNC',
  68. IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Domain')),'null'),''),
  69. LEFT(CONCAT('ASN:', IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Id')),''), s.source_row_id), ':', {OrdLineExpr}), 200),
  70. 'FG_SHIP',
  71. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.ShType')),'null'),
  72. 'SALES_SHIP', 'ASNBOLShipperDetail',
  73. IF(IFNULL(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.IsConfirm')),'0') IN ('1','true','True'), 1, 0),
  74. IF(IFNULL(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Canceled')),'0') NOT IN ('0','')
  75. OR IFNULL(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.IsActive')),'1') IN ('0','false'), 1, 0),
  76. 0,
  77. COALESCE(wol.item_code,
  78. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.CustItem')),'null'),
  79. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.ContainerItem')),'null')),
  80. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.LotSerial')),'null'),
  81. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Location')),'null'),
  82. IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Dimension1')),'null'),''),
  83. IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Dimension2')),'null'),''),
  84. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Site')),'null'),
  85. -ABS(IFNULL(CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.QtyShipped')),'null') AS DECIMAL(18,6)), 0)),
  86. CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.QtyToShip')),'null') AS DECIMAL(18,6)),
  87. {ShipDateExpr},
  88. {ShipDateExpr},
  89. {ShipDateExpr},
  90. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdNbr')),'null'),
  91. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdNbr')),'null'),
  92. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Id')),'null'),
  93. NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.CreateUser')),'null'),
  94. CURDATE(), NOW(), IFNULL(@batch, s.sync_batch_id)
  95. FROM mdp_stg_ship_trans s
  96. LEFT JOIN mdp_std_work_order_line wol
  97. ON wol.tenant_id=@tid AND wol.source_system='AIDOP_NATIVE' AND wol.doc_type='SALES_ORDER'
  98. AND wol.order_no=NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdNbr')),'null')
  99. AND wol.line_no=CAST({OrdLineExpr} AS SIGNED)
  100. WHERE {AsnStagingFilter}
  101. ON DUPLICATE KEY UPDATE
  102. qty_change=VALUES(qty_change), doc_qty=VALUES(doc_qty), item_num=VALUES(item_num),
  103. location=VALUES(location), lot_serial=VALUES(lot_serial), trans_type=VALUES(trans_type),
  104. biz_doc_type=VALUES(biz_doc_type), eff_date=VALUES(eff_date),
  105. trans_time=VALUES(trans_time), approved_time=VALUES(approved_time),
  106. approved_flag=VALUES(approved_flag), void_flag=VALUES(void_flag),
  107. as_of=VALUES(as_of), sync_batch_id=VALUES(sync_batch_id)
  108. """,
  109. new SugarParameter("@tid", tenantId),
  110. new SugarParameter("@batch", batchId ?? ""));
  111. await _db.Ado.ExecuteCommandAsync(
  112. $"""
  113. INSERT INTO mdp_source_gate_log
  114. (tenant_id, std_object, source_system, gate_reason, row_count, sample_keys, sync_batch_id)
  115. SELECT @tid, 'INV_TRANS', 'DOPDEMORQ_SQLSERVER', 'SO_LINE_NOT_FOUND',
  116. COUNT(DISTINCT CONCAT(IFNULL(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Id')),''), ':', {OrdLineExpr})) AS asn_rows,
  117. LEFT(GROUP_CONCAT(DISTINCT CONCAT(IFNULL(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdNbr')),''), ':', {OrdLineExpr})), 500),
  118. @batch
  119. FROM mdp_stg_ship_trans s
  120. LEFT JOIN mdp_std_work_order_line wol
  121. ON wol.tenant_id=@tid AND wol.source_system='AIDOP_NATIVE' AND wol.doc_type='SALES_ORDER'
  122. AND wol.order_no=NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdNbr')),'null')
  123. AND wol.line_no=CAST({OrdLineExpr} AS SIGNED)
  124. WHERE {AsnStagingFilter}
  125. AND wol.id IS NULL
  126. AND NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.CustItem')),'null') IS NULL
  127. AND NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.ContainerItem')),'null') IS NULL
  128. HAVING asn_rows>0
  129. """,
  130. new SugarParameter("@tid", tenantId),
  131. new SugarParameter("@batch", batchId ?? ""));
  132. await _db.Ado.ExecuteCommandAsync(
  133. $"""
  134. INSERT INTO mdp_ship_recon_diff
  135. (tenant_id, source_system, domain, ord_nbr, ord_line, biz_date, asn_qty, inv_iss_so_qty, diff_qty)
  136. SELECT @tid, 'DOPDEMORQ_SQLSERVER', d.domain, d.ord_nbr, d.ord_line, d.biz_date,
  137. d.asn_qty, d.iss_qty, d.asn_qty - d.iss_qty
  138. FROM (
  139. SELECT domain, ord_nbr, IFNULL(ord_line,'') ord_line, biz_date,
  140. SUM(asn_qty) asn_qty, SUM(iss_qty) iss_qty
  141. FROM (
  142. SELECT a.domain, a.ord_nbr, a.ord_line, a.biz_date, a.asn_qty, 0 iss_qty
  143. FROM (
  144. SELECT IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Domain')),'null'),'') domain,
  145. IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdNbr')),'null'),'') ord_nbr,
  146. IFNULL(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.OrdLine')),'null'),'') ord_line,
  147. DATE({ShipDateExpr}) biz_date,
  148. IFNULL(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Id')),'') asn_id,
  149. MAX(ABS(IFNULL(CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.QtyShipped')),'null') AS DECIMAL(18,6)),0))) asn_qty
  150. FROM mdp_stg_ship_trans s
  151. WHERE {AsnStagingFilter}
  152. GROUP BY domain, ord_nbr, ord_line, biz_date, asn_id
  153. ) a
  154. UNION ALL
  155. SELECT IFNULL(t.domain,''), IFNULL(t.ord_nbr,''), '', DATE(t.eff_date), 0, ABS(IFNULL(t.qty_change,0))
  156. FROM mdp_std_inv_trans t
  157. WHERE t.tenant_id=@tid AND t.source_system='DOPDEMORQ_SQLSERVER'
  158. AND t.src_trans_type_raw='iss-so'
  159. AND t.eff_date>=DATE_SUB(CURDATE(), INTERVAL 14 DAY)
  160. ) u
  161. WHERE biz_date IS NOT NULL AND ord_nbr<>''
  162. GROUP BY domain, ord_nbr, ord_line, biz_date
  163. ) d
  164. WHERE ABS(d.asn_qty - d.iss_qty) > 0.0001
  165. ON DUPLICATE KEY UPDATE
  166. asn_qty=VALUES(asn_qty), inv_iss_so_qty=VALUES(inv_iss_so_qty),
  167. diff_qty=VALUES(diff_qty), detected_at=CURRENT_TIMESTAMP
  168. """,
  169. new SugarParameter("@tid", tenantId));
  170. return inserted;
  171. }
  172. }