liu
昨天 4ca4444ad33a2a1a51f5c06e78ee9365dfb1eed6
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,6 +19,9 @@
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;
@@ -29,6 +34,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;
@@ -43,6 +51,7 @@
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;
@@ -78,6 +87,8 @@
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmUnitMeasureApi unitMeasureApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmBatchService mesWmBatchService;
@@ -100,13 +111,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);
            // 2. 聚合全程信息(作废码仍正常返回并记录扫码:作废码被扫是重要的防伪信号,不应记为查询失败)
            MesProTracePublicRespVO result = buildRespVO(rawCode, traceCode, entry);
            recordConsumerScan(rawCode, traceCode, entry.sourceType, entry.batchId, true);
            return result;
        } catch (RuntimeException ex) {
@@ -114,6 +125,15 @@
                    entry == null ? null : entry.batchId, false);
            throw ex;
        }
    }
    @Override
    public MesProTracePublicConfigRespVO getPublicConfig() {
        MesProTracePublicConfigRespVO respVO = new MesProTracePublicConfigRespVO();
        respVO.setOperatorName(MesProQrLabelBuilder.resolveOperatorName());
        respVO.setTraceUrl(MesProQrLabelBuilder.resolveTraceUrl());
        respVO.setImported(MesProQrLabelBuilder.isImported());
        return respVO;
    }
    /**
@@ -166,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);
        }
        // ③ 批次码(含临时批次码,getBatchByCode 内部兼容)
        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()))
@@ -219,15 +271,45 @@
        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) {
@@ -333,12 +415,15 @@
            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());
        }
    }
@@ -454,7 +539,20 @@
        };
    }
    private record TraceEntry(String sourceType, Long batchId) {
    /**
     * 追溯入口解析结果
     *
     * @param sourceType 入口类型:BAG / 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());
        }
    }
}