From 1586a6e8530195fffed4c99efdecc70a4d627086 Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Wed, 18 Feb 2026 20:33:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(mes):=20=E6=89=A9=E5=B1=95=E6=B7=B7?= =?UTF-8?q?=E7=82=BC=E8=BF=BD=E6=BA=AF=E6=8A=A5=E5=91=8A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=9B=B4=E8=AF=A6=E7=BB=86=E7=9A=84?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E8=BF=87=E6=BB=A4=E5=92=8C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 IProdMixTraceReportService 中为 queryTraceDetail 方法添加参数映射支持 - 向 MixTraceDetailVo 添加追溯汇总、材料追溯树、耗用明细、工步明细、批次明细和历史曲线字段 - 向 MixTraceListVo 添加计划、班次、班组等相关信息字段用于追溯详情展示 - 修改控制器中的 detail 方法以接受并规范化查询参数 - 更新数据库映射器以支持通过参数映射进行配方信息、汇总、耗用、工步和批次查询 - 在 XML 映射文件中实现追溯选取应用逻辑和多维度查询条件支持 - 添加追溯汇总、耗用明细、工步过程和批次明细的数据查询接口实现 - 重构服务实现类中的常量定义和代码注释以提高可读性 --- .../ProdMixTraceReportController.java | 5 +- .../mes/domain/vo/MixTraceBatchVo.java | 29 + .../mes/domain/vo/MixTraceCurvePointVo.java | 28 + .../mes/domain/vo/MixTraceDetailVo.java | 18 + .../dromara/mes/domain/vo/MixTraceListVo.java | 51 ++ .../vo/MixTraceMaterialTraceTreeVo.java | 32 + .../dromara/mes/domain/vo/MixTraceStepVo.java | 43 ++ .../mes/domain/vo/MixTraceSummaryVo.java | 57 ++ .../mes/domain/vo/MixTraceUsageVo.java | 34 + .../mes/mapper/ProdMixTraceReportMapper.java | 27 +- .../service/IProdMixTraceReportService.java | 2 +- .../impl/ProdMixTraceReportServiceImpl.java | 523 +++++++++++----- .../mapper/mes/ProdMixTraceReportMapper.xml | 589 +++++++++++++++++- 13 files changed, 1286 insertions(+), 152 deletions(-) create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceBatchVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceCurvePointVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceMaterialTraceTreeVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceStepVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceSummaryVo.java create mode 100644 ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceUsageVo.java diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ProdMixTraceReportController.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ProdMixTraceReportController.java index ced918a9..d5760fd0 100644 --- a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ProdMixTraceReportController.java +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/controller/ProdMixTraceReportController.java @@ -52,8 +52,9 @@ public class ProdMixTraceReportController extends BaseController { /** 追溯详情 */ //@SaCheckPermission("mes:mixTrace:list") @GetMapping("/detail/{recipeId}") - public R detail(@PathVariable Long recipeId) { - return R.ok(mixTraceReportService.queryTraceDetail(recipeId)); + public R detail(@PathVariable Long recipeId, + @RequestParam(required = false) Map params) { + return R.ok(mixTraceReportService.queryTraceDetail(recipeId, normalizeParams(params))); } /** SPC 样本 */ diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceBatchVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceBatchVo.java new file mode 100644 index 00000000..33a44024 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceBatchVo.java @@ -0,0 +1,29 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 密炼追溯-批次明细 + */ +@Data +public class MixTraceBatchVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long batchId; + private String productionBarcode; + private String batchCode; + private String inputBarcode; + + private Long materialId; + private String materialName; + + private Date instockTime; + private String supplierName; +} + diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceCurvePointVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceCurvePointVo.java new file mode 100644 index 00000000..dcb481fc --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceCurvePointVo.java @@ -0,0 +1,28 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 密炼追溯-历史曲线点 + */ +@Data +public class MixTraceCurvePointVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long stepNo; + private Long timelineSecond; + private String xLabel; + + private BigDecimal temperature; + private BigDecimal power; + private BigDecimal energy; + private BigDecimal pressure; + private Long speed; +} + diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceDetailVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceDetailVo.java index a3b9a604..67cb3d99 100644 --- a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceDetailVo.java +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceDetailVo.java @@ -27,4 +27,22 @@ public class MixTraceDetailVo implements Serializable { /** 混炼明细列表(按 mix_id 排序) */ private List mixingList; + + /** 顶部摘要信息(按 mix.jpg 字段对齐) */ + private MixTraceSummaryVo summaryInfo; + + /** 左侧耗用追溯树 */ + private List materialTraceTree; + + /** 耗用明细(类别/设重/实重/公差/状态) */ + private List usageList; + + /** 工步明细(时间/温度/能量/功率/动作/压力/转速) */ + private List mixingStepList; + + /** 批次明细(批次/物料/入库时间/供应商) */ + private List batchList; + + /** 历史曲线(温度/功率/能量/压力/转速) */ + private List curveSeries; } diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceListVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceListVo.java index 625f7f97..9f8e8a86 100644 --- a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceListVo.java +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceListVo.java @@ -103,4 +103,55 @@ public class MixTraceListVo implements Serializable { /** 混炼工步数 */ @ExcelProperty(value = "混炼工步数") private Integer mixingCount; + + /** 计划ID */ + private Long planId; + + /** 计划编号 */ + @ExcelProperty(value = "计划编号") + private String planCode; + + /** 计划明细ID */ + private Long planDetailId; + + /** 计划明细编号 */ + @ExcelProperty(value = "明细编号") + private String planDetailCode; + + /** 生产条码 */ + @ExcelProperty(value = "生产条码") + private String productionBarcode; + + /** 班次ID */ + private Long shiftId; + + /** 班次名称 */ + @ExcelProperty(value = "班次") + private String shiftName; + + /** 班组ID */ + private Long classTeamId; + + /** 班组名称 */ + @ExcelProperty(value = "班组") + private String classTeamName; + + /** 计划数量 */ + @ExcelProperty(value = "计划数") + private BigDecimal planAmount; + + /** 完成数量 */ + @ExcelProperty(value = "完成数") + private BigDecimal completeAmount; + + /** 车次 */ + @ExcelProperty(value = "密炼车次") + private Long trainNumber; + + /** 实际开始时间 */ + @ExcelProperty(value = "开始生产时间") + private Date realBeginTime; + + /** 实际结束时间 */ + private Date realEndTime; } diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceMaterialTraceTreeVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceMaterialTraceTreeVo.java new file mode 100644 index 00000000..a5983e5d --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceMaterialTraceTreeVo.java @@ -0,0 +1,32 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 密炼追溯-左侧树节点 + */ +@Data +public class MixTraceMaterialTraceTreeVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String id; + private String label; + private String nodeType; + + private Long recipeId; + private Long planDetailId; + private String productionBarcode; + private Long materialId; + private String materialName; + private String batchCode; + + private List children = new ArrayList<>(); +} + diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceStepVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceStepVo.java new file mode 100644 index 00000000..7b8bcb06 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceStepVo.java @@ -0,0 +1,43 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 密炼追溯-工步过程 + */ +@Data +public class MixTraceStepVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long mixingId; + private Long mixId; + private Long timelineSecond; + + private String termCode; + private String termName; + private String condCode; + private String condName; + private String actCode; + private String actName; + + private Long mixingTime; + private BigDecimal mixingTemp; + private BigDecimal mixingEnergy; + private BigDecimal mixingPower; + private BigDecimal mixingPress; + private Long mixingSpeed; + + private Long setTime; + private Long setTemp; + private BigDecimal setEnergy; + private BigDecimal setPower; + private BigDecimal setPres; + private Long setRota; +} + diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceSummaryVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceSummaryVo.java new file mode 100644 index 00000000..8c0de9a0 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceSummaryVo.java @@ -0,0 +1,57 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 密炼追溯详情顶部摘要信息 + */ +@Data +public class MixTraceSummaryVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long recipeId; + private String recipeCode; + private Long machineId; + private String machineName; + private Long materialId; + private String materialName; + + private Long planId; + private String planCode; + private Long planDetailId; + private String planDetailCode; + private String productionBarcode; + + private Long shiftId; + private String shiftName; + private Long classTeamId; + private String classTeamName; + + private BigDecimal planAmount; + private BigDecimal settingWeight; + private BigDecimal completedWeight; + + private Long trayCount; + private Long mixingTrainNo; + private Long totalTrainNo; + + private String overToleranceAlarm; + private BigDecimal eachCarEnergy; + private BigDecimal dischargeTemp; + private BigDecimal dischargePower; + private BigDecimal dischargeEnergy; + private String mixingStatus; + + private Long mixingTime; + private Long consumeTime; + private Long intervalTime; + private Date beginProduceTime; + private Date endProduceTime; +} diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceUsageVo.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceUsageVo.java new file mode 100644 index 00000000..e90c1413 --- /dev/null +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/domain/vo/MixTraceUsageVo.java @@ -0,0 +1,34 @@ +package org.dromara.mes.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 密炼追溯-耗用明细 + */ +@Data +public class MixTraceUsageVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long usageId; + private Long weightSeq; + private String categoryName; + private String materialName; + + private BigDecimal setWeight; + private BigDecimal actualWeight; + private BigDecimal tolerance; + private BigDecimal diffWeight; + + private String overToleranceFlag; + private String controlMode; + + private String actCode; + private String actName; +} + diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ProdMixTraceReportMapper.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ProdMixTraceReportMapper.java index 71f0b53d..c1cd54e1 100644 --- a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ProdMixTraceReportMapper.java +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/mapper/ProdMixTraceReportMapper.java @@ -2,8 +2,12 @@ package org.dromara.mes.mapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Param; +import org.dromara.mes.domain.vo.MixTraceBatchVo; +import org.dromara.mes.domain.vo.MixTraceStepVo; import org.dromara.mes.domain.vo.MixTraceListVo; +import org.dromara.mes.domain.vo.MixTraceSummaryVo; import org.dromara.mes.domain.vo.MixTraceSpcSampleVo; +import org.dromara.mes.domain.vo.MixTraceUsageVo; import org.dromara.mes.domain.vo.ProdRecipeMixingVo; import org.dromara.mes.domain.vo.ProdRecipeWeightVo; @@ -32,7 +36,7 @@ public interface ProdMixTraceReportMapper { /** * 追溯详情 - 配方基础信息(含关联名称) */ - MixTraceListVo selectTraceRecipeInfo(@Param("recipeId") Long recipeId); + MixTraceListVo selectTraceRecipeInfo(@Param("map") Map params); /** * 追溯详情 - 称量明细(按 weight_seq 排序) @@ -44,6 +48,26 @@ public interface ProdMixTraceReportMapper { */ List selectMixingListByRecipeId(@Param("recipeId") Long recipeId); + /** + * 追溯详情-顶部摘要(本车维度优先) + */ + MixTraceSummaryVo selectTraceSummary(@Param("map") Map params); + + /** + * 追溯详情-耗用明细 + */ + List selectTraceUsageList(@Param("map") Map params); + + /** + * 追溯详情-工步过程 + */ + List selectTraceStepList(@Param("map") Map params); + + /** + * 追溯详情-批次明细 + */ + List selectTraceBatchList(@Param("map") Map params); + /** * SPC样本查询(分页) */ @@ -54,4 +78,5 @@ public interface ProdMixTraceReportMapper { * SPC样本查询(全量,用于统计计算) */ List selectSpcSamples(@Param("map") Map params); + } diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IProdMixTraceReportService.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IProdMixTraceReportService.java index 77c5e164..2638137e 100644 --- a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IProdMixTraceReportService.java +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/IProdMixTraceReportService.java @@ -31,7 +31,7 @@ public interface IProdMixTraceReportService { /** * 追溯详情 - 图9 */ - MixTraceDetailVo queryTraceDetail(Long recipeId); + MixTraceDetailVo queryTraceDetail(Long recipeId, Map params); /** * SPC样本列表(分页)- 图6 diff --git a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ProdMixTraceReportServiceImpl.java b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ProdMixTraceReportServiceImpl.java index 988b2e67..614fb6db 100644 --- a/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ProdMixTraceReportServiceImpl.java +++ b/ruoyi-modules/hwmom-mes/src/main/java/org/dromara/mes/service/impl/ProdMixTraceReportServiceImpl.java @@ -2,25 +2,23 @@ package org.dromara.mes.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; +import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.mes.domain.vo.*; import org.dromara.mes.mapper.ProdMixTraceReportMapper; import org.dromara.mes.service.IProdMixTraceReportService; import org.springframework.stereotype.Service; import java.math.BigDecimal; -import java.math.MathContext; import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** - * 密炼追溯报表Service业务层处理 + * 密炼追溯报表 Service 实现 * * @author Yinq * @date 2026-02-14 @@ -31,44 +29,31 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService private final ProdMixTraceReportMapper mixTraceReportMapper; - /** SPC计算精度 */ + /** SPC 数值保留精度 */ private static final int SCALE = 4; private static final RoundingMode RM = RoundingMode.HALF_UP; /** - * Xbar-R 图常量表(子组大小 n -> A2, D3, D4) - * 仅列出常用子组大小 2~10 + * Xbar-R 控制图常数(n=2~10 对应 A2、D3、D4) */ private static final double[][] XBAR_R_CONSTANTS = { - // n=2: A2=1.880, D3=0, D4=3.267 {1.880, 0.0, 3.267}, - // n=3: A2=1.023, D3=0, D4=2.575 {1.023, 0.0, 2.575}, - // n=4: A2=0.729, D3=0, D4=2.282 {0.729, 0.0, 2.282}, - // n=5: A2=0.577, D3=0, D4=2.115 {0.577, 0.0, 2.115}, - // n=6: A2=0.483, D3=0, D4=2.004 {0.483, 0.0, 2.004}, - // n=7: A2=0.419, D3=0.076, D4=1.924 {0.419, 0.076, 1.924}, - // n=8: A2=0.373, D3=0.136, D4=1.864 {0.373, 0.136, 1.864}, - // n=9: A2=0.337, D3=0.184, D4=1.816 {0.337, 0.184, 1.816}, - // n=10: A2=0.308, D3=0.223, D4=1.777 {0.308, 0.223, 1.777} }; - // ==================== 追溯列表(图5) ==================== - @Override public TableDataInfo queryTraceList(Map params, PageQuery pageQuery) { - // 统一把 null 查询参数转为空 Map,避免 Mapper XML 中 OGNL 访问空指针。 + // 统一参数清洗,避免 Mapper 中空字符串带来的条件误判 Map queryParams = safeParams(params); - // 由 PageQuery 生成分页对象,确保前后端分页语义一致(pageNum/pageSize)。 + // 分页查询由 MyBatis-Plus 负责总数与分页窗口,Service 仅做参数口径统一 Page page = mixTraceReportMapper.selectTraceList(queryParams, pageQuery.build()); - // 统一封装成 TableDataInfo,前端可直接读取 rows/total。 return TableDataInfo.build(page); } @@ -77,67 +62,123 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService return mixTraceReportMapper.selectTraceList(safeParams(params)); } - // ==================== 追溯详情(图9) ==================== - @Override - public MixTraceDetailVo queryTraceDetail(Long recipeId) { - // 先查主信息;不存在时直接返回 null,前端按“无详情”处理。 - MixTraceListVo recipeInfo = mixTraceReportMapper.selectTraceRecipeInfo(recipeId); + public MixTraceDetailVo queryTraceDetail(Long recipeId, Map params) { + // 入口参数统一规范后,再注入 recipeId,避免前端同名空值覆盖 + Map detailParams = safeParams(params); + detailParams.put("recipeId", recipeId); + + // 先查主信息,主信息不存在时不继续查询子表,避免无意义 IO + MixTraceListVo recipeInfo = mixTraceReportMapper.selectTraceRecipeInfo(detailParams); if (recipeInfo == null) { return null; } + MixTraceDetailVo detail = new MixTraceDetailVo(); - // 主表信息(配方基础信息)。 detail.setRecipeInfo(recipeInfo); - // 称量明细按 weight_seq 排序 detail.setWeightList(mixTraceReportMapper.selectWeightListByRecipeId(recipeId)); - // 混炼明细按 mix_id 排序 detail.setMixingList(mixTraceReportMapper.selectMixingListByRecipeId(recipeId)); + + MixTraceSummaryVo summaryInfo = mixTraceReportMapper.selectTraceSummary(detailParams); + detail.setSummaryInfo(summaryInfo); + + if (summaryInfo == null && hasDetailScopeFilter(detailParams)) { + // 带“本车范围”筛选但未命中摘要时,直接返回空明细,避免串车 + // 这里必须返回空集合而不是 null,前端渲染更稳定,避免出现 NPE + detail.setUsageList(new ArrayList<>()); + detail.setMixingStepList(new ArrayList<>()); + detail.setCurveSeries(new ArrayList<>()); + detail.setBatchList(new ArrayList<>()); + detail.setMaterialTraceTree(new ArrayList<>()); + return detail; + } + + if (summaryInfo != null) { + // 详情查询优先沿用摘要命中的计划上下文,确保后续子查询口径一致 + // 计划主键优先:planId/planDetailId 命中后,后续所有明细查询都按同口径收敛 + if (detailParams.get("planId") == null && summaryInfo.getPlanId() != null) { + detailParams.put("planId", summaryInfo.getPlanId()); + } + if (detailParams.get("planDetailId") == null && summaryInfo.getPlanDetailId() != null) { + detailParams.put("planDetailId", summaryInfo.getPlanDetailId()); + } + String planCode = getStringParam(detailParams, "planCode"); + if (StringUtils.isBlank(planCode) && StringUtils.isNotBlank(summaryInfo.getPlanCode())) { + // 编码字段只在前端未传时回填,避免覆盖前端明确指定的筛选值 + detailParams.put("planCode", summaryInfo.getPlanCode()); + } + String planDetailCode = getStringParam(detailParams, "planDetailCode"); + if (StringUtils.isBlank(planDetailCode) && StringUtils.isNotBlank(summaryInfo.getPlanDetailCode())) { + detailParams.put("planDetailCode", summaryInfo.getPlanDetailCode()); + } + } + + // 本车条码优先级:入参 > 摘要命中 > 主信息兜底 + // 说明:条码是“本车生产耗用追溯”最强约束条件,优先级必须最高 + String productionBarcode = getStringParam(detailParams, "productionBarcode"); + if (StringUtils.isBlank(productionBarcode)) { + if (summaryInfo != null && StringUtils.isNotBlank(summaryInfo.getProductionBarcode())) { + detailParams.put("productionBarcode", summaryInfo.getProductionBarcode()); + } else if (StringUtils.isNotBlank(recipeInfo.getProductionBarcode())) { + detailParams.put("productionBarcode", recipeInfo.getProductionBarcode()); + } + } + + // 称量耗用:用于“本车生产耗用追溯”表格 + List usageList = mixTraceReportMapper.selectTraceUsageList(detailParams); + detail.setUsageList(usageList); + + // 混炼工步:用于工步明细与历史曲线 + List mixingStepList = mixTraceReportMapper.selectTraceStepList(detailParams); + detail.setMixingStepList(mixingStepList); + detail.setCurveSeries(buildCurveSeries(mixingStepList)); + + // 批次追溯只保留高选择性条件,优先 productionBarcode 以减少扫描范围 + // 注意:批次查询 SQL 内有 TOP 限制,必须用高选择性条件避免误截断有效数据 + List batchList = mixTraceReportMapper.selectTraceBatchList(buildBatchQueryParams(detailParams)); + detail.setBatchList(batchList); + // 将批次明细组装为“车 -> 物料 -> 批次”树,供左侧树形追溯展示 + detail.setMaterialTraceTree(buildMaterialTraceTree(summaryInfo, batchList)); + return detail; } - // ==================== SPC样本(图6) ==================== - @Override public TableDataInfo querySpcSamples(Map params, PageQuery pageQuery) { - // SPC 样本分页查询:入参做空安全处理,避免条件拼接异常。 Map queryParams = safeParams(params); Page page = mixTraceReportMapper.selectSpcSamples(queryParams, pageQuery.build()); return TableDataInfo.build(page); } - // ==================== SPC能力分析(图7) ==================== - @Override public MixTraceSpcResultVo calculateSpcCapability(Map params) { - // 1) 参数归一化:保证后续读取参数时不会出现 null Map。 Map queryParams = safeParams(params); - // 获取分析参数名称,默认分析混炼温度 + // paramName 仅允许白名单字段,避免前端传非法参数导致统计口径失控 String paramName = getParamName(queryParams); - // 2) 查询参与统计的全量样本(能力分析必须用全样本,不用分页样本)。 List samples = mixTraceReportMapper.selectSpcSamples(queryParams); if (samples == null || samples.isEmpty()) { - // 无样本直接返回空结构,前端用 sampleCount=0 判断无图表数据。 + // 无样本直接返回空结构,前端仍可拿到参数标签用于占位显示 return buildEmptyResult(paramName); } - // 3) 根据参数名选择“实测值/设定值”的提取器,避免写 if-else 大分支。 Function actualExtractor = getActualExtractor(paramName); Function setExtractor = getSetExtractor(paramName); - // 4) 仅保留非空实测值参与统计,屏蔽脏数据对均值/方差计算的影响。 + // 仅保留可参与统计的实际值 + // 例如温度/能量为空的工步不参与均值和方差计算 List values = samples.stream() .map(actualExtractor) .filter(v -> v != null) .collect(Collectors.toList()); if (values.size() < 2) { - // 标准差至少需要 2 个样本点,少于 2 个无法做能力计算。 - return buildEmptyResult(paramName); + // 标准差至少需要 2 个样本,样本不足时只回传样本数 + MixTraceSpcResultVo result = buildEmptyResult(paramName); + result.setSampleCount(values.size()); + return result; } - // 5) 计算基础统计量(均值、标准差、极值)。 MixTraceSpcResultVo result = new MixTraceSpcResultVo(); result.setParamName(paramName); result.setParamLabel(getParamLabel(paramName)); @@ -147,26 +188,23 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService BigDecimal sigma = calcStdDev(values, mean); BigDecimal min = values.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); BigDecimal max = values.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO); - result.setMean(mean); result.setSigma(sigma); result.setMinValue(min); result.setMaxValue(max); - // 6) 计算规格限: - // - 优先使用设定值的最小/最大作为 LSL/USL(最贴近工艺设定)。 - // - 若设定值没有波动(全相同),退化为 target ± 10% 的经验容差。 + // 设定值用于推导目标值与规格线(USL/LSL) List setValues = samples.stream() .map(setExtractor) .filter(v -> v != null) .collect(Collectors.toList()); if (!setValues.isEmpty()) { + // 规格线优先使用设定值范围;若设定值没有波动则按 target ±10% 兜底 BigDecimal target = calcMean(setValues); result.setTarget(target); BigDecimal setMin = setValues.stream().min(BigDecimal::compareTo).orElse(target); BigDecimal setMax = setValues.stream().max(BigDecimal::compareTo).orElse(target); - // 若设定值有范围差异,使用最大/最小作为USL/LSL;否则使用 target ± 10% if (setMax.compareTo(setMin) > 0) { result.setUsl(setMax); result.setLsl(setMin); @@ -177,36 +215,36 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService } } - // 7) 计算能力指数(前提:σ>0 且已得到上下规格限)。 if (sigma.compareTo(BigDecimal.ZERO) > 0 && result.getUsl() != null && result.getLsl() != null) { + // Cp=(USL-LSL)/(6σ), Cpu=(USL-μ)/(3σ), Cpl=(μ-LSL)/(3σ), Cpk=min(Cpu,Cpl) + // 仅当“标准差>0 且规格线齐全”时计算能力指数,避免除零和无意义值 BigDecimal sixSigma = sigma.multiply(new BigDecimal("6")); BigDecimal threeSigma = sigma.multiply(new BigDecimal("3")); BigDecimal usl = result.getUsl(); BigDecimal lsl = result.getLsl(); - // Cp 反映潜在能力;Cpu/Cpl 反映均值偏移;Cpk 取二者较小值。 result.setCp(usl.subtract(lsl).divide(sixSigma, SCALE, RM)); result.setCpu(usl.subtract(mean).divide(threeSigma, SCALE, RM)); result.setCpl(mean.subtract(lsl).divide(threeSigma, SCALE, RM)); result.setCpk(result.getCpu().min(result.getCpl())); - // Pp/Ppk 使用总体标准差(此处简化为与Cp/Cpk相同口径) + // 当前实现按常见近似直接复用 Cp/Cpk 作为 Pp/Ppk result.setPp(result.getCp()); result.setPpk(result.getCpk()); } - // 8) 构造直方图分箱数据,供前端能力图直接渲染。 buildHistogram(result, values); - - // 9) 构造运行图序列数据(值 + 标签)。 - // 这里标签优先用配方编码,若缺失则回退为样本序号,保证前端 x 轴稳定。 + // 运行图直接复用 sampleValues,因此这里先统一填充 result.setSampleValues(values); + + // 标签与样本值一一对应,仅记录有效样本的标签 + // 标签优先 recipeCode,缺失时退化为序号,保证前端 x 轴总有文本 List labels = new ArrayList<>(); for (int i = 0; i < samples.size(); i++) { - MixTraceSpcSampleVo s = samples.get(i); - BigDecimal v = actualExtractor.apply(s); - if (v != null) { - labels.add(s.getRecipeCode() != null ? s.getRecipeCode() : String.valueOf(i + 1)); + MixTraceSpcSampleVo sample = samples.get(i); + BigDecimal value = actualExtractor.apply(sample); + if (value != null) { + labels.add(StringUtils.defaultIfBlank(sample.getRecipeCode(), String.valueOf(i + 1))); } } result.setSampleLabels(labels); @@ -214,61 +252,53 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService return result; } - // ==================== SPC运行图(图8) ==================== - @Override public MixTraceSpcResultVo calculateSpcRunChart(Map params) { - // 运行图复用样本统计结果,但仅返回运行图所需数据,避免语义混用。 + // 运行图复用能力分析样本,只清空直方图相关数据 + // 这样可避免重复查询数据库,同时保持均值、规格线、能力指数一致 MixTraceSpcResultVo result = calculateSpcCapability(safeParams(params)); if (result != null) { - // 显式清空直方图字段,避免前端误把 runChart 结果当 capability 结果使用。 result.setHistogramBins(null); result.setHistogramCounts(null); } return result; } - // ==================== SPC Xbar-R图(图10) ==================== - @Override public MixTraceSpcResultVo calculateSpcXbarR(Map params) { - // 1) 参数归一化并确定分析参数。 Map queryParams = safeParams(params); String paramName = getParamName(queryParams); - // 2) 取全量样本,Xbar-R 需要连续样本进行分组统计。 List samples = mixTraceReportMapper.selectSpcSamples(queryParams); if (samples == null || samples.isEmpty()) { return buildEmptyResult(paramName); } Function actualExtractor = getActualExtractor(paramName); - - // 仅保留有效实测值,保证子组计算准确。 + // Xbar-R 计算同样只使用有效实测值 List values = samples.stream() .map(actualExtractor) .filter(v -> v != null) .collect(Collectors.toList()); - // 子组大小,默认5,可通过参数配置 int subgroupSize = 5; + // 子组大小允许前端传入,但必须限制在 2~10(对应常数表范围) if (queryParams.containsKey("subgroupSize")) { try { subgroupSize = Integer.parseInt(String.valueOf(queryParams.get("subgroupSize"))); } catch (NumberFormatException ignored) { + // 非法子组大小直接忽略,回退默认值 5 } } - // 子组大小范围限制 2~10 subgroupSize = Math.max(2, Math.min(10, subgroupSize)); + // 至少保证 2 个子组,否则 Xbar-R 控制限意义不足 if (values.size() < subgroupSize * 2) { - // 业务约束:至少需要 2 个完整子组,否则控制图没有统计意义。 MixTraceSpcResultVo result = buildEmptyResult(paramName); result.setSampleCount(values.size()); return result; } - // 3) 顺序分组:按样本顺序切分为固定大小子组(忽略最后不完整子组)。 int groupCount = values.size() / subgroupSize; List xbarValues = new ArrayList<>(); List rValues = new ArrayList<>(); @@ -276,33 +306,28 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService for (int i = 0; i < groupCount; i++) { int start = i * subgroupSize; + // 每个子组按固定窗口切片,不做滑动窗口,便于和传统 SPC 口径对齐 List group = values.subList(start, start + subgroupSize); + BigDecimal groupMean = calcMean(group); - BigDecimal groupMax = group.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO); BigDecimal groupMin = group.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); + BigDecimal groupMax = group.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO); BigDecimal groupRange = groupMax.subtract(groupMin); - // 每个子组提取两个关键统计量:均值(Xbar) 和 极差(R)。 xbarValues.add(groupMean); rValues.add(groupRange); subgroupLabels.add("G" + (i + 1)); } - // 总均值与平均极差 BigDecimal xbarbar = calcMean(xbarValues); BigDecimal rbar = calcMean(rValues); - // 4) 按子组大小查控制图常量 A2/D3/D4(行业标准常量表)。 - int constIndex = subgroupSize - 2; // 数组从 n=2 开始 - double a2 = XBAR_R_CONSTANTS[constIndex][0]; - double d3 = XBAR_R_CONSTANTS[constIndex][1]; - double d4 = XBAR_R_CONSTANTS[constIndex][2]; + // 常数表下标与子组大小映射关系:index = n - 2 + int constIndex = subgroupSize - 2; + BigDecimal a2 = BigDecimal.valueOf(XBAR_R_CONSTANTS[constIndex][0]); + BigDecimal d3 = BigDecimal.valueOf(XBAR_R_CONSTANTS[constIndex][1]); + BigDecimal d4 = BigDecimal.valueOf(XBAR_R_CONSTANTS[constIndex][2]); - BigDecimal A2 = BigDecimal.valueOf(a2); - BigDecimal D3 = BigDecimal.valueOf(d3); - BigDecimal D4 = BigDecimal.valueOf(d4); - - // 构建结果 MixTraceSpcResultVo result = new MixTraceSpcResultVo(); result.setParamName(paramName); result.setParamLabel(getParamLabel(paramName)); @@ -317,118 +342,242 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService result.setXbarbar(xbarbar); result.setRbar(rbar); - // 5) Xbar 图控制限: - // UCLx = Xbarbar + A2 * Rbar - // CLx = Xbarbar - // LCLx = Xbarbar - A2 * Rbar - result.setUclX(xbarbar.add(A2.multiply(rbar)).setScale(SCALE, RM)); + // Xbar 控制限:UCLx=Xbarbar+A2*Rbar,CLx=Xbarbar,LCLx=Xbarbar-A2*Rbar + result.setUclX(xbarbar.add(a2.multiply(rbar)).setScale(SCALE, RM)); result.setClX(xbarbar.setScale(SCALE, RM)); - result.setLclX(xbarbar.subtract(A2.multiply(rbar)).setScale(SCALE, RM)); + result.setLclX(xbarbar.subtract(a2.multiply(rbar)).setScale(SCALE, RM)); - // 6) R 图控制限: - // UCLr = D4 * Rbar - // CLr = Rbar - // LCLr = D3 * Rbar - result.setUclR(D4.multiply(rbar).setScale(SCALE, RM)); + // R 控制限:UCLr=D4*Rbar,CLr=Rbar,LCLr=D3*Rbar + result.setUclR(d4.multiply(rbar).setScale(SCALE, RM)); result.setClR(rbar.setScale(SCALE, RM)); - result.setLclR(D3.multiply(rbar).setScale(SCALE, RM)); + result.setLclR(d3.multiply(rbar).setScale(SCALE, RM)); return result; } - // ==================== 工具方法 ==================== - /** - * 获取分析参数名称,默认 mixingTemp + * 获取统计参数名,默认 mixingTemp */ private String getParamName(Map params) { if (params == null || params.isEmpty()) { - // 未传分析参数时默认按混炼温度分析,保持接口可直接调用。 + // 默认按温度统计,符合历史页面默认关注项 return "mixingTemp"; } Object paramNameObj = params.get("paramName"); - // 兼容前端传空字符串场景,统一回退默认参数。 - return (paramNameObj != null && !String.valueOf(paramNameObj).isEmpty()) - ? String.valueOf(paramNameObj) : "mixingTemp"; - } - - private Map safeParams(Map params) { - // Mapper 层统一假设 map 非空,服务层在入口处兜底。 - return params == null ? new HashMap<>() : params; + if (paramNameObj == null || StringUtils.isBlank(String.valueOf(paramNameObj))) { + return "mixingTemp"; + } + return normalizeParamName(String.valueOf(paramNameObj)); } /** - * 参数名称 -> 中文标签映射 + * 参数规范化:去除空串,避免 SQL 条件误命中 + */ + private Map safeParams(Map params) { + Map normalized = new HashMap<>(); + if (params == null || params.isEmpty()) { + return normalized; + } + for (Map.Entry entry : params.entrySet()) { + if (entry.getKey() == null) { + // 无键值参数直接丢弃,防止后续 put 触发异常 + continue; + } + Object value = entry.getValue(); + if (value instanceof String) { + String trimmed = StringUtils.trimToNull((String) value); + if (trimmed != null) { + // 仅保留非空字符串,避免 SQL 出现 like '%%' 之类宽查询 + normalized.put(entry.getKey(), trimmed); + } + } else { + // 非字符串值(Long、Integer、Date 等)原样透传 + normalized.put(entry.getKey(), value); + } + } + return normalized; + } + + /** + * 批次查询参数构建:只保留高选择性条件,优先本车条码 + */ + private Map buildBatchQueryParams(Map detailParams) { + Map batchParams = new HashMap<>(); + if (detailParams == null || detailParams.isEmpty()) { + return batchParams; + } + + // 添加租户ID,用于多租户过滤 + batchParams.put("tenantId", TenantHelper.getTenantId()); + + String productionBarcode = getStringParam(detailParams, "productionBarcode"); + String planDetailId = getStringParam(detailParams, "planDetailId"); + String planId = getStringParam(detailParams, "planId"); + String planCode = getStringParam(detailParams, "planCode"); + + if (StringUtils.isNotBlank(productionBarcode)) { + // 命中本车条码后立即返回,避免再放大到计划级别 + batchParams.put("productionBarcode", productionBarcode); + return batchParams; + } + if (StringUtils.isNotBlank(planDetailId)) { + // 次优先:计划明细 ID + batchParams.put("planDetailId", planDetailId); + } else if (StringUtils.isNotBlank(planId)) { + // 再次优先:计划主表 ID + batchParams.put("planId", planId); + } else if (StringUtils.isNotBlank(planCode)) { + // 最后兜底:计划编码(模糊匹配) + batchParams.put("planCode", planCode); + } + return batchParams; + } + + private String getStringParam(Map params, String key) { + if (params == null || key == null) { + return null; + } + Object value = params.get(key); + return value == null ? null : StringUtils.trimToNull(String.valueOf(value)); + } + + /** + * 判断是否显式携带了“本车范围”筛选条件 + */ + private boolean hasDetailScopeFilter(Map params) { + // 这里列出的字段均属于“本车/本计划”范围字段,只要任一存在即视为强约束查询 + return isNotBlankParam(params, "planId") + || isNotBlankParam(params, "planCode") + || isNotBlankParam(params, "planDetailId") + || isNotBlankParam(params, "planDetailCode") + || isNotBlankParam(params, "productionBarcode") + || isNotBlankParam(params, "shiftId") + || isNotBlankParam(params, "classTeamId") + || isNotBlankParam(params, "shiftName") + || isNotBlankParam(params, "classTeamName"); + } + + private boolean isNotBlankParam(Map params, String key) { + if (params == null || key == null) { + return false; + } + Object value = params.get(key); + return value != null && StringUtils.isNotBlank(String.valueOf(value)); + } + + /** + * 参数名转中文标签 */ private String getParamLabel(String paramName) { - return paramName; + switch (paramName) { + case "mixingTemp": + return "混炼温度"; + case "mixingTime": + return "混炼时间"; + case "mixingEnergy": + return "混炼能量"; + case "mixingPower": + return "混炼功率"; + case "mixingPress": + return "混炼压力"; + case "mixingSpeed": + return "混炼转速"; + default: + return "混炼温度"; + } } /** - * 根据参数名获取实测值提取器 + * 参数白名单,避免前端传入非法字段名 + */ + private String normalizeParamName(String paramName) { + switch (paramName) { + case "mixingTemp": + case "mixingTime": + case "mixingEnergy": + case "mixingPower": + case "mixingPress": + case "mixingSpeed": + return paramName; + default: + return "mixingTemp"; + } + } + + /** + * 根据参数名选择实测值提取器 */ private Function getActualExtractor(String paramName) { - // 参数名与“实测字段”一一映射,时间/转速等整型字段转 BigDecimal 统一参与计算。 switch (paramName) { - case "mixingTemp": return MixTraceSpcSampleVo::getMixingTemp; - case "mixingTime": return s -> s.getMixingTime() != null ? BigDecimal.valueOf(s.getMixingTime()) : null; - case "mixingEnergy": return MixTraceSpcSampleVo::getMixingEnergy; - case "mixingPower": return MixTraceSpcSampleVo::getMixingPower; - case "mixingPress": return MixTraceSpcSampleVo::getMixingPress; - case "mixingSpeed": return s -> s.getMixingSpeed() != null ? BigDecimal.valueOf(s.getMixingSpeed()) : null; - default: return MixTraceSpcSampleVo::getMixingTemp; + case "mixingTemp": + return MixTraceSpcSampleVo::getMixingTemp; + case "mixingTime": + return sample -> sample.getMixingTime() == null ? null : BigDecimal.valueOf(sample.getMixingTime()); + case "mixingEnergy": + return MixTraceSpcSampleVo::getMixingEnergy; + case "mixingPower": + return MixTraceSpcSampleVo::getMixingPower; + case "mixingPress": + return MixTraceSpcSampleVo::getMixingPress; + case "mixingSpeed": + return sample -> sample.getMixingSpeed() == null ? null : BigDecimal.valueOf(sample.getMixingSpeed()); + default: + return MixTraceSpcSampleVo::getMixingTemp; } } /** - * 根据参数名获取设定值提取器 + * 根据参数名选择设定值提取器 */ private Function getSetExtractor(String paramName) { - // 参数名与“设定字段”一一映射,口径与实测字段保持一致。 switch (paramName) { - case "mixingTemp": return s -> s.getSetTemp() != null ? BigDecimal.valueOf(s.getSetTemp()) : null; - case "mixingTime": return s -> s.getSetTime() != null ? BigDecimal.valueOf(s.getSetTime()) : null; - case "mixingEnergy": return MixTraceSpcSampleVo::getSetEnergy; - case "mixingPower": return MixTraceSpcSampleVo::getSetPower; - case "mixingPress": return MixTraceSpcSampleVo::getSetPres; - case "mixingSpeed": return s -> s.getSetRota() != null ? BigDecimal.valueOf(s.getSetRota()) : null; - default: return s -> s.getSetTemp() != null ? BigDecimal.valueOf(s.getSetTemp()) : null; + case "mixingTemp": + return sample -> sample.getSetTemp() == null ? null : BigDecimal.valueOf(sample.getSetTemp()); + case "mixingTime": + return sample -> sample.getSetTime() == null ? null : BigDecimal.valueOf(sample.getSetTime()); + case "mixingEnergy": + return MixTraceSpcSampleVo::getSetEnergy; + case "mixingPower": + return MixTraceSpcSampleVo::getSetPower; + case "mixingPress": + return MixTraceSpcSampleVo::getSetPres; + case "mixingSpeed": + return sample -> sample.getSetRota() == null ? null : BigDecimal.valueOf(sample.getSetRota()); + default: + return sample -> sample.getSetTemp() == null ? null : BigDecimal.valueOf(sample.getSetTemp()); } } /** - * 计算均值 + * 算术平均值 */ private BigDecimal calcMean(List values) { if (values == null || values.isEmpty()) { return BigDecimal.ZERO; } - // BigDecimal 全程计算,避免 double 误差影响 SPC 指标。 BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add); return sum.divide(BigDecimal.valueOf(values.size()), SCALE, RM); } /** - * 计算样本标准差 + * 样本标准差(分母 n-1) */ private BigDecimal calcStdDev(List values, BigDecimal mean) { if (values == null || values.size() < 2) { return BigDecimal.ZERO; } - // 先算离差平方和,再按 n-1 求样本方差。 + // 先算离差平方和,再按样本方差(n-1)开方得到样本标准差 BigDecimal sumSq = values.stream() .map(v -> v.subtract(mean).pow(2)) .reduce(BigDecimal.ZERO, BigDecimal::add); - // 样本标准差使用 n-1 BigDecimal variance = sumSq.divide(BigDecimal.valueOf(values.size() - 1), SCALE * 2, RM); return BigDecimal.valueOf(Math.sqrt(variance.doubleValue())).setScale(SCALE, RM); } /** - * 构建空结果 + * 构建空结果对象 */ private MixTraceSpcResultVo buildEmptyResult(String paramName) { - // 空结果也回传参数信息,前端可继续显示“当前分析参数”。 MixTraceSpcResultVo result = new MixTraceSpcResultVo(); result.setParamName(paramName); result.setParamLabel(getParamLabel(paramName)); @@ -437,21 +586,19 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService } /** - * 构建直方图数据(等宽分箱,默认10个区间) + * 构建直方图(等宽 10 箱) */ private void buildHistogram(MixTraceSpcResultVo result, List values) { if (values == null || values.size() < 2) { - // 样本过少时不生成分箱,前端直方图区域自然不显示。 return; } - // 采用固定 10 箱的等宽分箱策略,优先保证可解释性与稳定渲染。 int binCount = 10; BigDecimal min = result.getMinValue(); BigDecimal max = result.getMaxValue(); BigDecimal range = max.subtract(min); if (range.compareTo(BigDecimal.ZERO) == 0) { - // 所有值相同 + // 全部样本值相同:直方图退化为单箱 List counts = new ArrayList<>(); List bins = new ArrayList<>(); counts.add(values.size()); @@ -471,11 +618,10 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService int finalI = i; long count = values.stream() .filter(v -> { - // 最后一个分箱右闭区间,确保最大值不会因边界比较丢失。 if (finalI == binCount - 1) { + // 最后一个箱包含上边界,避免 max 值丢失 return v.compareTo(lower) >= 0 && v.compareTo(upper) <= 0; } - // 其余分箱采用左闭右开,避免边界值重复计数。 return v.compareTo(lower) >= 0 && v.compareTo(upper) < 0; }) .count(); @@ -486,4 +632,91 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService result.setHistogramCounts(counts); result.setHistogramBins(bins); } + + private List buildCurveSeries(List mixingStepList) { + if (mixingStepList == null || mixingStepList.isEmpty()) { + return new ArrayList<>(); + } + + List points = new ArrayList<>(); + for (MixTraceStepVo step : mixingStepList.stream() + .sorted(Comparator.comparing(MixTraceStepVo::getMixId, Comparator.nullsLast(Long::compareTo))) + .collect(Collectors.toList())) { + // 先按 mixId 排序,保证曲线时间顺序与工步表一致 + MixTraceCurvePointVo point = new MixTraceCurvePointVo(); + point.setStepNo(step.getMixId()); + point.setTimelineSecond(step.getTimelineSecond()); + // 横轴统一使用“步骤N”,和工步表格保持一致 + point.setXLabel(step.getMixId() == null ? "-" : "步骤" + step.getMixId()); + point.setTemperature(step.getMixingTemp()); + point.setPower(step.getMixingPower()); + point.setEnergy(step.getMixingEnergy()); + point.setPressure(step.getMixingPress()); + point.setSpeed(step.getMixingSpeed()); + points.add(point); + } + return points; + } + + private List buildMaterialTraceTree(MixTraceSummaryVo summaryInfo, + List batchList) { + if (summaryInfo == null) { + return new ArrayList<>(); + } + + // 根节点代表“本车” + MixTraceMaterialTraceTreeVo root = new MixTraceMaterialTraceTreeVo(); + String rootBarcode = StringUtils.defaultString(summaryInfo.getProductionBarcode(), ""); + root.setId("root-" + summaryInfo.getRecipeId() + "-" + rootBarcode); + root.setNodeType("car"); + root.setRecipeId(summaryInfo.getRecipeId()); + root.setPlanDetailId(summaryInfo.getPlanDetailId()); + root.setProductionBarcode(summaryInfo.getProductionBarcode()); + root.setMaterialId(summaryInfo.getMaterialId()); + root.setMaterialName(summaryInfo.getMaterialName()); + root.setLabel(StringUtils.defaultIfBlank(summaryInfo.getRecipeCode(), "") + " / " + rootBarcode); + + if (batchList == null || batchList.isEmpty()) { + // 没有批次时仍返回根节点,前端可展示“本车无耗用批次” + List tree = new ArrayList<>(); + tree.add(root); + return tree; + } + + // 使用 LinkedHashMap 保留插入顺序,前端树节点顺序稳定 + Map materialNodeMap = new LinkedHashMap<>(); + for (int i = 0; i < batchList.size(); i++) { + MixTraceBatchVo batch = batchList.get(i); + String materialKey = (batch.getMaterialId() == null ? "0" : String.valueOf(batch.getMaterialId())) + + "_" + StringUtils.defaultString(batch.getMaterialName(), ""); + + MixTraceMaterialTraceTreeVo materialNode = materialNodeMap.computeIfAbsent(materialKey, key -> { + // 物料层节点按 materialId + materialName 去重 + MixTraceMaterialTraceTreeVo node = new MixTraceMaterialTraceTreeVo(); + node.setId("mat-" + key); + node.setNodeType("material"); + node.setMaterialId(batch.getMaterialId()); + node.setMaterialName(batch.getMaterialName()); + node.setLabel(StringUtils.defaultIfBlank(batch.getMaterialName(), + batch.getMaterialId() == null ? "" : String.valueOf(batch.getMaterialId()))); + return node; + }); + + MixTraceMaterialTraceTreeVo batchNode = new MixTraceMaterialTraceTreeVo(); + batchNode.setId("batch-" + (batch.getBatchId() == null ? i : batch.getBatchId())); + batchNode.setNodeType("batch"); + batchNode.setProductionBarcode(batch.getProductionBarcode()); + batchNode.setMaterialId(batch.getMaterialId()); + batchNode.setMaterialName(batch.getMaterialName()); + batchNode.setBatchCode(batch.getBatchCode()); + batchNode.setLabel(StringUtils.defaultIfBlank(batch.getBatchCode(), + StringUtils.defaultIfBlank(batch.getInputBarcode(), StringUtils.defaultString(batch.getProductionBarcode(), "")))); + materialNode.getChildren().add(batchNode); + } + + root.getChildren().addAll(materialNodeMap.values()); + List tree = new ArrayList<>(); + tree.add(root); + return tree; + } } diff --git a/ruoyi-modules/hwmom-mes/src/main/resources/mapper/mes/ProdMixTraceReportMapper.xml b/ruoyi-modules/hwmom-mes/src/main/resources/mapper/mes/ProdMixTraceReportMapper.xml index 69184820..d4d3b72b 100644 --- a/ruoyi-modules/hwmom-mes/src/main/resources/mapper/mes/ProdMixTraceReportMapper.xml +++ b/ruoyi-modules/hwmom-mes/src/main/resources/mapper/mes/ProdMixTraceReportMapper.xml @@ -16,6 +16,93 @@ ) + + OUTER APPLY ( + SELECT TOP 1 + p.plan_id AS planId, + p.plan_code AS planCode, + p.plan_amount AS planAmount, + p.complete_amount AS planCompleteAmount, + p.real_begin_time AS planRealBeginTime, + p.real_end_time AS planRealEndTime, + ppd.plan_detail_id AS planDetailId, + ppd.plan_detail_code AS planDetailCode, + ppd.complete_amount AS detailCompleteAmount, + ppd.real_begin_time AS detailRealBeginTime, + ppd.real_end_time AS detailRealEndTime, + ppd.train_number AS trainNumber, + ppd.alarm_flag AS alarmFlag, + ppd.plan_detail_status AS planDetailStatus, + COALESCE(ppd.shift_id, p.shift_id) AS shiftId, + COALESCE(ppd.class_team_id, p.class_team_id) AS classTeamId, + bsi.shift_name AS shiftName, + bcti.team_name AS classTeamName, + COALESCE(NULLIF(LTRIM(RTRIM(ppd.return_barcode)), ''), NULLIF(LTRIM(RTRIM(ppd.material_barcode)), '')) AS productionBarcode + FROM prod_plan_info_1 p + LEFT JOIN prod_product_plan_detail_1 ppd ON ppd.plan_id = p.plan_id AND ISNULL(ppd.del_flag, '0') = '0' + LEFT JOIN base_shift_info bsi ON bsi.shift_id = COALESCE(ppd.shift_id, p.shift_id) + LEFT JOIN base_class_team_info bcti ON bcti.class_team_id = COALESCE(ppd.class_team_id, p.class_team_id) + WHERE ( + p.recipe_id = ri.recipe_id + OR ( + ISNULL(p.recipe_id, 0) = 0 + AND p.material_id = ri.material_id + ) + ) + AND ( + p.release_type IS NULL + OR p.release_type <> '1' + OR p.release_id = ri.machine_id + ) + + AND p.plan_id = #{map.planId} + + + AND p.plan_code LIKE CONCAT('%', #{map.planCode}, '%') + + + AND ppd.plan_detail_id = #{map.planDetailId} + + + AND ppd.plan_detail_code LIKE CONCAT('%', #{map.planDetailCode}, '%') + + + AND COALESCE(ppd.shift_id, p.shift_id) = #{map.shiftId} + + + AND COALESCE(ppd.class_team_id, p.class_team_id) = #{map.classTeamId} + + + AND bsi.shift_name LIKE CONCAT('%', #{map.shiftName}, '%') + + + AND bcti.team_name LIKE CONCAT('%', #{map.classTeamName}, '%') + + + AND ( + LTRIM(RTRIM(COALESCE(ppd.return_barcode, ''))) = LTRIM(RTRIM(#{map.productionBarcode})) + OR LTRIM(RTRIM(COALESCE(ppd.material_barcode, ''))) = LTRIM(RTRIM(#{map.productionBarcode})) + ) + + ORDER BY + CASE + WHEN #{map.planDetailId} IS NOT NULL + AND LTRIM(RTRIM(CONVERT(VARCHAR(64), #{map.planDetailId}))) <> '' + AND CONVERT(VARCHAR(64), ppd.plan_detail_id) = CONVERT(VARCHAR(64), #{map.planDetailId}) THEN 0 + ELSE 1 + END, + CASE + WHEN #{map.productionBarcode} IS NOT NULL + AND ( + LTRIM(RTRIM(COALESCE(ppd.return_barcode, ''))) = LTRIM(RTRIM(#{map.productionBarcode})) + OR LTRIM(RTRIM(COALESCE(ppd.material_barcode, ''))) = LTRIM(RTRIM(#{map.productionBarcode})) + ) THEN 0 + ELSE 1 + END, + COALESCE(ppd.real_begin_time, ppd.create_time, p.real_begin_time, p.create_time) DESC + ) tp + + @@ -111,12 +224,148 @@ (SELECT COUNT(1) FROM prod_recipe_weight w WHERE w.recipe_id = ri.recipe_id AND w.del_flag = '0') AS weightCount, (SELECT COUNT(1) FROM prod_recipe_mixing m - WHERE m.recipe_id = ri.recipe_id AND m.del_flag = '0') AS mixingCount + WHERE m.recipe_id = ri.recipe_id AND m.del_flag = '0') AS mixingCount, + tp.planId AS planId, + tp.planCode AS planCode, + tp.planDetailId AS planDetailId, + tp.planDetailCode AS planDetailCode, + tp.productionBarcode AS productionBarcode, + tp.shiftId AS shiftId, + tp.shiftName AS shiftName, + tp.classTeamId AS classTeamId, + tp.classTeamName AS classTeamName, + tp.planAmount AS planAmount, + COALESCE(tp.detailCompleteAmount, tp.planCompleteAmount) AS completeAmount, + tp.trainNumber AS trainNumber, + COALESCE(tp.detailRealBeginTime, tp.planRealBeginTime) AS realBeginTime, + COALESCE(tp.detailRealEndTime, tp.planRealEndTime) AS realEndTime FROM prod_recipe_info ri LEFT JOIN prod_base_machine_info bm ON bm.machine_id = ri.machine_id LEFT JOIN base_material_info bmi ON bmi.material_id = ri.material_id + WHERE ri.del_flag = '0' - AND ri.recipe_id = #{recipeId} + AND ri.recipe_id = #{map.recipeId} + + AND tp.planId IS NOT NULL + + + + + @@ -157,6 +406,87 @@ ORDER BY w.weight_seq ASC + + + + + + + + +