| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538 |
- using System.Text.Json;
- using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
- using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
- using Microsoft.Extensions.Logging;
- using SqlSugar;
- namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
- /// <summary>
- /// S5 IQC P3+P5:检验员提交结果后,按字段级回写 165(MissedPrint / TransHist / 报检分录 / 检验单),
- /// 并对解禁箱码入队 <c>op=PROC</c> 调用 <c>pr_WMS_BPM_SaveInvUpShelf @TransCode=QcCheck</c> 完成质检库存→合格库过账。
- /// 不写 PurOrdRctDetail;库存过账仅经白名单存储过程。
- /// </summary>
- public sealed class IqcWmsResultPushService : ITransient
- {
- public const string TargetSource = "DOPDEMORQ_SQLSERVER";
- public const string ActionMp = "IQC_WMS_MP";
- public const string ActionMpHist = "IQC_WMS_MP_HIST";
- public const string ActionEntry = "IQC_WMS_ENTRY";
- public const string ActionBill = "IQC_WMS_BILL";
- public const string ActionUpShelf = "IQC_WMS_UPSHELF";
- public const string QmsApiAction = "S5_IQC_RESULT_PUSH";
- private readonly ISqlSugarClient _db;
- private readonly MdpOutboxEnqueueService _enqueue;
- private readonly MdpOutboxWakeSignal _wake;
- private readonly ILogger<IqcWmsResultPushService> _logger;
- public IqcWmsResultPushService(
- ISqlSugarClient db,
- MdpOutboxEnqueueService enqueue,
- MdpOutboxWakeSignal wake,
- ILogger<IqcWmsResultPushService> logger)
- {
- _db = db;
- _enqueue = enqueue;
- _wake = wake;
- _logger = logger;
- }
- /// <summary>
- /// 提交结果后入队;失败不抛(不阻断主流程)。同时保留 QMS_API 占位分支。
- /// </summary>
- public async Task TryEnqueueAfterSubmitAsync(long billId, long tenantId, CancellationToken ct = default)
- {
- try
- {
- await EnqueueCoreAsync(billId, tenantId, ct);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "[IqcWmsPush] 入队失败 billId={BillId}", billId);
- }
- }
- private async Task EnqueueCoreAsync(long billId, long tenantId, CancellationToken ct)
- {
- var bill = await _db.Ado.SqlQuerySingleAsync<BillRow>(
- """
- SELECT
- id AS Id, FBILLNO AS BillNo, lydjbh AS ApplyBillNo, hid AS Hid,
- pd AS Pd, dhsl AS Dhsl, bhgsl AS Bhgsl, clfs AS Clfs,
- FMATERIALCFG AS MaterialCode, jyr AS Inspector
- FROM qms_qcp_inspbill
- WHERE id = @Id AND tenant_id = @TenantId
- LIMIT 1
- """,
- new SugarParameter("@Id", billId),
- new SugarParameter("@TenantId", tenantId));
- if (bill == null) return;
- // 保留 QMS_API 占位(真实地址落地后可双投)
- await TryEnqueueQmsApiAsync(bill, tenantId, ct);
- var entry = await _db.Ado.SqlQuerySingleAsync<EntryRow>(
- """
- SELECT
- id AS Id, FSRCORDERNUM AS Receiver, FMATERIALCFG AS ItemNum,
- FAPPLYQTY AS ApplyQty
- FROM qms_qcp_insappnentry
- WHERE id = @Hid AND tenant_id = @TenantId
- LIMIT 1
- """,
- new SugarParameter("@Hid", bill.Hid ?? 0),
- new SugarParameter("@TenantId", tenantId));
- if (entry == null || string.IsNullOrWhiteSpace(entry.Receiver) || string.IsNullOrWhiteSpace(entry.ItemNum))
- {
- _logger.LogWarning(
- "[IqcWmsPush] 报检分录缺失 Receiver/ItemNum,跳过 165 推送 bill={BillNo}", bill.BillNo);
- return;
- }
- var domain = "8010";
- var qcNbr = string.IsNullOrWhiteSpace(bill.ApplyBillNo) ? bill.BillNo : bill.ApplyBillNo!;
- var userNo = string.IsNullOrWhiteSpace(bill.Inspector) ? "AIDOP" : bill.Inspector!;
- var now = DateTime.Now;
- // Ai-DOP clfs(0让步/1挑选/2报废) → 旧 DOP MRB 矩阵
- var (status, qtyAccFactor, qtyRejectFactor, resultDescr) = MapResult(bill.Pd, bill.Clfs);
- // 入队前预检:本库箱码(已与 165 同步)
- var barcodes = await _db.Ado.SqlQueryAsync<BarcodeRow>(
- """
- SELECT BarCode, Qty, Status, IFNULL(Domain,'8010') AS Domain
- FROM MissedPrint
- WHERE IFNULL(Domain,'') IN ('', @Domain)
- AND RctNbr = @Receiver AND ItemNum = @ItemNum
- AND IFNULL(Status,'') <> 'C'
- AND IFNULL(Type,'') <> 'Card'
- """,
- new SugarParameter("@Domain", domain),
- new SugarParameter("@Receiver", entry.Receiver),
- new SugarParameter("@ItemNum", entry.ItemNum));
- if (barcodes == null || barcodes.Count == 0)
- {
- _logger.LogWarning(
- "[IqcWmsPush] 无箱码可推 Receiver={Receiver} Item={Item}", entry.Receiver, entry.ItemNum);
- return;
- }
- var sumQty = barcodes.Sum(x => x.Qty);
- var qtyReceived = await _db.Ado.SqlQuerySingleAsync<decimal?>(
- """
- SELECT SUM(IFNULL(QtyReceived,0))
- FROM PurOrdRctDetail
- WHERE IFNULL(Domain,'') IN ('', @Domain)
- AND Receiver = @Receiver AND ItemNum = @ItemNum
- """,
- new SugarParameter("@Domain", domain),
- new SugarParameter("@Receiver", entry.Receiver),
- new SugarParameter("@ItemNum", entry.ItemNum)) ?? 0m;
- // 数量平衡:用检验数量口径(合格分支 dhsl 应等于收货数)
- var checkQty = (bill.Dhsl ?? 0m) + (bill.Bhgsl ?? 0m);
- if (checkQty <= 0) checkQty = entry.ApplyQty ?? 0m;
- if (qtyReceived > 0 && checkQty > 0 && checkQty != qtyReceived)
- {
- _logger.LogWarning(
- "[IqcWmsPush] 数量不平衡 check={Check} received={Recv},仍尝试推送(箱码侧)",
- checkQty, qtyReceived);
- }
- if (sumQty > 0 && qtyReceived > 0 && sumQty != qtyReceived)
- {
- _logger.LogWarning(
- "[IqcWmsPush] 箱码总数与收货数不一致 sum={Sum} received={Recv}", sumQty, qtyReceived);
- }
- // 待上架箱码(N=合格库 / R=不良品库);KEEP 不解禁也不上架
- var upShelfTargets = new List<(BarcodeRow Bc, string NewStatus)>();
- foreach (var bc in barcodes)
- {
- if (string.IsNullOrWhiteSpace(bc.BarCode)) continue;
- var qtyAccN = qtyAccFactor ? bc.Qty : 0m;
- var qtyRejectN = qtyRejectFactor ? bc.Qty : 0m;
- var newStatus = status;
- // 挑选且无箱码清单:旧系统不解禁任何箱码 → 保持原状态
- if (status == "KEEP") newStatus = string.IsNullOrWhiteSpace(bc.Status) ? "I" : bc.Status!;
- var mpPayload = JsonSerializer.Serialize(new
- {
- op = "UPDATE",
- table = "MissedPrint",
- keys = new Dictionary<string, object?>
- {
- ["Domain"] = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain,
- ["BarCode"] = bc.BarCode
- },
- update = new Dictionary<string, object?>
- {
- ["Status"] = newStatus,
- ["QtyAccN"] = qtyAccN,
- ["QtyRejectN"] = qtyRejectN,
- ["UpdateUser"] = userNo,
- ["UpdateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss")
- }
- });
- await _enqueue.TryEnqueueAsync(new MdpOutbox
- {
- TenantId = tenantId,
- TargetSourceCode = TargetSource,
- ActionCode = ActionMp,
- IdemKey = $"iqc|mp|{qcNbr}|{bc.BarCode}",
- PayloadJson = mpPayload
- }, ct, pulse: false);
- var histPayload = JsonSerializer.Serialize(new
- {
- op = "INSERT",
- table = "MissedPrintTransHist",
- keys = new Dictionary<string, object?>
- {
- ["Domain"] = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain,
- ["BarCode"] = bc.BarCode,
- ["TransType"] = "来料检验-通过",
- ["CreateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss.fff")
- },
- insert = new Dictionary<string, object?>
- {
- ["Domain"] = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain,
- ["ItemNum"] = entry.ItemNum,
- ["BarCode"] = bc.BarCode,
- ["Qty"] = bc.Qty,
- ["OrdNbr"] = null,
- ["Remark"] = "来料检验单:" + qcNbr,
- ["RctNbr"] = entry.Receiver,
- ["Status"] = newStatus,
- ["TransType"] = bill.Pd == 0 ? "来料检验-通过" : $"来料检验-{resultDescr}",
- ["CreateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss"),
- ["CreateUser"] = userNo,
- ["UpdateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss"),
- ["UpdateUser"] = userNo
- }
- });
- await _enqueue.TryEnqueueAsync(new MdpOutbox
- {
- TenantId = tenantId,
- TargetSourceCode = TargetSource,
- ActionCode = ActionMpHist,
- IdemKey = $"iqc|mph|{qcNbr}|{bc.BarCode}|{now:yyyyMMddHHmmss}",
- PayloadJson = histPayload
- }, ct, pulse: false);
- if (newStatus is "N" or "R")
- upShelfTargets.Add((bc, newStatus));
- }
- // 报检分录状态
- var entryPayload = JsonSerializer.Serialize(new
- {
- op = "UPDATE",
- table = "qms_qcp_insappnentry",
- keys = new Dictionary<string, object?> { ["id"] = entry.Id },
- update = new Dictionary<string, object?>
- {
- ["FINSPECTSTATUS"] = "检验完成",
- ["jywcsj"] = now.ToString("yyyy-MM-dd HH:mm:ss")
- }
- });
- await _enqueue.TryEnqueueAsync(new MdpOutbox
- {
- TenantId = tenantId,
- TargetSourceCode = TargetSource,
- ActionCode = ActionEntry,
- IdemKey = $"iqc|entry|{entry.Id}|{qcNbr}",
- PayloadJson = entryPayload
- }, ct, pulse: false);
- // 检验单推 165(含本库 id;用 FBILLNO 做自然键 UPSERT)
- var billPayload = JsonSerializer.Serialize(new
- {
- op = "UPSERT",
- table = "qms_qcp_inspbill",
- keys = new Dictionary<string, object?> { ["FBILLNO"] = bill.BillNo },
- insert = new Dictionary<string, object?>
- {
- ["id"] = bill.Id,
- ["FBILLNO"] = bill.BillNo,
- ["lydjbh"] = bill.ApplyBillNo,
- ["hid"] = entry.Id,
- ["FMATERIALCFG"] = bill.MaterialCode,
- ["pd"] = bill.Pd,
- ["dhsl"] = bill.Dhsl,
- ["bhgsl"] = bill.Bhgsl,
- ["clfs"] = bill.Clfs,
- ["jyr"] = bill.Inspector,
- ["FRINSQTY"] = entry.ApplyQty
- },
- update = new Dictionary<string, object?>
- {
- ["pd"] = bill.Pd,
- ["dhsl"] = bill.Dhsl,
- ["bhgsl"] = bill.Bhgsl,
- ["clfs"] = bill.Clfs,
- ["jyr"] = bill.Inspector
- }
- });
- // 无上架候选时由 bill 唤醒;有候选时由最后一条 UPSHELF 唤醒
- await _enqueue.TryEnqueueAsync(new MdpOutbox
- {
- TenantId = tenantId,
- TargetSourceCode = TargetSource,
- ActionCode = ActionBill,
- IdemKey = $"iqc|bill|{bill.BillNo}",
- PayloadJson = billPayload
- }, ct, pulse: upShelfTargets.Count == 0);
- // 本库同步置检验完成(写回过程在旧系统自动写;Ai-DOP 权威侧先落本地)
- await _db.Ado.ExecuteCommandAsync(
- """
- UPDATE qms_qcp_insappnentry
- SET FINSPECTSTATUS = '检验完成', jywcsj = @Now
- WHERE id = @Id AND tenant_id = @TenantId
- """,
- new SugarParameter("@Now", now.ToString("yyyy-MM-dd HH:mm:ss")),
- new SugarParameter("@Id", entry.Id),
- new SugarParameter("@TenantId", tenantId));
- var upShelfEnqueued = await EnqueueUpShelfAsync(
- upShelfTargets, entry, domain, qcNbr!, userNo, tenantId, ct);
- // 有候选但全部跳过(暂收 / 无默认库位)时 bill 未 pulse,补唤醒
- if (upShelfTargets.Count > 0 && upShelfEnqueued == 0)
- _wake.Pulse();
- }
- /// <summary>
- /// 合格/让步 → ItemMaster.Location;退货 → PurOrdControl.RejectInspectionLocation(165 resolve)。
- /// 暂收路径(ReceiptQty>0)不入队,避免与 165 IQC 自带上架重复(任务书 U-F)。
- /// </summary>
- /// <returns>实际入队条数。</returns>
- private async Task<int> EnqueueUpShelfAsync(
- List<(BarcodeRow Bc, string NewStatus)> targets,
- EntryRow entry,
- string domain,
- string qcNbr,
- string userNo,
- long tenantId,
- CancellationToken ct)
- {
- if (targets.Count == 0) return 0;
- var receiptQty = await _db.Ado.SqlQuerySingleAsync<decimal?>(
- """
- SELECT SUM(IFNULL(d.ReceiptQty, 0))
- FROM PurOrdRctDetail r
- INNER JOIN PurOrdDetail d
- ON IFNULL(d.Domain,'') IN ('', @Domain)
- AND d.PurOrd = r.OrdNbr AND d.Line = r.OrdLine
- WHERE IFNULL(r.Domain,'') IN ('', @Domain)
- AND r.Receiver = @Receiver AND r.ItemNum = @ItemNum
- """,
- new SugarParameter("@Domain", domain),
- new SugarParameter("@Receiver", entry.Receiver!),
- new SugarParameter("@ItemNum", entry.ItemNum!)) ?? 0m;
- if (receiptQty > 0)
- {
- _logger.LogWarning(
- "[IqcWmsPush] ReceiptQty={Qty}>0(暂收路径),跳过 QcCheck 上架入队 Receiver={Receiver}",
- receiptQty, entry.Receiver);
- return 0;
- }
- string? passShelf = null;
- if (targets.Any(t => t.NewStatus == "N"))
- {
- passShelf = await _db.Ado.SqlQuerySingleAsync<string>(
- """
- SELECT Location
- FROM ItemMaster
- WHERE ItemNum = @ItemNum
- AND IFNULL(Domain,'') IN ('', @Domain)
- LIMIT 1
- """,
- new SugarParameter("@ItemNum", entry.ItemNum!),
- new SugarParameter("@Domain", domain));
- if (string.IsNullOrWhiteSpace(passShelf))
- {
- _logger.LogWarning(
- "[IqcWmsPush] ItemMaster.Location 为空,跳过合格品上架 Item={Item}", entry.ItemNum);
- }
- }
- var enqueued = 0;
- var pendingPulse = false;
- for (var i = 0; i < targets.Count; i++)
- {
- var (bc, newStatus) = targets[i];
- var bcDomain = string.IsNullOrWhiteSpace(bc.Domain) ? domain : bc.Domain!;
- Dictionary<string, object?> args;
- Dictionary<string, object>? resolve;
- if (newStatus == "N")
- {
- if (string.IsNullOrWhiteSpace(passShelf)) continue;
- args = new Dictionary<string, object?>
- {
- ["Domain"] = bcDomain,
- ["Shelf"] = passShelf,
- ["TransCode"] = "QcCheck",
- ["UserNo"] = userNo,
- ["IsProcCall"] = 1
- };
- resolve = new Dictionary<string, object>
- {
- ["Details"] = new Dictionary<string, object>
- {
- ["table"] = "MissedPrint",
- ["column"] = "RecID",
- ["match"] = new Dictionary<string, object?>
- {
- ["Domain"] = bcDomain,
- ["BarCode"] = bc.BarCode
- }
- }
- };
- }
- else // R:不良品库位由 165 PurOrdControl 现查
- {
- args = new Dictionary<string, object?>
- {
- ["Domain"] = bcDomain,
- ["TransCode"] = "QcCheck",
- ["UserNo"] = userNo,
- ["IsProcCall"] = 1
- };
- resolve = new Dictionary<string, object>
- {
- ["Details"] = new Dictionary<string, object>
- {
- ["table"] = "MissedPrint",
- ["column"] = "RecID",
- ["match"] = new Dictionary<string, object?>
- {
- ["Domain"] = bcDomain,
- ["BarCode"] = bc.BarCode
- }
- },
- ["Shelf"] = new Dictionary<string, object>
- {
- ["table"] = "PurOrdControl",
- ["column"] = "RejectInspectionLocation",
- ["match"] = new Dictionary<string, object?> { ["Domain"] = bcDomain }
- }
- };
- }
- var payload = JsonSerializer.Serialize(new
- {
- op = "PROC",
- proc = "pr_WMS_BPM_SaveInvUpShelf",
- args,
- resolve,
- output = "ReturnMsg",
- success = "成功",
- idempotent = new[] { "已是此货架", "已是此位置" }
- });
- // 先不 pulse,循环结束后对最后成功入队的一条补唤醒(避免中间 continue 导致未 pulse)
- var ok = await _enqueue.TryEnqueueAsync(new MdpOutbox
- {
- TenantId = tenantId,
- TargetSourceCode = TargetSource,
- ActionCode = ActionUpShelf,
- IdemKey = $"iqc|upshelf|{qcNbr}|{bc.BarCode}",
- PayloadJson = payload
- }, ct, pulse: false);
- if (ok)
- {
- enqueued++;
- pendingPulse = true;
- }
- }
- if (pendingPulse) _wake.Pulse();
- return enqueued;
- }
- private async Task TryEnqueueQmsApiAsync(BillRow bill, long tenantId, CancellationToken ct)
- {
- var billNo = string.IsNullOrWhiteSpace(bill.BillNo) ? bill.Id.ToString() : bill.BillNo!;
- var payload = JsonSerializer.Serialize(new
- {
- path = "/iqc/result",
- method = "POST",
- body = new
- {
- billId = bill.Id,
- billNo,
- pd = bill.Pd,
- dhsl = bill.Dhsl,
- bhgsl = bill.Bhgsl,
- action = QmsApiAction
- }
- });
- await _enqueue.TryEnqueueAsync(new MdpOutbox
- {
- TenantId = tenantId,
- TargetSourceCode = "QMS_API",
- ActionCode = QmsApiAction,
- IdemKey = billNo,
- PayloadJson = payload
- }, ct, pulse: false);
- }
- /// <summary>
- /// 返回 (箱码Status, 是否QtyAccN=Qty, 是否QtyRejectN=Qty, 描述)。
- /// Status=KEEP 表示挑选且无箱码清单时保持原状态。
- /// </summary>
- private static (string Status, bool QtyAcc, bool QtyReject, string Descr) MapResult(int? pd, long? clfs)
- {
- if (pd == 0)
- return ("N", true, false, "通过");
- // Ai-DOP:0让步 / 1挑选 / 2报废
- return clfs switch
- {
- 0 => ("N", true, false, "MRB让步接收"),
- 1 => ("KEEP", false, false, "MRB挑选使用"),
- 2 => ("R", false, true, "MRB退货"),
- _ => ("R", false, true, "退货")
- };
- }
- private sealed class BillRow
- {
- public long Id { get; set; }
- public string? BillNo { get; set; }
- public string? ApplyBillNo { get; set; }
- public long? Hid { get; set; }
- public int? Pd { get; set; }
- public decimal? Dhsl { get; set; }
- public decimal? Bhgsl { get; set; }
- public long? Clfs { get; set; }
- public string? MaterialCode { get; set; }
- public string? Inspector { get; set; }
- }
- private sealed class EntryRow
- {
- public long Id { get; set; }
- public string? Receiver { get; set; }
- public string? ItemNum { get; set; }
- public decimal? ApplyQty { get; set; }
- }
- private sealed class BarcodeRow
- {
- public string? BarCode { get; set; }
- public decimal Qty { get; set; }
- public string? Status { get; set; }
- public string? Domain { get; set; }
- }
- }
|