using System.Text.Json; using Admin.NET.Plugin.AiDOP.DataPlatform.Executors; using Admin.NET.Plugin.AiDOP.DataPlatform.HotWatch; using Admin.NET.Plugin.AiDOP.Entity.DataPlatform; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.DataPlatform.Wms; /// /// WP4 · 采购链入 165:自建采购单 / 交货计划 / 送货单 → Outbox UPSERT,供 WMS 扫码收货读取。 /// 旧 DOP 与 WMS 同库,建单即可见;新架构拆库后必须补这段回写。 /// 范围:Potype='po' 且 ReqBy in ('PO','DO') 的自建单,SAP 下发单不回推。 /// [ApiDescriptionSettings(Order = 327, Description = "采购链推送 WMS")] [Route("api/aidop/wms-purord")] [AllowAnonymous] [NonUnify] public class PurOrdWmsPushService : IDynamicApiController, ITransient { public const string TargetSource = "DOPDEMORQ_SQLSERVER"; public const string ActionPom = "PO_WMS_POM"; public const string ActionPod = "PO_WMS_POD"; public const string ActionDs = "PO_WMS_DS"; public const string ActionShd = "PO_WMS_SHD"; public const string ActionShdzb = "PO_WMS_SHDZB"; public const string ActionShph = "PO_WMS_SHPH"; public const string ActionMp = "PO_WMS_MP"; private readonly ISqlSugarClient _db; private readonly MdpOutboxEnqueueService _enqueue; private readonly MdpOutboxWakeSignal _wake; private readonly MdpHotWatchService _hotWatch; private readonly UserManager _userManager; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; public PurOrdWmsPushService( ISqlSugarClient db, MdpOutboxEnqueueService enqueue, MdpOutboxWakeSignal wake, MdpHotWatchService hotWatch, UserManager userManager, IHttpContextAccessor httpContextAccessor, ILogger logger) { _db = db; _enqueue = enqueue; _wake = wake; _hotWatch = hotWatch; _userManager = userManager; _httpContextAccessor = httpContextAccessor; _logger = logger; } public sealed class PushInput { /// 采购单号。与 Shddh 二选一。 public string? PurOrd { get; set; } /// 送货单号。给定时按「PO + 交货计划 + 送货单」整链补推。 public string? Shddh { get; set; } public long TenantId { get; set; } } public sealed class PushResult { public bool Ok { get; set; } public int Enqueued { get; set; } public int PurOrdCount { get; set; } public int DetailCount { get; set; } public int ScheduleCount { get; set; } public int ShipmentCount { get; set; } public int LabelCount { get; set; } public int BarcodeCount { get; set; } public List Skipped { get; set; } = new(); public string? Message { get; set; } } /// 补偿 / 联调:按本库快照把采购链推 165。 [DisplayName("推送采购链到 WMS")] [HttpPost("push")] public async Task Push([FromBody] PushInput input, CancellationToken ct = default) { if (input == null) throw Oops.Oh("请求体不能为空"); if (string.IsNullOrWhiteSpace(input.PurOrd) && string.IsNullOrWhiteSpace(input.Shddh)) throw Oops.Oh("采购单号与送货单号至少给一个"); if (input.TenantId <= 0) throw Oops.Oh("租户号无效,请指定有效的 TenantId"); var jwtTenantId = _userManager.TenantId; if (jwtTenantId > 0 && input.TenantId != jwtTenantId) throw Oops.Oh("请求租户与当前登录租户不一致"); if (jwtTenantId <= 0) { var clientIp = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? "unknown"; _logger.LogInformation( "[PurOrdWmsPush] 无登录态推送 audit ip={ClientIp} purOrd={PurOrd} shddh={Shddh} tenantId={TenantId}", clientIp, input.PurOrd, input.Shddh, input.TenantId); } var result = new PushResult { Ok = true }; if (!string.IsNullOrWhiteSpace(input.Shddh)) await EnqueueShipmentChainAsync(input.TenantId, input.Shddh.Trim(), result, ct); else await EnqueuePurchaseOrderAsync(input.TenantId, input.PurOrd!.Trim(), result, ct); _wake.Pulse(); result.Message = $"已入队 {result.Enqueued} 条(采购单 {result.PurOrdCount}/明细 {result.DetailCount}/" + $"交货计划 {result.ScheduleCount}/送货单 {result.ShipmentCount}/装箱标签 {result.LabelCount}/" + $"箱码 {result.BarcodeCount})"; return result; } /// 业务挂接点用:失败只记日志与 warning,不拖垮本库事务。 public async Task TryEnqueuePurchaseOrderSafeAsync(long tenantId, string purOrd, List? warnings = null) { if (string.IsNullOrWhiteSpace(purOrd)) return; try { var r = new PushResult(); await EnqueuePurchaseOrderAsync(tenantId, purOrd.Trim(), r, CancellationToken.None); _wake.Pulse(); if (r.Enqueued > 0) warnings?.Add($"采购单 {purOrd} 已入队推送 WMS({r.Enqueued} 条)"); } catch (Exception ex) { _logger.LogWarning(ex, "[PurOrdWmsPush] purOrd enqueue failed tenant={Tenant} po={PurOrd}", tenantId, purOrd); warnings?.Add($"采购单 {purOrd} 推送 WMS 入队失败:{ex.Message}(可调用 /api/aidop/wms-purord/push 补偿)"); } } /// 业务挂接点用:交货计划发布/取消后同步 165。 public async Task TryEnqueueSchedulesSafeAsync(long tenantId, List ids, List? warnings = null) { if (ids == null || ids.Count == 0) return; try { var r = new PushResult(); await EnqueueSchedulesByIdsAsync(tenantId, ids, r, CancellationToken.None); _wake.Pulse(); if (r.Enqueued > 0) warnings?.Add($"交货计划已入队推送 WMS({r.Enqueued} 条)"); } catch (Exception ex) { _logger.LogWarning(ex, "[PurOrdWmsPush] ds enqueue failed tenant={Tenant} ids={Count}", tenantId, ids.Count); warnings?.Add($"交货计划推送 WMS 入队失败:{ex.Message}"); } } /// 业务挂接点用:发货单生成标签后推整链(PO → 交货计划 → 送货单 → 装箱标签)。 public async Task TryEnqueueShipmentChainSafeAsync(long tenantId, string shddh, List? warnings = null) { if (string.IsNullOrWhiteSpace(shddh)) return; try { var r = new PushResult(); await EnqueueShipmentChainAsync(tenantId, shddh.Trim(), r, CancellationToken.None); _wake.Pulse(); if (r.Enqueued > 0) warnings?.Add($"送货单 {shddh} 及采购链已入队推送 WMS({r.Enqueued} 条)"); foreach (var s in r.Skipped) warnings?.Add(s); } catch (Exception ex) { _logger.LogWarning(ex, "[PurOrdWmsPush] shipment enqueue failed tenant={Tenant} shddh={Shddh}", tenantId, shddh); warnings?.Add($"送货单 {shddh} 推送 WMS 入队失败:{ex.Message}(可调用 /api/aidop/wms-purord/push 补偿)"); } } // ── 采购单主表 + 明细 ────────────────────────────────────────────── private async Task EnqueuePurchaseOrderAsync( long tenantId, string purOrd, PushResult result, CancellationToken ct) { var masters = await _db.Ado.SqlQueryAsync( """ SELECT TRIM(IFNULL(`Domain`, '')) AS Domain, TRIM(IFNULL(PurOrd, '')) AS PurOrd, TRIM(IFNULL(Potype, '')) AS Potype, TRIM(IFNULL(Typed, '')) AS Typed, TRIM(IFNULL(Supp, '')) AS Supp, TRIM(IFNULL(Buyer, '')) AS Buyer, TRIM(IFNULL(ReqBy, '')) AS ReqBy, TRIM(IFNULL(`Usage`, '')) AS UsageText, TRIM(IFNULL(FSTID, '')) AS FSTID, TRIM(IFNULL(Remark, '')) AS Remark, OrdDate, DueDate, IFNULL(IsActive, 1) AS IsActive, IFNULL(IsConfirm, 0) AS IsConfirm, CreateUser, CreateTime, UpdateUser, UpdateTime FROM PurOrdMaster WHERE tenant_id = @TenantId AND PurOrd = @PurOrd LIMIT 1 """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@PurOrd", purOrd)); if (masters.Count == 0) throw Oops.Oh($"本库未找到采购单 {purOrd}"); var m = masters[0]; if (!IsSelfBuilt(m.Potype, m.ReqBy)) { result.Skipped.Add($"采购单 {purOrd} 非自建单(Potype={m.Potype} / ReqBy={m.ReqBy}),按约定不回推 165"); return; } var domain = string.IsNullOrWhiteSpace(m.Domain) ? "8010" : m.Domain; var potype = string.IsNullOrWhiteSpace(m.Potype) ? "po" : m.Potype; var now = DateTime.Now; var keys = new Dictionary { ["Domain"] = domain, ["PurOrd"] = purOrd, ["Potype"] = potype }; var insert = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Domain"] = domain, ["PurOrd"] = Trunc(purOrd, 48), ["Potype"] = Trunc(potype, 8), ["Typed"] = Trunc(m.Typed, 18), ["Supp"] = Trunc(m.Supp, 20), ["Buyer"] = Trunc(m.Buyer, 30), ["ReqBy"] = Trunc(m.ReqBy, 8), // Status 只建行写空串;收货态由 WMS 维护 ["Status"] = "", ["Usage"] = Trunc(m.UsageText, 30), ["FSTID"] = Trunc(m.FSTID, 24), ["Remark"] = Trunc(m.Remark, 200), ["IsActive"] = m.IsActive != 0, ["IsConfirm"] = m.IsConfirm != 0, ["CreateUser"] = Trunc(m.CreateUser ?? "aidop", 24), ["UpdateUser"] = Trunc(m.UpdateUser ?? m.CreateUser ?? "aidop", 24), ["CreateTime"] = Fmt(m.CreateTime ?? now), ["UpdateTime"] = Fmt(m.UpdateTime ?? now) }; if (m.OrdDate != null) insert["OrdDate"] = Fmt(m.OrdDate); if (m.DueDate != null) insert["DueDate"] = Fmt(m.DueDate); var update = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Typed"] = Trunc(m.Typed, 18), ["Supp"] = Trunc(m.Supp, 20), ["Buyer"] = Trunc(m.Buyer, 30), ["ReqBy"] = Trunc(m.ReqBy, 8), ["Usage"] = Trunc(m.UsageText, 30), ["FSTID"] = Trunc(m.FSTID, 24), ["Remark"] = Trunc(m.Remark, 200), ["IsActive"] = m.IsActive != 0, ["UpdateUser"] = Trunc(m.UpdateUser ?? "aidop", 24), ["UpdateTime"] = Fmt(now) }; if (m.DueDate != null) update["DueDate"] = Fmt(m.DueDate); if (await EnqueueRowAsync(tenantId, $"po|{domain}|{potype}|{purOrd}|pom", ActionPom, "PurOrdMaster", keys, insert, update, null, ct)) result.Enqueued++; result.PurOrdCount++; var details = await _db.Ado.SqlQueryAsync( """ SELECT TRIM(IFNULL(`Domain`, '')) AS Domain, IFNULL(Line, 0) AS Line, TRIM(IFNULL(ItemNum, '')) AS ItemNum, TRIM(IFNULL(Descr, '')) AS Descr, TRIM(IFNULL(UM, '')) AS UM, TRIM(IFNULL(Rev, '')) AS Rev, TRIM(IFNULL(Drawing, '')) AS Drawing, TRIM(IFNULL(Location, '')) AS Location, TRIM(IFNULL(LotSerial, '')) AS LotSerial, TRIM(IFNULL(Req, '')) AS Req, IFNULL(QtyOrded, 0) AS QtyOrded, DueDate, NeedDate, IFNULL(IsActive, 1) AS IsActive, IFNULL(IsConfirm, 0) AS IsConfirm, CreateUser, CreateTime, UpdateUser, UpdateTime FROM PurOrdDetail WHERE tenant_id = @TenantId AND PurOrd = @PurOrd ORDER BY Line """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@PurOrd", purOrd)); foreach (var d in details) { var dKeys = new Dictionary { ["Domain"] = domain, ["PurOrd"] = purOrd, ["Potype"] = potype, ["Line"] = d.Line, ["BlanketLine"] = 0 }; var dInsert = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Domain"] = domain, ["PurOrd"] = Trunc(purOrd, 48), ["Potype"] = Trunc(potype, 8), ["Line"] = d.Line, ["BlanketLine"] = 0, ["ItemNum"] = Trunc(d.ItemNum, 60), ["Descr"] = Trunc(d.Descr, 255), ["UM"] = Trunc(d.UM, 8), ["Rev"] = Trunc(d.Rev, 8), ["Drawing"] = Trunc(d.Drawing, 24), ["Location"] = Trunc(d.Location, 8), ["LotSerial"] = Trunc(d.LotSerial, 120), ["Req"] = Trunc(d.Req, 20), ["QtyOrded"] = d.QtyOrded, // 收货累计列建行给 0,之后归 WMS ["RctQty"] = 0m, ["ReceiptQty"] = 0m, ["QtyReturned"] = 0m, ["Status"] = "", ["IsActive"] = d.IsActive != 0, ["IsConfirm"] = d.IsConfirm != 0, ["CreateUser"] = Trunc(d.CreateUser ?? "aidop", 24), ["UpdateUser"] = Trunc(d.UpdateUser ?? d.CreateUser ?? "aidop", 24), ["CreateTime"] = Fmt(d.CreateTime ?? now), ["UpdateTime"] = Fmt(d.UpdateTime ?? now) }; if (d.DueDate != null) dInsert["DueDate"] = Fmt(d.DueDate); if (d.NeedDate != null) dInsert["NeedDate"] = Fmt(d.NeedDate); var dUpdate = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["ItemNum"] = Trunc(d.ItemNum, 60), ["Descr"] = Trunc(d.Descr, 255), ["UM"] = Trunc(d.UM, 8), ["Rev"] = Trunc(d.Rev, 8), ["Drawing"] = Trunc(d.Drawing, 24), ["Location"] = Trunc(d.Location, 8), ["QtyOrded"] = d.QtyOrded, ["IsActive"] = d.IsActive != 0, ["UpdateUser"] = Trunc(d.UpdateUser ?? "aidop", 24), ["UpdateTime"] = Fmt(now) }; if (d.DueDate != null) dUpdate["DueDate"] = Fmt(d.DueDate); if (d.NeedDate != null) dUpdate["NeedDate"] = Fmt(d.NeedDate); // 165 的 RecID 是 IDENTITY,明细外键必须在目标库现查主表 RecID var resolve = new Dictionary { ["PurOrdRecID"] = new Dictionary { ["table"] = "PurOrdMaster", ["column"] = "RecID", ["match"] = new Dictionary { ["Domain"] = domain, ["PurOrd"] = purOrd, ["Potype"] = potype } } }; if (await EnqueueRowAsync(tenantId, $"po|{domain}|{potype}|{purOrd}|pod|{d.Line}", ActionPod, "PurOrdDetail", dKeys, dInsert, dUpdate, resolve, ct)) result.Enqueued++; result.DetailCount++; } } // ── 交货计划 ────────────────────────────────────────────────────── private async Task EnqueueSchedulesByIdsAsync( long tenantId, List ids, PushResult result, CancellationToken ct) { var rows = await _db.Ado.SqlQueryAsync( $""" SELECT Id, TRIM(IFNULL(domain, '')) AS Domain, IFNULL(icdsid, 0) AS Icdsid, TRIM(IFNULL(dsnum, '')) AS Dsnum, TRIM(IFNULL(status, '')) AS Status, TRIM(IFNULL(itemnum, '')) AS Itemnum, TRIM(IFNULL(um, '')) AS Um, TRIM(IFNULL(purgroup, '')) AS Purgroup, TRIM(IFNULL(suppliercode, '')) AS Suppliercode, TRIM(IFNULL(supplier, '')) AS Supplier, submitdate, requestdate, needdate, TRIM(IFNULL(ponumber, '')) AS Ponumber, IFNULL(poline, 0) AS Poline, IFNULL(schedqty, 0) AS Schedqty, TRIM(IFNULL(remarks, '')) AS Remarks, IFNULL(isactive, 1) AS Isactive, createuser, createtime, updateuser, updatetime FROM srm_polist_ds WHERE tenant_id = @TenantId AND Id IN ({string.Join(",", ids.Select((_, i) => $"@i{i}"))}) """, ids.Select((v, i) => new SugarParameter($"@i{i}", v)) .Append(new SugarParameter("@TenantId", tenantId)) .ToList()); foreach (var ds in rows) await EnqueueScheduleRowAsync(tenantId, ds, result, ct); } private async Task EnqueueSchedulesByPurOrdAsync( long tenantId, string purOrd, PushResult result, CancellationToken ct) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT Id, TRIM(IFNULL(domain, '')) AS Domain, IFNULL(icdsid, 0) AS Icdsid, TRIM(IFNULL(dsnum, '')) AS Dsnum, TRIM(IFNULL(status, '')) AS Status, TRIM(IFNULL(itemnum, '')) AS Itemnum, TRIM(IFNULL(um, '')) AS Um, TRIM(IFNULL(purgroup, '')) AS Purgroup, TRIM(IFNULL(suppliercode, '')) AS Suppliercode, TRIM(IFNULL(supplier, '')) AS Supplier, submitdate, requestdate, needdate, TRIM(IFNULL(ponumber, '')) AS Ponumber, IFNULL(poline, 0) AS Poline, IFNULL(schedqty, 0) AS Schedqty, TRIM(IFNULL(remarks, '')) AS Remarks, IFNULL(isactive, 1) AS Isactive, createuser, createtime, updateuser, updatetime FROM srm_polist_ds WHERE tenant_id = @TenantId AND ponumber = @PurOrd AND IFNULL(isactive, 1) = 1 ORDER BY poline """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@PurOrd", purOrd)); foreach (var ds in rows) await EnqueueScheduleRowAsync(tenantId, ds, result, ct); } private async Task EnqueueScheduleRowAsync( long tenantId, DeliveryScheduleRow ds, PushResult result, CancellationToken ct) { var domain = string.IsNullOrWhiteSpace(ds.Domain) ? "8010" : ds.Domain; var now = DateTime.Now; var keys = new Dictionary { ["domain"] = domain, ["dsnum"] = ds.Dsnum }; var insert = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Id"] = ds.Id, ["domain"] = domain, ["icdsid"] = ds.Icdsid, ["dsnum"] = Trunc(ds.Dsnum, 128), ["status"] = Trunc(ds.Status, 10), ["itemnum"] = Trunc(ds.Itemnum, 128), ["um"] = Trunc(ds.Um, 124), ["purgroup"] = Trunc(ds.Purgroup, 50), ["suppliercode"] = Trunc(ds.Suppliercode, 50), ["supplier"] = Trunc(ds.Supplier, 50), ["ponumber"] = Trunc(ds.Ponumber, 50), ["poline"] = ds.Poline, ["schedqty"] = ds.Schedqty, // 收货累计列建行给 0 / 全额待交,之后归 WMS ["lastsentqty"] = 0m, ["sentqty"] = 0m, ["restqty"] = ds.Schedqty, ["remarks"] = Trunc(ds.Remarks, 500), ["isactive"] = ds.Isactive, ["createuser"] = Trunc(ds.Createuser ?? "aidop", 24), ["updateuser"] = Trunc(ds.Updateuser ?? ds.Createuser ?? "aidop", 24), ["createtime"] = Fmt(ds.Createtime ?? now), ["updatetime"] = Fmt(ds.Updatetime ?? now) }; if (ds.Submitdate != null) insert["submitdate"] = Fmt(ds.Submitdate); if (ds.Requestdate != null) insert["requestdate"] = Fmt(ds.Requestdate); if (ds.Needdate != null) insert["needdate"] = Fmt(ds.Needdate); // sentqty / restqty / lastsentdate 归 WMS,更新时一律不碰 var update = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["status"] = Trunc(ds.Status, 10), ["schedqty"] = ds.Schedqty, ["isactive"] = ds.Isactive, ["remarks"] = Trunc(ds.Remarks, 500), ["updateuser"] = Trunc(ds.Updateuser ?? "aidop", 24), ["updatetime"] = Fmt(now) }; if (ds.Needdate != null) update["needdate"] = Fmt(ds.Needdate); if (ds.Submitdate != null) update["submitdate"] = Fmt(ds.Submitdate); if (await EnqueueRowAsync(tenantId, $"po|{domain}|ds|{ds.Dsnum}", ActionDs, "srm_polist_ds", keys, insert, update, null, ct)) result.Enqueued++; result.ScheduleCount++; } // ── 送货单 + 明细 + 装箱标签 ─────────────────────────────────────── private async Task EnqueueShipmentChainAsync( long tenantId, string shddh, PushResult result, CancellationToken ct) { var masters = await _db.Ado.SqlQueryAsync( """ SELECT id AS Id, IFNULL(sh_purchase_id, 0) AS ShPurchaseId, TRIM(IFNULL(sh_purchase_name, '')) AS ShPurchaseName, TRIM(IFNULL(sh_purchase_num, '')) AS ShPurchaseNum, TRIM(IFNULL(sh_purchase_address, '')) AS ShPurchaseAddress, TRIM(IFNULL(sh_purchase_lxr, '')) AS ShPurchaseLxr, TRIM(IFNULL(sh_purchase_phone, '')) AS ShPurchasePhone, TRIM(IFNULL(delivery_Address, '')) AS DeliveryAddress, TRIM(IFNULL(expected_consignee, '')) AS ExpectedConsignee, TRIM(IFNULL(consignee_phone, '')) AS ConsigneePhone, estimated_delivery_date AS EstimatedDeliveryDate, TRIM(IFNULL(po_billno, '')) AS PoBillno, TRIM(IFNULL(shddh, '')) AS Shddh, TRIM(IFNULL(jhshrq, '')) AS Jhshrq, TRIM(IFNULL(tjrid, '')) AS Tjrid, TRIM(IFNULL(tjrxm, '')) AS Tjrxm, TRIM(IFNULL(tjrq, '')) AS Tjrq, IFNULL(scbq, 0) AS Scbq, TRIM(IFNULL(chbg, '')) AS Chbg, IFNULL(sfpc, 0) AS Sfpc, TRIM(IFNULL(pcsm, '')) AS Pcsm, TRIM(IFNULL(wlsc, '')) AS Wlsc, TRIM(IFNULL(yjdhrq, '')) AS Yjdhrq, IFNULL(state, 0) AS State, TRIM(IFNULL(shzt, '')) AS Shzt, TRIM(IFNULL(wldh, '')) AS Wldh, IFNULL(dycs, 0) AS Dycs FROM scm_shd WHERE tenant_id = @TenantId AND shddh = @Shddh LIMIT 1 """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@Shddh", shddh)); if (masters.Count == 0) throw Oops.Oh($"本库未找到送货单 {shddh}"); var s = masters[0]; var lines = await _db.Ado.SqlQueryAsync( """ SELECT id AS Id, TRIM(IFNULL(glid, '')) AS Glid, TRIM(IFNULL(sh_material_code, '')) AS ShMaterialCode, TRIM(IFNULL(sh_material_name, '')) AS ShMaterialName, TRIM(IFNULL(sh_material_ggxh, '')) AS ShMaterialGgxh, IFNULL(sh_delivery_quantity, 0) AS ShDeliveryQuantity, TRIM(IFNULL(sh_material_dw, '')) AS ShMaterialDw, TRIM(IFNULL(remarks, '')) AS Remarks, IFNULL(bzsl, 0) AS Bzsl, IFNULL(bqsl, 0) AS Bqsl, TRIM(IFNULL(order_type, '')) AS OrderType, TRIM(IFNULL(po_bill, '')) AS PoBill, TRIM(IFNULL(po_billline, '')) AS PoBillline, IFNULL(hh, 0) AS Hh, TRIM(IFNULL(scrq, '')) AS Scrq, TRIM(IFNULL(scph, '')) AS Scph, TRIM(IFNULL(th, '')) AS Th, TRIM(IFNULL(bbh, '')) AS Bbh, IFNULL(djsl, 0) AS Djsl, TRIM(IFNULL(ccrq, '')) AS Ccrq, TRIM(IFNULL(cgyt, '')) AS Cgyt, TRIM(IFNULL(jybb, '')) AS Jybb, TRIM(IFNULL(jhdbh, '')) AS Jhdbh, TRIM(IFNULL(jhdhh, '')) AS Jhdhh, TRIM(IFNULL(shpc, '')) AS Shpc, TRIM(IFNULL(shzt, '')) AS Shzt, IFNULL(rksl, 0) AS Rksl, IFNULL(thsl, 0) AS Thsl FROM scm_shdzb WHERE glid = @Glid ORDER BY hh, id """, new SugarParameter("@Glid", s.Id.ToString())); var labels = await _db.Ado.SqlQueryAsync( """ SELECT id AS Id, TRIM(IFNULL(xh, '')) AS Xh, TRIM(IFNULL(wlbm, '')) AS Wlbm, TRIM(IFNULL(scph, '')) AS Scph, TRIM(IFNULL(shdh, '')) AS Shdh, TRIM(IFNULL(shpc, '')) AS Shpc, TRIM(IFNULL(gysbm, '')) AS Gysbm, TRIM(IFNULL(csrq, '')) AS Csrq FROM scm_shdshph WHERE shdh = @Shddh ORDER BY id """, new SugarParameter("@Shddh", shddh)); // 先补上游:送货明细引用的采购单与交货计划,否则 WMS 查不到可收行 var purOrds = lines.Select(x => (x.PoBill ?? "").Trim()) .Where(x => x.Length > 0) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); foreach (var po in purOrds) { await EnqueuePurchaseOrderAsync(tenantId, po, result, ct); await EnqueueSchedulesByPurOrdAsync(tenantId, po, result, ct); } var now = DateTime.Now; var sInsert = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["id"] = s.Id, ["sh_purchase_id"] = s.ShPurchaseId, ["sh_purchase_name"] = Trunc(s.ShPurchaseName, 255), ["sh_purchase_num"] = Trunc(s.ShPurchaseNum, 255), ["sh_purchase_address"] = Trunc(s.ShPurchaseAddress, 255), ["sh_purchase_lxr"] = Trunc(s.ShPurchaseLxr, 255), ["sh_purchase_phone"] = Trunc(s.ShPurchasePhone, 255), ["delivery_Address"] = Trunc(s.DeliveryAddress, 255), ["expected_consignee"] = Trunc(s.ExpectedConsignee, 255), ["consignee_phone"] = Trunc(s.ConsigneePhone, 255), ["po_billno"] = Trunc(s.PoBillno, 255), ["shddh"] = Trunc(s.Shddh, 255), ["jhshrq"] = Trunc(s.Jhshrq, 50), ["tjrid"] = Trunc(s.Tjrid, 50), ["tjrxm"] = Trunc(s.Tjrxm, 50), ["tjrq"] = Trunc(s.Tjrq, 50), ["scbq"] = s.Scbq, ["chbg"] = Trunc(s.Chbg, 255), ["sfpc"] = s.Sfpc, ["pcsm"] = Trunc(s.Pcsm, 255), ["wlsc"] = Trunc(s.Wlsc, 500), ["yjdhrq"] = Trunc(s.Yjdhrq, 500), ["state"] = s.State, ["shzt"] = Trunc(s.Shzt, 50), ["wldh"] = Trunc(s.Wldh, 50), ["dycs"] = s.Dycs }; if (s.EstimatedDeliveryDate != null) sInsert["estimated_delivery_date"] = Fmt(s.EstimatedDeliveryDate); // shzt / state 收货开始后归 WMS('收货中'),更新时不回写覆盖 var sUpdate = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sh_purchase_name"] = Trunc(s.ShPurchaseName, 255), ["sh_purchase_num"] = Trunc(s.ShPurchaseNum, 255), ["delivery_Address"] = Trunc(s.DeliveryAddress, 255), ["expected_consignee"] = Trunc(s.ExpectedConsignee, 255), ["consignee_phone"] = Trunc(s.ConsigneePhone, 255), ["po_billno"] = Trunc(s.PoBillno, 255), ["jhshrq"] = Trunc(s.Jhshrq, 50), ["wlsc"] = Trunc(s.Wlsc, 500), ["yjdhrq"] = Trunc(s.Yjdhrq, 500) }; if (s.EstimatedDeliveryDate != null) sUpdate["estimated_delivery_date"] = Fmt(s.EstimatedDeliveryDate); if (await EnqueueRowAsync(tenantId, $"po|shd|{shddh}", ActionShd, "scm_shd", new Dictionary { ["id"] = s.Id }, sInsert, sUpdate, null, ct)) result.Enqueued++; result.ShipmentCount++; foreach (var l in lines) { var lInsert = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["id"] = l.Id, ["glid"] = Trunc(l.Glid, 255), ["sh_material_code"] = Trunc(l.ShMaterialCode, 255), ["sh_material_name"] = Trunc(l.ShMaterialName, 255), ["sh_material_ggxh"] = Trunc(l.ShMaterialGgxh, 255), ["sh_delivery_quantity"] = l.ShDeliveryQuantity, ["sh_material_dw"] = Trunc(l.ShMaterialDw, 255), ["remarks"] = Trunc(l.Remarks, 255), ["bzsl"] = l.Bzsl, ["bqsl"] = l.Bqsl, ["order_type"] = Trunc(l.OrderType, 255), ["po_bill"] = Trunc(l.PoBill, 255), ["po_billline"] = Trunc(l.PoBillline, 50), ["hh"] = l.Hh, ["scrq"] = Trunc(l.Scrq, 255), ["scph"] = Trunc(l.Scph, 255), ["th"] = Trunc(l.Th, 255), ["bbh"] = Trunc(l.Bbh, 255), ["djsl"] = l.Djsl, ["ccrq"] = Trunc(l.Ccrq, 255), ["cgyt"] = Trunc(l.Cgyt, 255), ["jybb"] = Trunc(l.Jybb, 255), ["jhdbh"] = Trunc(l.Jhdbh, 50), ["jhdhh"] = Trunc(l.Jhdhh, 50), ["shpc"] = Trunc(l.Shpc, 50), ["shzt"] = Trunc(l.Shzt, 255), // 入库 / 退货数量归 WMS ["rksl"] = 0m, ["thsl"] = 0m }; var lUpdate = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sh_material_code"] = Trunc(l.ShMaterialCode, 255), ["sh_material_name"] = Trunc(l.ShMaterialName, 255), ["sh_material_ggxh"] = Trunc(l.ShMaterialGgxh, 255), ["sh_delivery_quantity"] = l.ShDeliveryQuantity, ["sh_material_dw"] = Trunc(l.ShMaterialDw, 255), ["bzsl"] = l.Bzsl, ["bqsl"] = l.Bqsl, ["po_bill"] = Trunc(l.PoBill, 255), ["po_billline"] = Trunc(l.PoBillline, 50), ["jhdbh"] = Trunc(l.Jhdbh, 50), ["jhdhh"] = Trunc(l.Jhdhh, 50), ["shpc"] = Trunc(l.Shpc, 50) }; if (await EnqueueRowAsync(tenantId, $"po|shdzb|{shddh}|{l.Id}", ActionShdzb, "scm_shdzb", new Dictionary { ["id"] = l.Id }, lInsert, lUpdate, null, ct)) result.Enqueued++; } foreach (var lb in labels) { // 165 的 scm_shdshph.id 是 IDENTITY,只能按 (shdh, xh) 自然键幂等 var bKeys = new Dictionary { ["shdh"] = Trunc(lb.Shdh, 500), ["xh"] = Trunc(lb.Xh, 50) }; var bInsert = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["xh"] = Trunc(lb.Xh, 50), ["wlbm"] = Trunc(lb.Wlbm, 500), ["scph"] = Trunc(lb.Scph, 500), ["shdh"] = Trunc(lb.Shdh, 500), ["shpc"] = Trunc(lb.Shpc, 500), ["gysbm"] = Trunc(lb.Gysbm, 500), ["csrq"] = Trunc(lb.Csrq, 50) }; if (await EnqueueRowAsync(tenantId, $"po|shph|{shddh}|{lb.Xh}", ActionShph, "scm_shdshph", bKeys, bInsert, bInsert, null, ct)) result.Enqueued++; result.LabelCount++; } await EnqueueBarcodesAsync(tenantId, shddh, result, ct); } /// /// 箱码入 165 MissedPrint:WMS 扫码就是拿箱码查这张表(pr_WMS_GetBarCodes), /// 查不到即报「该标签不存在」。唯一索引 IX_MissedPrint=(Domain,BarCode),RecID 是 IDENTITY 不推。 /// private async Task EnqueueBarcodesAsync( long tenantId, string shddh, PushResult result, CancellationToken ct) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT TRIM(IFNULL(`Domain`, '')) AS Domain, TRIM(IFNULL(Site, '')) AS Site, TRIM(IFNULL(BarCode, '')) AS BarCode, TRIM(IFNULL(ItemNum, '')) AS ItemNum, TRIM(IFNULL(Descr, '')) AS Descr, TRIM(IFNULL(Product, '')) AS Product, TRIM(IFNULL(Carton, '')) AS Carton, TRIM(IFNULL(OrdNbr, '')) AS OrdNbr, IFNULL(PackingQty, 0) AS PackingQty, IFNULL(Qty, 0) AS Qty, TRIM(IFNULL(Status, '')) AS Status, TRIM(IFNULL(Supply, '')) AS Supply, TRIM(IFNULL(LotSerial, '')) AS LotSerial, IFNULL(CartonQty, 1) AS CartonQty, TRIM(IFNULL(SuppLotSerial, '')) AS SuppLotSerial, TRIM(IFNULL(ShipperNbr, '')) AS ShipperNbr, IFNULL(ShipperLine, 0) AS ShipperLine, ProdDate, ExpireDate, TRIM(IFNULL(PurOrd, '')) AS PurOrd, IFNULL(PurLine, 0) AS PurLine, IFNULL(PurQty, 0) AS PurQty, TRIM(IFNULL(LabelFormat, '')) AS LabelFormat, TRIM(IFNULL(StandItem, '')) AS StandItem, TRIM(IFNULL(EffSize, '')) AS EffSize, IFNULL(GP12CheckedQty, 0) AS GP12CheckedQty, IFNULL(NetWeight, 0) AS NetWeight, TRIM(IFNULL(Remark, '')) AS Remark, TRIM(IFNULL(LevelChar, '')) AS LevelChar, TRIM(IFNULL(PurOrdDetBatchNbr, '')) AS PurOrdDetBatchNbr, TRIM(IFNULL(FirmString5, '')) AS FirmString5, CreateUser, CreateTime, UpdateUser, UpdateTime FROM MissedPrint WHERE ShipperNbr = @Shddh AND IFNULL(Status, '') = 'U' AND IFNULL(PurOrd, '') NOT LIKE '作废%' ORDER BY ShipperLine, Carton """, new SugarParameter("@Shddh", shddh)); var now = DateTime.Now; var watchKeys = new HashSet<(string Domain, string PurOrd)>(); foreach (var b in rows) { var domain = string.IsNullOrWhiteSpace(b.Domain) ? "8010" : b.Domain; var keys = new Dictionary { ["Domain"] = domain, ["BarCode"] = b.BarCode }; var insert = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Domain"] = domain, ["Site"] = Trunc(b.Site, 24), ["BarCode"] = Trunc(b.BarCode, 250), ["ItemNum"] = Trunc(b.ItemNum, 60), ["Descr"] = Trunc(b.Descr, 255), ["Product"] = Trunc(b.Product, 255), ["Carton"] = Trunc(b.Carton, 20), ["OrdNbr"] = Trunc(b.OrdNbr, 48), ["PackingQty"] = b.PackingQty, ["Qty"] = b.Qty, // 标签状态归 WMS:建行给待收货 U,收货后由 WMS 改,更新时不回写 ["Status"] = "U", ["RelatedBarCode"] = "", ["Supply"] = Trunc(b.Supply, 40), ["LotSerial"] = Trunc(b.LotSerial, 120), ["CartonQty"] = b.CartonQty, ["SuppLotSerial"] = Trunc(b.SuppLotSerial, 120), ["ShipperNbr"] = Trunc(b.ShipperNbr, 60), ["ShipperLine"] = b.ShipperLine, ["PurOrd"] = Trunc(b.PurOrd, 48), ["PurLine"] = b.PurLine, ["PurQty"] = b.PurQty, ["LabelFormat"] = Trunc(b.LabelFormat, 20), ["StandItem"] = Trunc(b.StandItem, 60), ["EffSize"] = Trunc(b.EffSize, 20), ["GP12CheckedQty"] = b.GP12CheckedQty, ["NetWeight"] = b.NetWeight, ["Remark"] = Trunc(b.Remark, 200), ["LevelChar"] = Trunc(b.LevelChar, 20), ["PurOrdDetBatchNbr"] = Trunc(b.PurOrdDetBatchNbr, 48), ["FirmString5"] = Trunc(b.FirmString5, 48), ["CreateUser"] = Trunc(b.CreateUser ?? "aidop", 24), ["UpdateUser"] = Trunc(b.UpdateUser ?? b.CreateUser ?? "aidop", 24), ["CreateTime"] = Fmt(b.CreateTime ?? now), ["UpdateTime"] = Fmt(b.UpdateTime ?? now) }; if (b.ProdDate != null) insert["ProdDate"] = Fmt(b.ProdDate); if (b.ExpireDate != null) insert["ExpireDate"] = Fmt(b.ExpireDate); var update = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["ItemNum"] = Trunc(b.ItemNum, 60), ["Descr"] = Trunc(b.Descr, 255), ["Product"] = Trunc(b.Product, 255), ["PackingQty"] = b.PackingQty, ["Qty"] = b.Qty, ["LotSerial"] = Trunc(b.LotSerial, 120), ["SuppLotSerial"] = Trunc(b.SuppLotSerial, 120), ["ShipperNbr"] = Trunc(b.ShipperNbr, 60), ["ShipperLine"] = b.ShipperLine, ["PurOrd"] = Trunc(b.PurOrd, 48), ["PurLine"] = b.PurLine, ["PurQty"] = b.PurQty, ["StandItem"] = Trunc(b.StandItem, 60), ["EffSize"] = Trunc(b.EffSize, 20), ["LevelChar"] = Trunc(b.LevelChar, 20), ["PurOrdDetBatchNbr"] = Trunc(b.PurOrdDetBatchNbr, 48), ["Remark"] = Trunc(b.Remark, 200), ["UpdateUser"] = Trunc(b.UpdateUser ?? "aidop", 24), ["UpdateTime"] = Fmt(now) }; if (b.ProdDate != null) update["ProdDate"] = Fmt(b.ProdDate); if (await EnqueueRowAsync(tenantId, $"po|mp|{domain}|{b.BarCode}", ActionMp, "MissedPrint", keys, insert, update, null, ct)) result.Enqueued++; result.BarcodeCount++; if (!string.IsNullOrWhiteSpace(b.PurOrd)) watchKeys.Add((domain, b.PurOrd.Trim())); } // 标签已推 → 登记热关注,回读 WMS 收货结果(收货数、箱码状态、送货单状态) try { foreach (var (domain, purOrd) in watchKeys) await _hotWatch.EnrollPurOrderAsync(domain, purOrd, tenantId, ct); } catch (Exception ex) { // 登记失败不应影响标签推送本身,下次生成标签会再登记一次 _logger.LogWarning(ex, "[PurOrdWmsPush] 登记采购单热关注失败 shddh={Shddh}", shddh); } } // ── 基础设施 ────────────────────────────────────────────────────── /// 自建单口径:SAP 下发单不回推 165(决策:SAP 维持现状)。 private static bool IsSelfBuilt(string? potype, string? reqBy) { var p = (potype ?? "").Trim(); var r = (reqBy ?? "").Trim(); return string.Equals(p, "po", StringComparison.OrdinalIgnoreCase) && (string.Equals(r, "PO", StringComparison.OrdinalIgnoreCase) || string.Equals(r, "DO", StringComparison.OrdinalIgnoreCase)); } private async Task EnqueueRowAsync( long tenantId, string idem, string action, string table, Dictionary keys, Dictionary insert, Dictionary update, Dictionary? resolve, CancellationToken ct) { var payload = new Dictionary { ["op"] = "UPSERT", ["table"] = table, ["keys"] = keys, ["insert"] = insert, ["update"] = update, ["expect"] = new Dictionary() }; if (resolve != null && resolve.Count > 0) payload["resolve"] = resolve; var item = new MdpOutbox { TenantId = tenantId, TargetSourceCode = TargetSource, ActionCode = action, IdemKey = idem.Length > 200 ? idem[..200] : idem, PayloadJson = JsonSerializer.Serialize(payload) }; return await _enqueue.TryEnqueueOrRefreshAsync(item, ct, pulse: false); } private static string Fmt(DateTime? dt) => (dt ?? DateTime.Now).ToString("yyyy-MM-dd HH:mm:ss"); private static string Trunc(string? s, int max) { if (string.IsNullOrEmpty(s)) return ""; return s.Length <= max ? s : s[..max]; } private sealed class PurOrdMasterRow { public string? Domain { get; set; } public string? PurOrd { get; set; } public string? Potype { get; set; } public string? Typed { get; set; } public string? Supp { get; set; } public string? Buyer { get; set; } public string? ReqBy { get; set; } public string? UsageText { get; set; } public string? FSTID { get; set; } public string? Remark { get; set; } public DateTime? OrdDate { get; set; } public DateTime? DueDate { get; set; } public int IsActive { get; set; } public int IsConfirm { get; set; } public string? CreateUser { get; set; } public DateTime? CreateTime { get; set; } public string? UpdateUser { get; set; } public DateTime? UpdateTime { get; set; } } private sealed class PurOrdDetailRow { public string? Domain { get; set; } public int Line { get; set; } public string? ItemNum { get; set; } public string? Descr { get; set; } public string? UM { get; set; } public string? Rev { get; set; } public string? Drawing { get; set; } public string? Location { get; set; } public string? LotSerial { get; set; } public string? Req { get; set; } public decimal QtyOrded { get; set; } public DateTime? DueDate { get; set; } public DateTime? NeedDate { get; set; } public int IsActive { get; set; } public int IsConfirm { get; set; } public string? CreateUser { get; set; } public DateTime? CreateTime { get; set; } public string? UpdateUser { get; set; } public DateTime? UpdateTime { get; set; } } private sealed class DeliveryScheduleRow { public long Id { get; set; } public string? Domain { get; set; } public long Icdsid { get; set; } public string? Dsnum { get; set; } public string? Status { get; set; } public string? Itemnum { get; set; } public string? Um { get; set; } public string? Purgroup { get; set; } public string? Suppliercode { get; set; } public string? Supplier { get; set; } public DateTime? Submitdate { get; set; } public DateTime? Requestdate { get; set; } public DateTime? Needdate { get; set; } public string? Ponumber { get; set; } public int Poline { get; set; } public decimal Schedqty { get; set; } public string? Remarks { get; set; } public int Isactive { get; set; } public string? Createuser { get; set; } public DateTime? Createtime { get; set; } public string? Updateuser { get; set; } public DateTime? Updatetime { get; set; } } private sealed class ShipmentRow { public long Id { get; set; } public long ShPurchaseId { get; set; } public string? ShPurchaseName { get; set; } public string? ShPurchaseNum { get; set; } public string? ShPurchaseAddress { get; set; } public string? ShPurchaseLxr { get; set; } public string? ShPurchasePhone { get; set; } public string? DeliveryAddress { get; set; } public string? ExpectedConsignee { get; set; } public string? ConsigneePhone { get; set; } public DateTime? EstimatedDeliveryDate { get; set; } public string? PoBillno { get; set; } public string? Shddh { get; set; } public string? Jhshrq { get; set; } public string? Tjrid { get; set; } public string? Tjrxm { get; set; } public string? Tjrq { get; set; } public int Scbq { get; set; } public string? Chbg { get; set; } public int Sfpc { get; set; } public string? Pcsm { get; set; } public string? Wlsc { get; set; } public string? Yjdhrq { get; set; } public int State { get; set; } public string? Shzt { get; set; } public string? Wldh { get; set; } public int Dycs { get; set; } } private sealed class ShipmentLineRow { public long Id { get; set; } public string? Glid { get; set; } public string? ShMaterialCode { get; set; } public string? ShMaterialName { get; set; } public string? ShMaterialGgxh { get; set; } public decimal ShDeliveryQuantity { get; set; } public string? ShMaterialDw { get; set; } public string? Remarks { get; set; } public decimal Bzsl { get; set; } public decimal Bqsl { get; set; } public string? OrderType { get; set; } public string? PoBill { get; set; } public string? PoBillline { get; set; } public int Hh { get; set; } public string? Scrq { get; set; } public string? Scph { get; set; } public string? Th { get; set; } public string? Bbh { get; set; } public decimal Djsl { get; set; } public string? Ccrq { get; set; } public string? Cgyt { get; set; } public string? Jybb { get; set; } public string? Jhdbh { get; set; } public string? Jhdhh { get; set; } public string? Shpc { get; set; } public string? Shzt { get; set; } public decimal Rksl { get; set; } public decimal Thsl { get; set; } } private sealed class BarcodeRow { public string? Domain { get; set; } public string? Site { get; set; } public string? BarCode { get; set; } public string? ItemNum { get; set; } public string? Descr { get; set; } public string? Product { get; set; } public string? Carton { get; set; } public string? OrdNbr { get; set; } public decimal PackingQty { get; set; } public decimal Qty { get; set; } public string? Status { get; set; } public string? Supply { get; set; } public string? LotSerial { get; set; } public int CartonQty { get; set; } public string? SuppLotSerial { get; set; } public string? ShipperNbr { get; set; } public int ShipperLine { get; set; } public DateTime? ProdDate { get; set; } public DateTime? ExpireDate { get; set; } public string? PurOrd { get; set; } public int PurLine { get; set; } public decimal PurQty { get; set; } public string? LabelFormat { get; set; } public string? StandItem { get; set; } public string? EffSize { get; set; } public decimal GP12CheckedQty { get; set; } public decimal NetWeight { get; set; } public string? Remark { get; set; } public string? LevelChar { get; set; } public string? PurOrdDetBatchNbr { get; set; } public string? FirmString5 { get; set; } public string? CreateUser { get; set; } public DateTime? CreateTime { get; set; } public string? UpdateUser { get; set; } public DateTime? UpdateTime { get; set; } } private sealed class ShipmentLabelRow { public long Id { get; set; } public string? Xh { get; set; } public string? Wlbm { get; set; } public string? Scph { get; set; } public string? Shdh { get; set; } public string? Shpc { get; set; } public string? Gysbm { get; set; } public string? Csrq { get; set; } } }