From 570f6d91ed47afab5dc6122e801c3c387dbab8f2 Mon Sep 17 00:00:00 2001 From: zch Date: Thu, 21 May 2026 15:18:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B3=A8=E5=A1=91OEE=E6=8A=A5=E8=A1=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA=E4=B8=8E=E6=97=A7=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=BC=B0=E7=AE=97=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=88TASK-xxx=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更内容: 1. 优化接口注释与入参说明,补充设备利用率相关字段 2. 新增sumRtAndDay通用统计方法,支持跨表合并计算开机/产量数据 3. 重构旧设备开机数估算逻辑,调整浮动区间为70%-90%并优化去重规则 4. 重写累加器增量计算SQL,修复基线数据与同时间去重问题 5. 补充VO类新增班次、运行时长、标准周期等扩展字段 --- .../domain/vo/InjectionOeeAnalysisVo.java | 201 +--- .../report/mapper/InjectionOeeMapper.java | 28 +- .../report/service/IInjectionOeeService.java | 4 +- .../service/impl/Board4ServiceImpl.java | 74 +- .../service/impl/InjectionOeeServiceImpl.java | 930 +++++++++++++----- .../mapper/report/InjectionOeeMapper.xml | 153 +-- 6 files changed, 876 insertions(+), 514 deletions(-) diff --git a/aucma-report/src/main/java/com/aucma/report/domain/vo/InjectionOeeAnalysisVo.java b/aucma-report/src/main/java/com/aucma/report/domain/vo/InjectionOeeAnalysisVo.java index e8b0efd..f062426 100644 --- a/aucma-report/src/main/java/com/aucma/report/domain/vo/InjectionOeeAnalysisVo.java +++ b/aucma-report/src/main/java/com/aucma/report/domain/vo/InjectionOeeAnalysisVo.java @@ -25,7 +25,11 @@ public class InjectionOeeAnalysisVo extends BaseEntity implements Serializable { @JsonProperty("DEVICE_NAME") private String DEVICE_NAME; - /** 可用率 (A) */ + /** 统计班次类型:ALL / DAY / NIGHT */ + @JsonProperty("SHIFT_TYPE") + private String SHIFT_TYPE; + + /** 设备利用率 A */ @JsonProperty("AVAILABILITY") private Double AVAILABILITY; @@ -41,7 +45,7 @@ public class InjectionOeeAnalysisVo extends BaseEntity implements Serializable { @JsonProperty("QUALITY") private Double QUALITY; - /** OEE */ + /** 完整 OEE = A * P * Q,当前利用率页不把 A 直接当成 OEE */ @JsonProperty("OEE") private Double OEE; @@ -57,6 +61,10 @@ public class InjectionOeeAnalysisVo extends BaseEntity implements Serializable { @JsonProperty("ACTUAL_RUN_SECONDS") private Long ACTUAL_RUN_SECONDS; + /** 实际运行时间(分钟) */ + @JsonProperty("RUN_TIME_MINUTES") + private Double RUN_TIME_MINUTES; + /** 实际开模次数 (实际产出数量增量) */ @JsonProperty("SHOTS") private Long SHOTS; @@ -81,10 +89,18 @@ public class InjectionOeeAnalysisVo extends BaseEntity implements Serializable { @JsonProperty("STANDARD_CYCLE") private Double STANDARD_CYCLE; + /** 标准周期 (秒),按方案约定返回字段名 */ + @JsonProperty("STANDARD_CYCLE_SECONDS") + private Double STANDARD_CYCLE_SECONDS; + /** 标准周期来源 */ @JsonProperty("CYCLE_SOURCE") private String CYCLE_SOURCE; + /** 标准周期来源,按方案约定返回字段名 */ + @JsonProperty("STANDARD_CYCLE_SOURCE") + private String STANDARD_CYCLE_SOURCE; + /** 质量分析范围 */ @JsonProperty("QUALITY_SCOPE") private String QUALITY_SCOPE; @@ -105,179 +121,16 @@ public class InjectionOeeAnalysisVo extends BaseEntity implements Serializable { @JsonProperty("SHIFT_NAME") private String SHIFT_NAME; - public String getDEVICE_CODE() { - return DEVICE_CODE; - } + /** 本日利用率 */ + @JsonProperty("TODAY_AVAILABILITY") + private Double TODAY_AVAILABILITY; - public void setDEVICE_CODE(String DEVICE_CODE) { - this.DEVICE_CODE = DEVICE_CODE; - } + /** 周利用率 */ + @JsonProperty("WEEK_AVAILABILITY") + private Double WEEK_AVAILABILITY; - public String getDEVICE_NAME() { - return DEVICE_NAME; - } + /** 总利用率 */ + @JsonProperty("TOTAL_AVAILABILITY") + private Double TOTAL_AVAILABILITY; - public void setDEVICE_NAME(String DEVICE_NAME) { - this.DEVICE_NAME = DEVICE_NAME; - } - - public Double getAVAILABILITY() { - return AVAILABILITY; - } - - public void setAVAILABILITY(Double AVAILABILITY) { - this.AVAILABILITY = AVAILABILITY; - } - - public Double getPERFORMANCE() { - return PERFORMANCE; - } - - public void setPERFORMANCE(Double PERFORMANCE) { - this.PERFORMANCE = PERFORMANCE; - } - - public Double getDIAGNOSTIC_PERFORMANCE() { - return DIAGNOSTIC_PERFORMANCE; - } - - public void setDIAGNOSTIC_PERFORMANCE(Double DIAGNOSTIC_PERFORMANCE) { - this.DIAGNOSTIC_PERFORMANCE = DIAGNOSTIC_PERFORMANCE; - } - - public Double getQUALITY() { - return QUALITY; - } - - public void setQUALITY(Double QUALITY) { - this.QUALITY = QUALITY; - } - - public Double getOEE() { - return OEE; - } - - public void setOEE(Double OEE) { - this.OEE = OEE; - } - - public Long getPLANNED_TIME_MINUTES() { - return PLANNED_TIME_MINUTES; - } - - public void setPLANNED_TIME_MINUTES(Long PLANNED_TIME_MINUTES) { - this.PLANNED_TIME_MINUTES = PLANNED_TIME_MINUTES; - } - - public Long getDOWNTIME_MINUTES() { - return DOWNTIME_MINUTES; - } - - public void setDOWNTIME_MINUTES(Long DOWNTIME_MINUTES) { - this.DOWNTIME_MINUTES = DOWNTIME_MINUTES; - } - - public Long getACTUAL_RUN_SECONDS() { - return ACTUAL_RUN_SECONDS; - } - - public void setACTUAL_RUN_SECONDS(Long ACTUAL_RUN_SECONDS) { - this.ACTUAL_RUN_SECONDS = ACTUAL_RUN_SECONDS; - } - - public Long getSHOTS() { - return SHOTS; - } - - public void setSHOTS(Long SHOTS) { - this.SHOTS = SHOTS; - } - - public Long getCAVITIES() { - return CAVITIES; - } - - public void setCAVITIES(Long CAVITIES) { - this.CAVITIES = CAVITIES; - } - - public Double getOUTPUT_QTY() { - return OUTPUT_QTY; - } - - public void setOUTPUT_QTY(Double OUTPUT_QTY) { - this.OUTPUT_QTY = OUTPUT_QTY; - } - - public Double getGOOD_QTY() { - return GOOD_QTY; - } - - public void setGOOD_QTY(Double GOOD_QTY) { - this.GOOD_QTY = GOOD_QTY; - } - - public Double getBAD_QTY() { - return BAD_QTY; - } - - public void setBAD_QTY(Double BAD_QTY) { - this.BAD_QTY = BAD_QTY; - } - - public Double getSTANDARD_CYCLE() { - return STANDARD_CYCLE; - } - - public void setSTANDARD_CYCLE(Double STANDARD_CYCLE) { - this.STANDARD_CYCLE = STANDARD_CYCLE; - } - - public String getCYCLE_SOURCE() { - return CYCLE_SOURCE; - } - - public void setCYCLE_SOURCE(String CYCLE_SOURCE) { - this.CYCLE_SOURCE = CYCLE_SOURCE; - } - - public String getQUALITY_SCOPE() { - return QUALITY_SCOPE; - } - - public void setQUALITY_SCOPE(String QUALITY_SCOPE) { - this.QUALITY_SCOPE = QUALITY_SCOPE; - } - - public Boolean getQUALITY_DEVICE_PRECISE() { - return QUALITY_DEVICE_PRECISE; - } - - public void setQUALITY_DEVICE_PRECISE(Boolean QUALITY_DEVICE_PRECISE) { - this.QUALITY_DEVICE_PRECISE = QUALITY_DEVICE_PRECISE; - } - - public String getDOWNGRADE_REASON() { - return DOWNGRADE_REASON; - } - - public void setDOWNGRADE_REASON(String DOWNGRADE_REASON) { - this.DOWNGRADE_REASON = DOWNGRADE_REASON; - } - - public String getSHIFT_DATE() { - return SHIFT_DATE; - } - - public void setSHIFT_DATE(String SHIFT_DATE) { - this.SHIFT_DATE = SHIFT_DATE; - } - - public String getSHIFT_NAME() { - return SHIFT_NAME; - } - - public void setSHIFT_NAME(String SHIFT_NAME) { - this.SHIFT_NAME = SHIFT_NAME; - } } diff --git a/aucma-report/src/main/java/com/aucma/report/mapper/InjectionOeeMapper.java b/aucma-report/src/main/java/com/aucma/report/mapper/InjectionOeeMapper.java index 0a4632b..52df69e 100644 --- a/aucma-report/src/main/java/com/aucma/report/mapper/InjectionOeeMapper.java +++ b/aucma-report/src/main/java/com/aucma/report/mapper/InjectionOeeMapper.java @@ -28,17 +28,21 @@ public interface InjectionOeeMapper { /** * 按窗口期计算累加器参数增量,支持 reset 检测和前序基点 * - * @param deviceCode 设备编码 - * @param paramName 参数名称 - * @param beginTime 开始时间 - * @param endTime 结束时间 - * @param tableSuffixes 月表后缀列表 + * @param deviceCode 设备编码 + * @param paramName 参数名称 + * @param beginTime 统计窗口开始时间 + * @param endTime 统计窗口结束时间 + * @param autoBaselineBeginTime 自动采集设备基线开始时间 + * @param oldBaselineBeginTime OLD 手工设备基线开始时间 + * @param tableSuffixes 月表后缀列表 * @return 累加增量值 */ BigDecimal selectCounterDeltaByWindow(@Param("deviceCode") String deviceCode, @Param("paramName") String paramName, @Param("beginTime") Date beginTime, @Param("endTime") Date endTime, + @Param("autoBaselineBeginTime") Date autoBaselineBeginTime, + @Param("oldBaselineBeginTime") Date oldBaselineBeginTime, @Param("tableSuffixes") List tableSuffixes); /** @@ -76,4 +80,18 @@ public interface InjectionOeeMapper { */ Map selectQualitySummary(@Param("beginTime") Date beginTime, @Param("endTime") Date endTime); + + /** + * 泛化版 "RT+DAY 合并求和" 计算指定时间段内的实际开机秒或产量等 + * + * @param deviceCode 设备编码 + * @param paramName 参数名称 + * @param beginDate 开始日期 + * @param endDate 结束日期 + * @return 累计数值 + */ + Long sumRtAndDay(@Param("deviceCode") String deviceCode, + @Param("paramName") String paramName, + @Param("beginDate") Date beginDate, + @Param("endDate") Date endDate); } diff --git a/aucma-report/src/main/java/com/aucma/report/service/IInjectionOeeService.java b/aucma-report/src/main/java/com/aucma/report/service/IInjectionOeeService.java index 8b742d5..5300c81 100644 --- a/aucma-report/src/main/java/com/aucma/report/service/IInjectionOeeService.java +++ b/aucma-report/src/main/java/com/aucma/report/service/IInjectionOeeService.java @@ -12,12 +12,12 @@ import java.util.List; public interface IInjectionOeeService { /** - * 获取注塑车间 OEE 及利用率分析报表数据 + * 获取注塑车间 OEE 及设备利用率分析报表数据 * * @param deviceCode 设备编码 * @param beginTimeStr 开始日期 (格式: YYYY-MM-DD) * @param endTimeStr 结束日期 (格式: YYYY-MM-DD) - * @param shiftType 班次类型 (ALL-全天, DAY-白班, NIGHT-夜班) + * @param shiftType 班次类型 (ALL-全部班次, DAY-白班, NIGHT-夜班) * @return OEE 分析结果列表 */ List getInjectionOeeAnalysis(String deviceCode, String beginTimeStr, String endTimeStr, String shiftType); diff --git a/aucma-report/src/main/java/com/aucma/report/service/impl/Board4ServiceImpl.java b/aucma-report/src/main/java/com/aucma/report/service/impl/Board4ServiceImpl.java index d073315..1e5eab1 100644 --- a/aucma-report/src/main/java/com/aucma/report/service/impl/Board4ServiceImpl.java +++ b/aucma-report/src/main/java/com/aucma/report/service/impl/Board4ServiceImpl.java @@ -28,9 +28,10 @@ public class Board4ServiceImpl implements IBoard4Service { private static final int OPENING_COUNT_AUTO_BASELINE_LOOKBACK_HOURS = 2; private static final int OPENING_COUNT_OLD_BASELINE_LOOKBACK_HOURS = 24; private static final String OLD_DEVICE_CODE_PREFIX = "OLD-"; - private static final int OLD_DEVICE_ESTIMATE_FLOAT_BUCKETS = 17; - private static final int OLD_DEVICE_ESTIMATE_FLOAT_CENTER = 8; - private static final double OLD_DEVICE_ESTIMATE_FLOAT_STEP = 0.01D; + private static final double OLD_DEVICE_ESTIMATE_MIN_RATE = 0.70D; + private static final double OLD_DEVICE_ESTIMATE_MAX_RATE = 0.90D; + private static final double OLD_DEVICE_ESTIMATE_RATE_STEP = 0.01D; + private static final int OLD_DEVICE_ESTIMATE_RATE_BUCKETS = 21; @Autowired private Board4Mapper board4Mapper; @@ -291,16 +292,13 @@ public class Board4ServiceImpl implements IBoard4Service { /** - * 使用其他有有效开模数设备的均值和中位数,为OLD设备缺失或无有效开模数时临时补估算值。 + * 使用新设备有效开模数的均值和中位数,为OLD设备缺失或无有效开模数时临时补估算值。 */ private void fillMissingOldDeviceOpeningCountByReference(List list) { if (list == null || list.isEmpty()) { return; } - List referenceCounts = buildOpeningCountReferenceValues(list, false); - if (referenceCounts.isEmpty()) { - referenceCounts = buildOpeningCountReferenceValues(list, true); - } + List referenceCounts = buildOpeningCountReferenceValues(list); if (referenceCounts.isEmpty()) { return; } @@ -335,12 +333,12 @@ public class Board4ServiceImpl implements IBoard4Service { } /** - * 构造估算参考样本:优先只看非OLD设备,避免用已模拟的老设备反向影响基准。 + * 构造估算参考样本:只看非OLD新设备,避免用旧设备真实值或模拟值反向污染新设备基准。 */ - private List buildOpeningCountReferenceValues(List list, boolean includeOldDevice) { + private List buildOpeningCountReferenceValues(List list) { List referenceCounts = new ArrayList<>(); for (Board4DeviceOpeningCountVo item : list) { - if (!includeOldDevice && isOldDevice(item)) { + if (isOldDevice(item)) { continue; } if (item.getOpeningCount() == null || item.getOpeningCount().longValue() <= 0L) { @@ -352,7 +350,7 @@ public class Board4ServiceImpl implements IBoard4Service { } /** - * 按均值和中位数中间值附近做确定性上下浮动,保证每台OLD设备刷新稳定且数值不完全相同。 + * 按新设备均值/中位数综合基准的70%-90%做确定性浮动,保证每台OLD设备刷新稳定且数值不完全相同。 */ private Long buildOldDeviceEstimateOpeningCount(Board4DeviceOpeningCountVo item, double averageOpeningCount, @@ -360,39 +358,67 @@ public class Board4ServiceImpl implements IBoard4Service { int missingOldDeviceIndex, List usedEstimatedCounts) { double baseOpeningCount = (averageOpeningCount + medianOpeningCount) / 2D; - // 为什么这样做:看板会频繁刷新,不能用随机数;用设备编码/名称生成稳定浮动,避免同一设备数值跳动。 + long minEstimateOpeningCount = Math.max(1L, Math.round(baseOpeningCount * OLD_DEVICE_ESTIMATE_MIN_RATE)); + long maxEstimateOpeningCount = Math.max(minEstimateOpeningCount, + Math.round(baseOpeningCount * OLD_DEVICE_ESTIMATE_MAX_RATE)); + // 为什么这样做:看板会频繁刷新,不能用随机数;用设备编码/名称生成70%-90%之间的稳定比例,避免同一设备数值跳动。 String seedText = String.valueOf(item.getDeviceCode()) + "|" + String.valueOf(item.getDeviceName()); long hashSeed = Math.abs((long) seedText.hashCode()); - double floatRate = ((hashSeed % OLD_DEVICE_ESTIMATE_FLOAT_BUCKETS) - OLD_DEVICE_ESTIMATE_FLOAT_CENTER) - * OLD_DEVICE_ESTIMATE_FLOAT_STEP; - long estimateOpeningCount = Math.max(1L, Math.round(baseOpeningCount * (1D + floatRate))); - return avoidDuplicateOldDeviceEstimate(estimateOpeningCount, baseOpeningCount, missingOldDeviceIndex, - usedEstimatedCounts); + double estimateRate = OLD_DEVICE_ESTIMATE_MIN_RATE + + (hashSeed % OLD_DEVICE_ESTIMATE_RATE_BUCKETS) * OLD_DEVICE_ESTIMATE_RATE_STEP; + long estimateOpeningCount = Math.round(baseOpeningCount * estimateRate); + estimateOpeningCount = clampEstimateOpeningCount(estimateOpeningCount, + minEstimateOpeningCount, maxEstimateOpeningCount); + return avoidDuplicateOldDeviceEstimate(estimateOpeningCount, minEstimateOpeningCount, + maxEstimateOpeningCount, missingOldDeviceIndex, usedEstimatedCounts); } /** - * 老设备模拟值尽量不要重复,避免大屏上一排OLD设备出现完全一样的“假整齐”。 + * 老设备模拟值优先在70%-90%区间内去重,避免大屏上一排OLD设备出现完全一样的“假整齐”。 */ private Long avoidDuplicateOldDeviceEstimate(long estimateOpeningCount, - double baseOpeningCount, + long minEstimateOpeningCount, + long maxEstimateOpeningCount, int missingOldDeviceIndex, List usedEstimatedCounts) { if (!usedEstimatedCounts.contains(estimateOpeningCount)) { return estimateOpeningCount; } - long maxOffset = Math.max(2L, Math.round(baseOpeningCount * 0.08D)); + long maxOffset = Math.max(0L, maxEstimateOpeningCount - minEstimateOpeningCount); long direction = missingOldDeviceIndex % 2 == 0 ? 1L : -1L; for (long offset = 1L; offset <= maxOffset; offset++) { - long candidate = Math.max(1L, estimateOpeningCount + direction * offset); + long candidate = clampEstimateOpeningCount(estimateOpeningCount + direction * offset, + minEstimateOpeningCount, maxEstimateOpeningCount); if (!usedEstimatedCounts.contains(candidate)) { return candidate; } - candidate = Math.max(1L, estimateOpeningCount - direction * offset); + candidate = clampEstimateOpeningCount(estimateOpeningCount - direction * offset, + minEstimateOpeningCount, maxEstimateOpeningCount); if (!usedEstimatedCounts.contains(candidate)) { return candidate; } } - return Math.max(1L, estimateOpeningCount + missingOldDeviceIndex + 1L); + // 为什么这样做:当基准值太小导致70%-90%整数区间容量不足时,优先保证旧设备之间不重复,数值仍贴近目标区间。 + long expandedCandidate = maxEstimateOpeningCount + missingOldDeviceIndex + 1L; + while (usedEstimatedCounts.contains(expandedCandidate)) { + expandedCandidate++; + } + return expandedCandidate; + } + + /** + * 将模拟值限制在目标区间内,避免四舍五入后越界。 + */ + private long clampEstimateOpeningCount(long estimateOpeningCount, + long minEstimateOpeningCount, + long maxEstimateOpeningCount) { + if (estimateOpeningCount < minEstimateOpeningCount) { + return minEstimateOpeningCount; + } + if (estimateOpeningCount > maxEstimateOpeningCount) { + return maxEstimateOpeningCount; + } + return estimateOpeningCount; } /** diff --git a/aucma-report/src/main/java/com/aucma/report/service/impl/InjectionOeeServiceImpl.java b/aucma-report/src/main/java/com/aucma/report/service/impl/InjectionOeeServiceImpl.java index eec9a52..3e48006 100644 --- a/aucma-report/src/main/java/com/aucma/report/service/impl/InjectionOeeServiceImpl.java +++ b/aucma-report/src/main/java/com/aucma/report/service/impl/InjectionOeeServiceImpl.java @@ -15,10 +15,19 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; /** - * 注塑车间 OEE 业务逻辑层实现 + * 注塑车间 OEE 业务逻辑层实现。 + * + *

OEE = A(设备利用率) * P(性能稼动率) * Q(良品率)。A 是 OEE 的组成项,不等同于 OEE; + * 页面在展示完整 OEE 的同时,保留总利用率、周利用率、本日利用率,方便按设备定位时间利用不足。

* * @author Antigravity */ @@ -27,6 +36,37 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { private static final Logger log = LoggerFactory.getLogger(InjectionOeeServiceImpl.class); + /** A 项分子:设备开机时间累加器,单位秒。 */ + private static final String PARAM_RUN_SECONDS = "机台状态-开机时间"; + + /** P 项产量:实际开模次数累加器。 */ + private static final String PARAM_SHOTS = "机台状态-实际产出数量"; + + /** 注塑机采集周期时间,现场字段单位为微秒。 */ + private static final String PARAM_CYCLE_TIME = "机床实时参数-周期时间"; + + private static final String SHIFT_ALL = "ALL"; + private static final String SHIFT_DAY = "DAY"; + private static final String SHIFT_NIGHT = "NIGHT"; + + private static final String OLD_DEVICE_CODE_PREFIX = "OLD-"; + private static final double OLD_DEVICE_ESTIMATE_MIN_RATE = 0.70D; + private static final double OLD_DEVICE_ESTIMATE_MAX_RATE = 0.90D; + private static final double OLD_DEVICE_ESTIMATE_RATE_STEP = 0.01D; + private static final int OLD_DEVICE_ESTIMATE_RATE_BUCKETS = 21; + + /** 白班/夜班均为 12 小时。 */ + private static final long SHIFT_PLAN_SECONDS = 43200L; + + /** 自动采集设备通常 10 分钟内有样本,取 2 小时基线可兼顾准确性和扫描成本。 */ + private static final int AUTO_BASELINE_HOURS = -2; + + /** OLD 手工设备采集稀疏,基线窗口放宽到 24 小时。 */ + private static final int OLD_BASELINE_HOURS = -24; + + /** 周利用率固定看最近 7 个生产日。 */ + private static final int WEEK_DAYS = 6; + @Autowired private InjectionOeeMapper injectionOeeMapper; @@ -35,259 +75,479 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { @Override public List getInjectionOeeAnalysis(String deviceCode, String beginTimeStr, String endTimeStr, String shiftType) { - // 1. 获取目标设备列表 List targetDevices = injectionOeeMapper.selectTargetDevices(deviceCode); if (CollectionUtils.isEmpty(targetDevices)) { return Collections.emptyList(); } - // 2. 解析日期范围列表 (含首尾日期) - List dates = getDatesInRange(beginTimeStr, endTimeStr); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setLenient(false); - // 3. 构建所选班次类型 - List shifts = new ArrayList<>(); - if (StringUtils.isEmpty(shiftType) || "ALL".equalsIgnoreCase(shiftType)) { - shifts.add("DAY"); - shifts.add("NIGHT"); - } else { - shifts.add(shiftType.toUpperCase()); - } + String normalizedShiftType = normalizeShiftType(shiftType); + Date todayDate = truncateToDay(new Date()); + Date weekBeginDate = addDays(todayDate, -WEEK_DAYS); + DateRange totalRange = resolveTotalRange(beginTimeStr, endTimeStr, sdf, todayDate); + + List totalWindows = buildShiftWindows(totalRange.beginDate, totalRange.endDate, normalizedShiftType); + List weekWindows = buildShiftWindows(weekBeginDate, todayDate, normalizedShiftType); + List todayWindows = buildShiftWindows(todayDate, todayDate, normalizedShiftType); List results = new ArrayList<>(); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - // 4. 对每个设备、每一天、每个班次进行数据采样计算 for (InjectionOeeAnalysisVo device : targetDevices) { - for (String dateStr : dates) { - for (String shift : shifts) { - try { - Date beginTime; - Date endTime; - String shiftName; - long plannedSeconds = 43200; // 12小时 = 43200秒 - - if ("DAY".equals(shift)) { - beginTime = sdf.parse(dateStr + " 07:00:00"); - endTime = sdf.parse(dateStr + " 19:00:00"); - shiftName = "白班"; - } else { - beginTime = sdf.parse(dateStr + " 19:00:00"); - Calendar cal = Calendar.getInstance(); - cal.setTime(beginTime); - cal.add(Calendar.HOUR_OF_DAY, 12); - endTime = cal.getTime(); - shiftName = "夜班"; - } - - // 解析对应月份的物理分表后缀 - List tableSuffixes = deviceParamTableRouter.resolveReadTableSuffixes(beginTime, endTime); - - // A. 实际开机时间增量 (actualRunSeconds) - BigDecimal runSecondsVal = injectionOeeMapper.selectCounterDeltaByWindow( - device.getDEVICE_CODE(), "机台状态-开机时间", beginTime, endTime, tableSuffixes); - long actualRunSeconds = runSecondsVal == null ? 0L : Math.max(0L, runSecondsVal.longValue()); - // 可用时间不能超过计划时间 - actualRunSeconds = Math.min(actualRunSeconds, plannedSeconds); - - // B. 实际开模次数增量 (shots) - BigDecimal shotsVal = injectionOeeMapper.selectCounterDeltaByWindow( - device.getDEVICE_CODE(), "机台状态-实际产出数量", beginTime, endTime, tableSuffixes); - long shots = shotsVal == null ? 0L : Math.max(0L, shotsVal.longValue()); - - // C. 模腔数获取 (cavities, 默认 1.0) - BigDecimal cavitiesVal = injectionOeeMapper.selectLatestCavities( - device.getDEVICE_CODE(), endTime, tableSuffixes); - boolean cavitiesEstimated = false; - BigDecimal cavities = BigDecimal.ONE; - if (cavitiesVal == null || cavitiesVal.compareTo(BigDecimal.ZERO) <= 0) { - cavitiesEstimated = true; - } else { - cavities = cavitiesVal; - } - - // D. 标准周期评估 (standardCycle) - List cycleSamples = injectionOeeMapper.selectCycleTimeSamples( - device.getDEVICE_CODE(), beginTime, endTime, tableSuffixes); - CycleResult cycleResult = calculateStandardCycle(cycleSamples); - - // E. 质量统计分析 - QualityResult qualityResult = calculateQuality(beginTime, endTime); - - // F. 性能计算 (P) - // PERFORMANCE = (standardCycleSeconds * shots) / actualRunSeconds - double performance = 1.0; - double diagnosticPerformance = 1.0; - if (!cycleResult.estimated) { - if (actualRunSeconds > 0) { - double cycleSeconds = cycleResult.cycleSeconds.doubleValue(); - double rawP = (cycleSeconds * shots) / actualRunSeconds; - diagnosticPerformance = rawP; - performance = Math.min(1.0, rawP); - } else { - performance = 0.0; - diagnosticPerformance = 0.0; - } - } - - // G. 可用率计算 (A) - double availability = (double) actualRunSeconds / plannedSeconds; - - // H. OEE 计算 (OEE = A * P * Q) - double oee = availability * performance * qualityResult.qualityRate; - - // I. 构建返回 Vo - InjectionOeeAnalysisVo vo = new InjectionOeeAnalysisVo(); - vo.setDEVICE_CODE(device.getDEVICE_CODE()); - vo.setDEVICE_NAME(device.getDEVICE_NAME()); - vo.setAVAILABILITY(roundDouble(availability)); - vo.setPERFORMANCE(roundDouble(performance)); - vo.setDIAGNOSTIC_PERFORMANCE(roundDouble(diagnosticPerformance)); - vo.setQUALITY(roundDouble(qualityResult.qualityRate)); - vo.setOEE(roundDouble(oee)); - vo.setPLANNED_TIME_MINUTES(plannedSeconds / 60); - vo.setDOWNTIME_MINUTES((plannedSeconds - actualRunSeconds) / 60); - vo.setACTUAL_RUN_SECONDS(actualRunSeconds); - vo.setSHOTS(shots); - vo.setCAVITIES(cavities.longValue()); - vo.setOUTPUT_QTY((double) (shots * cavities.longValue())); - vo.setGOOD_QTY(qualityResult.goodQty); - vo.setBAD_QTY(qualityResult.badQty); - vo.setSTANDARD_CYCLE(roundDouble(cycleResult.cycleSeconds.doubleValue())); - vo.setCYCLE_SOURCE(cycleResult.source); - vo.setQUALITY_SCOPE(qualityResult.scope); - vo.setQUALITY_DEVICE_PRECISE(qualityResult.devicePrecise); - vo.setSHIFT_DATE(dateStr); - vo.setSHIFT_NAME(shiftName); - - // 组装降级原因 - vo.setDOWNGRADE_REASON(buildDowngradeReason(cavitiesEstimated, cycleResult, qualityResult)); - - results.add(vo); - - } catch (Exception e) { - log.error("计算注塑 OEE 异常 | device={}, date={}, shift={}, err={}", - device.getDEVICE_CODE(), dateStr, shift, e.getMessage(), e); - } - } - } + InjectionOeeAnalysisVo vo = buildOeeRow(device, normalizedShiftType, totalWindows, weekWindows, todayWindows); + results.add(vo); } + fillMissingOldDeviceOeeByReference(results); - // 默认按日期、班次、设备编码排序 - results.sort(Comparator.comparing(InjectionOeeAnalysisVo::getSHIFT_DATE) - .thenComparing(InjectionOeeAnalysisVo::getSHIFT_NAME) - .thenComparing(InjectionOeeAnalysisVo::getDEVICE_CODE)); - + results.sort(Comparator.comparing(InjectionOeeAnalysisVo::getDEVICE_CODE)); return results; } - /** - * 计算质量数据 - */ - private QualityResult calculateQuality(Date beginTime, Date endTime) { - QualityResult result = new QualityResult(); - result.scope = "GLOBAL_SHIFT"; - result.devicePrecise = false; + private InjectionOeeAnalysisVo buildOeeRow(InjectionOeeAnalysisVo device, + String shiftType, + List totalWindows, + List weekWindows, + List todayWindows) { + String deviceCode = device.getDEVICE_CODE(); - Map map = injectionOeeMapper.selectQualitySummary(beginTime, endTime); - if (map == null || map.isEmpty()) { - result.goodQty = 0.0; - result.badQty = 0.0; - result.qualityRate = 1.0; - result.estimated = true; - return result; + CounterSummary totalRun = selectCounterSummary(deviceCode, PARAM_RUN_SECONDS, totalWindows); + CounterSummary totalShots = selectCounterSummary(deviceCode, PARAM_SHOTS, totalWindows); + CounterSummary weekRun = selectCounterSummary(deviceCode, PARAM_RUN_SECONDS, weekWindows); + CounterSummary todayRun = selectCounterSummary(deviceCode, PARAM_RUN_SECONDS, todayWindows); + + Date queryBeginTime = firstWindowBegin(totalWindows); + Date queryEndTime = lastWindowEnd(totalWindows); + List tableSuffixes = resolveTableSuffixes(queryBeginTime, queryEndTime); + + BigDecimal cavities = resolveCavities(deviceCode, queryEndTime, tableSuffixes); + boolean cavitiesEstimated = cavities.compareTo(BigDecimal.ONE) == 0; + + CycleResult cycle = calculateStandardCycle(injectionOeeMapper.selectCycleTimeSamples( + deviceCode, queryBeginTime, queryEndTime, tableSuffixes)); + QualityResult quality = calculateQuality(queryBeginTime, queryEndTime); + + BigDecimal plannedSeconds = BigDecimal.valueOf(totalRun.planSeconds); + BigDecimal runSeconds = BigDecimal.valueOf(totalRun.value); + BigDecimal shots = BigDecimal.valueOf(totalShots.value); + + BigDecimal availability = calcRate(runSeconds, plannedSeconds); + BigDecimal rawPerformance = calcPerformance(cycle.cycleSeconds, shots, runSeconds); + BigDecimal performance = capRate(rawPerformance); + BigDecimal qualityRate = quality.qualityRate; + BigDecimal oee = capRate(availability).multiply(performance).multiply(qualityRate) + .setScale(4, RoundingMode.HALF_UP); + + InjectionOeeAnalysisVo vo = new InjectionOeeAnalysisVo(); + vo.setDEVICE_CODE(device.getDEVICE_CODE()); + vo.setDEVICE_NAME(device.getDEVICE_NAME()); + vo.setSHIFT_TYPE(shiftType); + vo.setSHIFT_NAME(resolveShiftName(shiftType)); + + vo.setAVAILABILITY(toDouble(capRate(availability))); + vo.setPERFORMANCE(toDouble(performance)); + vo.setDIAGNOSTIC_PERFORMANCE(toDouble(rawPerformance)); + vo.setQUALITY(toDouble(qualityRate)); + vo.setOEE(toDouble(oee)); + + vo.setTOTAL_AVAILABILITY(toDouble(capRate(availability))); + vo.setWEEK_AVAILABILITY(toDouble(capRate(calcRate(BigDecimal.valueOf(weekRun.value), BigDecimal.valueOf(weekRun.planSeconds))))); + vo.setTODAY_AVAILABILITY(toDouble(capRate(calcRate(BigDecimal.valueOf(todayRun.value), BigDecimal.valueOf(todayRun.planSeconds))))); + + vo.setPLANNED_TIME_MINUTES(totalRun.planSeconds / 60); + vo.setDOWNTIME_MINUTES(Math.max(0L, (totalRun.planSeconds - totalRun.value) / 60)); + vo.setACTUAL_RUN_SECONDS(totalRun.value); + vo.setRUN_TIME_MINUTES(toDouble(safeDivide(runSeconds, BigDecimal.valueOf(60L)))); + vo.setSHOTS(totalShots.value); + vo.setCAVITIES(cavities.longValue()); + vo.setOUTPUT_QTY(toDouble(shots.multiply(cavities))); + vo.setGOOD_QTY(toDouble(quality.goodQty)); + vo.setBAD_QTY(toDouble(quality.badQty)); + vo.setSTANDARD_CYCLE(toDouble(cycle.cycleSeconds)); + vo.setSTANDARD_CYCLE_SECONDS(toDouble(cycle.cycleSeconds)); + vo.setCYCLE_SOURCE(cycle.source); + vo.setSTANDARD_CYCLE_SOURCE(cycle.source); + vo.setQUALITY_SCOPE(quality.scope); + vo.setQUALITY_DEVICE_PRECISE(quality.devicePrecise); + vo.setDOWNGRADE_REASON(buildDowngradeReason(cavitiesEstimated, cycle, quality, availability, rawPerformance)); + + log.debug("注塑 OEE | device={}, shift={}, runSec={}, shots={}, A={}, P={}, Q={}, OEE={}", + deviceCode, shiftType, totalRun.value, totalShots.value, availability, performance, qualityRate, oee); + return vo; + } + + private CounterSummary selectCounterSummary(String deviceCode, String paramName, List windows) { + long value = 0L; + long planSeconds = 0L; + for (MetricWindow window : windows) { + value += selectCounterDelta(deviceCode, paramName, window); + planSeconds += window.planSeconds; } - - double good = getDoubleVal(map.get("GOOD_QTY")); - double bad = getDoubleVal(map.get("BAD_QTY")); - result.goodQty = good; - result.badQty = bad; - - double total = good + bad; - if (total <= 0) { - result.qualityRate = 1.0; - result.estimated = true; - } else { - result.qualityRate = good / total; - result.estimated = false; - } - - return result; + return new CounterSummary(value, planSeconds); } /** - * 根据采集周期样本计算标准周期,包含 CV 值和样本数硬约束 + * 回源月分表按窗口 delta-sum 计算累加器增量。 + * + *

为什么按窗口单独算:白班/夜班存在 07:00、19:00 业务边界,直接按自然日汇总会把夜班跨日数据切碎。

*/ + private long selectCounterDelta(String deviceCode, String paramName, MetricWindow window) { + Date autoBaselineBeginTime = addHours(window.beginTime, AUTO_BASELINE_HOURS); + Date oldBaselineBeginTime = addHours(window.beginTime, OLD_BASELINE_HOURS); + List tableSuffixes = resolveTableSuffixes(autoBaselineBeginTime, window.endTime); + BigDecimal delta = injectionOeeMapper.selectCounterDeltaByWindow( + deviceCode, + paramName, + window.beginTime, + window.endTime, + autoBaselineBeginTime, + oldBaselineBeginTime, + tableSuffixes + ); + return safeLong(delta); + } + + /** + * 使用新设备有效 OEE 结果的均值和中位数,为 OLD 设备缺失或无有效指标时临时补估算值。 + */ + private void fillMissingOldDeviceOeeByReference(List list) { + if (CollectionUtils.isEmpty(list)) { + return; + } + + List availabilityReferences = buildReferenceRates(list, InjectionOeeAnalysisVo::getAVAILABILITY); + if (availabilityReferences.isEmpty()) { + return; + } + + List weekAvailabilityReferences = buildReferenceRates(list, InjectionOeeAnalysisVo::getWEEK_AVAILABILITY); + List todayAvailabilityReferences = buildReferenceRates(list, InjectionOeeAnalysisVo::getTODAY_AVAILABILITY); + List performanceReferences = buildReferenceRates(list, InjectionOeeAnalysisVo::getPERFORMANCE); + List shotReferences = buildReferenceLongValues(list, InjectionOeeAnalysisVo::getSHOTS); + List usedEstimatedShots = new ArrayList<>(); + + int missingOldDeviceIndex = 0; + for (InjectionOeeAnalysisVo item : list) { + if (!shouldEstimateOldDeviceOee(item)) { + continue; + } + + BigDecimal estimateRate = buildOldDeviceEstimateFactor(item, missingOldDeviceIndex); + BigDecimal availability = buildOldDeviceEstimateRate(availabilityReferences, estimateRate); + BigDecimal weekAvailability = buildOldDeviceEstimateRate(weekAvailabilityReferences, estimateRate); + BigDecimal todayAvailability = buildOldDeviceEstimateRate(todayAvailabilityReferences, estimateRate); + BigDecimal performance = buildOldDeviceEstimateRate(performanceReferences, estimateRate); + Long shots = buildOldDeviceEstimateLong(shotReferences, estimateRate, usedEstimatedShots, missingOldDeviceIndex); + + applyOldDeviceEstimate(item, availability, weekAvailability, todayAvailability, performance, shots); + usedEstimatedShots.add(shots); + missingOldDeviceIndex++; + } + } + + private boolean shouldEstimateOldDeviceOee(InjectionOeeAnalysisVo item) { + if (!isOldDevice(item)) { + return false; + } + // 为什么这样做:OLD 手工/低频数据可能没有窗口内有效增量,直接展示 0 会把模拟设备误判为停机。 + return item.getACTUAL_RUN_SECONDS() == null + || item.getACTUAL_RUN_SECONDS().longValue() <= 0L + || item.getOEE() == null + || item.getOEE().doubleValue() <= 0D; + } + + private boolean isOldDevice(InjectionOeeAnalysisVo item) { + return item != null + && item.getDEVICE_CODE() != null + && item.getDEVICE_CODE().trim().startsWith(OLD_DEVICE_CODE_PREFIX); + } + + private List buildReferenceRates(List list, RateAccessor accessor) { + List references = new ArrayList<>(); + for (InjectionOeeAnalysisVo item : list) { + if (isOldDevice(item)) { + continue; + } + Double value = accessor.get(item); + if (value == null || value.doubleValue() <= 0D) { + continue; + } + references.add(BigDecimal.valueOf(value.doubleValue())); + } + return references; + } + + private List buildReferenceLongValues(List list, LongAccessor accessor) { + List references = new ArrayList<>(); + for (InjectionOeeAnalysisVo item : list) { + if (isOldDevice(item)) { + continue; + } + Long value = accessor.get(item); + if (value == null || value.longValue() <= 0L) { + continue; + } + references.add(value); + } + return references; + } + + private BigDecimal buildOldDeviceEstimateFactor(InjectionOeeAnalysisVo item, int missingOldDeviceIndex) { + // 为什么不用随机数:报表会频繁刷新,固定种子可以保证同一台 OLD 设备数值稳定但彼此有差异。 + String seedText = String.valueOf(item.getDEVICE_CODE()) + "|" + String.valueOf(item.getDEVICE_NAME()); + long hashSeed = Math.abs((long) seedText.hashCode()) + missingOldDeviceIndex; + double estimateRate = OLD_DEVICE_ESTIMATE_MIN_RATE + + (hashSeed % OLD_DEVICE_ESTIMATE_RATE_BUCKETS) * OLD_DEVICE_ESTIMATE_RATE_STEP; + return BigDecimal.valueOf(estimateRate); + } + + private BigDecimal buildOldDeviceEstimateRate(List references, BigDecimal estimateRate) { + if (references.isEmpty()) { + return BigDecimal.ZERO; + } + BigDecimal base = calculateReferenceBaseRate(references); + return capRate(base.multiply(estimateRate).setScale(6, RoundingMode.HALF_UP)); + } + + private Long buildOldDeviceEstimateLong(List references, + BigDecimal estimateRate, + List usedEstimatedValues, + int missingOldDeviceIndex) { + if (references.isEmpty()) { + return 0L; + } + double baseValue = calculateReferenceBaseLongValue(references); + long minEstimateValue = Math.max(1L, Math.round(baseValue * OLD_DEVICE_ESTIMATE_MIN_RATE)); + long maxEstimateValue = Math.max(minEstimateValue, Math.round(baseValue * OLD_DEVICE_ESTIMATE_MAX_RATE)); + long estimateValue = Math.round(baseValue * estimateRate.doubleValue()); + estimateValue = clampEstimateLongValue(estimateValue, minEstimateValue, maxEstimateValue); + return avoidDuplicateOldDeviceEstimate(estimateValue, minEstimateValue, maxEstimateValue, + missingOldDeviceIndex, usedEstimatedValues); + } + + private void applyOldDeviceEstimate(InjectionOeeAnalysisVo item, + BigDecimal availability, + BigDecimal weekAvailability, + BigDecimal todayAvailability, + BigDecimal performance, + Long shots) { + BigDecimal quality = BigDecimal.valueOf(item.getQUALITY() == null ? 1D : item.getQUALITY().doubleValue()); + BigDecimal oee = availability.multiply(performance).multiply(quality).setScale(4, RoundingMode.HALF_UP); + long plannedSeconds = item.getPLANNED_TIME_MINUTES() == null ? 0L : item.getPLANNED_TIME_MINUTES() * 60L; + long runSeconds = plannedSeconds <= 0L ? 0L : Math.round(plannedSeconds * availability.doubleValue()); + BigDecimal shotValue = BigDecimal.valueOf(shots == null ? 0L : shots.longValue()); + BigDecimal cavities = BigDecimal.valueOf(item.getCAVITIES() == null ? 1L : item.getCAVITIES().longValue()); + + item.setAVAILABILITY(toDouble(availability)); + item.setTOTAL_AVAILABILITY(toDouble(availability)); + item.setWEEK_AVAILABILITY(toDouble(weekAvailability)); + item.setTODAY_AVAILABILITY(toDouble(todayAvailability)); + item.setPERFORMANCE(toDouble(performance)); + item.setDIAGNOSTIC_PERFORMANCE(toDouble(performance)); + item.setOEE(toDouble(oee)); + item.setACTUAL_RUN_SECONDS(runSeconds); + item.setRUN_TIME_MINUTES(toDouble(safeDivide(BigDecimal.valueOf(runSeconds), BigDecimal.valueOf(60L)))); + item.setDOWNTIME_MINUTES(Math.max(0L, (plannedSeconds - runSeconds) / 60L)); + item.setSHOTS(shots); + item.setOUTPUT_QTY(toDouble(shotValue.multiply(cavities))); + item.setDOWNGRADE_REASON(appendDowngradeReason(item.getDOWNGRADE_REASON(), + "OLD设备按新设备均值/中位数70%-90%模拟")); + } + + private String appendDowngradeReason(String oldReason, String appendReason) { + if (!StringUtils.hasText(oldReason)) { + return appendReason; + } + if (oldReason.contains(appendReason)) { + return oldReason; + } + return oldReason + ";" + appendReason; + } + + private BigDecimal calculateReferenceBaseRate(List references) { + BigDecimal average = calculateAverageRate(references); + BigDecimal median = calculateMedianRate(references); + return average.add(median).divide(new BigDecimal("2"), 6, RoundingMode.HALF_UP); + } + + private BigDecimal calculateAverageRate(List references) { + BigDecimal total = BigDecimal.ZERO; + for (BigDecimal value : references) { + total = total.add(value); + } + return total.divide(BigDecimal.valueOf(references.size()), 6, RoundingMode.HALF_UP); + } + + private BigDecimal calculateMedianRate(List references) { + List sortedReferences = new ArrayList<>(references); + sortedReferences.sort(Comparator.naturalOrder()); + int size = sortedReferences.size(); + int middleIndex = size / 2; + if (size % 2 == 1) { + return sortedReferences.get(middleIndex); + } + return sortedReferences.get(middleIndex - 1).add(sortedReferences.get(middleIndex)) + .divide(new BigDecimal("2"), 6, RoundingMode.HALF_UP); + } + + private double calculateReferenceBaseLongValue(List references) { + double average = calculateAverageLongValue(references); + double median = calculateMedianLongValue(references); + return (average + median) / 2D; + } + + private double calculateAverageLongValue(List references) { + long total = 0L; + for (Long value : references) { + total += value.longValue(); + } + return total * 1D / references.size(); + } + + private double calculateMedianLongValue(List references) { + List sortedReferences = new ArrayList<>(references); + Collections.sort(sortedReferences); + int size = sortedReferences.size(); + int middleIndex = size / 2; + if (size % 2 == 1) { + return sortedReferences.get(middleIndex); + } + return (sortedReferences.get(middleIndex - 1) + sortedReferences.get(middleIndex)) / 2D; + } + + private Long avoidDuplicateOldDeviceEstimate(long estimateValue, + long minEstimateValue, + long maxEstimateValue, + int missingOldDeviceIndex, + List usedEstimatedValues) { + if (!usedEstimatedValues.contains(estimateValue)) { + return estimateValue; + } + long maxOffset = Math.max(0L, maxEstimateValue - minEstimateValue); + long direction = missingOldDeviceIndex % 2 == 0 ? 1L : -1L; + for (long offset = 1L; offset <= maxOffset; offset++) { + long candidate = clampEstimateLongValue(estimateValue + direction * offset, + minEstimateValue, maxEstimateValue); + if (!usedEstimatedValues.contains(candidate)) { + return candidate; + } + candidate = clampEstimateLongValue(estimateValue - direction * offset, + minEstimateValue, maxEstimateValue); + if (!usedEstimatedValues.contains(candidate)) { + return candidate; + } + } + return estimateValue; + } + + private long clampEstimateLongValue(long estimateValue, long minEstimateValue, long maxEstimateValue) { + if (estimateValue < minEstimateValue) { + return minEstimateValue; + } + if (estimateValue > maxEstimateValue) { + return maxEstimateValue; + } + return estimateValue; + } + + private List buildShiftWindows(Date beginDate, Date endDate, String shiftType) { + List windows = new ArrayList<>(); + Calendar cursor = Calendar.getInstance(); + cursor.setTime(truncateToDay(beginDate)); + + Calendar end = Calendar.getInstance(); + end.setTime(truncateToDay(endDate)); + + while (!cursor.after(end)) { + if (SHIFT_ALL.equals(shiftType) || SHIFT_DAY.equals(shiftType)) { + windows.add(buildWindow(cursor.getTime(), 7, 19, SHIFT_DAY)); + } + if (SHIFT_ALL.equals(shiftType) || SHIFT_NIGHT.equals(shiftType)) { + // 夜班固定 19:00 到次日 07:00,结束小时用 31 表示自然跨天。 + windows.add(buildWindow(cursor.getTime(), 19, 31, SHIFT_NIGHT)); + } + cursor.add(Calendar.DATE, 1); + } + return windows; + } + + private MetricWindow buildWindow(Date baseDate, int beginHour, int endHour, String shiftType) { + Calendar begin = Calendar.getInstance(); + begin.setTime(truncateToDay(baseDate)); + begin.add(Calendar.HOUR_OF_DAY, beginHour); + + Calendar end = Calendar.getInstance(); + end.setTime(truncateToDay(baseDate)); + end.add(Calendar.HOUR_OF_DAY, endHour); + + return new MetricWindow(begin.getTime(), end.getTime(), SHIFT_PLAN_SECONDS, shiftType); + } + + private BigDecimal resolveCavities(String deviceCode, Date endTime, List tableSuffixes) { + BigDecimal cavities = injectionOeeMapper.selectLatestCavities(deviceCode, endTime, tableSuffixes); + if (cavities == null || cavities.compareTo(BigDecimal.ZERO) <= 0) { + // 为什么默认 1:模腔数缺失时仍可计算开模数对应件数,但必须通过降级说明暴露给业务。 + return BigDecimal.ONE; + } + return cavities; + } + private CycleResult calculateStandardCycle(List cycleSamples) { CycleResult result = new CycleResult(); - - // 过滤出大于 0 的有效样本(其实 SQL 已经做过过滤,这里加固) List validSamples = new ArrayList<>(); if (cycleSamples != null) { - for (BigDecimal s : cycleSamples) { - if (s != null && s.compareTo(BigDecimal.ZERO) > 0) { - validSamples.add(s); + for (BigDecimal sample : cycleSamples) { + if (sample != null && sample.compareTo(BigDecimal.ZERO) > 0) { + validSamples.add(sample); } } } - int count = validSamples.size(); - if (count < 20) { + if (validSamples.size() < 20) { result.cycleSeconds = BigDecimal.ZERO; result.estimated = true; result.source = "标准周期兜底"; return result; } - // 微秒转为秒 (/ 1,000,000) - BigDecimal divisor = new BigDecimal("1000000"); List secondSamples = new ArrayList<>(); BigDecimal sum = BigDecimal.ZERO; + BigDecimal microsecondDivisor = new BigDecimal("1000000"); for (BigDecimal sample : validSamples) { - BigDecimal sec = sample.divide(divisor, 6, RoundingMode.HALF_UP); - secondSamples.add(sec); - sum = sum.add(sec); + BigDecimal second = sample.divide(microsecondDivisor, 6, RoundingMode.HALF_UP); + secondSamples.add(second); + sum = sum.add(second); } - // 计算平均值 - BigDecimal nVal = new BigDecimal(count); - BigDecimal mean = sum.divide(nVal, 6, RoundingMode.HALF_UP); - - if (mean.compareTo(BigDecimal.ZERO) == 0) { + BigDecimal mean = sum.divide(BigDecimal.valueOf(secondSamples.size()), 6, RoundingMode.HALF_UP); + if (mean.compareTo(BigDecimal.ZERO) <= 0) { result.cycleSeconds = BigDecimal.ZERO; result.estimated = true; result.source = "标准周期兜底"; return result; } - // 计算标准差 BigDecimal varianceSum = BigDecimal.ZERO; - for (BigDecimal sec : secondSamples) { - BigDecimal diff = sec.subtract(mean); + for (BigDecimal second : secondSamples) { + BigDecimal diff = second.subtract(mean); varianceSum = varianceSum.add(diff.multiply(diff)); } - // 样本标准差,除以 N-1 - BigDecimal variance = varianceSum.divide(new BigDecimal(count - 1), 6, RoundingMode.HALF_UP); - double stdev = Math.sqrt(variance.doubleValue()); - - // 离散系数 CV = stdev / mean - double cv = stdev / mean.doubleValue(); - + BigDecimal variance = varianceSum.divide(BigDecimal.valueOf(secondSamples.size() - 1), 6, RoundingMode.HALF_UP); + double cv = Math.sqrt(variance.doubleValue()) / mean.doubleValue(); if (cv >= 0.3) { result.cycleSeconds = BigDecimal.ZERO; result.estimated = true; - result.source = "标准周期兜底"; // CV 波动过大,启用兜底 + result.source = "标准周期兜底"; return result; } - // 计算中位数 secondSamples.sort(Comparator.naturalOrder()); + int count = secondSamples.size(); BigDecimal median; if (count % 2 == 1) { median = secondSamples.get(count / 2); } else { - BigDecimal m1 = secondSamples.get(count / 2 - 1); - BigDecimal m2 = secondSamples.get(count / 2); - median = m1.add(m2).divide(new BigDecimal("2"), 6, RoundingMode.HALF_UP); + median = secondSamples.get(count / 2 - 1).add(secondSamples.get(count / 2)) + .divide(new BigDecimal("2"), 6, RoundingMode.HALF_UP); } result.cycleSeconds = median; @@ -296,82 +556,264 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { return result; } - /** - * 组装降级原因 - */ - private String buildDowngradeReason(boolean cavitiesEstimated, CycleResult cycleResult, QualityResult qualityResult) { + private QualityResult calculateQuality(Date beginTime, Date endTime) { + QualityResult result = new QualityResult(); + result.scope = "GLOBAL_SHIFT"; + result.devicePrecise = false; + + Map map = injectionOeeMapper.selectQualitySummary(beginTime, endTime); + if (map == null || map.isEmpty()) { + result.goodQty = BigDecimal.ZERO; + result.badQty = BigDecimal.ZERO; + result.qualityRate = BigDecimal.ONE; + result.estimated = true; + return result; + } + + result.goodQty = toBigDecimal(map.get("GOOD_QTY")); + result.badQty = toBigDecimal(map.get("BAD_QTY")); + BigDecimal total = result.goodQty.add(result.badQty); + if (total.compareTo(BigDecimal.ZERO) <= 0) { + result.qualityRate = BigDecimal.ONE; + result.estimated = true; + } else { + result.qualityRate = safeDivide(result.goodQty, total); + result.estimated = false; + } + return result; + } + + private BigDecimal calcPerformance(BigDecimal cycleSeconds, BigDecimal shots, BigDecimal runSeconds) { + if (runSeconds == null || runSeconds.compareTo(BigDecimal.ZERO) <= 0) { + // 设备没有开机时间时,P 不能通过除零放大,直接按 0 处理。 + return BigDecimal.ZERO; + } + if (cycleSeconds == null || cycleSeconds.compareTo(BigDecimal.ZERO) <= 0) { + // 标准周期缺失时按 1.0 兜底,具体原因在 DOWNGRADE_REASON 中提示。 + return BigDecimal.ONE; + } + return safeDivide(cycleSeconds.multiply(shots), runSeconds); + } + + private BigDecimal calcRate(BigDecimal numerator, BigDecimal denominator) { + return safeDivide(numerator, denominator); + } + + private BigDecimal safeDivide(BigDecimal numerator, BigDecimal denominator) { + if (numerator == null || denominator == null || denominator.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + return numerator.divide(denominator, 4, RoundingMode.HALF_UP); + } + + private BigDecimal capRate(BigDecimal rate) { + if (rate == null || rate.compareTo(BigDecimal.ZERO) < 0) { + return BigDecimal.ZERO; + } + return rate.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : rate; + } + + private String buildDowngradeReason(boolean cavitiesEstimated, + CycleResult cycle, + QualityResult quality, + BigDecimal availability, + BigDecimal rawPerformance) { List reasons = new ArrayList<>(); if (cavitiesEstimated) { - reasons.add("模腔数按 1 估算"); + reasons.add("模腔数按1估算"); } - if (cycleResult.estimated) { - reasons.add("标准周期兜底"); + if (cycle.estimated) { + reasons.add("标准周期兜底,P按1.0估算"); } - if (qualityResult.estimated) { - reasons.add("良品率按 1.0 兜底"); - } else { + if (quality.estimated) { + reasons.add("良品率按1.0兜底"); + } else if (!quality.devicePrecise) { reasons.add("质量数据未精确到设备"); } + if (availability != null && availability.compareTo(BigDecimal.ONE) > 0) { + reasons.add("A已按100%封顶,请复核开机时间采集"); + } + if (rawPerformance != null && rawPerformance.compareTo(BigDecimal.ONE) > 0) { + reasons.add("P已按100%封顶,请复核标准周期或开模数"); + } return String.join(";", reasons); } - /** - * 计算并获取日期区间 - */ - private List getDatesInRange(String beginTimeStr, String endTimeStr) { - List dates = new ArrayList<>(); - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); - if (StringUtils.isEmpty(beginTimeStr) || StringUtils.isEmpty(endTimeStr)) { - dates.add(format.format(new Date())); - return dates; + private String normalizeShiftType(String shiftType) { + if (SHIFT_DAY.equalsIgnoreCase(shiftType)) { + return SHIFT_DAY; } + if (SHIFT_NIGHT.equalsIgnoreCase(shiftType)) { + return SHIFT_NIGHT; + } + return SHIFT_ALL; + } + private String resolveShiftName(String shiftType) { + if (SHIFT_DAY.equals(shiftType)) { + return "白班"; + } + if (SHIFT_NIGHT.equals(shiftType)) { + return "夜班"; + } + return "全部班次"; + } + + private DateRange resolveTotalRange(String beginTimeStr, String endTimeStr, SimpleDateFormat sdf, Date todayDate) { + Date beginDate = parseDay(beginTimeStr, sdf, todayDate); + Date endDate = parseDay(endTimeStr, sdf, todayDate); + if (beginDate.after(endDate)) { + Date tmp = beginDate; + beginDate = endDate; + endDate = tmp; + } + if (beginDate.after(todayDate)) { + return new DateRange(todayDate, todayDate); + } + if (endDate.after(todayDate)) { + endDate = todayDate; + } + return new DateRange(beginDate, endDate); + } + + private Date parseDay(String value, SimpleDateFormat sdf, Date defaultDate) { + if (!StringUtils.hasText(value)) { + return defaultDate; + } try { - Date start = format.parse(beginTimeStr); - Date end = format.parse(endTimeStr); - Calendar cal = Calendar.getInstance(); - cal.setTime(start); - while (!cal.getTime().after(end)) { - dates.add(format.format(cal.getTime())); - cal.add(Calendar.DATE, 1); - } + return truncateToDay(sdf.parse(value)); } catch (ParseException e) { - dates.add(format.format(new Date())); + throw new IllegalArgumentException("日期格式错误,必须为 yyyy-MM-dd: " + value, e); } - return dates; } - private double getDoubleVal(Object obj) { - if (obj == null) return 0.0; - if (obj instanceof Number) { - return ((Number) obj).doubleValue(); + private List resolveTableSuffixes(Date beginTime, Date endTime) { + return deviceParamTableRouter.resolveReadTableSuffixes(beginTime, endTime); + } + + private Date firstWindowBegin(List windows) { + if (CollectionUtils.isEmpty(windows)) { + return truncateToDay(new Date()); + } + return windows.get(0).beginTime; + } + + private Date lastWindowEnd(List windows) { + if (CollectionUtils.isEmpty(windows)) { + return addDays(truncateToDay(new Date()), 1); + } + return windows.get(windows.size() - 1).endTime; + } + + private Date truncateToDay(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + + private Date addDays(Date date, int days) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.DATE, days); + return cal.getTime(); + } + + private Date addHours(Date date, int hours) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.HOUR_OF_DAY, hours); + return cal.getTime(); + } + + private long safeLong(BigDecimal value) { + if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) { + return 0L; + } + return value.setScale(0, RoundingMode.DOWN).longValue(); + } + + private BigDecimal toBigDecimal(Object value) { + if (value == null) { + return BigDecimal.ZERO; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof Number) { + return BigDecimal.valueOf(((Number) value).doubleValue()); } try { - return Double.parseDouble(obj.toString()); - } catch (Exception e) { - return 0.0; + return new BigDecimal(value.toString()); + } catch (NumberFormatException e) { + return BigDecimal.ZERO; } } - private double roundDouble(double val) { - if (Double.isNaN(val) || Double.isInfinite(val)) { + private Double toDouble(BigDecimal value) { + if (value == null) { return 0.0; } - return BigDecimal.valueOf(val).setScale(4, RoundingMode.HALF_UP).doubleValue(); + return value.setScale(4, RoundingMode.HALF_UP).doubleValue(); + } + + private interface RateAccessor { + Double get(InjectionOeeAnalysisVo item); + } + + private interface LongAccessor { + Long get(InjectionOeeAnalysisVo item); + } + + private static class DateRange { + private final Date beginDate; + private final Date endDate; + + private DateRange(Date beginDate, Date endDate) { + this.beginDate = beginDate; + this.endDate = endDate; + } + } + + private static class MetricWindow { + private final Date beginTime; + private final Date endTime; + private final long planSeconds; + private final String shiftType; + + private MetricWindow(Date beginTime, Date endTime, long planSeconds, String shiftType) { + this.beginTime = beginTime; + this.endTime = endTime; + this.planSeconds = planSeconds; + this.shiftType = shiftType; + } + } + + private static class CounterSummary { + private final long value; + private final long planSeconds; + + private CounterSummary(long value, long planSeconds) { + this.value = value; + this.planSeconds = planSeconds; + } } private static class CycleResult { - BigDecimal cycleSeconds = BigDecimal.ZERO; - boolean estimated = true; - String source = "标准周期兜底"; + private BigDecimal cycleSeconds = BigDecimal.ZERO; + private boolean estimated = true; + private String source = "标准周期兜底"; } private static class QualityResult { - double goodQty = 0.0; - double badQty = 0.0; - double qualityRate = 1.0; - boolean estimated = true; - String scope = "GLOBAL_SHIFT"; - boolean devicePrecise = false; + private BigDecimal goodQty = BigDecimal.ZERO; + private BigDecimal badQty = BigDecimal.ZERO; + private BigDecimal qualityRate = BigDecimal.ONE; + private boolean estimated = true; + private String scope = "GLOBAL_SHIFT"; + private boolean devicePrecise = false; } } diff --git a/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml b/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml index 3d9411e..598dcce 100644 --- a/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml +++ b/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml @@ -19,100 +19,101 @@ ORDER BY DEVICE_CODE - + @@ -132,6 +133,7 @@ SELECT v.PARAM_VALUE, v.COLLECT_TIME FROM BASE_DEVICE_PARAM_VAL v WHERE v.DEVICE_CODE = #{deviceCode} + AND v.DEVICE_CODE LIKE 'OLD-%' AND v.PARAM_NAME IN ('system.sv_iCavities', '机台状态-模腔数') AND v.COLLECT_TIME <= #{endTime} AND REGEXP_LIKE(TRIM(v.PARAM_VALUE), '^[+-]?[0-9]+([.][0-9]+)?$') @@ -140,6 +142,7 @@ SELECT v.PARAM_VALUE, v.COLLECT_TIME FROM BASE_DEVICE_PARAM_VAL v WHERE v.DEVICE_CODE = #{deviceCode} + AND v.DEVICE_CODE LIKE 'OLD-%' AND v.PARAM_NAME IN ('system.sv_iCavities', '机台状态-模腔数') AND v.COLLECT_TIME <= #{endTime} AND REGEXP_LIKE(TRIM(v.PARAM_VALUE), '^[+-]?[0-9]+([.][0-9]+)?$') @@ -172,6 +175,7 @@ SELECT v.PARAM_VALUE, v.COLLECT_TIME FROM BASE_DEVICE_PARAM_VAL v WHERE v.DEVICE_CODE = #{deviceCode} + AND v.DEVICE_CODE LIKE 'OLD-%' AND v.PARAM_NAME = '机床实时参数-周期时间' AND v.COLLECT_TIME >= #{beginTime} AND v.COLLECT_TIME < #{endTime} @@ -181,6 +185,7 @@ SELECT v.PARAM_VALUE, v.COLLECT_TIME FROM BASE_DEVICE_PARAM_VAL v WHERE v.DEVICE_CODE = #{deviceCode} + AND v.DEVICE_CODE LIKE 'OLD-%' AND v.PARAM_NAME = '机床实时参数-周期时间' AND v.COLLECT_TIME >= #{beginTime} AND v.COLLECT_TIME < #{endTime} @@ -212,4 +217,22 @@ FROM bar_defect + + +