feat(mes): 优化密炼追溯报表功能

- 移除日志注解,简化控制器依赖
- 重构请求路径,统一为 /mixTrace 前缀
- 添加参数归一化处理,防止空指针异常
- 增加机器名称模糊查询支持
- 优化日期范围查询逻辑,提升时间精度
- 改进 SPC 统计计算算法,增加数据验证
- 添加系统主数据关联查询,丰富显示内容
- 优化 Xbar-R 控制图计算公式,提高准确性
- 统一参数处理流程,增强代码健壮性
master
zangch@mesnac.com 3 days ago
parent 9e084f61f8
commit 1d42a0f70b

@ -5,8 +5,6 @@ import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit; 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.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController; 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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Controller * Controller
* 5()9()4(线)6(SPC)7/8/10(SPC)
*
* @author Yinq
* @date 2026-02-14
*/ */
@Validated @Validated
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/mes/mixTrace") @RequestMapping("/mixTrace")
public class ProdMixTraceReportController extends BaseController { public class ProdMixTraceReportController extends BaseController {
private final IProdMixTraceReportService mixTraceReportService; private final IProdMixTraceReportService mixTraceReportService;
/** /** 追溯列表(分页) */
* 5-
*/
//@SaCheckPermission("mes:mixTrace:list") //@SaCheckPermission("mes:mixTrace:list")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo<MixTraceListVo> list(@RequestParam(required = false) Map<String, Object> params, public TableDataInfo<MixTraceListVo> list(@RequestParam(required = false) Map<String, Object> params,
PageQuery pageQuery) { PageQuery pageQuery) {
return mixTraceReportService.queryTraceList(params, pageQuery); return mixTraceReportService.queryTraceList(normalizeParams(params), pageQuery);
} }
/** /** 追溯导出 */
*
*/
//@SaCheckPermission("mes:mixTrace:export") //@SaCheckPermission("mes:mixTrace:export")
@Log(title = "密炼追溯报表导出", businessType = BusinessType.EXPORT)
@RepeatSubmit() @RepeatSubmit()
@PostMapping("/export") @PostMapping("/export")
public void export(@RequestParam(required = false) Map<String, Object> params, public void export(@RequestParam(required = false) Map<String, Object> params,
HttpServletResponse response) { HttpServletResponse response) {
List<MixTraceListVo> list = mixTraceReportService.queryTraceList(params); List<MixTraceListVo> list = mixTraceReportService.queryTraceList(normalizeParams(params));
ExcelUtil.exportExcel(list, "密炼追溯报表", MixTraceListVo.class, response); ExcelUtil.exportExcel(list, "密炼追溯报表", MixTraceListVo.class, response);
} }
/** /** 追溯详情 */
* 9- + +
*/
//@SaCheckPermission("mes:mixTrace:list") //@SaCheckPermission("mes:mixTrace:list")
@GetMapping("/detail/{recipeId}") @GetMapping("/detail/{recipeId}")
public R<MixTraceDetailVo> detail(@PathVariable Long recipeId) { public R<MixTraceDetailVo> detail(@PathVariable Long recipeId) {
return R.ok(mixTraceReportService.queryTraceDetail(recipeId)); return R.ok(mixTraceReportService.queryTraceDetail(recipeId));
} }
/** /** SPC 样本 */
* SPC6-
*/
//@SaCheckPermission("mes:mixTrace:list") //@SaCheckPermission("mes:mixTrace:list")
@GetMapping("/spc/samples") @GetMapping("/spc/samples")
public TableDataInfo<MixTraceSpcSampleVo> spcSamples(@RequestParam(required = false) Map<String, Object> params, public TableDataInfo<MixTraceSpcSampleVo> spcSamples(@RequestParam(required = false) Map<String, Object> params,
PageQuery pageQuery) { PageQuery pageQuery) {
return mixTraceReportService.querySpcSamples(params, pageQuery); return mixTraceReportService.querySpcSamples(normalizeParams(params), pageQuery);
} }
/** /** SPC 能力 */
* SPC7- + + Cp/Cpk/Ppk
*/
//@SaCheckPermission("mes:mixTrace:list") //@SaCheckPermission("mes:mixTrace:list")
@GetMapping("/spc/capability") @GetMapping("/spc/capability")
public R<MixTraceSpcResultVo> spcCapability(@RequestParam(required = false) Map<String, Object> params) { public R<MixTraceSpcResultVo> spcCapability(@RequestParam(required = false) Map<String, Object> params) {
return R.ok(mixTraceReportService.calculateSpcCapability(params)); return R.ok(mixTraceReportService.calculateSpcCapability(normalizeParams(params)));
} }
/** /** SPC 运行图 */
* SPC8- + USL/LSL/Target
*/
//@SaCheckPermission("mes:mixTrace:list") //@SaCheckPermission("mes:mixTrace:list")
@GetMapping("/spc/runChart") @GetMapping("/spc/runChart")
public R<MixTraceSpcResultVo> spcRunChart(@RequestParam(required = false) Map<String, Object> params) { public R<MixTraceSpcResultVo> spcRunChart(@RequestParam(required = false) Map<String, Object> params) {
return R.ok(mixTraceReportService.calculateSpcRunChart(params)); return R.ok(mixTraceReportService.calculateSpcRunChart(normalizeParams(params)));
} }
/** /** SPC Xbar-R */
* SPC10- Xbar-R
*/
//@SaCheckPermission("mes:mixTrace:list") //@SaCheckPermission("mes:mixTrace:list")
@GetMapping("/spc/xbarR") @GetMapping("/spc/xbarR")
public R<MixTraceSpcResultVo> spcXbarR(@RequestParam(required = false) Map<String, Object> params) { public R<MixTraceSpcResultVo> spcXbarR(@RequestParam(required = false) Map<String, Object> params) {
return R.ok(mixTraceReportService.calculateSpcXbarR(params)); return R.ok(mixTraceReportService.calculateSpcXbarR(normalizeParams(params)));
}
private Map<String, Object> normalizeParams(Map<String, Object> params) {
return params == null ? Collections.emptyMap() : params;
} }
} }

@ -13,6 +13,7 @@ import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
@ -63,24 +64,30 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
@Override @Override
public TableDataInfo<MixTraceListVo> queryTraceList(Map<String, Object> params, PageQuery pageQuery) { public TableDataInfo<MixTraceListVo> queryTraceList(Map<String, Object> params, PageQuery pageQuery) {
Page<MixTraceListVo> page = mixTraceReportMapper.selectTraceList(params, pageQuery.build()); // 统一把 null 查询参数转为空 Map避免 Mapper XML 中 OGNL 访问空指针。
Map<String, Object> queryParams = safeParams(params);
// 由 PageQuery 生成分页对象确保前后端分页语义一致pageNum/pageSize
Page<MixTraceListVo> page = mixTraceReportMapper.selectTraceList(queryParams, pageQuery.build());
// 统一封装成 TableDataInfo前端可直接读取 rows/total。
return TableDataInfo.build(page); return TableDataInfo.build(page);
} }
@Override @Override
public List<MixTraceListVo> queryTraceList(Map<String, Object> params) { public List<MixTraceListVo> queryTraceList(Map<String, Object> params) {
return mixTraceReportMapper.selectTraceList(params); return mixTraceReportMapper.selectTraceList(safeParams(params));
} }
// ==================== 追溯详情图9 ==================== // ==================== 追溯详情图9 ====================
@Override @Override
public MixTraceDetailVo queryTraceDetail(Long recipeId) { public MixTraceDetailVo queryTraceDetail(Long recipeId) {
// 先查主信息;不存在时直接返回 null前端按“无详情”处理。
MixTraceListVo recipeInfo = mixTraceReportMapper.selectTraceRecipeInfo(recipeId); MixTraceListVo recipeInfo = mixTraceReportMapper.selectTraceRecipeInfo(recipeId);
if (recipeInfo == null) { if (recipeInfo == null) {
return null; return null;
} }
MixTraceDetailVo detail = new MixTraceDetailVo(); MixTraceDetailVo detail = new MixTraceDetailVo();
// 主表信息(配方基础信息)。
detail.setRecipeInfo(recipeInfo); detail.setRecipeInfo(recipeInfo);
// 称量明细按 weight_seq 排序 // 称量明细按 weight_seq 排序
detail.setWeightList(mixTraceReportMapper.selectWeightListByRecipeId(recipeId)); detail.setWeightList(mixTraceReportMapper.selectWeightListByRecipeId(recipeId));
@ -93,7 +100,9 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
@Override @Override
public TableDataInfo<MixTraceSpcSampleVo> querySpcSamples(Map<String, Object> params, PageQuery pageQuery) { public TableDataInfo<MixTraceSpcSampleVo> querySpcSamples(Map<String, Object> params, PageQuery pageQuery) {
Page<MixTraceSpcSampleVo> page = mixTraceReportMapper.selectSpcSamples(params, pageQuery.build()); // SPC 样本分页查询:入参做空安全处理,避免条件拼接异常。
Map<String, Object> queryParams = safeParams(params);
Page<MixTraceSpcSampleVo> page = mixTraceReportMapper.selectSpcSamples(queryParams, pageQuery.build());
return TableDataInfo.build(page); return TableDataInfo.build(page);
} }
@ -101,29 +110,34 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
@Override @Override
public MixTraceSpcResultVo calculateSpcCapability(Map<String, Object> params) { public MixTraceSpcResultVo calculateSpcCapability(Map<String, Object> params) {
// 1) 参数归一化:保证后续读取参数时不会出现 null Map。
Map<String, Object> queryParams = safeParams(params);
// 获取分析参数名称,默认分析混炼温度 // 获取分析参数名称,默认分析混炼温度
String paramName = getParamName(params); String paramName = getParamName(queryParams);
// 查询全量样本 // 2) 查询参与统计的全量样本(能力分析必须用全样本,不用分页样本)。
List<MixTraceSpcSampleVo> samples = mixTraceReportMapper.selectSpcSamples(params); List<MixTraceSpcSampleVo> samples = mixTraceReportMapper.selectSpcSamples(queryParams);
if (samples == null || samples.isEmpty()) { if (samples == null || samples.isEmpty()) {
// 无样本直接返回空结构,前端用 sampleCount=0 判断无图表数据。
return buildEmptyResult(paramName); return buildEmptyResult(paramName);
} }
// 提取指定参数的实测值和设定值 // 3) 根据参数名选择“实测值/设定值”的提取器,避免写 if-else 大分支。
Function<MixTraceSpcSampleVo, BigDecimal> actualExtractor = getActualExtractor(paramName); Function<MixTraceSpcSampleVo, BigDecimal> actualExtractor = getActualExtractor(paramName);
Function<MixTraceSpcSampleVo, BigDecimal> setExtractor = getSetExtractor(paramName); Function<MixTraceSpcSampleVo, BigDecimal> setExtractor = getSetExtractor(paramName);
// 4) 仅保留非空实测值参与统计,屏蔽脏数据对均值/方差计算的影响。
List<BigDecimal> values = samples.stream() List<BigDecimal> values = samples.stream()
.map(actualExtractor) .map(actualExtractor)
.filter(v -> v != null) .filter(v -> v != null)
.collect(Collectors.toList()); .collect(Collectors.toList());
if (values.size() < 2) { if (values.size() < 2) {
// 标准差至少需要 2 个样本点,少于 2 个无法做能力计算。
return buildEmptyResult(paramName); return buildEmptyResult(paramName);
} }
// 基本统计 // 5) 计算基础统计量(均值、标准差、极值)。
MixTraceSpcResultVo result = new MixTraceSpcResultVo(); MixTraceSpcResultVo result = new MixTraceSpcResultVo();
result.setParamName(paramName); result.setParamName(paramName);
result.setParamLabel(getParamLabel(paramName)); result.setParamLabel(getParamLabel(paramName));
@ -139,7 +153,9 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
result.setMinValue(min); result.setMinValue(min);
result.setMaxValue(max); result.setMaxValue(max);
// 规格限从设定值中取平均作为TargetUSL/LSL可按 Target ± 容差 或取最大/最小设定值 // 6) 计算规格限:
// - 优先使用设定值的最小/最大作为 LSL/USL最贴近工艺设定
// - 若设定值没有波动(全相同),退化为 target ± 10% 的经验容差。
List<BigDecimal> setValues = samples.stream() List<BigDecimal> setValues = samples.stream()
.map(setExtractor) .map(setExtractor)
.filter(v -> v != null) .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) { if (sigma.compareTo(BigDecimal.ZERO) > 0 && result.getUsl() != null && result.getLsl() != null) {
BigDecimal sixSigma = sigma.multiply(new BigDecimal("6")); BigDecimal sixSigma = sigma.multiply(new BigDecimal("6"));
BigDecimal threeSigma = sigma.multiply(new BigDecimal("3")); BigDecimal threeSigma = sigma.multiply(new BigDecimal("3"));
BigDecimal usl = result.getUsl(); BigDecimal usl = result.getUsl();
BigDecimal lsl = result.getLsl(); BigDecimal lsl = result.getLsl();
// Cp 反映潜在能力Cpu/Cpl 反映均值偏移Cpk 取二者较小值。
result.setCp(usl.subtract(lsl).divide(sixSigma, SCALE, RM)); result.setCp(usl.subtract(lsl).divide(sixSigma, SCALE, RM));
result.setCpu(usl.subtract(mean).divide(threeSigma, SCALE, RM)); result.setCpu(usl.subtract(mean).divide(threeSigma, SCALE, RM));
result.setCpl(mean.subtract(lsl).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()); result.setPpk(result.getCpk());
} }
// 直方图数据 // 8) 构造直方图分箱数据,供前端能力图直接渲染。
buildHistogram(result, values); buildHistogram(result, values);
// 运行图数据 // 9) 构造运行图序列数据(值 + 标签)。
// 这里标签优先用配方编码,若缺失则回退为样本序号,保证前端 x 轴稳定。
result.setSampleValues(values); result.setSampleValues(values);
List<String> labels = new ArrayList<>(); List<String> labels = new ArrayList<>();
for (int i = 0; i < samples.size(); i++) { for (int i = 0; i < samples.size(); i++) {
@ -200,23 +218,33 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
@Override @Override
public MixTraceSpcResultVo calculateSpcRunChart(Map<String, Object> params) { public MixTraceSpcResultVo calculateSpcRunChart(Map<String, Object> 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 ==================== // ==================== SPC Xbar-R图图10 ====================
@Override @Override
public MixTraceSpcResultVo calculateSpcXbarR(Map<String, Object> params) { public MixTraceSpcResultVo calculateSpcXbarR(Map<String, Object> params) {
String paramName = getParamName(params); // 1) 参数归一化并确定分析参数。
Map<String, Object> queryParams = safeParams(params);
String paramName = getParamName(queryParams);
List<MixTraceSpcSampleVo> samples = mixTraceReportMapper.selectSpcSamples(params); // 2) 取全量样本Xbar-R 需要连续样本进行分组统计。
List<MixTraceSpcSampleVo> samples = mixTraceReportMapper.selectSpcSamples(queryParams);
if (samples == null || samples.isEmpty()) { if (samples == null || samples.isEmpty()) {
return buildEmptyResult(paramName); return buildEmptyResult(paramName);
} }
Function<MixTraceSpcSampleVo, BigDecimal> actualExtractor = getActualExtractor(paramName); Function<MixTraceSpcSampleVo, BigDecimal> actualExtractor = getActualExtractor(paramName);
// 仅保留有效实测值,保证子组计算准确。
List<BigDecimal> values = samples.stream() List<BigDecimal> values = samples.stream()
.map(actualExtractor) .map(actualExtractor)
.filter(v -> v != null) .filter(v -> v != null)
@ -224,9 +252,9 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
// 子组大小默认5可通过参数配置 // 子组大小默认5可通过参数配置
int subgroupSize = 5; int subgroupSize = 5;
if (params.containsKey("subgroupSize")) { if (queryParams.containsKey("subgroupSize")) {
try { try {
subgroupSize = Integer.parseInt(String.valueOf(params.get("subgroupSize"))); subgroupSize = Integer.parseInt(String.valueOf(queryParams.get("subgroupSize")));
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
} }
} }
@ -234,13 +262,13 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
subgroupSize = Math.max(2, Math.min(10, subgroupSize)); subgroupSize = Math.max(2, Math.min(10, subgroupSize));
if (values.size() < subgroupSize * 2) { if (values.size() < subgroupSize * 2) {
// 样本不足至少需要2个子组 // 业务约束:至少需要 2 个完整子组,否则控制图没有统计意义。
MixTraceSpcResultVo result = buildEmptyResult(paramName); MixTraceSpcResultVo result = buildEmptyResult(paramName);
result.setSampleCount(values.size()); result.setSampleCount(values.size());
return result; return result;
} }
// 按子组分组 // 3) 顺序分组:按样本顺序切分为固定大小子组(忽略最后不完整子组)。
int groupCount = values.size() / subgroupSize; int groupCount = values.size() / subgroupSize;
List<BigDecimal> xbarValues = new ArrayList<>(); List<BigDecimal> xbarValues = new ArrayList<>();
List<BigDecimal> rValues = new ArrayList<>(); List<BigDecimal> rValues = new ArrayList<>();
@ -254,6 +282,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
BigDecimal groupMin = group.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); BigDecimal groupMin = group.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
BigDecimal groupRange = groupMax.subtract(groupMin); BigDecimal groupRange = groupMax.subtract(groupMin);
// 每个子组提取两个关键统计量:均值(Xbar) 和 极差(R)。
xbarValues.add(groupMean); xbarValues.add(groupMean);
rValues.add(groupRange); rValues.add(groupRange);
subgroupLabels.add("G" + (i + 1)); subgroupLabels.add("G" + (i + 1));
@ -263,7 +292,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
BigDecimal xbarbar = calcMean(xbarValues); BigDecimal xbarbar = calcMean(xbarValues);
BigDecimal rbar = calcMean(rValues); BigDecimal rbar = calcMean(rValues);
// 查找常量 A2, D3, D4 // 4) 按子组大小查控制图常量 A2/D3/D4行业标准常量表
int constIndex = subgroupSize - 2; // 数组从 n=2 开始 int constIndex = subgroupSize - 2; // 数组从 n=2 开始
double a2 = XBAR_R_CONSTANTS[constIndex][0]; double a2 = XBAR_R_CONSTANTS[constIndex][0];
double d3 = XBAR_R_CONSTANTS[constIndex][1]; double d3 = XBAR_R_CONSTANTS[constIndex][1];
@ -288,12 +317,18 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
result.setXbarbar(xbarbar); result.setXbarbar(xbarbar);
result.setRbar(rbar); 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.setUclX(xbarbar.add(A2.multiply(rbar)).setScale(SCALE, RM));
result.setClX(xbarbar.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));
// R 控制限 // 6) R 图控制限:
// UCLr = D4 * Rbar
// CLr = Rbar
// LCLr = D3 * Rbar
result.setUclR(D4.multiply(rbar).setScale(SCALE, RM)); result.setUclR(D4.multiply(rbar).setScale(SCALE, RM));
result.setClR(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));
@ -307,30 +342,33 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
* mixingTemp * mixingTemp
*/ */
private String getParamName(Map<String, Object> params) { private String getParamName(Map<String, Object> params) {
if (params == null || params.isEmpty()) {
// 未传分析参数时默认按混炼温度分析,保持接口可直接调用。
return "mixingTemp";
}
Object paramNameObj = params.get("paramName"); Object paramNameObj = params.get("paramName");
// 兼容前端传空字符串场景,统一回退默认参数。
return (paramNameObj != null && !String.valueOf(paramNameObj).isEmpty()) return (paramNameObj != null && !String.valueOf(paramNameObj).isEmpty())
? String.valueOf(paramNameObj) : "mixingTemp"; ? String.valueOf(paramNameObj) : "mixingTemp";
} }
private Map<String, Object> safeParams(Map<String, Object> params) {
// Mapper 层统一假设 map 非空,服务层在入口处兜底。
return params == null ? new HashMap<>() : params;
}
/** /**
* -> * ->
*/ */
private String getParamLabel(String paramName) { private String getParamLabel(String paramName) {
switch (paramName) { return paramName;
case "mixingTemp": return "混炼温度";
case "mixingTime": return "混炼时间";
case "mixingEnergy": return "混炼能量";
case "mixingPower": return "混炼功率";
case "mixingPress": return "混炼压力";
case "mixingSpeed": return "混炼转速";
default: return paramName;
}
} }
/** /**
* *
*/ */
private Function<MixTraceSpcSampleVo, BigDecimal> getActualExtractor(String paramName) { private Function<MixTraceSpcSampleVo, BigDecimal> getActualExtractor(String paramName) {
// 参数名与“实测字段”一一映射,时间/转速等整型字段转 BigDecimal 统一参与计算。
switch (paramName) { switch (paramName) {
case "mixingTemp": return MixTraceSpcSampleVo::getMixingTemp; case "mixingTemp": return MixTraceSpcSampleVo::getMixingTemp;
case "mixingTime": return s -> s.getMixingTime() != null ? BigDecimal.valueOf(s.getMixingTime()) : null; case "mixingTime": return s -> s.getMixingTime() != null ? BigDecimal.valueOf(s.getMixingTime()) : null;
@ -346,6 +384,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
* *
*/ */
private Function<MixTraceSpcSampleVo, BigDecimal> getSetExtractor(String paramName) { private Function<MixTraceSpcSampleVo, BigDecimal> getSetExtractor(String paramName) {
// 参数名与“设定字段”一一映射,口径与实测字段保持一致。
switch (paramName) { switch (paramName) {
case "mixingTemp": return s -> s.getSetTemp() != null ? BigDecimal.valueOf(s.getSetTemp()) : null; 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 "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()) { if (values == null || values.isEmpty()) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
// BigDecimal 全程计算,避免 double 误差影响 SPC 指标。
BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(BigDecimal.valueOf(values.size()), SCALE, RM); return sum.divide(BigDecimal.valueOf(values.size()), SCALE, RM);
} }
@ -375,6 +415,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
if (values == null || values.size() < 2) { if (values == null || values.size() < 2) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
// 先算离差平方和,再按 n-1 求样本方差。
BigDecimal sumSq = values.stream() BigDecimal sumSq = values.stream()
.map(v -> v.subtract(mean).pow(2)) .map(v -> v.subtract(mean).pow(2))
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
@ -387,6 +428,7 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
* *
*/ */
private MixTraceSpcResultVo buildEmptyResult(String paramName) { private MixTraceSpcResultVo buildEmptyResult(String paramName) {
// 空结果也回传参数信息,前端可继续显示“当前分析参数”。
MixTraceSpcResultVo result = new MixTraceSpcResultVo(); MixTraceSpcResultVo result = new MixTraceSpcResultVo();
result.setParamName(paramName); result.setParamName(paramName);
result.setParamLabel(getParamLabel(paramName)); result.setParamLabel(getParamLabel(paramName));
@ -399,8 +441,10 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
*/ */
private void buildHistogram(MixTraceSpcResultVo result, List<BigDecimal> values) { private void buildHistogram(MixTraceSpcResultVo result, List<BigDecimal> values) {
if (values == null || values.size() < 2) { if (values == null || values.size() < 2) {
// 样本过少时不生成分箱,前端直方图区域自然不显示。
return; return;
} }
// 采用固定 10 箱的等宽分箱策略,优先保证可解释性与稳定渲染。
int binCount = 10; int binCount = 10;
BigDecimal min = result.getMinValue(); BigDecimal min = result.getMinValue();
BigDecimal max = result.getMaxValue(); BigDecimal max = result.getMaxValue();
@ -427,9 +471,11 @@ public class ProdMixTraceReportServiceImpl implements IProdMixTraceReportService
int finalI = i; int finalI = i;
long count = values.stream() long count = values.stream()
.filter(v -> { .filter(v -> {
// 最后一个分箱右闭区间,确保最大值不会因边界比较丢失。
if (finalI == binCount - 1) { if (finalI == binCount - 1) {
return v.compareTo(lower) >= 0 && v.compareTo(upper) <= 0; return v.compareTo(lower) >= 0 && v.compareTo(upper) <= 0;
} }
// 其余分箱采用左闭右开,避免边界值重复计数。
return v.compareTo(lower) >= 0 && v.compareTo(upper) < 0; return v.compareTo(lower) >= 0 && v.compareTo(upper) < 0;
}) })
.count(); .count();

@ -4,7 +4,19 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.mes.mapper.ProdMixTraceReportMapper"> <mapper namespace="org.dromara.mes.mapper.ProdMixTraceReportMapper">
<!-- 追溯列表查询图5 --> <sql id="routerMasterDataCte">
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'
)
</sql>
<!-- trace list -->
<select id="selectTraceList" resultType="org.dromara.mes.domain.vo.MixTraceListVo"> <select id="selectTraceList" resultType="org.dromara.mes.domain.vo.MixTraceListVo">
SELECT SELECT
ri.recipe_id AS recipeId, ri.recipe_id AS recipeId,
@ -40,6 +52,9 @@
<if test="map.machineId != null and map.machineId != ''"> <if test="map.machineId != null and map.machineId != ''">
AND ri.machine_id = #{map.machineId} AND ri.machine_id = #{map.machineId}
</if> </if>
<if test="map.machineName != null and map.machineName != ''">
AND bm.machine_name LIKE CONCAT('%', #{map.machineName}, '%')
</if>
<if test="map.materialId != null and map.materialId != ''"> <if test="map.materialId != null and map.materialId != ''">
AND ri.material_id = #{map.materialId} AND ri.material_id = #{map.materialId}
</if> </if>
@ -65,12 +80,13 @@
AND bmi.material_name LIKE CONCAT('%', #{map.materialName}, '%') AND bmi.material_name LIKE CONCAT('%', #{map.materialName}, '%')
</if> </if>
<if test="map.beginDate != null and map.beginDate != '' and map.endDate != null and map.endDate != ''"> <if test="map.beginDate != null and map.beginDate != '' and map.endDate != null and map.endDate != ''">
AND FORMAT(ri.create_time, 'yyyy-MM-dd') BETWEEN #{map.beginDate} AND #{map.endDate} AND ri.create_time &gt;= CAST(CONCAT(#{map.beginDate}, ' 00:00:00') AS DATETIME)
AND ri.create_time &lt; DATEADD(DAY, 1, CAST(#{map.endDate} AS DATE))
</if> </if>
ORDER BY ri.create_time DESC ORDER BY ri.create_time DESC
</select> </select>
<!-- 追溯详情 - 配方基础信息(含关联名称) --> <!-- trace detail recipe info -->
<select id="selectTraceRecipeInfo" resultType="org.dromara.mes.domain.vo.MixTraceListVo"> <select id="selectTraceRecipeInfo" resultType="org.dromara.mes.domain.vo.MixTraceListVo">
SELECT SELECT
ri.recipe_id AS recipeId, ri.recipe_id AS recipeId,
@ -103,8 +119,9 @@
AND ri.recipe_id = #{recipeId} AND ri.recipe_id = #{recipeId}
</select> </select>
<!-- 追溯详情 - 称量明细(按 weight_seq 排序) --> <!-- trace detail weight list -->
<select id="selectWeightListByRecipeId" resultType="org.dromara.mes.domain.vo.ProdRecipeWeightVo"> <select id="selectWeightListByRecipeId" resultType="org.dromara.mes.domain.vo.ProdRecipeWeightVo">
<include refid="routerMasterDataCte"/>
SELECT SELECT
w.weight_id AS weightId, w.weight_id AS weightId,
w.recipe_id AS recipeId, w.recipe_id AS recipeId,
@ -113,7 +130,10 @@
w.edt_code AS edtCode, w.edt_code AS edtCode,
w.weight_type AS weightType, w.weight_type AS weightType,
w.scale_code AS scaleCode, w.scale_code AS scaleCode,
w.act_code AS actCode, CASE
WHEN act_detail.data_detail_name IS NULL OR LTRIM(RTRIM(act_detail.data_detail_name)) = '' THEN RTRIM(LTRIM(COALESCE(w.act_code, '')))
ELSE RTRIM(LTRIM(act_detail.data_detail_name))
END AS actCode,
w.set_weight AS setWeight, w.set_weight AS setWeight,
w.error_allow AS errorAllow, w.error_allow AS errorAllow,
w.father_code AS fatherCode, w.father_code AS fatherCode,
@ -122,30 +142,50 @@
w.if_use_bat AS ifUseBat, w.if_use_bat AS ifUseBat,
w.max_rate AS maxRate w.max_rate AS maxRate
FROM prod_recipe_weight w FROM prod_recipe_weight w
CROSS JOIN router_master_data rmd
OUTER APPLY (
SELECT TOP 1 d.data_detail_name
FROM sys_master_data_detail d
WHERE d.del_flag = '0'
AND d.active_flag = '1'
AND d.master_data_id = rmd.act_master_data_id
AND RTRIM(LTRIM(COALESCE(d.data_detail_code, ''))) = RTRIM(LTRIM(COALESCE(w.act_code, '')))
ORDER BY d.master_data_detail_id DESC
) act_detail
WHERE w.del_flag = '0' WHERE w.del_flag = '0'
AND w.recipe_id = #{recipeId} AND w.recipe_id = #{recipeId}
ORDER BY w.weight_seq ASC ORDER BY w.weight_seq ASC
</select> </select>
<!-- 追溯详情 - 混炼明细(按 mix_id 排序) --> <!-- trace detail mixing list -->
<select id="selectMixingListByRecipeId" resultType="org.dromara.mes.domain.vo.ProdRecipeMixingVo"> <select id="selectMixingListByRecipeId" resultType="org.dromara.mes.domain.vo.ProdRecipeMixingVo">
<include refid="routerMasterDataCte"/>
SELECT SELECT
m.mixing_id AS mixingId, m.mixing_id AS mixingId,
m.recipe_id AS recipeId, m.recipe_id AS recipeId,
m.mix_id AS mixId, m.mix_id AS mixId,
m.machine_id AS machineId, m.machine_id AS machineId,
m.edt_code AS edtCode, m.edt_code AS edtCode,
m.cond_code AS condCode, CASE
WHEN cond_detail.data_detail_name IS NULL OR LTRIM(RTRIM(cond_detail.data_detail_name)) = '' THEN RTRIM(LTRIM(COALESCE(m.cond_code, '')))
ELSE RTRIM(LTRIM(cond_detail.data_detail_name))
END AS condCode,
m.mixing_time AS mixingTime, m.mixing_time AS mixingTime,
m.mixing_temp AS mixingTemp, m.mixing_temp AS mixingTemp,
m.mixing_energy AS mixingEnergy, m.mixing_energy AS mixingEnergy,
m.mixing_power AS mixingPower, m.mixing_power AS mixingPower,
m.mixing_press AS mixingPress, m.mixing_press AS mixingPress,
m.mixing_speed AS mixingSpeed, m.mixing_speed AS mixingSpeed,
m.act_code AS actCode, CASE
WHEN act_detail.data_detail_name IS NULL OR LTRIM(RTRIM(act_detail.data_detail_name)) = '' THEN RTRIM(LTRIM(COALESCE(m.act_code, '')))
ELSE RTRIM(LTRIM(act_detail.data_detail_name))
END AS actCode,
m.father_code AS fatherCode, m.father_code AS fatherCode,
m.child_code AS childCode, m.child_code AS childCode,
m.term_code AS termCode, CASE
WHEN term_detail.data_detail_name IS NULL OR LTRIM(RTRIM(term_detail.data_detail_name)) = '' THEN RTRIM(LTRIM(COALESCE(m.term_code, '')))
ELSE RTRIM(LTRIM(term_detail.data_detail_name))
END AS termCode,
m.set_time AS setTime, m.set_time AS setTime,
m.set_temp AS setTemp, m.set_temp AS setTemp,
m.set_energy AS setEnergy, m.set_energy AS setEnergy,
@ -153,22 +193,60 @@
m.set_pres AS setPres, m.set_pres AS setPres,
m.set_rota AS setRota m.set_rota AS setRota
FROM prod_recipe_mixing m FROM prod_recipe_mixing m
CROSS JOIN router_master_data rmd
OUTER APPLY (
SELECT TOP 1 d.data_detail_name
FROM sys_master_data_detail d
WHERE d.del_flag = '0'
AND d.active_flag = '1'
AND d.master_data_id = rmd.term_master_data_id
AND RTRIM(LTRIM(COALESCE(d.data_detail_code, ''))) = RTRIM(LTRIM(COALESCE(m.cond_code, '')))
ORDER BY d.master_data_detail_id DESC
) cond_detail
OUTER APPLY (
SELECT TOP 1 d.data_detail_name
FROM sys_master_data_detail d
WHERE d.del_flag = '0'
AND d.active_flag = '1'
AND d.master_data_id = rmd.act_master_data_id
AND RTRIM(LTRIM(COALESCE(d.data_detail_code, ''))) = RTRIM(LTRIM(COALESCE(m.act_code, '')))
ORDER BY d.master_data_detail_id DESC
) act_detail
OUTER APPLY (
SELECT TOP 1 d.data_detail_name
FROM sys_master_data_detail d
WHERE d.del_flag = '0'
AND d.active_flag = '1'
AND d.master_data_id = rmd.term_master_data_id
AND RTRIM(LTRIM(COALESCE(d.data_detail_code, ''))) = RTRIM(LTRIM(COALESCE(m.term_code, '')))
ORDER BY d.master_data_detail_id DESC
) term_detail
WHERE m.del_flag = '0' WHERE m.del_flag = '0'
AND m.recipe_id = #{recipeId} AND m.recipe_id = #{recipeId}
ORDER BY m.mix_id ASC ORDER BY m.mix_id ASC
</select> </select>
<!-- SPC样本查询图6查询多个配方的混炼实测值作为SPC样本 --> <!-- spc samples -->
<select id="selectSpcSamples" resultType="org.dromara.mes.domain.vo.MixTraceSpcSampleVo"> <select id="selectSpcSamples" resultType="org.dromara.mes.domain.vo.MixTraceSpcSampleVo">
<include refid="routerMasterDataCte"/>
SELECT SELECT
ri.recipe_id AS recipeId, ri.recipe_id AS recipeId,
ri.recipe_code AS recipeCode, ri.recipe_code AS recipeCode,
ri.machine_id AS machineId, ri.machine_id AS machineId,
bm.machine_name AS machineName, bm.machine_name AS machineName,
rm.mix_id AS mixId, rm.mix_id AS mixId,
rm.term_code AS termCode, CASE
rm.act_code AS actCode, WHEN term_detail.data_detail_name IS NULL OR LTRIM(RTRIM(term_detail.data_detail_name)) = '' THEN RTRIM(LTRIM(COALESCE(rm.term_code, '')))
rm.cond_code AS condCode, ELSE RTRIM(LTRIM(term_detail.data_detail_name))
END AS termCode,
CASE
WHEN act_detail.data_detail_name IS NULL OR LTRIM(RTRIM(act_detail.data_detail_name)) = '' THEN RTRIM(LTRIM(COALESCE(rm.act_code, '')))
ELSE RTRIM(LTRIM(act_detail.data_detail_name))
END AS actCode,
CASE
WHEN cond_detail.data_detail_name IS NULL OR LTRIM(RTRIM(cond_detail.data_detail_name)) = '' THEN RTRIM(LTRIM(COALESCE(rm.cond_code, '')))
ELSE RTRIM(LTRIM(cond_detail.data_detail_name))
END AS condCode,
rm.mixing_time AS mixingTime, rm.mixing_time AS mixingTime,
rm.mixing_temp AS mixingTemp, rm.mixing_temp AS mixingTemp,
rm.mixing_energy AS mixingEnergy, rm.mixing_energy AS mixingEnergy,
@ -185,10 +263,41 @@
FROM prod_recipe_mixing rm FROM prod_recipe_mixing rm
INNER JOIN prod_recipe_info ri ON ri.recipe_id = rm.recipe_id AND ri.del_flag = '0' INNER JOIN prod_recipe_info ri ON ri.recipe_id = rm.recipe_id AND ri.del_flag = '0'
LEFT JOIN prod_base_machine_info bm ON bm.machine_id = ri.machine_id LEFT JOIN prod_base_machine_info bm ON bm.machine_id = ri.machine_id
CROSS JOIN router_master_data rmd
OUTER APPLY (
SELECT TOP 1 d.data_detail_name
FROM sys_master_data_detail d
WHERE d.del_flag = '0'
AND d.active_flag = '1'
AND d.master_data_id = rmd.act_master_data_id
AND RTRIM(LTRIM(COALESCE(d.data_detail_code, ''))) = RTRIM(LTRIM(COALESCE(rm.act_code, '')))
ORDER BY d.master_data_detail_id DESC
) act_detail
OUTER APPLY (
SELECT TOP 1 d.data_detail_name
FROM sys_master_data_detail d
WHERE d.del_flag = '0'
AND d.active_flag = '1'
AND d.master_data_id = rmd.term_master_data_id
AND RTRIM(LTRIM(COALESCE(d.data_detail_code, ''))) = RTRIM(LTRIM(COALESCE(rm.cond_code, '')))
ORDER BY d.master_data_detail_id DESC
) cond_detail
OUTER APPLY (
SELECT TOP 1 d.data_detail_name
FROM sys_master_data_detail d
WHERE d.del_flag = '0'
AND d.active_flag = '1'
AND d.master_data_id = rmd.term_master_data_id
AND RTRIM(LTRIM(COALESCE(d.data_detail_code, ''))) = RTRIM(LTRIM(COALESCE(rm.term_code, '')))
ORDER BY d.master_data_detail_id DESC
) term_detail
WHERE rm.del_flag = '0' WHERE rm.del_flag = '0'
<if test="map.machineId != null and map.machineId != ''"> <if test="map.machineId != null and map.machineId != ''">
AND ri.machine_id = #{map.machineId} AND ri.machine_id = #{map.machineId}
</if> </if>
<if test="map.machineName != null and map.machineName != ''">
AND bm.machine_name LIKE CONCAT('%', #{map.machineName}, '%')
</if>
<if test="map.materialId != null and map.materialId != ''"> <if test="map.materialId != null and map.materialId != ''">
AND ri.material_id = #{map.materialId} AND ri.material_id = #{map.materialId}
</if> </if>
@ -198,6 +307,9 @@
<if test="map.rubTypecode != null and map.rubTypecode != ''"> <if test="map.rubTypecode != null and map.rubTypecode != ''">
AND ri.rub_typecode LIKE CONCAT('%', #{map.rubTypecode}, '%') AND ri.rub_typecode LIKE CONCAT('%', #{map.rubTypecode}, '%')
</if> </if>
<if test="map.rubType != null and map.rubType != ''">
AND ri.rub_type LIKE CONCAT('%', #{map.rubType}, '%')
</if>
<if test="map.mixId != null and map.mixId != ''"> <if test="map.mixId != null and map.mixId != ''">
AND rm.mix_id = #{map.mixId} AND rm.mix_id = #{map.mixId}
</if> </if>
@ -205,7 +317,8 @@
AND rm.term_code = #{map.termCode} AND rm.term_code = #{map.termCode}
</if> </if>
<if test="map.beginDate != null and map.beginDate != '' and map.endDate != null and map.endDate != ''"> <if test="map.beginDate != null and map.beginDate != '' and map.endDate != null and map.endDate != ''">
AND FORMAT(ri.create_time, 'yyyy-MM-dd') BETWEEN #{map.beginDate} AND #{map.endDate} AND ri.create_time &gt;= CAST(CONCAT(#{map.beginDate}, ' 00:00:00') AS DATETIME)
AND ri.create_time &lt; DATEADD(DAY, 1, CAST(#{map.endDate} AS DATE))
</if> </if>
ORDER BY ri.create_time ASC, rm.mix_id ASC ORDER BY ri.create_time ASC, rm.mix_id ASC
</select> </select>

Loading…
Cancel
Save