package cn.iocoder.yudao.module.mes.service.trace; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; 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.MesProTracePublicRespVO; import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO; 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.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.service.qc.seedquality.MesQcSeedQualityService; 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; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.MES_TRACE_CODE_NOT_FOUND; /** * 公开扫码溯源 Service 实现 *

* 追溯码解析顺序:袋码 → 托盘码 → 批次码(含临时批次码)。以生产批次为主数据源聚合全程信息。 */ @Service @Validated @Slf4j public class MesProTracePublicServiceImpl implements MesProTracePublicService { /** * H5 袋码列表展示上限(全量统计 + 限量明细) */ private static final int BAG_LIST_LIMIT = 100; @Resource private MesProBagCodeService bagCodeService; @Resource private MesProPalletService palletService; @Resource private MesProBatchService batchService; @Resource private MesProScanEventService scanEventService; @Resource private MesQcSeedQualityService qcSeedQualityService; @Resource private MesMdmItemService mdmItemService; @Resource private AdminUserApi adminUserApi; @Resource private MesWmBatchService mesWmBatchService; @Override public MesProTracePublicRespVO queryTrace(String code) { String rawCode = StrUtil.trim(StrUtil.nullToEmpty(code)); String traceCode = normalize(rawCode); if (StrUtil.isEmpty(traceCode)) { throw exception(MES_TRACE_CODE_NOT_FOUND); } // 1. 解析追溯入口,定位批次 TraceEntry entry = resolveEntry(traceCode); if (entry.batchId == null) { throw exception(MES_TRACE_CODE_NOT_FOUND); } // 2. 聚合全程信息 return buildRespVO(rawCode, traceCode, entry.sourceType, entry.batchId); } // ==================== 追溯码解析 ==================== /** * 兼容整 URL 扫码(二维码内容为 H5 地址时),仅提取 code 参数;否则原样返回 */ private String normalize(String code) { if (StrUtil.startWithIgnoreCase(code, "http")) { String query = StrUtil.subAfter(code, "?", false); if (StrUtil.isNotEmpty(query)) { // 去掉 # 锚点 query = StrUtil.subBefore(query, "#", false); for (String pair : StrUtil.split(query, "&")) { String[] kv = pair.split("=", 2); if (kv.length == 2 && "code".equals(kv[0])) { return StrUtil.trim(URLDecoder.decode(kv[1], StandardCharsets.UTF_8)); } } } return ""; } return code; } private TraceEntry resolveEntry(String traceCode) { // ① 袋码 MesProBagCodeDO bag = bagCodeService.getBagCodeByCode(traceCode); if (bag != null) { return new TraceEntry("BAG", bag.getBatchId()); } // ② 托盘码 MesProPalletDO pallet = palletService.getPalletByCode(traceCode); if (pallet != null) { return new TraceEntry("PALLET", pallet.getBatchId()); } // ③ 批次码(含临时批次码,getBatchByCode 内部兼容) MesProBatchDO batch = batchService.getBatchByCode(traceCode); if (batch != null) { return new TraceEntry("BATCH", batch.getId()); } return new TraceEntry("", null); } // ==================== 聚合构建 ==================== private MesProTracePublicRespVO buildRespVO(String requestCode, String traceCode, String sourceType, Long batchId) { MesProBatchDO batch = batchService.getBatch(batchId); if (batch == null) { throw exception(MES_TRACE_CODE_NOT_FOUND); } MesProTracePublicRespVO resp = new MesProTracePublicRespVO(); resp.setSourceType(sourceType); resp.setRequestCode(requestCode); // 品种区块 fillItem(resp, batch); // 批次区块 resp.setBatchCode(batch.getCode()).setTempCode(batch.getTempCode()) .setBatchStatus(batch.getStatus()).setBatchStatusName(batchStatusName(batch.getStatus())) .setProduceDate(batch.getProduceDate()).setLineCode(batch.getLineCode()).setLineName(batch.getLineName()) .setOwnerUserId(batch.getOwnerUserId()).setOwnerUserName(userName(batch.getOwnerUserId())) .setPlanQuantity(batch.getPlanQuantity()).setBagQuantity(batch.getBagQuantity()) .setPalletQuantity(batch.getPalletQuantity()).setInboundTime(batch.getInboundTime()) .setRemark(batch.getRemark()); // 人员区块 resp.setWorkers(buildWorkers(batchId)); // 托盘区块 fillPallets(resp, batchId); // 袋码区块 fillBags(resp, batchId); // 入库事件区块 resp.setInbound(buildInbound(batchId)); // 质检区块 resp.setQuality(buildQuality(batch)); // 可选扩展区块(有数据才返回) fillOptionalModules(resp, batch); return resp; } private void fillItem(MesProTracePublicRespVO resp, MesProBatchDO batch) { if (batch.getItemId() == null) { return; } MesMdmItemDO item = mdmItemService.getItem(batch.getItemId()); if (item == null) { return; } resp.setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification()); } private List buildWorkers(Long batchId) { List workers = batchService.getBatchWorkerListByBatchId(batchId); if (CollUtil.isEmpty(workers)) { return List.of(); } Set userIds = convertSet(workers, MesProBatchWorkerDO::getUserId); Map userMap = adminUserApi.getUserMap(userIds); return convertList(workers, worker -> { MesProTracePublicRespVO.WorkerVO vo = new MesProTracePublicRespVO.WorkerVO(); vo.setWorkerType(worker.getWorkerType()); vo.setWorkerTypeName(workerTypeName(worker.getWorkerType())); vo.setRemark(worker.getRemark()); AdminUserRespDTO user = userMap.get(worker.getUserId()); vo.setUserName(user == null ? null : user.getNickname()); return vo; }); } private void fillPallets(MesProTracePublicRespVO resp, Long batchId) { List pallets = palletService.getPalletListByBatchId(batchId); resp.setPalletCount(CollUtil.isEmpty(pallets) ? 0 : pallets.size()); resp.setPallets(convertList(pallets, pallet -> { MesProTracePublicRespVO.PalletVO vo = new MesProTracePublicRespVO.PalletVO(); vo.setCode(pallet.getCode()).setPalletNo(pallet.getPalletNo()) .setBagCount(pallet.getBagCount()).setCapacity(pallet.getCapacity()); return vo; })); } private void fillBags(MesProTracePublicRespVO resp, Long batchId) { List bags = bagCodeService.getBagCodeListByBatchId(batchId); List safeBags = bags == null ? List.of() : bags; resp.setBagTotal((long) safeBags.size()); resp.setBags(convertList(safeBags.stream().limit(BAG_LIST_LIMIT).toList(), bag -> { MesProTracePublicRespVO.BagVO vo = new MesProTracePublicRespVO.BagVO(); vo.setCode(bag.getCode()).setBagNo(bag.getBagNo()).setPalletCode(bag.getPalletCode()) .setStatus(bag.getStatus()).setStatusName(bagCodeStatusName(bag.getStatus())) .setInboundTime(bag.getInboundTime()); return vo; })); } private MesProTracePublicRespVO.InboundVO buildInbound(Long batchId) { List events = scanEventService.getByBatchId(batchId); if (CollUtil.isEmpty(events)) { return null; } MesProScanEventDO event = events.get(0); MesProTracePublicRespVO.InboundVO vo = new MesProTracePublicRespVO.InboundVO(); vo.setEventId(event.getEventId()).setDeviceCode(event.getDeviceCode()).setLineCode(event.getLineCode()) .setQuantity(event.getQuantity()).setStatus(event.getStatus()).setEventTime(event.getEventTime()); return vo; } private MesProTracePublicRespVO.QualityVO buildQuality(MesProBatchDO batch) { List list = qcSeedQualityService.getQualityByBatchCode(batch.getCode()); if (CollUtil.isEmpty(list)) { return null; } MesQcSeedQualityDO quality = list.get(0); MesProTracePublicRespVO.QualityVO vo = new MesProTracePublicRespVO.QualityVO(); vo.setResult(quality.getResult()).setResultName(qualityResultName(quality.getResult())) .setReason(quality.getReason()).setInspectorName(userName(quality.getInspectorId())) .setInspectedAt(quality.getInspectedAt()); List indicators = qcSeedQualityService.getIndicatorsByQualityId(quality.getId()); vo.setIndicators(convertList(indicators, ind -> { MesProTracePublicRespVO.IndicatorVO i = new MesProTracePublicRespVO.IndicatorVO(); i.setIndicatorCode(ind.getIndicatorCode()).setIndicatorName(indicatorName(ind.getIndicatorCode())) .setValue(ind.getValue()).setMinValue(ind.getMinValue()).setMaxValue(ind.getMaxValue()); i.setPass(ind.getValue() != null && ind.getMinValue() != null && ind.getMaxValue() != null && ind.getValue().compareTo(ind.getMinValue()) >= 0 && ind.getValue().compareTo(ind.getMaxValue()) <= 0); return i; })); return vo; } private void fillOptionalModules(MesProTracePublicRespVO resp, MesProBatchDO batch) { try { List purchaseSources = mesWmBatchService.getPurchaseSourceList(batch.getCode()); List salesTargets = mesWmBatchService.getSalesTargetList(batch.getCode()); List processTraces = mesWmBatchService.getProcessTraceList(batch.getCode()); resp.setPurchaseSources(CollUtil.isNotEmpty(purchaseSources) ? purchaseSources : List.of()); resp.setSalesTargets(CollUtil.isNotEmpty(salesTargets) ? salesTargets : List.of()); resp.setProcessTraces(CollUtil.isNotEmpty(processTraces) ? processTraces : List.of()); } catch (Exception ex) { // 仓库批次体系(采购/销售/流转)未启用时容错,不影响核心追溯 log.warn("[fillOptionalModules][批次 {} 扩展模块查询失败: {}]", batch.getCode(), ex.getMessage()); resp.setPurchaseSources(List.of()); resp.setSalesTargets(List.of()); resp.setProcessTraces(List.of()); } } // ==================== 名称映射 ==================== private String userName(Long userId) { if (userId == null) { return null; } AdminUserRespDTO user = adminUserApi.getUser(userId); return user == null ? null : user.getNickname(); } private String batchStatusName(Integer status) { if (status == null) { return ""; } for (MesProBatchStatusEnum e : MesProBatchStatusEnum.values()) { if (e.getStatus().equals(status)) { return e.getName(); } } return String.valueOf(status); } private String bagCodeStatusName(Integer status) { if (status == null) { return ""; } for (MesProBagCodeStatusEnum e : MesProBagCodeStatusEnum.values()) { if (e.getStatus().equals(status)) { return e.getName(); } } return String.valueOf(status); } private String workerTypeName(Integer workerType) { if (workerType == null) { return ""; } for (MesProBatchWorkerTypeEnum e : MesProBatchWorkerTypeEnum.values()) { if (e.getType().equals(workerType)) { return e.getName(); } } return String.valueOf(workerType); } private String qualityResultName(Integer result) { if (result == null) { return ""; } return switch (result) { case 0 -> "处理中"; case 1 -> "合格"; case 2 -> "不合格"; default -> String.valueOf(result); }; } private String indicatorName(String code) { if (code == null) { return ""; } return switch (code) { case "purity" -> "纯度"; case "cleanliness" -> "净度"; case "germination_rate" -> "发芽率"; case "moisture" -> "水分"; default -> code; }; } private record TraceEntry(String sourceType, Long batchId) { } }