2 天以前 1ba0dbfde5634c8bf2ec20891d6b226b996f6b59
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -22,7 +22,12 @@
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.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -33,6 +38,9 @@
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.Set;
@@ -73,21 +81,62 @@
    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. 解析追溯入口,定位批次
        TraceEntry entry = resolveEntry(traceCode);
        if (entry.batchId == null) {
            throw exception(MES_TRACE_CODE_NOT_FOUND);
        TraceEntry entry = null;
        try {
            // 1. 解析追溯入口,定位批次
            entry = resolveEntry(traceCode);
            if (entry.batchId == null) {
                throw exception(MES_TRACE_CODE_NOT_FOUND);
            }
            // 2. 聚合全程信息
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId);
            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);
    }
    /**
     * 记录消费者扫码事件,并触发防伪防窜货扩展(窜货判定 + 扫码摘要),均容错不影响溯源主流程
     */
    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;
    }
    // ==================== 追溯码解析 ====================
@@ -162,8 +211,11 @@
        resp.setInbound(buildInbound(batchId));
        // 质检区块
        resp.setQuality(buildQuality(batch));
        // 防伪验证区块
        fillAntiFake(resp, traceCode);
        // 可选扩展区块(有数据才返回)
        fillOptionalModules(resp, batch);
        fillTimelineAndDecision(resp);
        return resp;
    }
@@ -254,6 +306,25 @@
        return vo;
    }
    /**
     * 填充防伪验证区块:该追溯码累计扫码次数、首次/最近扫码时间(正品验证增强)
     */
    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());
@@ -271,6 +342,47 @@
        }
    }
    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;
    }
    // ==================== 名称映射 ====================
    private String userName(Long userId) {