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 实现
|
* <p>
|
* 追溯码解析顺序:袋码 → 托盘码 → 批次码(含临时批次码)。以生产批次为主数据源聚合全程信息。
|
*/
|
@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<MesProTracePublicRespVO.WorkerVO> buildWorkers(Long batchId) {
|
List<MesProBatchWorkerDO> workers = batchService.getBatchWorkerListByBatchId(batchId);
|
if (CollUtil.isEmpty(workers)) {
|
return List.of();
|
}
|
Set<Long> userIds = convertSet(workers, MesProBatchWorkerDO::getUserId);
|
Map<Long, AdminUserRespDTO> 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<MesProPalletDO> 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<MesProBagCodeDO> bags = bagCodeService.getBagCodeListByBatchId(batchId);
|
List<MesProBagCodeDO> 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<MesProScanEventDO> 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<MesQcSeedQualityDO> 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<MesQcSeedQualityIndicatorDO> 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<MesWmBatchPurchaseSourceRespVO> purchaseSources = mesWmBatchService.getPurchaseSourceList(batch.getCode());
|
List<MesWmBatchSalesTargetRespVO> salesTargets = mesWmBatchService.getSalesTargetList(batch.getCode());
|
List<MesWmBatchProcessTraceRespVO> 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) {
|
}
|
|
}
|