|
|
|
|
@ -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();
|
|
|
|
|
}
|
|
|
|
|
}
|