From 1d42a0f70b5c9ae0cf0d3fa9b70c1e3d0fa6720b Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Sun, 15 Feb 2026 20:05:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(mes):=20=E4=BC=98=E5=8C=96=E5=AF=86?= =?UTF-8?q?=E7=82=BC=E8=BF=BD=E6=BA=AF=E6=8A=A5=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除日志注解,简化控制器依赖 - 重构请求路径,统一为 /mixTrace 前缀 - 添加参数归一化处理,防止空指针异常 - 增加机器名称模糊查询支持 - 优化日期范围查询逻辑,提升时间精度 - 改进 SPC 统计计算算法,增加数据验证 - 添加系统主数据关联查询,丰富显示内容 - 优化 Xbar-R 控制图计算公式,提高准确性 - 统一参数处理流程,增强代码健壮性 --- .../ProdMixTraceReportController.java | 60 +++----- .../impl/ProdMixTraceReportServiceImpl.java | 110 ++++++++++---- .../mapper/mes/ProdMixTraceReportMapper.xml | 141 ++++++++++++++++-- 3 files changed, 227 insertions(+), 84 deletions(-) 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 4d911807..ced918a9 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 @@ -5,8 +5,6 @@ import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.common.idempotent.annotation.RepeatSubmit; -import org.dromara.common.log.annotation.Log; -import org.dromara.common.log.enums.BusinessType; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.web.core.BaseController; @@ -18,90 +16,76 @@ import org.dromara.mes.service.IProdMixTraceReportService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.Collections; import java.util.List; import java.util.Map; /** - * 密炼追溯报表Controller - * 对应图5(追溯列表)、图9(追溯详情)、图4(曲线)、图6(SPC样本)、图7/8/10(SPC图表) - * - * @author Yinq - * @date 2026-02-14 + * 密炼追溯报表 Controller */ @Validated @RequiredArgsConstructor @RestController -@RequestMapping("/mes/mixTrace") +@RequestMapping("/mixTrace") public class ProdMixTraceReportController extends BaseController { private final IProdMixTraceReportService mixTraceReportService; - /** - * 追溯列表(图5)- 分页 - */ + /** 追溯列表(分页) */ //@SaCheckPermission("mes:mixTrace:list") @GetMapping("/list") public TableDataInfo list(@RequestParam(required = false) Map params, - PageQuery pageQuery) { - return mixTraceReportService.queryTraceList(params, pageQuery); + PageQuery pageQuery) { + return mixTraceReportService.queryTraceList(normalizeParams(params), pageQuery); } - /** - * 追溯列表导出 - */ + /** 追溯导出 */ //@SaCheckPermission("mes:mixTrace:export") - @Log(title = "密炼追溯报表导出", businessType = BusinessType.EXPORT) @RepeatSubmit() @PostMapping("/export") public void export(@RequestParam(required = false) Map params, HttpServletResponse response) { - List list = mixTraceReportService.queryTraceList(params); + List list = mixTraceReportService.queryTraceList(normalizeParams(params)); ExcelUtil.exportExcel(list, "密炼追溯报表", MixTraceListVo.class, response); } - /** - * 追溯详情(图9)- 基础信息 + 称量 + 混炼 - */ + /** 追溯详情 */ //@SaCheckPermission("mes:mixTrace:list") @GetMapping("/detail/{recipeId}") public R detail(@PathVariable Long recipeId) { return R.ok(mixTraceReportService.queryTraceDetail(recipeId)); } - /** - * SPC样本列表(图6)- 分页 - */ + /** SPC 样本 */ //@SaCheckPermission("mes:mixTrace:list") @GetMapping("/spc/samples") public TableDataInfo spcSamples(@RequestParam(required = false) Map params, - PageQuery pageQuery) { - return mixTraceReportService.querySpcSamples(params, pageQuery); + PageQuery pageQuery) { + return mixTraceReportService.querySpcSamples(normalizeParams(params), pageQuery); } - /** - * SPC能力分析(图7)- 直方图 + 正态拟合 + Cp/Cpk/Ppk - */ + /** SPC 能力 */ //@SaCheckPermission("mes:mixTrace:list") @GetMapping("/spc/capability") public R spcCapability(@RequestParam(required = false) Map params) { - return R.ok(mixTraceReportService.calculateSpcCapability(params)); + return R.ok(mixTraceReportService.calculateSpcCapability(normalizeParams(params))); } - /** - * SPC运行图(图8)- 点图 + USL/LSL/Target - */ + /** SPC 运行图 */ //@SaCheckPermission("mes:mixTrace:list") @GetMapping("/spc/runChart") public R spcRunChart(@RequestParam(required = false) Map params) { - return R.ok(mixTraceReportService.calculateSpcRunChart(params)); + return R.ok(mixTraceReportService.calculateSpcRunChart(normalizeParams(params))); } - /** - * SPC均值极差图(图10)- Xbar-R - */ + /** SPC Xbar-R */ //@SaCheckPermission("mes:mixTrace:list") @GetMapping("/spc/xbarR") public R spcXbarR(@RequestParam(required = false) Map params) { - return R.ok(mixTraceReportService.calculateSpcXbarR(params)); + return R.ok(mixTraceReportService.calculateSpcXbarR(normalizeParams(params))); + } + + private Map normalizeParams(Map params) { + return params == null ? Collections.emptyMap() : params; } } 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 9b2ea5c0..988b2e67 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 @@ -13,6 +13,7 @@ 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.function.Function; @@ -63,24 +64,30 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService @Override public TableDataInfo queryTraceList(Map params, PageQuery pageQuery) { - Page page = mixTraceReportMapper.selectTraceList(params, pageQuery.build()); + // 统一把 null 查询参数转为空 Map,避免 Mapper XML 中 OGNL 访问空指针。 + Map queryParams = safeParams(params); + // 由 PageQuery 生成分页对象,确保前后端分页语义一致(pageNum/pageSize)。 + Page page = mixTraceReportMapper.selectTraceList(queryParams, pageQuery.build()); + // 统一封装成 TableDataInfo,前端可直接读取 rows/total。 return TableDataInfo.build(page); } @Override public List queryTraceList(Map params) { - return mixTraceReportMapper.selectTraceList(params); + return mixTraceReportMapper.selectTraceList(safeParams(params)); } // ==================== 追溯详情(图9) ==================== @Override public MixTraceDetailVo queryTraceDetail(Long recipeId) { + // 先查主信息;不存在时直接返回 null,前端按“无详情”处理。 MixTraceListVo recipeInfo = mixTraceReportMapper.selectTraceRecipeInfo(recipeId); if (recipeInfo == null) { return null; } MixTraceDetailVo detail = new MixTraceDetailVo(); + // 主表信息(配方基础信息)。 detail.setRecipeInfo(recipeInfo); // 称量明细按 weight_seq 排序 detail.setWeightList(mixTraceReportMapper.selectWeightListByRecipeId(recipeId)); @@ -93,7 +100,9 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService @Override public TableDataInfo querySpcSamples(Map params, PageQuery pageQuery) { - Page page = mixTraceReportMapper.selectSpcSamples(params, pageQuery.build()); + // SPC 样本分页查询:入参做空安全处理,避免条件拼接异常。 + Map queryParams = safeParams(params); + Page page = mixTraceReportMapper.selectSpcSamples(queryParams, pageQuery.build()); return TableDataInfo.build(page); } @@ -101,29 +110,34 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService @Override public MixTraceSpcResultVo calculateSpcCapability(Map params) { + // 1) 参数归一化:保证后续读取参数时不会出现 null Map。 + Map queryParams = safeParams(params); // 获取分析参数名称,默认分析混炼温度 - String paramName = getParamName(params); + String paramName = getParamName(queryParams); - // 查询全量样本 - List samples = mixTraceReportMapper.selectSpcSamples(params); + // 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); } - // 基本统计 + // 5) 计算基础统计量(均值、标准差、极值)。 MixTraceSpcResultVo result = new MixTraceSpcResultVo(); result.setParamName(paramName); result.setParamLabel(getParamLabel(paramName)); @@ -139,7 +153,9 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService result.setMinValue(min); result.setMaxValue(max); - // 规格限:从设定值中取平均作为Target,USL/LSL可按 Target ± 容差 或取最大/最小设定值 + // 6) 计算规格限: + // - 优先使用设定值的最小/最大作为 LSL/USL(最贴近工艺设定)。 + // - 若设定值没有波动(全相同),退化为 target ± 10% 的经验容差。 List setValues = samples.stream() .map(setExtractor) .filter(v -> v != null) @@ -161,13 +177,14 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService } } - // 能力指数 + // 7) 计算能力指数(前提:σ>0 且已得到上下规格限)。 if (sigma.compareTo(BigDecimal.ZERO) > 0 && result.getUsl() != null && result.getLsl() != null) { 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)); @@ -178,10 +195,11 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService result.setPpk(result.getCpk()); } - // 直方图数据 + // 8) 构造直方图分箱数据,供前端能力图直接渲染。 buildHistogram(result, values); - // 运行图数据 + // 9) 构造运行图序列数据(值 + 标签)。 + // 这里标签优先用配方编码,若缺失则回退为样本序号,保证前端 x 轴稳定。 result.setSampleValues(values); List labels = new ArrayList<>(); for (int i = 0; i < samples.size(); i++) { @@ -200,23 +218,33 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService @Override public MixTraceSpcResultVo calculateSpcRunChart(Map params) { - // 运行图与能力分析共用数据,直接复用 - return calculateSpcCapability(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) { - String paramName = getParamName(params); + // 1) 参数归一化并确定分析参数。 + Map queryParams = safeParams(params); + String paramName = getParamName(queryParams); - List samples = mixTraceReportMapper.selectSpcSamples(params); + // 2) 取全量样本,Xbar-R 需要连续样本进行分组统计。 + List samples = mixTraceReportMapper.selectSpcSamples(queryParams); if (samples == null || samples.isEmpty()) { return buildEmptyResult(paramName); } Function actualExtractor = getActualExtractor(paramName); + // 仅保留有效实测值,保证子组计算准确。 List values = samples.stream() .map(actualExtractor) .filter(v -> v != null) @@ -224,9 +252,9 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService // 子组大小,默认5,可通过参数配置 int subgroupSize = 5; - if (params.containsKey("subgroupSize")) { + if (queryParams.containsKey("subgroupSize")) { try { - subgroupSize = Integer.parseInt(String.valueOf(params.get("subgroupSize"))); + subgroupSize = Integer.parseInt(String.valueOf(queryParams.get("subgroupSize"))); } catch (NumberFormatException ignored) { } } @@ -234,13 +262,13 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService subgroupSize = Math.max(2, Math.min(10, subgroupSize)); if (values.size() < subgroupSize * 2) { - // 样本不足,至少需要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<>(); @@ -254,6 +282,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService BigDecimal groupMin = group.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); BigDecimal groupRange = groupMax.subtract(groupMin); + // 每个子组提取两个关键统计量:均值(Xbar) 和 极差(R)。 xbarValues.add(groupMean); rValues.add(groupRange); subgroupLabels.add("G" + (i + 1)); @@ -263,7 +292,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService BigDecimal xbarbar = calcMean(xbarValues); BigDecimal rbar = calcMean(rValues); - // 查找常量 A2, D3, D4 + // 4) 按子组大小查控制图常量 A2/D3/D4(行业标准常量表)。 int constIndex = subgroupSize - 2; // 数组从 n=2 开始 double a2 = XBAR_R_CONSTANTS[constIndex][0]; double d3 = XBAR_R_CONSTANTS[constIndex][1]; @@ -288,12 +317,18 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService result.setXbarbar(xbarbar); result.setRbar(rbar); - // Xbar 控制限 + // 5) 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)); - // R 控制限 + // 6) 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)); @@ -307,30 +342,33 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService * 获取分析参数名称,默认 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; + } + /** * 参数名称 -> 中文标签映射 */ private String getParamLabel(String paramName) { - switch (paramName) { - case "mixingTemp": return "混炼温度"; - case "mixingTime": return "混炼时间"; - case "mixingEnergy": return "混炼能量"; - case "mixingPower": return "混炼功率"; - case "mixingPress": return "混炼压力"; - case "mixingSpeed": return "混炼转速"; - default: return paramName; - } + return paramName; } /** * 根据参数名获取实测值提取器 */ 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; @@ -346,6 +384,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService * 根据参数名获取设定值提取器 */ 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; @@ -364,6 +403,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService 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); } @@ -375,6 +415,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService if (values == null || values.size() < 2) { return BigDecimal.ZERO; } + // 先算离差平方和,再按 n-1 求样本方差。 BigDecimal sumSq = values.stream() .map(v -> v.subtract(mean).pow(2)) .reduce(BigDecimal.ZERO, BigDecimal::add); @@ -387,6 +428,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService * 构建空结果 */ private MixTraceSpcResultVo buildEmptyResult(String paramName) { + // 空结果也回传参数信息,前端可继续显示“当前分析参数”。 MixTraceSpcResultVo result = new MixTraceSpcResultVo(); result.setParamName(paramName); result.setParamLabel(getParamLabel(paramName)); @@ -399,8 +441,10 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService */ 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(); @@ -427,9 +471,11 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService int finalI = i; long count = values.stream() .filter(v -> { + // 最后一个分箱右闭区间,确保最大值不会因边界比较丢失。 if (finalI == binCount - 1) { return v.compareTo(lower) >= 0 && v.compareTo(upper) <= 0; } + // 其余分箱采用左闭右开,避免边界值重复计数。 return v.compareTo(lower) >= 0 && v.compareTo(upper) < 0; }) .count(); 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 7b4fd4a2..69184820 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 @@ -4,7 +4,19 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - + + WITH router_master_data AS ( + SELECT + MAX(CASE WHEN REPLACE(sm.query_param, ' ', '') LIKE '%"router":"act"%' THEN sm.master_data_id END) AS act_master_data_id, + MAX(CASE WHEN REPLACE(sm.query_param, ' ', '') LIKE '%"router":"term"%' THEN sm.master_data_id END) AS term_master_data_id, + MAX(CASE WHEN REPLACE(sm.query_param, ' ', '') LIKE '%"router":"jar"%' THEN sm.master_data_id END) AS jar_master_data_id + FROM sys_master_data sm + WHERE sm.del_flag = '0' + AND sm.active_flag = '1' + ) + + + - + - + - + - +