|
|
|
|
@ -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<MixTraceListVo> queryTraceList(Map<String, Object> params, PageQuery pageQuery) {
|
|
|
|
|
// 统一把 null 查询参数转为空 Map,避免 Mapper XML 中 OGNL 访问空指针。
|
|
|
|
|
// 统一参数清洗,避免 Mapper 中空字符串带来的条件误判
|
|
|
|
|
Map<String, Object> queryParams = safeParams(params);
|
|
|
|
|
// 由 PageQuery 生成分页对象,确保前后端分页语义一致(pageNum/pageSize)。
|
|
|
|
|
// 分页查询由 MyBatis-Plus 负责总数与分页窗口,Service 仅做参数口径统一
|
|
|
|
|
Page<MixTraceListVo> 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<String, Object> params) {
|
|
|
|
|
// 入口参数统一规范后,再注入 recipeId,避免前端同名空值覆盖
|
|
|
|
|
Map<String, Object> 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<MixTraceUsageVo> usageList = mixTraceReportMapper.selectTraceUsageList(detailParams);
|
|
|
|
|
detail.setUsageList(usageList);
|
|
|
|
|
|
|
|
|
|
// 混炼工步:用于工步明细与历史曲线
|
|
|
|
|
List<MixTraceStepVo> mixingStepList = mixTraceReportMapper.selectTraceStepList(detailParams);
|
|
|
|
|
detail.setMixingStepList(mixingStepList);
|
|
|
|
|
detail.setCurveSeries(buildCurveSeries(mixingStepList));
|
|
|
|
|
|
|
|
|
|
// 批次追溯只保留高选择性条件,优先 productionBarcode 以减少扫描范围
|
|
|
|
|
// 注意:批次查询 SQL 内有 TOP 限制,必须用高选择性条件避免误截断有效数据
|
|
|
|
|
List<MixTraceBatchVo> batchList = mixTraceReportMapper.selectTraceBatchList(buildBatchQueryParams(detailParams));
|
|
|
|
|
detail.setBatchList(batchList);
|
|
|
|
|
// 将批次明细组装为“车 -> 物料 -> 批次”树,供左侧树形追溯展示
|
|
|
|
|
detail.setMaterialTraceTree(buildMaterialTraceTree(summaryInfo, batchList));
|
|
|
|
|
|
|
|
|
|
return detail;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== SPC样本(图6) ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public TableDataInfo<MixTraceSpcSampleVo> querySpcSamples(Map<String, Object> params, PageQuery pageQuery) {
|
|
|
|
|
// SPC 样本分页查询:入参做空安全处理,避免条件拼接异常。
|
|
|
|
|
Map<String, Object> queryParams = safeParams(params);
|
|
|
|
|
Page<MixTraceSpcSampleVo> page = mixTraceReportMapper.selectSpcSamples(queryParams, pageQuery.build());
|
|
|
|
|
return TableDataInfo.build(page);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== SPC能力分析(图7) ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public MixTraceSpcResultVo calculateSpcCapability(Map<String, Object> params) {
|
|
|
|
|
// 1) 参数归一化:保证后续读取参数时不会出现 null Map。
|
|
|
|
|
Map<String, Object> queryParams = safeParams(params);
|
|
|
|
|
// 获取分析参数名称,默认分析混炼温度
|
|
|
|
|
// paramName 仅允许白名单字段,避免前端传非法参数导致统计口径失控
|
|
|
|
|
String paramName = getParamName(queryParams);
|
|
|
|
|
|
|
|
|
|
// 2) 查询参与统计的全量样本(能力分析必须用全样本,不用分页样本)。
|
|
|
|
|
List<MixTraceSpcSampleVo> samples = mixTraceReportMapper.selectSpcSamples(queryParams);
|
|
|
|
|
if (samples == null || samples.isEmpty()) {
|
|
|
|
|
// 无样本直接返回空结构,前端用 sampleCount=0 判断无图表数据。
|
|
|
|
|
// 无样本直接返回空结构,前端仍可拿到参数标签用于占位显示
|
|
|
|
|
return buildEmptyResult(paramName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3) 根据参数名选择“实测值/设定值”的提取器,避免写 if-else 大分支。
|
|
|
|
|
Function<MixTraceSpcSampleVo, BigDecimal> actualExtractor = getActualExtractor(paramName);
|
|
|
|
|
Function<MixTraceSpcSampleVo, BigDecimal> setExtractor = getSetExtractor(paramName);
|
|
|
|
|
|
|
|
|
|
// 4) 仅保留非空实测值参与统计,屏蔽脏数据对均值/方差计算的影响。
|
|
|
|
|
// 仅保留可参与统计的实际值
|
|
|
|
|
// 例如温度/能量为空的工步不参与均值和方差计算
|
|
|
|
|
List<BigDecimal> 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<BigDecimal> 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<String> 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<String, Object> 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<String, Object> params) {
|
|
|
|
|
// 1) 参数归一化并确定分析参数。
|
|
|
|
|
Map<String, Object> queryParams = safeParams(params);
|
|
|
|
|
String paramName = getParamName(queryParams);
|
|
|
|
|
|
|
|
|
|
// 2) 取全量样本,Xbar-R 需要连续样本进行分组统计。
|
|
|
|
|
List<MixTraceSpcSampleVo> samples = mixTraceReportMapper.selectSpcSamples(queryParams);
|
|
|
|
|
if (samples == null || samples.isEmpty()) {
|
|
|
|
|
return buildEmptyResult(paramName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Function<MixTraceSpcSampleVo, BigDecimal> actualExtractor = getActualExtractor(paramName);
|
|
|
|
|
|
|
|
|
|
// 仅保留有效实测值,保证子组计算准确。
|
|
|
|
|
// Xbar-R 计算同样只使用有效实测值
|
|
|
|
|
List<BigDecimal> 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<BigDecimal> xbarValues = new ArrayList<>();
|
|
|
|
|
List<BigDecimal> 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<BigDecimal> 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<String, Object> 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<String, Object> safeParams(Map<String, Object> 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<String, Object> safeParams(Map<String, Object> params) {
|
|
|
|
|
Map<String, Object> normalized = new HashMap<>();
|
|
|
|
|
if (params == null || params.isEmpty()) {
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
for (Map.Entry<String, Object> 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<String, Object> buildBatchQueryParams(Map<String, Object> detailParams) {
|
|
|
|
|
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<MixTraceSpcSampleVo, BigDecimal> 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<MixTraceSpcSampleVo, BigDecimal> 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<BigDecimal> 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<BigDecimal> 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<BigDecimal> 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<Integer> counts = new ArrayList<>();
|
|
|
|
|
List<String> 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<MixTraceCurvePointVo> buildCurveSeries(List<MixTraceStepVo> mixingStepList) {
|
|
|
|
|
if (mixingStepList == null || mixingStepList.isEmpty()) {
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<MixTraceCurvePointVo> 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<MixTraceMaterialTraceTreeVo> buildMaterialTraceTree(MixTraceSummaryVo summaryInfo,
|
|
|
|
|
List<MixTraceBatchVo> 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<MixTraceMaterialTraceTreeVo> tree = new ArrayList<>();
|
|
|
|
|
tree.add(root);
|
|
|
|
|
return tree;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用 LinkedHashMap 保留插入顺序,前端树节点顺序稳定
|
|
|
|
|
Map<String, MixTraceMaterialTraceTreeVo> 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<MixTraceMaterialTraceTreeVo> tree = new ArrayList<>();
|
|
|
|
|
tree.add(root);
|
|
|
|
|
return tree;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|