酸轧OEE后端重构完成
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package com.klp.pocket.acid.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE日汇总视图对象
|
||||
* 用于按日、按产线聚合的KPI与趋势数据
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Data
|
||||
public class AcidOeeDailySummaryVo {
|
||||
|
||||
/** 统计日期(yyyy-MM-dd) */
|
||||
private String statDate;
|
||||
|
||||
/** 产线ID(固定为 SY) */
|
||||
private String lineId;
|
||||
|
||||
/** 产线名称(酸轧线) */
|
||||
private String lineName;
|
||||
|
||||
/** 计划时间(min,可选) */
|
||||
private Long plannedTimeMin;
|
||||
|
||||
/** 计划停机(min,可选;若无则置 0) */
|
||||
private Long plannedDowntimeMin;
|
||||
|
||||
/** 负荷时间(min)= planned_time_min - planned_downtime_min */
|
||||
private Long loadingTimeMin;
|
||||
|
||||
/** 停机时间(min,来自停机事件汇总) */
|
||||
private Long downtimeMin;
|
||||
|
||||
/** 实际运转时间(min)= loading_time_min - downtime_min */
|
||||
private Long runTimeMin;
|
||||
|
||||
/** 总产量(吨) */
|
||||
private BigDecimal totalOutputTon;
|
||||
|
||||
/** 总产量(卷) */
|
||||
private Long totalOutputCoil;
|
||||
|
||||
/** 良品量(吨) */
|
||||
private BigDecimal goodOutputTon;
|
||||
|
||||
/** 良品量(卷) */
|
||||
private Long goodOutputCoil;
|
||||
|
||||
/** 不良量(吨)= total_output_ton - good_output_ton */
|
||||
private BigDecimal defectOutputTon;
|
||||
|
||||
/** 不良量(卷)= total_output_coil - good_output_coil */
|
||||
private Long defectOutputCoil;
|
||||
|
||||
/** 理论节拍(min/吨;回归斜率) */
|
||||
private BigDecimal idealCycleTimeMinPerTon;
|
||||
|
||||
/** 理论节拍(min/卷;回归斜率) */
|
||||
private BigDecimal idealCycleTimeMinPerCoil;
|
||||
|
||||
/** 派生指标:时间稼动率(0~1 或 0~100) */
|
||||
private BigDecimal availability;
|
||||
|
||||
/** 派生指标:性能稼动率(吨维度) */
|
||||
private BigDecimal performanceTon;
|
||||
|
||||
/** 派生指标:性能稼动率(卷维度) */
|
||||
private BigDecimal performanceCoil;
|
||||
|
||||
/** 派生指标:良品率 */
|
||||
private BigDecimal quality;
|
||||
|
||||
/** 派生指标:OEE(建议以吨维度为主) */
|
||||
private BigDecimal oee;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.klp.pocket.acid.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE 7大损失汇总视图对象
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Data
|
||||
public class AcidOeeLoss7Vo {
|
||||
|
||||
/** 损失类别编码(1-7,或直接使用 stop_type 名称) */
|
||||
private String lossCategoryCode;
|
||||
|
||||
/** 损失类别名称 */
|
||||
private String lossCategoryName;
|
||||
|
||||
/** 损失时间(分钟) */
|
||||
private Long lossTimeMin;
|
||||
|
||||
/** 损失时间占比(%) */
|
||||
private BigDecimal lossTimeRate;
|
||||
|
||||
/** 发生次数(部分分类可能为空) */
|
||||
private Integer count;
|
||||
|
||||
/** 平均时长(分钟,部分分类可能为空) */
|
||||
private BigDecimal avgDurationMin;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.klp.pocket.acid.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE回归数据视图对象
|
||||
* 用于理论节拍计算和前端散点图展示
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Data
|
||||
public class AcidOeeRegressionVo {
|
||||
|
||||
/** 产线ID(固定为 SY) */
|
||||
private String lineId;
|
||||
|
||||
/** 产线名称(酸轧线) */
|
||||
private String lineName;
|
||||
|
||||
/** 回归斜率:分钟/吨(核心值,可作为理论节拍) */
|
||||
private BigDecimal slopeMinPerTon;
|
||||
|
||||
/** 回归斜率:分钟/卷(核心值,可作为理论节拍) */
|
||||
private BigDecimal slopeMinPerCoil;
|
||||
|
||||
/** 截距(分钟) */
|
||||
private BigDecimal interceptMin;
|
||||
|
||||
/** 拟合优度(R²) */
|
||||
private BigDecimal r2;
|
||||
|
||||
/** 参与回归样本数 */
|
||||
private Integer sampleCount;
|
||||
|
||||
/** 回归数据开始时间 */
|
||||
private String startTime;
|
||||
|
||||
/** 回归数据结束时间 */
|
||||
private String endTime;
|
||||
|
||||
/** 散点列表 */
|
||||
private List<RegressionPointVo> points;
|
||||
|
||||
/** 拟合线两个端点(前端可直接画线) */
|
||||
private List<RegressionLinePointVo> linePoints;
|
||||
|
||||
/**
|
||||
* 散点数据
|
||||
*/
|
||||
@Data
|
||||
public static class RegressionPointVo {
|
||||
/** 重量(吨,X轴) */
|
||||
private BigDecimal weightTon;
|
||||
/** 卷数(X轴) */
|
||||
private Long coilCount;
|
||||
/** 时长(分钟,Y轴) */
|
||||
private Long durationMin;
|
||||
/** 关联的actionId(可选) */
|
||||
private String actionId;
|
||||
/** 创建时间 */
|
||||
private String createTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拟合线端点
|
||||
*/
|
||||
@Data
|
||||
public static class RegressionLinePointVo {
|
||||
/** 重量(吨,X轴) */
|
||||
private BigDecimal weightTon;
|
||||
/** 卷数(X轴) */
|
||||
private Long coilCount;
|
||||
/** 时长(分钟,Y轴) */
|
||||
private BigDecimal durationMin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.klp.pocket.acid.mapper;
|
||||
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE Mapper接口
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Mapper
|
||||
public interface AcidOeeMapper {
|
||||
|
||||
/**
|
||||
* 查询OEE日汇总(按日期范围)
|
||||
* 聚合产量(吨/卷)、停机时间等
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd)
|
||||
* @param endDate 结束日期(yyyy-MM-dd)
|
||||
* @return 日汇总列表
|
||||
*/
|
||||
List<AcidOeeDailySummaryVo> selectDailySummary(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 查询回归数据散点(用于理论节拍计算)
|
||||
* 返回:重量(吨)、卷数、时长(分钟)等
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd,可选)
|
||||
* @param endDate 结束日期(yyyy-MM-dd,可选)
|
||||
* @return 散点列表
|
||||
*/
|
||||
List<AcidOeeRegressionVo.RegressionPointVo> selectRegressionPoints(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 查询每日的钢卷号和重量(用于良品/次品判定)
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd)
|
||||
* @param endDate 结束日期(yyyy-MM-dd)
|
||||
* @return Map列表,key为日期,value为卷号和重量信息
|
||||
*/
|
||||
List<CoilInfoByDate> selectCoilInfoByDate(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 卷号信息内部类(用于Mapper返回)
|
||||
*/
|
||||
class CoilInfoByDate {
|
||||
private String statDate;
|
||||
private String coilNo;
|
||||
private java.math.BigDecimal weight;
|
||||
|
||||
public String getStatDate() { return statDate; }
|
||||
public void setStatDate(String statDate) { this.statDate = statDate; }
|
||||
public String getCoilNo() { return coilNo; }
|
||||
public void setCoilNo(String coilNo) { this.coilNo = coilNo; }
|
||||
public java.math.BigDecimal getWeight() { return weight; }
|
||||
public void setWeight(java.math.BigDecimal weight) { this.weight = weight; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.klp.pocket.acid.service;
|
||||
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE Service接口
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
public interface IAcidOeeService {
|
||||
|
||||
/**
|
||||
* 查询OEE日汇总(按日期范围)
|
||||
* 包含:产量(吨/卷)、停机时间、良品/次品、派生指标等
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd)
|
||||
* @param endDate 结束日期(yyyy-MM-dd)
|
||||
* @return 日汇总列表(按日期排序)
|
||||
*/
|
||||
List<AcidOeeDailySummaryVo> getDailySummary(String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 查询停机事件列表(用于OEE明细和7大损失)
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd)
|
||||
* @param endDate 结束日期(yyyy-MM-dd)
|
||||
* @return 停机事件列表
|
||||
*/
|
||||
List<Klptcm1ProStoppageVo> getStoppageEvents(String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 查询理论节拍回归数据(吨和卷两个维度)
|
||||
* 用于性能稼动率计算和前端散点图展示
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd,可选,默认近6个月)
|
||||
* @param endDate 结束日期(yyyy-MM-dd,可选)
|
||||
* @return 回归数据(包含斜率、截距、散点等)
|
||||
*/
|
||||
AcidOeeRegressionVo getRegressionData(String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 查询7大损失汇总(按日期范围)
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd)
|
||||
* @param endDate 结束日期(yyyy-MM-dd)
|
||||
* @return 7大损失汇总列表
|
||||
*/
|
||||
List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
package com.klp.pocket.acid.service.impl;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||
import com.klp.pocket.acid.domain.bo.Klptcm1ProStoppageBo;
|
||||
import com.klp.pocket.acid.mapper.AcidOeeMapper;
|
||||
import com.klp.pocket.acid.service.IAcidOeeService;
|
||||
import com.klp.pocket.acid.service.IKlptcm1ProStoppageService;
|
||||
import com.klp.pocket.common.service.ICoilQualityJudgeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE Service实现类
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@DS("acid")
|
||||
@Service
|
||||
public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
|
||||
/** 酸轧成品库库区ID */
|
||||
private static final Long ACID_FINISHED_WAREHOUSE_ID = 1988150099140866050L;
|
||||
|
||||
private final AcidOeeMapper acidOeeMapper;
|
||||
private final IKlptcm1ProStoppageService stoppageService;
|
||||
private final ICoilQualityJudgeService coilQualityJudgeService;
|
||||
|
||||
@Override
|
||||
public List<AcidOeeDailySummaryVo> getDailySummary(String startDate, String endDate) {
|
||||
// 1. 查询基础日汇总(产量、停机时间等)
|
||||
List<AcidOeeDailySummaryVo> summaries = acidOeeMapper.selectDailySummary(startDate, endDate);
|
||||
|
||||
if (summaries == null || summaries.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 查询停机事件,按日期聚合停机时间
|
||||
Map<String, Long> downtimeByDate = aggregateDowntimeByDate(startDate, endDate);
|
||||
|
||||
// 3. 查询产量明细,用于良品/次品判定
|
||||
Map<String, List<CoilInfo>> coilInfoByDate = getCoilNosByDate(startDate, endDate);
|
||||
|
||||
// 4. 填充每个日汇总的完整数据
|
||||
for (AcidOeeDailySummaryVo summary : summaries) {
|
||||
String statDate = summary.getStatDate();
|
||||
summary.setLineId("SY");
|
||||
summary.setLineName("酸轧线");
|
||||
|
||||
// 填充停机时间
|
||||
Long downtime = downtimeByDate.getOrDefault(statDate, 0L);
|
||||
summary.setDowntimeMin(downtime);
|
||||
|
||||
// 计算运转时间
|
||||
Long loadingTime = summary.getLoadingTimeMin() != null ? summary.getLoadingTimeMin() : 0L;
|
||||
Long runTime = Math.max(0, loadingTime - downtime);
|
||||
summary.setRunTimeMin(runTime);
|
||||
|
||||
// 良品/次品判定(通过WMS)
|
||||
if (coilInfoByDate.containsKey(statDate)) {
|
||||
List<CoilInfo> coilInfos = coilInfoByDate.get(statDate);
|
||||
calculateQualityOutput(summary, coilInfos);
|
||||
} else {
|
||||
// 如果没有卷号,默认全部为良品(或根据业务规则处理)
|
||||
summary.setGoodOutputTon(summary.getTotalOutputTon());
|
||||
summary.setGoodOutputCoil(summary.getTotalOutputCoil());
|
||||
summary.setDefectOutputTon(BigDecimal.ZERO);
|
||||
summary.setDefectOutputCoil(0L);
|
||||
}
|
||||
|
||||
// 填充理论节拍(从回归数据或缓存获取,这里暂时留空,由调用方填充)
|
||||
// summary.setIdealCycleTimeMinPerTon(...);
|
||||
// summary.setIdealCycleTimeMinPerCoil(...);
|
||||
|
||||
// 计算派生指标
|
||||
calculateDerivedMetrics(summary);
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Klptcm1ProStoppageVo> getStoppageEvents(String startDate, String endDate) {
|
||||
Klptcm1ProStoppageBo bo = new Klptcm1ProStoppageBo();
|
||||
bo.setStartDate(parseDate(startDate));
|
||||
bo.setEndDate(parseDate(endDate));
|
||||
return stoppageService.queryList(bo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AcidOeeRegressionVo getRegressionData(String startDate, String endDate) {
|
||||
// 1. 查询散点数据
|
||||
List<AcidOeeRegressionVo.RegressionPointVo> points = acidOeeMapper.selectRegressionPoints(startDate, endDate);
|
||||
|
||||
AcidOeeRegressionVo result = new AcidOeeRegressionVo();
|
||||
result.setLineId("SY");
|
||||
result.setLineName("酸轧线");
|
||||
result.setStartTime(startDate);
|
||||
result.setEndTime(endDate);
|
||||
result.setPoints(points);
|
||||
result.setSampleCount(points != null ? points.size() : 0);
|
||||
|
||||
if (points == null || points.isEmpty()) {
|
||||
// 没有数据时返回空结果
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. 计算回归(吨维度)
|
||||
RegressionResult tonResult = calculateRegression(
|
||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon).filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList())
|
||||
);
|
||||
if (tonResult != null) {
|
||||
result.setSlopeMinPerTon(tonResult.slope);
|
||||
result.setInterceptMin(tonResult.intercept);
|
||||
result.setR2(tonResult.r2);
|
||||
}
|
||||
|
||||
// 3. 计算回归(卷维度)
|
||||
RegressionResult coilResult = calculateRegression(
|
||||
points.stream().map(p -> p.getCoilCount() != null ? BigDecimal.valueOf(p.getCoilCount()) : null).filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList())
|
||||
);
|
||||
if (coilResult != null) {
|
||||
result.setSlopeMinPerCoil(coilResult.slope);
|
||||
}
|
||||
|
||||
// 4. 生成拟合线端点(用于前端画线)
|
||||
if (tonResult != null && !points.isEmpty()) {
|
||||
List<AcidOeeRegressionVo.RegressionLinePointVo> linePoints = generateLinePoints(points, tonResult);
|
||||
result.setLinePoints(linePoints);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate) {
|
||||
// 1. 查询停机事件(含 stopType、duration 等)
|
||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate);
|
||||
if (events == null || events.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 按 stopType 分组汇总
|
||||
Map<String, LossStats> statsByType = new HashMap<>();
|
||||
long totalLossMin = 0L;
|
||||
|
||||
for (Klptcm1ProStoppageVo event : events) {
|
||||
String stopType = event.getStopType();
|
||||
if (StringUtils.isBlank(stopType)) {
|
||||
// 没有类型的记录暂时忽略
|
||||
continue;
|
||||
}
|
||||
Long durationSec = event.getDuration();
|
||||
if (durationSec == null || durationSec <= 0) {
|
||||
continue;
|
||||
}
|
||||
long durationMin = durationSec / 60;
|
||||
if (durationMin <= 0) {
|
||||
durationMin = 1; // 最小记 1 分钟,避免全为 0
|
||||
}
|
||||
|
||||
LossStats stats = statsByType.computeIfAbsent(stopType, k -> new LossStats());
|
||||
stats.totalMin += durationMin;
|
||||
stats.count++;
|
||||
totalLossMin += durationMin;
|
||||
}
|
||||
|
||||
if (statsByType.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 3. 组装 VO 列表
|
||||
List<AcidOeeLoss7Vo> result = new ArrayList<>();
|
||||
for (Map.Entry<String, LossStats> entry : statsByType.entrySet()) {
|
||||
String stopType = entry.getKey();
|
||||
LossStats stats = entry.getValue();
|
||||
|
||||
AcidOeeLoss7Vo vo = new AcidOeeLoss7Vo();
|
||||
vo.setLossCategoryCode(stopType);
|
||||
vo.setLossCategoryName(stopType);
|
||||
vo.setLossTimeMin(stats.totalMin);
|
||||
|
||||
if (totalLossMin > 0) {
|
||||
BigDecimal rate = BigDecimal.valueOf(stats.totalMin)
|
||||
.divide(BigDecimal.valueOf(totalLossMin), 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
vo.setLossTimeRate(rate);
|
||||
} else {
|
||||
vo.setLossTimeRate(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
vo.setCount(stats.count);
|
||||
if (stats.count > 0) {
|
||||
BigDecimal avg = BigDecimal.valueOf(stats.totalMin)
|
||||
.divide(BigDecimal.valueOf(stats.count), 2, RoundingMode.HALF_UP);
|
||||
vo.setAvgDurationMin(avg);
|
||||
} else {
|
||||
vo.setAvgDurationMin(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
result.add(vo);
|
||||
}
|
||||
|
||||
// 4. 按损失时间从大到小排序
|
||||
result.sort(Comparator.comparingLong(AcidOeeLoss7Vo::getLossTimeMin).reversed());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按日期聚合停机时间
|
||||
*/
|
||||
private Map<String, Long> aggregateDowntimeByDate(String startDate, String endDate) {
|
||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate);
|
||||
Map<String, Long> downtimeMap = new HashMap<>();
|
||||
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
for (Klptcm1ProStoppageVo event : events) {
|
||||
if (event.getStartDate() != null && event.getDuration() != null) {
|
||||
String date = dateFormat.format(event.getStartDate());
|
||||
// duration单位是秒,转换为分钟
|
||||
Long minutes = event.getDuration() / 60;
|
||||
downtimeMap.merge(date, minutes, Long::sum);
|
||||
}
|
||||
}
|
||||
|
||||
return downtimeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取每日的钢卷号和重量(用于良品/次品判定)
|
||||
*/
|
||||
private Map<String, List<CoilInfo>> getCoilNosByDate(String startDate, String endDate) {
|
||||
List<AcidOeeMapper.CoilInfoByDate> coilInfoList = acidOeeMapper.selectCoilInfoByDate(startDate, endDate);
|
||||
Map<String, List<CoilInfo>> result = new HashMap<>();
|
||||
|
||||
for (AcidOeeMapper.CoilInfoByDate info : coilInfoList) {
|
||||
String date = info.getStatDate();
|
||||
result.computeIfAbsent(date, k -> new ArrayList<>())
|
||||
.add(new CoilInfo(info.getCoilNo(), info.getWeight()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卷号信息内部类
|
||||
*/
|
||||
private static class CoilInfo {
|
||||
final String coilNo;
|
||||
final BigDecimal weight;
|
||||
|
||||
CoilInfo(String coilNo, BigDecimal weight) {
|
||||
this.coilNo = coilNo;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算良品/次品产量
|
||||
*/
|
||||
private void calculateQualityOutput(AcidOeeDailySummaryVo summary, List<CoilInfo> coilInfos) {
|
||||
BigDecimal goodTon = BigDecimal.ZERO;
|
||||
long goodCoil = 0L;
|
||||
BigDecimal defectTon = BigDecimal.ZERO;
|
||||
long defectCoil = 0L;
|
||||
|
||||
for (CoilInfo coilInfo : coilInfos) {
|
||||
String coilNo = coilInfo.coilNo;
|
||||
BigDecimal coilWeight = coilInfo.weight != null ? coilInfo.weight : BigDecimal.ZERO;
|
||||
|
||||
// 通过WMS判定良品/次品
|
||||
Boolean isScrap = coilQualityJudgeService.isScrap(ACID_FINISHED_WAREHOUSE_ID, coilNo);
|
||||
if (isScrap == null) {
|
||||
// 匹配不到,忽略不计
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(isScrap)) {
|
||||
// 次品
|
||||
defectTon = defectTon.add(coilWeight);
|
||||
defectCoil++;
|
||||
} else {
|
||||
// 良品
|
||||
goodTon = goodTon.add(coilWeight);
|
||||
goodCoil++;
|
||||
}
|
||||
}
|
||||
|
||||
summary.setGoodOutputTon(goodTon);
|
||||
summary.setGoodOutputCoil(goodCoil);
|
||||
summary.setDefectOutputTon(defectTon);
|
||||
summary.setDefectOutputCoil(defectCoil);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算派生指标(时间稼动率、性能稼动率、良品率、OEE)
|
||||
*/
|
||||
private void calculateDerivedMetrics(AcidOeeDailySummaryVo summary) {
|
||||
// 时间稼动率
|
||||
Long loadingTime = summary.getLoadingTimeMin() != null ? summary.getLoadingTimeMin() : 0L;
|
||||
Long downtime = summary.getDowntimeMin() != null ? summary.getDowntimeMin() : 0L;
|
||||
if (loadingTime > 0) {
|
||||
BigDecimal availability = BigDecimal.valueOf(loadingTime - downtime)
|
||||
.divide(BigDecimal.valueOf(loadingTime), 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
summary.setAvailability(availability);
|
||||
}
|
||||
|
||||
// 性能稼动率(吨维度)
|
||||
Long runTime = summary.getRunTimeMin() != null ? summary.getRunTimeMin() : 0L;
|
||||
BigDecimal idealCycleTon = summary.getIdealCycleTimeMinPerTon();
|
||||
BigDecimal totalOutputTon = summary.getTotalOutputTon();
|
||||
if (runTime > 0 && idealCycleTon != null && totalOutputTon != null && totalOutputTon.compareTo(BigDecimal.ZERO) > 0) {
|
||||
BigDecimal idealTime = idealCycleTon.multiply(totalOutputTon);
|
||||
BigDecimal performanceTon = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
summary.setPerformanceTon(performanceTon);
|
||||
}
|
||||
|
||||
// 性能稼动率(卷维度)
|
||||
Long totalOutputCoil = summary.getTotalOutputCoil() != null ? summary.getTotalOutputCoil() : 0L;
|
||||
BigDecimal idealCycleCoil = summary.getIdealCycleTimeMinPerCoil();
|
||||
if (runTime > 0 && idealCycleCoil != null && totalOutputCoil > 0) {
|
||||
BigDecimal idealTime = idealCycleCoil.multiply(BigDecimal.valueOf(totalOutputCoil));
|
||||
BigDecimal performanceCoil = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
summary.setPerformanceCoil(performanceCoil);
|
||||
}
|
||||
|
||||
// 良品率
|
||||
if (totalOutputTon != null && totalOutputTon.compareTo(BigDecimal.ZERO) > 0) {
|
||||
BigDecimal goodOutputTon = summary.getGoodOutputTon() != null ? summary.getGoodOutputTon() : BigDecimal.ZERO;
|
||||
BigDecimal quality = goodOutputTon.divide(totalOutputTon, 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
summary.setQuality(quality);
|
||||
}
|
||||
|
||||
// OEE(以吨维度为主)
|
||||
BigDecimal availability = summary.getAvailability();
|
||||
BigDecimal performanceTon = summary.getPerformanceTon();
|
||||
BigDecimal quality = summary.getQuality();
|
||||
if (availability != null && performanceTon != null && quality != null) {
|
||||
BigDecimal oee = availability.multiply(performanceTon).multiply(quality)
|
||||
.divide(BigDecimal.valueOf(10000), 4, RoundingMode.HALF_UP);
|
||||
summary.setOee(oee);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算线性回归(最小二乘法)
|
||||
*/
|
||||
private RegressionResult calculateRegression(List<BigDecimal> xValues, List<Long> yValues) {
|
||||
if (xValues.size() != yValues.size() || xValues.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int n = xValues.size();
|
||||
BigDecimal sumX = BigDecimal.ZERO;
|
||||
BigDecimal sumY = BigDecimal.ZERO;
|
||||
BigDecimal sumXY = BigDecimal.ZERO;
|
||||
BigDecimal sumX2 = BigDecimal.ZERO;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
BigDecimal x = xValues.get(i);
|
||||
BigDecimal y = BigDecimal.valueOf(yValues.get(i));
|
||||
sumX = sumX.add(x);
|
||||
sumY = sumY.add(y);
|
||||
sumXY = sumXY.add(x.multiply(y));
|
||||
sumX2 = sumX2.add(x.multiply(x));
|
||||
}
|
||||
|
||||
BigDecimal nDecimal = BigDecimal.valueOf(n);
|
||||
BigDecimal denominator = nDecimal.multiply(sumX2).subtract(sumX.multiply(sumX));
|
||||
if (denominator.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// slope = (n*ΣXY - ΣX*ΣY) / (n*ΣX² - (ΣX)²)
|
||||
BigDecimal slope = nDecimal.multiply(sumXY).subtract(sumX.multiply(sumY))
|
||||
.divide(denominator, 6, RoundingMode.HALF_UP);
|
||||
|
||||
// intercept = (ΣY - slope*ΣX) / n
|
||||
BigDecimal intercept = sumY.subtract(slope.multiply(sumX))
|
||||
.divide(nDecimal, 6, RoundingMode.HALF_UP);
|
||||
|
||||
// 计算R²
|
||||
BigDecimal meanY = sumY.divide(nDecimal, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal ssTotal = BigDecimal.ZERO;
|
||||
BigDecimal ssResidual = BigDecimal.ZERO;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
BigDecimal x = xValues.get(i);
|
||||
BigDecimal y = BigDecimal.valueOf(yValues.get(i));
|
||||
BigDecimal predictedY = slope.multiply(x).add(intercept);
|
||||
BigDecimal diff = y.subtract(meanY);
|
||||
ssTotal = ssTotal.add(diff.multiply(diff));
|
||||
BigDecimal residual = y.subtract(predictedY);
|
||||
ssResidual = ssResidual.add(residual.multiply(residual));
|
||||
}
|
||||
|
||||
BigDecimal r2 = BigDecimal.ONE;
|
||||
if (ssTotal.compareTo(BigDecimal.ZERO) > 0) {
|
||||
r2 = BigDecimal.ONE.subtract(ssResidual.divide(ssTotal, 6, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
return new RegressionResult(slope, intercept, r2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成拟合线端点
|
||||
*/
|
||||
private List<AcidOeeRegressionVo.RegressionLinePointVo> generateLinePoints(
|
||||
List<AcidOeeRegressionVo.RegressionPointVo> points,
|
||||
RegressionResult result) {
|
||||
if (points.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 找到X轴的最小值和最大值
|
||||
BigDecimal minX = points.stream()
|
||||
.map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon)
|
||||
.filter(Objects::nonNull)
|
||||
.min(BigDecimal::compareTo)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
|
||||
BigDecimal maxX = points.stream()
|
||||
.map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon)
|
||||
.filter(Objects::nonNull)
|
||||
.max(BigDecimal::compareTo)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
|
||||
// 计算对应的Y值
|
||||
BigDecimal y1 = result.slope.multiply(minX).add(result.intercept);
|
||||
BigDecimal y2 = result.slope.multiply(maxX).add(result.intercept);
|
||||
|
||||
AcidOeeRegressionVo.RegressionLinePointVo p1 = new AcidOeeRegressionVo.RegressionLinePointVo();
|
||||
p1.setWeightTon(minX);
|
||||
p1.setDurationMin(y1);
|
||||
|
||||
AcidOeeRegressionVo.RegressionLinePointVo p2 = new AcidOeeRegressionVo.RegressionLinePointVo();
|
||||
p2.setWeightTon(maxX);
|
||||
p2.setDurationMin(y2);
|
||||
|
||||
return Arrays.asList(p1, p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串为Date对象
|
||||
*/
|
||||
private Date parseDate(String dateStr) {
|
||||
if (StringUtils.isBlank(dateStr)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
return sdf.parse(dateStr);
|
||||
} catch (Exception e) {
|
||||
log.warn("解析日期失败: {}", dateStr, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回归结果内部类
|
||||
*/
|
||||
private static class RegressionResult {
|
||||
final BigDecimal slope;
|
||||
final BigDecimal intercept;
|
||||
final BigDecimal r2;
|
||||
|
||||
RegressionResult(BigDecimal slope, BigDecimal intercept, BigDecimal r2) {
|
||||
this.slope = slope;
|
||||
this.intercept = intercept;
|
||||
this.r2 = r2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部统计类:某一 stopType 的总损失时间与次数
|
||||
*/
|
||||
private static class LossStats {
|
||||
long totalMin = 0L;
|
||||
int count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.klp.pocket.common.service;
|
||||
|
||||
/**
|
||||
* 钢卷良品/次品判定(OEE 口径用)。
|
||||
*
|
||||
* 规则:
|
||||
* - 以 WMS 的 wms_material_coil 为准
|
||||
* - 按“当前钢卷号(current_coil_no) + 所在库区ID(warehouse_id)”精确匹配
|
||||
* - quality_status 命中 {"C+","C","C-","D+","D","D-"} => 次品;否则 => 良品
|
||||
* - 若匹配不到(WMS 无记录),返回 null,调用方按“忽略不计”处理
|
||||
*/
|
||||
public interface ICoilQualityJudgeService {
|
||||
|
||||
/**
|
||||
* @param warehouseId 所在库区ID(成品库库区ID)
|
||||
* @param currentCoilNo 当前钢卷号(注意:pocket 侧查询到的“钢卷id/卷号”口径等同于该字段)
|
||||
* @return Boolean:true=次品,false=良品,null=匹配不到(忽略不计)
|
||||
*/
|
||||
Boolean isScrap(Long warehouseId, String currentCoilNo);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.klp.pocket.common.service.impl;
|
||||
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.pocket.common.service.ICoilQualityJudgeService;
|
||||
import com.klp.service.IWmsMaterialCoilService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class CoilQualityJudgeServiceImpl implements ICoilQualityJudgeService {
|
||||
|
||||
/**
|
||||
* 次品状态枚举:命中这些 quality_status 则判定为次品,否则判定为良品。
|
||||
*/
|
||||
private static final Set<String> SCRAP_QUALITY_STATUS = new HashSet<>(
|
||||
Arrays.asList("C+", "C", "C-", "D+", "D", "D-")
|
||||
);
|
||||
|
||||
private final IWmsMaterialCoilService wmsMaterialCoilService;
|
||||
|
||||
@Override
|
||||
public Boolean isScrap(Long warehouseId, String currentCoilNo) {
|
||||
if (warehouseId == null || StringUtils.isBlank(currentCoilNo)) {
|
||||
return null;
|
||||
}
|
||||
String qualityStatus = wmsMaterialCoilService
|
||||
.queryQualityStatusByWarehouseIdAndCurrentCoilNo(warehouseId, currentCoilNo);
|
||||
if (StringUtils.isBlank(qualityStatus)) {
|
||||
// WMS 匹配不到或字段为空:按“忽略不计”
|
||||
return null;
|
||||
}
|
||||
return SCRAP_QUALITY_STATUS.contains(qualityStatus.trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user