| | |
| | | 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; |
| | |
| | | |
| | | 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; |
| | |
| | | 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; |
| | | } |
| | | |
| | | // ==================== 追溯码解析 ==================== |
| | |
| | | resp.setInbound(buildInbound(batchId)); |
| | | // 质检区块 |
| | | resp.setQuality(buildQuality(batch)); |
| | | // 防伪验证区块 |
| | | fillAntiFake(resp, traceCode); |
| | | // 可选扩展区块(有数据才返回) |
| | | fillOptionalModules(resp, batch); |
| | | fillTimelineAndDecision(resp); |
| | | return resp; |
| | | } |
| | | |
| | |
| | | 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()); |
| | |
| | | } |
| | | } |
| | | |
| | | 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) { |