From e4c9c2acd7e36e2568b7799e12356c847c4645c5 Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Fri, 3 Apr 2026 09:36:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(mes/reverseTrace):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=BA=A7=E5=93=81=E5=8F=8D=E5=90=91=E8=BF=BD=E6=BA=AF=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增反向追溯功能,包含以下核心组件: 1. 新增 TraceQcCheckItemVo、TraceMaterialInputVo、ReverseTraceVo 等值对象 2. 新增 IReverseTraceService 接口及实现类,提供追溯核心逻辑 3. 新增 ReverseTraceController 提供 REST API 4. 新增 ReverseTraceMapper 及 XML 实现复杂查询 5. 实现分步查询逻辑:行业判定、锚点定位、分区块数据加载 功能特点: - 支持成品批次码全链路追溯 - 工单展开行懒加载原材料投料信息 - 质检明细弹窗懒加载 - 适配轮胎和机加两种行业类型 - 优化查询性能,减少不必要的数据传输 --- .../api/domain/RemoteBackProcess.java | 67 ++++++ .../controller/ReverseTraceController.java | 98 +++++++++ .../dromara/mes/domain/vo/ReverseTraceVo.java | 191 +++++++++++++++++ .../mes/domain/vo/TraceMaterialInputVo.java | 46 +++++ .../mes/domain/vo/TraceQcCheckItemVo.java | 28 +++ .../mes/mapper/ReverseTraceMapper.java | 132 ++++++++++++ .../mes/service/IReverseTraceService.java | 43 ++++ .../service/impl/ReverseTraceServiceImpl.java | 194 ++++++++++++++++++ 8 files changed, 799 insertions(+) create mode 100644 ruoyi-api/hwmom-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteBackProcess.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ReverseTraceController.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/ReverseTraceVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceMaterialInputVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceQcCheckItemVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ReverseTraceMapper.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IReverseTraceService.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ReverseTraceServiceImpl.java diff --git a/ruoyi-api/hwmom-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteBackProcess.java b/ruoyi-api/hwmom-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteBackProcess.java new file mode 100644 index 00000000..85a3fb07 --- /dev/null +++ b/ruoyi-api/hwmom-api-workflow/src/main/java/org/dromara/workflow/api/domain/RemoteBackProcess.java @@ -0,0 +1,67 @@ +package org.dromara.workflow.api.domain; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 驳回参数请求(远程调用) + * + * @author may + */ +@Data +public class RemoteBackProcess implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 任务ID + */ + @NotNull(message = "任务ID不能为空") + private Long taskId; + + /** + * 附件id + */ + private String fileId; + + /** + * 消息类型 + */ + private List messageType; + + /** + * 驳回的节点编码(允许为空,服务端将回退到申请人节点) + */ + private String nodeCode; + + /** + * 办理意见 + */ + private String message; + + /** + * 通知 + */ + private String notice; + + /** + * 流程变量 + */ + private Map variables; + + public Map getVariables() { + if (variables == null) { + return new HashMap<>(16); + } + variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); + return variables; + } +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ReverseTraceController.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ReverseTraceController.java new file mode 100644 index 00000000..205d95cd --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ReverseTraceController.java @@ -0,0 +1,98 @@ +package org.dromara.mes.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.domain.R; +import org.dromara.common.web.core.BaseController; +import org.dromara.mes.domain.vo.ReverseTraceVo; +import org.dromara.mes.domain.vo.TraceMaterialInputVo; +import org.dromara.mes.domain.vo.TraceQcCheckItemVo; +import org.dromara.mes.service.IReverseTraceService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 产品反向追溯 + *

+ * 三个接口对应前端页面的三次数据加载时机: + * 1. GET /mes/reverseTrace/batch/{batchCode} - 首屏追溯(输入批次码后点击"追溯"按钮) + * 2. GET /mes/reverseTrace/workOrder/materialInputs - 工单展开懒加载(点击工单行展开) + * 3. GET /mes/reverseTrace/qc/detail/{inspectionId} - 质检明细懒加载(点击"检验明细"按钮) + *

+ * 为什么不合并为一个接口: + * - 首屏数据量大但只查一次 + * - 工单展开是按需加载(用户不展开就不查) + * - 质检明细是弹窗展示(用户不点就不查) + * 拆分接口可以减少不必要的数据传输和数据库查询压力。 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/mes/reverseTrace") +public class ReverseTraceController extends BaseController { + + private final IReverseTraceService reverseTraceService; + + /** + * 按成品批次码进行全链路反向追溯(首屏加载) + *

+ * 返回内容包含: + * - 成品基础信息及状态 + * - 客户信息(仅出库时有数据) + * - 成品质检信息(含检验项明细) + * - 生产订单信息 + * - 生产工单列表(不含投料明细,展开时懒加载) + * + * @param batchCode 成品批次码 + * @return 追溯聚合数据 + */ + @SaCheckPermission("mes:reverseTrace:query") + @GetMapping("/batch/{batchCode}") + public R traceByBatchCode(@PathVariable String batchCode) { + ReverseTraceVo result = reverseTraceService.traceByBatchCode(batchCode); + if (result == null) { + return R.fail("未查询到批次码 [" + batchCode + "] 对应的追溯数据"); + } + return R.ok(result); + } + + /** + * 查询工单的原材料投料信息(展开行懒加载) + *

+ * 工单展开时的条码链追溯逻辑: + * 1. 根据 planId 查询该工单的产出条码(从分表或通用明细表) + * 2. 用产出条码在 prod_input_scan_info 表中匹配 production_barcode + * 3. 取出 input_barcode,关联原材料质检、出库、物料主数据 + * + * @param planId 工单ID + * @param industryType 行业类型 TIRE/JJ + * @return 原材料投料列表 + */ + @SaCheckPermission("mes:reverseTrace:query") + @GetMapping("/workOrder/materialInputs") + public R> listMaterialInputs( + @RequestParam Long planId, + @RequestParam String industryType) { + List list = reverseTraceService.listMaterialInputs(planId, industryType); + return R.ok(list); + } + + /** + * 查询质检检验项明细(弹窗懒加载) + *

+ * 用于成品质检区块和原材料质检弹窗的明细展示。 + * 前端点击"检验明细"按钮时,传入 inspectionId 加载对应检验项。 + * + * @param inspectionId 质检主表ID + * @return 检验项明细列表 + */ + @SaCheckPermission("mes:reverseTrace:query") + @GetMapping("/qc/detail/{inspectionId}") + public R> listQcCheckItems(@PathVariable Long inspectionId) { + List list = reverseTraceService.listQcCheckItems(inspectionId); + return R.ok(list); + } +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/ReverseTraceVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/ReverseTraceVo.java new file mode 100644 index 00000000..9dc1688f --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/ReverseTraceVo.java @@ -0,0 +1,191 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 产品反向追溯 - 整页聚合VO + *

+ * 为什么使用聚合VO而非多个独立接口返回: + * 反向追溯是一个完整的追溯链路展示页面,前端一次查询需要展示成品信息、客户信息、质检信息、 + * 生产订单和工单列表等多个区块。如果拆成多个接口,前端需要自行编排调用顺序和异常处理, + * 而且行业判定、成品锚点定位等逻辑不宜散落到前端。因此采用后端聚合查询、前端最小改动的策略。 + */ +@Data +public class ReverseTraceVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 行业类型: TIRE-轮胎, JJ-机加 */ + private String industryType; + + /** 成品信息 */ + private ProductInfo productInfo; + + /** 客户信息(仅出库时有值) */ + private CustomerInfo customerInfo; + + /** 成品质检信息 */ + private QcInfo qcInfo; + + /** 生产订单信息 */ + private ProductionOrder productionOrder; + + /** 生产工单列表 */ + private List workOrderList; + + // ========== 内嵌结构 ========== + + /** 成品信息 */ + @Data + public static class ProductInfo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 批次码 */ + private String batchCode; + /** 产品编码 */ + private String productCode; + /** 产品名称 */ + private String productName; + /** 规格 */ + private String spec; + /** 生产日期 */ + private String productionDate; + /** 状态: 已出库/已质检/生产完成 */ + private String status; + } + + /** 客户信息(条件展示) */ + @Data + public static class CustomerInfo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 是否有出库记录 */ + private Boolean hasOutbound; + /** 客户明细(hasOutbound=true时非空) */ + private CustomerData data; + } + + @Data + public static class CustomerData implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 客户编码 */ + private String customerCode; + /** 客户名称 */ + private String customerName; + /** 联系人 */ + private String contactPerson; + /** 联系电话 */ + private String contactPhone; + /** 交货地址 */ + private String deliveryAddress; + /** 出库时间 */ + private String outboundTime; + /** 出库数量 */ + private String outboundQty; + /** 出库单号 */ + private String invoiceNo; + } + + /** 成品质检信息 */ + @Data + public static class QcInfo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 质检主表ID(用于后续查询明细) */ + private Long inspectionId; + /** 质检单号 */ + private String qcCode; + /** 批次码 */ + private String batchCode; + /** 产品编码(为后续质检链路穿透预留,当前页面暂未展示) */ + private String productCode; + /** 产品名称(与质检主表冗余保持一致,避免后续弹窗还要二次查主数据) */ + private String productName; + /** 规格(当前质检主表无稳定规格字段时先保留占位,兼容规格文档结构) */ + private String spec; + /** 质检时间 */ + private String qcTime; + /** 质检类型 */ + private String qcType; + /** 质检员 */ + private String inspector; + /** 质检结果: 合格/不合格/未判定 */ + private String result; + /** 质检明细列表(成品质检在主查询时直接返回) */ + private List checkItems; + } + + /** 生产订单信息 */ + @Data + public static class ProductionOrder implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 订单号 */ + private String orderCode; + /** 批次码 */ + private String batchCode; + /** 产品名称 */ + private String productName; + /** 派工类型编码 */ + private String dispatchType; + /** 派工类型名称 */ + private String dispatchTypeName; + /** 派工信息 */ + private String dispatchInfo; + /** 计划数量 */ + private String planQty; + /** 已派工数量 */ + private String dispatchedQty; + /** 完成数量 */ + private String completedQty; + /** 开始时间 */ + private String startTime; + /** 完成时间 */ + private String endTime; + /** 订单状态 */ + private String status; + } + + /** 生产工单 */ + @Data + public static class WorkOrder implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** 工单主键ID(展开时传给后端查询投料信息) */ + private Long planId; + /** 行业类型(展开时传给后端,避免二次查询) */ + private String industryType; + /** 工序序号 */ + private Integer processSeq; + /** 工单编码 */ + private String workOrderCode; + /** 工序编码 */ + private String processCode; + /** 工序名称 */ + private String processName; + /** 机台编号 */ + private String machineNo; + /** 机台名称 */ + private String machineName; + /** 开始时间 */ + private String startTime; + /** 结束时间 */ + private String endTime; + /** 作业员 */ + private String worker; + /** 状态: 已完成/已开始/未开始/待处理 */ + private String status; + } +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceMaterialInputVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceMaterialInputVo.java new file mode 100644 index 00000000..19984461 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceMaterialInputVo.java @@ -0,0 +1,46 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 产品反向追溯 - 原材料投料信息VO + *

+ * 用于工单展开行懒加载接口返回。 + * 展开工单时,后端通过条码链(production_barcode -> input_barcode)反查投入记录, + * 并关联原材料质检主表和出库记录,一次性返回投料概要信息。 + * 质检明细(checkItems)不在此接口返回,而是在用户点击"检验明细"时再按 inspectionId 单独加载。 + */ +@Data +public class TraceMaterialInputVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 产出条码(用于向前还原是哪一支成品条码触发了本次投入链) */ + private String productionBarcode; + /** 投入扫描记录ID(后续若要做扫描明细跳转,可直接使用该主键) */ + private Long inputScanId; + /** 物料编码 */ + private String materialCode; + /** 物料名称 */ + private String materialName; + /** 投入批次码(即 input_barcode) */ + private String batchCode; + /** 供应商名称(来自质检主表 supplier_name) */ + private String supplier; + /** 投料数量(按同一批次 + 同一物料的出库记录汇总,避免分批领料时显示失真) */ + private String qty; + /** 单位 */ + private String unit; + /** 投料时间 */ + private String inTime; + /** 质检单号 */ + private String qcCode; + /** 质检结果: 合格/不合格/未判定 */ + private String qcResult; + /** 质检主表ID(点击"检验明细"时传给后端查询详情) */ + private Long inspectionId; +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceQcCheckItemVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceQcCheckItemVo.java new file mode 100644 index 00000000..83648052 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/TraceQcCheckItemVo.java @@ -0,0 +1,28 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 产品反向追溯 - 质检检验项明细VO + *

+ * 用于成品质检信息区块以及原材料质检明细弹窗。 + * 标准值根据 detect_type 区分:计量型展示"下限 ~ 上限",其他展示 spec_inspection 或 spec_name。 + */ +@Data +public class TraceQcCheckItemVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 检验项目名称 */ + private String itemName; + /** 标准值 */ + private String standard; + /** 实际检测值 */ + private String actual; + /** 检验结果: 合格/不合格/未判定 */ + private String result; +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ReverseTraceMapper.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ReverseTraceMapper.java new file mode 100644 index 00000000..4453ab63 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ReverseTraceMapper.java @@ -0,0 +1,132 @@ +package org.dromara.mes.mapper; + +import org.apache.ibatis.annotations.Param; +import org.dromara.mes.domain.vo.ReverseTraceVo; +import org.dromara.mes.domain.vo.TraceMaterialInputVo; +import org.dromara.mes.domain.vo.TraceQcCheckItemVo; + +import java.util.List; +import java.util.Map; + +/** + * 产品反向追溯 - Mapper接口 + *

+ * 为什么不继承 BaseMapperPlus: + * 反向追溯是纯聚合查询场景,没有对应的单一实体表(涉及 MES 生产表、WMS 出库表、QMS 质检表等多表联合查询), + * 不需要 BaseMapperPlus 提供的单表 CRUD 能力。所有查询均在 XML 中手写 SQL 实现。 + */ +public interface ReverseTraceMapper { + + /** + * 行业判定 + 成品锚点定位 + *

+ * 严格按《密炼.txt》4.1 + 4.2 执行: + * 1. 先通过 prod_order_info + base_material_info.tire_markings + qc_inspection_main.production_order 判定行业 + * 2. 再按行业去对应的成品明细表定位锚点 + * + * 为什么仍保留为一个 Mapper 方法: + * 对 Service 层来说,“行业判定”和“锚点定位”始终是一个原子前置步骤, + * 放在同一条 SQL 中更容易保证两者基于同一批次码上下文执行,减少调用层分支判断。 + * + * @param batchCode 成品批次码 + * @return 锚点信息(industryType, productOrderId, orderCode, materialId, batchCode, + * finalPlanId, finalPlanCode, finalPlanDetailId, finalPlanDetailCode, productionDate) + */ + Map selectAnchor(@Param("batchCode") String batchCode); + + /** + * 查询成品信息 + * + * @param batchCode 成品批次码 + * @param industryType 行业类型 TIRE/JJ + * @return 成品信息 + */ + ReverseTraceVo.ProductInfo selectProductInfo(@Param("batchCode") String batchCode, + @Param("industryType") String industryType); + + /** + * 查询客户信息(仅出库时有数据) + *

+ * 为什么独立查询: + * 客户信息来自 WMS 出库记录,不是所有成品都有出库记录。 + * 独立查询便于后端根据结果是否为空来决定返回 hasOutbound=true/false。 + * + * @param batchCode 成品批次码 + * @return 客户信息(无出库记录时返回 null) + */ + ReverseTraceVo.CustomerData selectCustomerInfo(@Param("batchCode") String batchCode); + + /** + * 查询成品质检主信息(含质检类型名称) + * + * @param batchCode 成品批次码 + * @return 质检主信息 + */ + ReverseTraceVo.QcInfo selectQcInfo(@Param("batchCode") String batchCode); + + /** + * 查询质检明细(检验项列表) + *

+ * 同时用于成品质检区块和原材料质检弹窗,按 inspectionId 查询。 + * + * @param inspectionId 质检主表ID + * @return 检验项明细列表 + */ + List selectQcCheckItems(@Param("inspectionId") Long inspectionId); + + /** + * 查询生产订单信息 + * + * @param productOrderId 生产订单ID + * @param batchCode 成品批次码 + * @return 生产订单信息 + */ + ReverseTraceVo.ProductionOrder selectProductionOrder(@Param("productOrderId") Long productOrderId, + @Param("batchCode") String batchCode); + + /** + * 查询生产工单列表 - 轮胎行业 + *

+ * 为什么轮胎行业用独立方法: + * 轮胎行业按工序分表(_1密炼、_2半制品、_3成型、_4硫化),需要 UNION ALL 四张表的数据。 + * 与机加行业单表查询的 SQL 结构完全不同,拆开可读性更好。 + * + * @param productOrderId 生产订单ID + * @return 工单列表 + */ + List selectWorkOrderListTire(@Param("productOrderId") Long productOrderId); + + /** + * 查询生产工单列表 - 机加行业 + * + * @param batchCode 成品批次码 + * @return 工单列表 + */ + List selectWorkOrderListJj(@Param("batchCode") String batchCode); + + /** + * 查询工单的产出条码列表 + *

+ * 为什么分步查询投料信息: + * 工单展开投料的链路是:产出条码 -> prod_input_scan_info.production_barcode -> input_barcode -> 关联质检/出库。 + * 第一步先查产出条码,第二步再用产出条码查投入记录。两步分开便于调试和优化。 + * + * @param planId 工单ID + * @param industryType 行业类型(决定查哪张明细表) + * @param tableSuffix 分表后缀(轮胎行业: _1/_2/_3/_4,机加行业: 空字符串) + * @return 产出条码列表 + */ + List selectProductionBarcodes(@Param("planId") Long planId, + @Param("industryType") String industryType, + @Param("tableSuffix") String tableSuffix); + + /** + * 根据产出条码查询原材料投料信息 + *

+ * 核心链路:prod_input_scan_info.production_barcode = 产出条码 → input_barcode → 关联物料/质检/出库/单位 + * + * @param productionBarcode 产出条码 + * @return 原材料投料列表 + */ + List selectMaterialInputs(@Param("productionBarcode") String productionBarcode); +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IReverseTraceService.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IReverseTraceService.java new file mode 100644 index 00000000..8d140117 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IReverseTraceService.java @@ -0,0 +1,43 @@ +package org.dromara.mes.service; + +import org.dromara.mes.domain.vo.ReverseTraceVo; +import org.dromara.mes.domain.vo.TraceMaterialInputVo; +import org.dromara.mes.domain.vo.TraceQcCheckItemVo; + +import java.util.List; + +/** + * 产品反向追溯 - Service接口 + *

+ * 三个接口方法对应前端页面的三次数据加载: + * 1. traceByBatchCode: 首屏全量加载(成品信息、客户、质检、订单、工单列表) + * 2. listMaterialInputs: 工单展开行懒加载(原材料投料列表) + * 3. listQcCheckItems: 质检明细弹窗懒加载(检验项明细) + */ +public interface IReverseTraceService { + + /** + * 按成品批次码进行全链路反向追溯 + * + * @param batchCode 成品批次码 + * @return 追溯数据聚合VO + */ + ReverseTraceVo traceByBatchCode(String batchCode); + + /** + * 查询指定工单的原材料投料信息(懒加载) + * + * @param planId 工单ID + * @param industryType 行业类型 TIRE/JJ + * @return 原材料投料列表 + */ + List listMaterialInputs(Long planId, String industryType); + + /** + * 查询质检检验项明细(懒加载) + * + * @param inspectionId 质检主表ID + * @return 检验项明细列表 + */ + List listQcCheckItems(Long inspectionId); +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ReverseTraceServiceImpl.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ReverseTraceServiceImpl.java new file mode 100644 index 00000000..dd210afc --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ReverseTraceServiceImpl.java @@ -0,0 +1,194 @@ +package org.dromara.mes.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mes.domain.vo.ReverseTraceVo; +import org.dromara.mes.domain.vo.TraceMaterialInputVo; +import org.dromara.mes.domain.vo.TraceQcCheckItemVo; +import org.dromara.mes.mapper.ReverseTraceMapper; +import org.dromara.mes.service.IReverseTraceService; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * 产品反向追溯 - Service实现 + *

+ * 核心追溯流程: + * 1. 行业判定 + 成品锚点定位(selectAnchor) + * 2. 基于锚点分区块查询成品信息、客户信息、质检信息、订单信息 + * 3. 根据行业类型查询工单列表(轮胎: 四张分表UNION ALL,机加: 单表) + * 4. 工单展开时通过条码链反查投料记录(selectMaterialInputs) + * 5. 质检明细按 inspectionId 查询(selectQcCheckItems) + *

+ * 为什么不在 SQL 层做所有 JOIN: + * 各区块数据的查询条件不同(有的用 batchCode,有的用 productOrderId), + * 在 Java 层组装便于处理条件逻辑(如客户信息为空时设置 hasOutbound=false) + * 和数据转换(如状态码翻译已在 SQL 中完成)。 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class ReverseTraceServiceImpl implements IReverseTraceService { + + private final ReverseTraceMapper reverseTraceMapper; + + /** + * 轮胎行业工序分表后缀映射 + * 键:prod_plan_info_x 中的后缀 + * 值:用于查 prod_product_plan_detail_x 的后缀 + *

+ * 为什么硬编码映射而不从配置表读: + * 分表后缀由代码架构决定(_1密炼/_2半制品/_3成型/_4硫化),不是业务可配项。 + * 工单列表 SQL 中的 UNION ALL 已经固化了这四张表,后缀映射也必须与之对应。 + */ + private static final Map TIRE_TABLE_SUFFIX_MAP = new LinkedHashMap<>(); + + static { + TIRE_TABLE_SUFFIX_MAP.put("_1", "_1"); + TIRE_TABLE_SUFFIX_MAP.put("_2", "_2"); + TIRE_TABLE_SUFFIX_MAP.put("_3", "_3"); + TIRE_TABLE_SUFFIX_MAP.put("_4", "_4"); + } + + @Override + public ReverseTraceVo traceByBatchCode(String batchCode) { + // 第一步:行业判定 + 成品锚点定位 + Map anchor = reverseTraceMapper.selectAnchor(batchCode); + if (anchor == null) { + return null; + } + + String industryType = (String) anchor.get("industryType"); + Long productOrderId = anchor.get("productOrderId") != null + ? Long.valueOf(anchor.get("productOrderId").toString()) : null; + + ReverseTraceVo result = new ReverseTraceVo(); + result.setIndustryType(industryType); + + // 第二步:分区块查询首屏数据 + // 成品信息 + ReverseTraceVo.ProductInfo productInfo = reverseTraceMapper.selectProductInfo(batchCode, industryType); + result.setProductInfo(productInfo); + + // 客户信息(条件展示:有出库记录才返回) + ReverseTraceVo.CustomerData customerData = reverseTraceMapper.selectCustomerInfo(batchCode); + ReverseTraceVo.CustomerInfo customerInfo = new ReverseTraceVo.CustomerInfo(); + if (customerData != null) { + customerInfo.setHasOutbound(true); + customerInfo.setData(customerData); + } else { + customerInfo.setHasOutbound(false); + customerInfo.setData(null); + } + result.setCustomerInfo(customerInfo); + + // 成品质检信息 + ReverseTraceVo.QcInfo qcInfo = reverseTraceMapper.selectQcInfo(batchCode); + if (qcInfo != null && qcInfo.getInspectionId() != null) { + // 成品质检明细直接加载(成品质检项通常不多,无需懒加载) + List checkItems = reverseTraceMapper.selectQcCheckItems(qcInfo.getInspectionId()); + qcInfo.setCheckItems(checkItems); + } + result.setQcInfo(qcInfo); + + // 生产订单信息 + if (productOrderId != null) { + ReverseTraceVo.ProductionOrder productionOrder = reverseTraceMapper.selectProductionOrder(productOrderId, batchCode); + result.setProductionOrder(productionOrder); + } + + // 第三步:工单列表(按行业分流) + List workOrderList; + if ("TIRE".equals(industryType)) { + workOrderList = reverseTraceMapper.selectWorkOrderListTire(productOrderId); + } else { + workOrderList = reverseTraceMapper.selectWorkOrderListJj(batchCode); + } + // 为每个工单补充 industryType,前端展开时直接传回 + if (workOrderList != null) { + for (ReverseTraceVo.WorkOrder wo : workOrderList) { + wo.setIndustryType(industryType); + } + } + result.setWorkOrderList(workOrderList != null ? workOrderList : Collections.emptyList()); + + return result; + } + + @Override + public List listMaterialInputs(Long planId, String industryType) { + if (planId == null) { + return Collections.emptyList(); + } + + /* + * 投料信息查询分两步: + * 1. 先查该工单的产出条码列表(从生产明细表) + * 2. 再用每个产出条码去扫描表反查投入记录并关联质检/出库 + * + * 为什么分步而非一条 SQL 搞定: + * - 分表路由需要根据行业类型动态选择(_1/_2/_3/_4 或空) + * - 一个工单可能有多个产出条码(多批次生产),每个产出条码对应一批投入 + * - 分步便于调试:先确认产出条码是否正确,再查投入 + */ + List barcodes; + if ("TIRE".equals(industryType)) { + // 轮胎行业:根据 planId 匹配哪个分表有数据 + barcodes = findTireProductionBarcodes(planId); + } else { + // 机加行业:查通用明细表 + barcodes = reverseTraceMapper.selectProductionBarcodes(planId, industryType, ""); + } + + if (barcodes == null || barcodes.isEmpty()) { + return Collections.emptyList(); + } + + // 用每个产出条码查投入记录,按扫描明细原样返回 + // 为什么这里不再做 batchCode + materialCode 去重: + // 《密炼.txt》4.9.3 的口径是保留每一条 input_record 扫描记录, + // 仅对出库数量按批次和物料做 SUM 汇总。如果同批次同物料发生多次扫描投料, + // 页面应该看到多条投料时间记录,而不是被 Service 层压缩成一条。 + List result = new ArrayList<>(); + for (String barcode : barcodes) { + if (barcode == null || barcode.trim().isEmpty()) { + continue; + } + List inputs = reverseTraceMapper.selectMaterialInputs(barcode); + if (inputs != null) { + result.addAll(inputs); + } + } + + return result; + } + + @Override + public List listQcCheckItems(Long inspectionId) { + if (inspectionId == null) { + return Collections.emptyList(); + } + return reverseTraceMapper.selectQcCheckItems(inspectionId); + } + + /** + * 轮胎行业:按分表查找工单产出条码 + *

+ * 为什么遍历四张表: + * 前端传入的 planId 来自工单列表,但列表中的 planId 对应哪个分表后缀前端不知道。 + * 由于 planId 是全局唯一主键,同一 planId 只会存在于一张分表中, + * 依次查询直到找到有数据的那张表即可。 + * 通常最多查 2-3 次就能命中(轮胎工序列表按工序排序,planId 与分表有一一对应关系)。 + */ + private List findTireProductionBarcodes(Long planId) { + for (Map.Entry entry : TIRE_TABLE_SUFFIX_MAP.entrySet()) { + List barcodes = reverseTraceMapper.selectProductionBarcodes( + planId, "TIRE", entry.getValue()); + if (barcodes != null && !barcodes.isEmpty()) { + return barcodes; + } + } + return Collections.emptyList(); + } +}