云
9 小时以前 8ea92030610fd1e62136f7b26e10d5396359d8f8
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -2,6 +2,7 @@
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;
@@ -29,6 +30,9 @@
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;
@@ -38,8 +42,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;
@@ -75,6 +83,8 @@
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmUnitMeasureApi unitMeasureApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmBatchService mesWmBatchService;
@@ -97,13 +107,13 @@
        }
        TraceEntry entry = null;
        try {
            // 1. 解析追溯入口,定位批次
            // 1. 解析追溯入口,定位批次(袋码/托盘已生成但未录入批次时,降级为仅返回品种信息)
            entry = resolveEntry(traceCode);
            if (entry.batchId == null) {
            if (entry.batchId == null && entry.bagItemId == null) {
                throw exception(MES_TRACE_CODE_NOT_FOUND);
            }
            // 2. 聚合全程信息
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId);
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId, entry.bagItemId);
            recordConsumerScan(rawCode, traceCode, entry.sourceType, entry.batchId, true);
            return result;
        } catch (RuntimeException ex) {
@@ -163,33 +173,44 @@
        // ① 袋码
        MesProBagCodeDO bag = bagCodeService.getBagCodeByCode(traceCode);
        if (bag != null) {
            return new TraceEntry("BAG", bag.getBatchId());
            return new TraceEntry("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;
            return new TraceEntry("PALLET", pallet.getBatchId(), itemId);
        }
        // ③ 批次码(含临时批次码,getBatchByCode 内部兼容)
        MesProBatchDO batch = batchService.getBatchByCode(traceCode);
        if (batch != null) {
            return new TraceEntry("BATCH", batch.getId());
            return new TraceEntry("BATCH", batch.getId(), null);
        }
        return new TraceEntry("", null);
        return new TraceEntry("", null, null);
    }
    // ==================== 聚合构建 ====================
    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, String sourceType, Long batchId, Long 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);
        // 品种区块
        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()))
@@ -212,18 +233,49 @@
        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) {
@@ -329,13 +381,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;
    }
    // ==================== 名称映射 ====================
@@ -409,7 +505,7 @@
        };
    }
    private record TraceEntry(String sourceType, Long batchId) {
    private record TraceEntry(String sourceType, Long batchId, Long bagItemId) {
    }
}