feat(mes/reverseTrace): 实现产品反向追溯功能

新增反向追溯功能,包含以下核心组件:
1. 新增 TraceQcCheckItemVo、TraceMaterialInputVo、ReverseTraceVo 等值对象
2. 新增 IReverseTraceService 接口及实现类,提供追溯核心逻辑
3. 新增 ReverseTraceController 提供 REST API
4. 新增 ReverseTraceMapper 及 XML 实现复杂查询
5. 实现分步查询逻辑:行业判定、锚点定位、分区块数据加载

功能特点:
- 支持成品批次码全链路追溯
- 工单展开行懒加载原材料投料信息
- 质检明细弹窗懒加载
- 适配轮胎和机加两种行业类型
- 优化查询性能,减少不必要的数据传输
master
zangch@mesnac.com 1 week ago
parent 7ef0866057
commit e4c9c2acd7

@ -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<String> messageType;
/**
* 退
*/
private String nodeCode;
/**
*
*/
private String message;
/**
*
*/
private String notice;
/**
*
*/
private Map<String, Object> variables;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}

@ -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;
/**
*
* <p>
*
* 1. GET /mes/reverseTrace/batch/{batchCode} - "追溯"
* 2. GET /mes/reverseTrace/workOrder/materialInputs -
* 3. GET /mes/reverseTrace/qc/detail/{inspectionId} - "检验明细"
* <p>
*
* -
* -
* -
*
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/mes/reverseTrace")
public class ReverseTraceController extends BaseController {
private final IReverseTraceService reverseTraceService;
/**
*
* <p>
*
* -
* -
* -
* -
* -
*
* @param batchCode
* @return
*/
@SaCheckPermission("mes:reverseTrace:query")
@GetMapping("/batch/{batchCode}")
public R<ReverseTraceVo> traceByBatchCode(@PathVariable String batchCode) {
ReverseTraceVo result = reverseTraceService.traceByBatchCode(batchCode);
if (result == null) {
return R.fail("未查询到批次码 [" + batchCode + "] 对应的追溯数据");
}
return R.ok(result);
}
/**
*
* <p>
*
* 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<List<TraceMaterialInputVo>> listMaterialInputs(
@RequestParam Long planId,
@RequestParam String industryType) {
List<TraceMaterialInputVo> list = reverseTraceService.listMaterialInputs(planId, industryType);
return R.ok(list);
}
/**
*
* <p>
*
* "检验明细" inspectionId
*
* @param inspectionId ID
* @return
*/
@SaCheckPermission("mes:reverseTrace:query")
@GetMapping("/qc/detail/{inspectionId}")
public R<List<TraceQcCheckItemVo>> listQcCheckItems(@PathVariable Long inspectionId) {
List<TraceQcCheckItemVo> list = reverseTraceService.listQcCheckItems(inspectionId);
return R.ok(list);
}
}

@ -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
* <p>
* 使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<WorkOrder> 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<TraceQcCheckItemVo> 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;
}
}

@ -0,0 +1,46 @@
package org.dromara.mes.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* - VO
* <p>
*
* 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;
}

@ -0,0 +1,28 @@
package org.dromara.mes.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* - VO
* <p>
*
* 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;
}

@ -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
* <p>
* BaseMapperPlus
* MES WMS QMS
* BaseMapperPlus CRUD XML SQL
*/
public interface ReverseTraceMapper {
/**
* +
* <p>
* .txt4.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<String, Object> selectAnchor(@Param("batchCode") String batchCode);
/**
*
*
* @param batchCode
* @param industryType TIRE/JJ
* @return
*/
ReverseTraceVo.ProductInfo selectProductInfo(@Param("batchCode") String batchCode,
@Param("industryType") String industryType);
/**
*
* <p>
*
* 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);
/**
*
* <p>
* inspectionId
*
* @param inspectionId ID
* @return
*/
List<TraceQcCheckItemVo> selectQcCheckItems(@Param("inspectionId") Long inspectionId);
/**
*
*
* @param productOrderId ID
* @param batchCode
* @return
*/
ReverseTraceVo.ProductionOrder selectProductionOrder(@Param("productOrderId") Long productOrderId,
@Param("batchCode") String batchCode);
/**
* -
* <p>
*
* _1_2_3_4 UNION ALL
* SQL
*
* @param productOrderId ID
* @return
*/
List<ReverseTraceVo.WorkOrder> selectWorkOrderListTire(@Param("productOrderId") Long productOrderId);
/**
* -
*
* @param batchCode
* @return
*/
List<ReverseTraceVo.WorkOrder> selectWorkOrderListJj(@Param("batchCode") String batchCode);
/**
*
* <p>
*
* -> prod_input_scan_info.production_barcode -> input_barcode -> /
* 便
*
* @param planId ID
* @param industryType
* @param tableSuffix : _1/_2/_3/_4:
* @return
*/
List<String> selectProductionBarcodes(@Param("planId") Long planId,
@Param("industryType") String industryType,
@Param("tableSuffix") String tableSuffix);
/**
*
* <p>
* prod_input_scan_info.production_barcode = input_barcode ///
*
* @param productionBarcode
* @return
*/
List<TraceMaterialInputVo> selectMaterialInputs(@Param("productionBarcode") String productionBarcode);
}

@ -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
* <p>
*
* 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<TraceMaterialInputVo> listMaterialInputs(Long planId, String industryType);
/**
*
*
* @param inspectionId ID
* @return
*/
List<TraceQcCheckItemVo> listQcCheckItems(Long inspectionId);
}

@ -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
* <p>
*
* 1. + selectAnchor
* 2.
* 3. : UNION ALL:
* 4. selectMaterialInputs
* 5. inspectionId selectQcCheckItems
* <p>
* 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
* <p>
*
* _1/_2/_3/_4
* SQL UNION ALL
*/
private static final Map<String, String> 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<String, Object> 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<TraceQcCheckItemVo> 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<ReverseTraceVo.WorkOrder> 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<TraceMaterialInputVo> listMaterialInputs(Long planId, String industryType) {
if (planId == null) {
return Collections.emptyList();
}
/*
*
* 1.
* 2. /
*
* SQL
* - _1/_2/_3/_4
* -
* - 便
*/
List<String> 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<TraceMaterialInputVo> result = new ArrayList<>();
for (String barcode : barcodes) {
if (barcode == null || barcode.trim().isEmpty()) {
continue;
}
List<TraceMaterialInputVo> inputs = reverseTraceMapper.selectMaterialInputs(barcode);
if (inputs != null) {
result.addAll(inputs);
}
}
return result;
}
@Override
public List<TraceQcCheckItemVo> listQcCheckItems(Long inspectionId) {
if (inspectionId == null) {
return Collections.emptyList();
}
return reverseTraceMapper.selectQcCheckItems(inspectionId);
}
/**
*
* <p>
*
* planId planId
* planId planId
*
* 2-3 planId
*/
private List<String> findTireProductionBarcodes(Long planId) {
for (Map.Entry<String, String> entry : TIRE_TABLE_SUFFIX_MAP.entrySet()) {
List<String> barcodes = reverseTraceMapper.selectProductionBarcodes(
planId, "TIRE", entry.getValue());
if (barcodes != null && !barcodes.isEmpty()) {
return barcodes;
}
}
return Collections.emptyList();
}
}
Loading…
Cancel
Save