From 4ca4444ad33a2a1a51f5c06e78ee9365dfb1eed6 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期四, 08 十月 2026 17:11:11 +0800
Subject: [PATCH] feat(mes): 种子二维码标签国标四要素合规改造

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java |  248 +++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 229 insertions(+), 19 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
index 744bccc..98da11f 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -2,9 +2,11 @@
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
+import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicConfigRespVO;
 import cn.iocoder.yudao.module.mes.controller.pub.trace.vo.MesProTracePublicRespVO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
@@ -17,13 +19,24 @@
 import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchWorkerTypeEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.trace.MesTraceCodeStatusEnum;
+import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder;
 import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
 import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
 import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
 import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
 import cn.iocoder.yudao.module.mes.service.pro.scan.MesProScanEventService;
+import cn.iocoder.yudao.module.mes.dal.dataobject.trace.consumer.MesTraceConsumerScanEventDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.trace.consumer.MesTraceConsumerScanEventMapper;
 import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
+import cn.iocoder.yudao.module.mes.service.trace.consumer.MesTraceConsumerScanEventService;
+import cn.iocoder.yudao.module.mes.service.trace.crossregion.MesTraceCrossRegionService;
+import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService;
 import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
+import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
+import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import jakarta.annotation.Resource;
@@ -33,8 +46,12 @@
 
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -70,24 +87,76 @@
     @Resource
     private MesMdmItemService mdmItemService;
     @Resource
+    private MdmUnitMeasureApi unitMeasureApi;
+    @Resource
     private AdminUserApi adminUserApi;
     @Resource
     private MesWmBatchService mesWmBatchService;
+    @Resource
+    private MesTraceConsumerScanEventService consumerScanEventService;
+    @Resource
+    private MesTraceCrossRegionService crossRegionService;
+    @Resource
+    private MesTraceIntegrityService integrityService;
+    @Resource
+    private MesTraceConsumerScanEventMapper consumerScanEventMapper;
 
     @Override
     public MesProTracePublicRespVO queryTrace(String code) {
         String rawCode = StrUtil.trim(StrUtil.nullToEmpty(code));
         String traceCode = normalize(rawCode);
         if (StrUtil.isEmpty(traceCode)) {
+            recordConsumerScan(rawCode, traceCode, null, null, false);
             throw exception(MES_TRACE_CODE_NOT_FOUND);
         }
-        // 1. 瑙f瀽杩芥函鍏ュ彛锛屽畾浣嶆壒娆�
-        TraceEntry entry = resolveEntry(traceCode);
-        if (entry.batchId == null) {
-            throw exception(MES_TRACE_CODE_NOT_FOUND);
+        TraceEntry entry = null;
+        try {
+            // 1. 瑙f瀽杩芥函鍏ュ彛锛屽畾浣嶆壒娆★紙琚嬬爜/鎵樼洏宸茬敓鎴愪絾鏈綍鍏ユ壒娆℃椂锛岄檷绾т负浠呰繑鍥炲搧绉嶄俊鎭級
+            entry = resolveEntry(traceCode);
+            if (entry.batchId == null && entry.bagItemId == null) {
+                throw exception(MES_TRACE_CODE_NOT_FOUND);
+            }
+            // 2. 鑱氬悎鍏ㄧ▼淇℃伅锛堜綔搴熺爜浠嶆甯歌繑鍥炲苟璁板綍鎵爜锛氫綔搴熺爜琚壂鏄噸瑕佺殑闃蹭吉淇″彿锛屼笉搴旇涓烘煡璇㈠け璐ワ級
+            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry);
+            recordConsumerScan(rawCode, traceCode, entry.sourceType, entry.batchId, true);
+            return result;
+        } catch (RuntimeException ex) {
+            recordConsumerScan(rawCode, traceCode, entry == null ? null : entry.sourceType,
+                    entry == null ? null : entry.batchId, false);
+            throw ex;
         }
-        // 2. 鑱氬悎鍏ㄧ▼淇℃伅
-        return buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId);
+    }
+
+    @Override
+    public MesProTracePublicConfigRespVO getPublicConfig() {
+        MesProTracePublicConfigRespVO respVO = new MesProTracePublicConfigRespVO();
+        respVO.setOperatorName(MesProQrLabelBuilder.resolveOperatorName());
+        respVO.setTraceUrl(MesProQrLabelBuilder.resolveTraceUrl());
+        respVO.setImported(MesProQrLabelBuilder.isImported());
+        return respVO;
+    }
+
+    /**
+     * 璁板綍娑堣垂鑰呮壂鐮佷簨浠讹紝骞惰Е鍙戦槻浼槻绐滆揣鎵╁睍锛堢獪璐у垽瀹� + 鎵爜鎽樿锛夛紝鍧囧閿欎笉褰卞搷婧簮涓绘祦绋�
+     */
+    private MesTraceConsumerScanEventDO recordConsumerScan(String rawCode, String traceCode, String sourceType,
+                                                           Long batchId, boolean success) {
+        MesTraceConsumerScanEventDO event = consumerScanEventService.recordScan(rawCode, traceCode, sourceType,
+                batchId, success, success ? null : MES_TRACE_CODE_NOT_FOUND.getCode().toString());
+        if (event == null) {
+            return null;
+        }
+        try {
+            crossRegionService.checkAndCreateWarning(event);
+        } catch (Exception ex) {
+            log.warn("[recordConsumerScan][绐滆揣鍒ゅ畾澶辫触] eventId={}", event.getId(), ex);
+        }
+        try {
+            integrityService.recordScanIntegrity(event);
+        } catch (Exception ex) {
+            log.warn("[recordConsumerScan][鎵爜鎽樿鐢熸垚澶辫触] eventId={}", event.getId(), ex);
+        }
+        return event;
     }
 
     // ==================== 杩芥函鐮佽В鏋� ====================
@@ -117,33 +186,65 @@
         // 鈶� 琚嬬爜
         MesProBagCodeDO bag = bagCodeService.getBagCodeByCode(traceCode);
         if (bag != null) {
-            return new TraceEntry("BAG", bag.getBatchId());
+            // 宸蹭綔搴熺殑琚嬬爜锛堢牬鎹熸浛鎹㈠悗浣滃簾锛夊繀椤诲澶栧け鏁堬細鍥芥爣瑕佹眰浜岀淮鐮佷笉寰楅噸澶嶄娇鐢紝
+            // 鑻ヤ綔搴熺爜浠嶈繑鍥炴湁鏁堟函婧愪俊鎭紝浣滃簾鐮佸嵆鍙鍐掔敤閿�鍞�
+            if (MesProBagCodeStatusEnum.CANCELED.getStatus().equals(bag.getStatus())) {
+                return new TraceEntry("BAG", bag.getBatchId(), bag.getItemId(),
+                        MesTraceCodeStatusEnum.CANCELED.getStatus());
+            }
+            return TraceEntry.valid("BAG", bag.getBatchId(), bag.getItemId());
         }
         // 鈶� 鎵樼洏鐮�
         MesProPalletDO pallet = palletService.getPalletByCode(traceCode);
         if (pallet != null) {
-            return new TraceEntry("PALLET", pallet.getBatchId());
+            // 鏈綍鍏ユ壒娆$殑鎵樼洏鍙栨墭鐩樺唴浠讳竴琚嬬爜鍝佺锛屼繚璇佷粛鑳藉睍绀哄搧绉嶄俊鎭�
+            Long itemId = pallet.getBatchId() == null
+                    ? firstItemId(bagCodeService.getBagCodeListByPalletId(pallet.getId())) : null;
+            if (MesProPalletStatusEnum.CANCELED.getStatus().equals(pallet.getStatus())) {
+                return new TraceEntry("PALLET", pallet.getBatchId(), itemId,
+                        MesTraceCodeStatusEnum.CANCELED.getStatus());
+            }
+            return TraceEntry.valid("PALLET", pallet.getBatchId(), itemId);
         }
         // 鈶� 鎵规鐮侊紙鍚复鏃舵壒娆$爜锛実etBatchByCode 鍐呴儴鍏煎锛�
         MesProBatchDO batch = batchService.getBatchByCode(traceCode);
         if (batch != null) {
-            return new TraceEntry("BATCH", batch.getId());
+            if (MesProBatchStatusEnum.CANCELED.getStatus().equals(batch.getStatus())) {
+                return new TraceEntry("BATCH", batch.getId(), null,
+                        MesTraceCodeStatusEnum.CANCELED.getStatus());
+            }
+            return TraceEntry.valid("BATCH", batch.getId(), null);
         }
-        return new TraceEntry("", null);
+        return new TraceEntry("", null, null, MesTraceCodeStatusEnum.VALID.getStatus());
     }
 
     // ==================== 鑱氬悎鏋勫缓 ====================
 
-    private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, String sourceType, Long batchId) {
-        MesProBatchDO batch = batchService.getBatch(batchId);
-        if (batch == null) {
+    private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, TraceEntry entry) {
+        String sourceType = entry.sourceType();
+        Long batchId = entry.batchId();
+        Long bagItemId = entry.bagItemId();
+        MesProBatchDO batch = batchId == null ? null : batchService.getBatch(batchId);
+        if (batchId != null && batch == null) {
             throw exception(MES_TRACE_CODE_NOT_FOUND);
         }
         MesProTracePublicRespVO resp = new MesProTracePublicRespVO();
         resp.setSourceType(sourceType);
         resp.setRequestCode(requestCode);
+        // 杩芥函鐮佹湁鏁堟�� + 鏍囩鍥涜绱犱腑鐨勭敓浜х粡钀ヨ�呬笌杩芥函缃戝潃锛屼繚璇佷笌鏍囩鍗板埛鍐呭涓�鑷�
+        resp.setCodeStatus(entry.codeStatus());
+        resp.setOperatorName(MesProQrLabelBuilder.resolveOperatorName());
+        resp.setTraceUrl(MesProQrLabelBuilder.resolveTraceUrl());
         // 鍝佺鍖哄潡
-        fillItem(resp, batch);
+        fillItem(resp, batch, bagItemId);
+        if (batch == null) {
+            // 琚嬬爜/鎵樼洏宸茬敓鎴愪絾灏氭湭褰曞叆鐢熶骇鎵规锛氬彧杩斿洖鍝佺淇℃伅锛岀敱鍓嶇鎻愮ず鏈粦瀹氭壒娆�
+            resp.setBatchBound(false);
+            fillAntiFake(resp, traceCode);
+            fillTimelineAndDecision(resp);
+            return resp;
+        }
+        resp.setBatchBound(true);
         // 鎵规鍖哄潡
         resp.setBatchCode(batch.getCode()).setTempCode(batch.getTempCode())
                 .setBatchStatus(batch.getStatus()).setBatchStatusName(batchStatusName(batch.getStatus()))
@@ -162,20 +263,53 @@
         resp.setInbound(buildInbound(batchId));
         // 璐ㄦ鍖哄潡
         resp.setQuality(buildQuality(batch));
+        // 闃蹭吉楠岃瘉鍖哄潡
+        fillAntiFake(resp, traceCode);
         // 鍙�夋墿灞曞尯鍧楋紙鏈夋暟鎹墠杩斿洖锛�
         fillOptionalModules(resp, batch);
+        fillTimelineAndDecision(resp);
         return resp;
     }
 
-    private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch) {
-        if (batch.getItemId() == null) {
+    /**
+     * 鍝佺宸蹭笅娌夊埌琚嬬爜鏁版嵁鍖咃細浼樺厛鍙栨壂鐮佽鐮佸搧绉嶏紝鍘嗗彶鎵规鍥為��鎵规鍝佺锛屾壒娆�/鎵樼洏鍏ュ彛鍙栨壒鍐呬换涓�琚嬬爜鍝佺
+     */
+    private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch, Long bagItemId) {
+        Long itemId = bagItemId != null ? bagItemId : (batch == null ? null : batch.getItemId());
+        if (itemId == null && batch != null) {
+            itemId = firstItemId(bagCodeService.getBagCodeListByBatchId(batch.getId()));
+        }
+        if (itemId == null) {
             return;
         }
-        MesMdmItemDO item = mdmItemService.getItem(batch.getItemId());
+        MesMdmItemDO item = mdmItemService.getItem(itemId);
         if (item == null) {
             return;
         }
-        resp.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification());
+        resp.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification())
+                .setItemUnitMeasureName(unitMeasureName(item.getUnitMeasureId()));
+    }
+
+    /**
+     * 璇诲彇琚嬬爜鍒楄〃涓涓�涓潪绌哄搧绉嶇紪鍙�
+     */
+    private Long firstItemId(List<MesProBagCodeDO> bags) {
+        return CollUtil.isEmpty(bags) ? null : bags.stream().map(MesProBagCodeDO::getItemId)
+                .filter(Objects::nonNull).findFirst().orElse(null);
+    }
+
+    /**
+     * 璇诲彇鍝佺涓昏閲忓崟浣嶅悕绉帮紝鏈厤缃崟浣嶆垨鍗曚綅涓嶅瓨鍦ㄦ椂杩斿洖 null
+     */
+    private String unitMeasureName(Long unitMeasureId) {
+        if (unitMeasureId == null) {
+            return null;
+        }
+        CommonResult<MdmUnitMeasureRespDTO> result = unitMeasureApi.getUnitMeasure(unitMeasureId);
+        if (result == null || !result.isSuccess() || result.getData() == null) {
+            return null;
+        }
+        return StrUtil.trimToNull(result.getData().getName());
     }
 
     private List<MesProTracePublicRespVO.WorkerVO> buildWorkers(Long batchId) {
@@ -254,6 +388,25 @@
         return vo;
     }
 
+    /**
+     * 濉厖闃蹭吉楠岃瘉鍖哄潡锛氳杩芥函鐮佺疮璁℃壂鐮佹鏁般�侀娆�/鏈�杩戞壂鐮佹椂闂达紙姝e搧楠岃瘉澧炲己锛�
+     */
+    private void fillAntiFake(MesProTracePublicRespVO resp, String traceCode) {
+        try {
+            List<MesTraceConsumerScanEventDO> events = consumerScanEventMapper.selectListByNormalizedCode(traceCode);
+            if (CollUtil.isEmpty(events)) {
+                return;
+            }
+            MesProTracePublicRespVO.AntiFakeVO vo = new MesProTracePublicRespVO.AntiFakeVO();
+            vo.setScanCount((long) events.size());
+            vo.setFirstScanTime(events.get(0).getEventTime());
+            vo.setLastScanTime(events.get(events.size() - 1).getEventTime());
+            resp.setAntiFake(vo);
+        } catch (Exception ex) {
+            log.warn("[fillAntiFake][杩芥函鐮� {} 闃蹭吉楠岃瘉缁熻澶辫触: {}]", traceCode, ex.getMessage());
+        }
+    }
+
     private void fillOptionalModules(MesProTracePublicRespVO resp, MesProBatchDO batch) {
         try {
             List<MesWmBatchPurchaseSourceRespVO> purchaseSources = mesWmBatchService.getPurchaseSourceList(batch.getCode());
@@ -262,13 +415,57 @@
             resp.setPurchaseSources(CollUtil.isNotEmpty(purchaseSources) ? purchaseSources : List.of());
             resp.setSalesTargets(CollUtil.isNotEmpty(salesTargets) ? salesTargets : List.of());
             resp.setProcessTraces(CollUtil.isNotEmpty(processTraces) ? processTraces : List.of());
+            List<MesWmBatchFlowRespVO> seedFlows = mesWmBatchService.getBatchFlowList(batch.getCode());
+            resp.setSeedFlows(CollUtil.isNotEmpty(seedFlows) ? seedFlows : List.of());
         } catch (Exception ex) {
             // 浠撳簱鎵规浣撶郴锛堥噰璐�/閿�鍞�/娴佽浆锛夋湭鍚敤鏃跺閿欙紝涓嶅奖鍝嶆牳蹇冭拷婧�
             log.warn("[fillOptionalModules][鎵规 {} 鎵╁睍妯″潡鏌ヨ澶辫触: {}]", batch.getCode(), ex.getMessage());
             resp.setPurchaseSources(List.of());
             resp.setSalesTargets(List.of());
             resp.setProcessTraces(List.of());
+            resp.setSeedFlows(List.of());
         }
+    }
+
+    private void fillTimelineAndDecision(MesProTracePublicRespVO resp) {
+        List<MesProTracePublicRespVO.TimelineVO> timeline = new ArrayList<>();
+        if (resp.getProduceDate() != null) {
+            timeline.add(timeline("鐢熶骇", "BATCH_CREATED", "鐢熶骇鎵规寤虹珛", resp.getProduceDate(), null,
+                    resp.getBatchStatusName()));
+        }
+        if (resp.getProcessTraces() != null) {
+            resp.getProcessTraces().forEach(item -> timeline.add(timeline("鍔犲伐", item.getEventType(),
+                    item.getEventType() == null ? "宸ュ簭娴佽浆" : ("IN".equals(item.getEventType()) ? "宸ュ簭鎶曞叆" : "宸ュ簭浜у嚭"),
+                    item.getEventTime(), item.getQuantity(), item.getQualityStatus() == null ? null : String.valueOf(item.getQualityStatus()))));
+        }
+        if (resp.getInbound() != null) {
+            timeline.add(timeline("浠撳偍", "INBOUND", "浠撳偍鍏ュ簱", resp.getInbound().getEventTime(),
+                    resp.getInbound().getQuantity(), resp.getInbound().getStatus()));
+        }
+        if (resp.getSalesTargets() != null) {
+            resp.getSalesTargets().forEach(item -> timeline.add(timeline("閿�鍞�", "OUTBOUND", "閿�鍞嚭搴�",
+                    item.getSalesDate(), item.getQuantity(), item.getQualityStatus() == null ? null : String.valueOf(item.getQualityStatus()))));
+        }
+        timeline.sort(Comparator.comparing(MesProTracePublicRespVO.TimelineVO::getEventTime,
+                Comparator.nullsLast(Comparator.naturalOrder())));
+        resp.setTimeline(timeline);
+
+        MesProTracePublicRespVO.DecisionVO decision = new MesProTracePublicRespVO.DecisionVO();
+        decision.setQualityRisk(resp.getQuality() != null && Integer.valueOf(2).equals(resp.getQuality().getResult()));
+        decision.setInboundQuantity(resp.getInbound() == null ? null : resp.getInbound().getQuantity());
+        decision.setSalesTargetCount(resp.getSalesTargets() == null ? 0 : resp.getSalesTargets().size());
+        decision.setConsumerScanCount(resp.getAntiFake() == null ? 0L : resp.getAntiFake().getScanCount());
+        decision.setRiskLevel(Boolean.TRUE.equals(decision.getQualityRisk()) ? "HIGH" : "NORMAL");
+        resp.setDecision(decision);
+    }
+
+    private MesProTracePublicRespVO.TimelineVO timeline(String stage, String eventType, String eventName,
+                                                         java.time.LocalDateTime eventTime, BigDecimal quantity,
+                                                         String status) {
+        MesProTracePublicRespVO.TimelineVO vo = new MesProTracePublicRespVO.TimelineVO();
+        vo.setStage(stage).setEventType(eventType).setEventName(eventName).setEventTime(eventTime)
+                .setQuantity(quantity).setStatus(status);
+        return vo;
     }
 
     // ==================== 鍚嶇О鏄犲皠 ====================
@@ -342,7 +539,20 @@
         };
     }
 
-    private record TraceEntry(String sourceType, Long batchId) {
+    /**
+     * 杩芥函鍏ュ彛瑙f瀽缁撴灉
+     *
+     * @param sourceType 鍏ュ彛绫诲瀷锛欱AG / PALLET / BATCH锛屾湭鍖归厤鍒版椂涓虹┖涓�
+     * @param batchId    鐢熶骇鎵规缂栧彿
+     * @param bagItemId  琚嬬爜缁戝畾鐨勫搧绉嶇紪鍙凤紙鏈綍鎵规鏃剁敤浜庡睍绀哄搧绉嶏級
+     * @param codeStatus 杩芥函鐮佹湁鏁堟�э細{@link MesTraceCodeStatusEnum#VALID} / {@link MesTraceCodeStatusEnum#CANCELED}
+     */
+    private record TraceEntry(String sourceType, Long batchId, Long bagItemId, String codeStatus) {
+
+        static TraceEntry valid(String sourceType, Long batchId, Long bagItemId) {
+            return new TraceEntry(sourceType, batchId, bagItemId, MesTraceCodeStatusEnum.VALID.getStatus());
+        }
+
     }
 
 }

--
Gitblit v1.9.3