diff --git a/aucma-report/src/main/java/com/aucma/report/controller/InjectionOeeController.java b/aucma-report/src/main/java/com/aucma/report/controller/InjectionOeeController.java index d6bf9d8..2876b44 100644 --- a/aucma-report/src/main/java/com/aucma/report/controller/InjectionOeeController.java +++ b/aucma-report/src/main/java/com/aucma/report/controller/InjectionOeeController.java @@ -32,10 +32,11 @@ public class InjectionOeeController extends BaseController { @RequestParam(required = false) String deviceCode, @RequestParam(required = false) String beginTime, @RequestParam(required = false) String endTime, - @RequestParam(required = false) String shiftType) { + @RequestParam(required = false) String shiftType, + @RequestParam(required = false) String availabilityType) { List list = injectionOeeService.getInjectionOeeAnalysis( - deviceCode, beginTime, endTime, shiftType); + deviceCode, beginTime, endTime, shiftType, availabilityType); return success(list); } 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 f062426..ec67bf6 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 @@ -121,15 +121,15 @@ public class InjectionOeeAnalysisVo extends BaseEntity implements Serializable { @JsonProperty("SHIFT_NAME") private String SHIFT_NAME; - /** 本日利用率 */ + /** 日利用率:按页面传入统计日期计算,不固定取服务器当天 */ @JsonProperty("TODAY_AVAILABILITY") private Double TODAY_AVAILABILITY; - /** 周利用率 */ + /** 周利用率:按页面结束日所在自然周计算 */ @JsonProperty("WEEK_AVAILABILITY") private Double WEEK_AVAILABILITY; - /** 总利用率 */ + /** 总利用率:按页面选择日期范围计算 */ @JsonProperty("TOTAL_AVAILABILITY") private Double TOTAL_AVAILABILITY; 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 52df69e..773ba7e 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 @@ -1,6 +1,7 @@ package com.aucma.report.mapper; import com.aucma.report.domain.vo.InjectionOeeAnalysisVo; +import com.aucma.report.domain.vo.ParamRawPoint; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -57,6 +58,20 @@ public interface InjectionOeeMapper { @Param("endTime") Date endTime, @Param("tableSuffixes") List tableSuffixes); + /** + * 批量获取指定时间点前每台设备最新模腔数。 + * + * @param deviceCodes 设备编码列表 + * @param endTime 截止时间 + * @param tableSuffixes 月表后缀列表 + * @param oldDevice 是否 OLD 手工设备 + * @return 包含 DEVICE_CODE、CAVITIES 的 Map 列表 + */ + List> selectLatestCavitiesBatch(@Param("deviceCodes") List deviceCodes, + @Param("endTime") Date endTime, + @Param("tableSuffixes") List tableSuffixes, + @Param("oldDevice") boolean oldDevice); + /** * 获取指定窗口内的周期时间采集样本列表,只取大于 0 的数值型样本 * @@ -71,6 +86,38 @@ public interface InjectionOeeMapper { @Param("endTime") Date endTime, @Param("tableSuffixes") List tableSuffixes); + /** + * 获取指定窗口内周期时间的统计信息,避免把大量采样点拉回 Java 再聚合。 + * + * @param deviceCode 设备编码 + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @param tableSuffixes 月表后缀列表 + * @param oldDevice 是否 OLD 手工设备 + * @return 包含 SAMPLE_COUNT、AVG_SECONDS、STDDEV_SECONDS、MEDIAN_SECONDS 的统计 Map + */ + Map selectCycleTimeStats(@Param("deviceCode") String deviceCode, + @Param("beginTime") Date beginTime, + @Param("endTime") Date endTime, + @Param("tableSuffixes") List tableSuffixes, + @Param("oldDevice") boolean oldDevice); + + /** + * 批量获取设备周期时间统计信息,避免全设备报表按设备反复扫描月分表。 + * + * @param deviceCodes 设备编码列表 + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @param tableSuffixes 月表后缀列表 + * @param oldDevice 是否 OLD 手工设备 + * @return 每台设备的周期时间统计 Map 列表 + */ + List> selectCycleTimeStatsBatch(@Param("deviceCodes") List deviceCodes, + @Param("beginTime") Date beginTime, + @Param("endTime") Date endTime, + @Param("tableSuffixes") List tableSuffixes, + @Param("oldDevice") boolean oldDevice); + /** * 按时间窗口统计去重后的质检记录 * @@ -94,4 +141,42 @@ public interface InjectionOeeMapper { @Param("paramName") String paramName, @Param("beginDate") Date beginDate, @Param("endDate") Date endDate); + + /** + * 批量查询设备在一个有效时间段内的原始去重参数值列表(以 COLLECT_TIME 升序排列)。 + * 用于在 Java 内存中直接做高频 OEE 的差值累计逻辑,消除 N+1 扫描问题。 + * + * @param deviceCode 设备编码 + * @param paramName 参数名称 + * @param minQueryBegin 查询时间段起点(通常为窗口基线起点) + * @param maxQueryEnd 查询时间段终点 + * @param tableSuffixes 月表后缀列表 + * @param oldDevice 是否 OLD 手工设备 + * @return 去重后的参数点列表 + */ + List selectRawParamValues(@Param("deviceCode") String deviceCode, + @Param("paramName") String paramName, + @Param("minQueryBegin") Date minQueryBegin, + @Param("maxQueryEnd") Date maxQueryEnd, + @Param("tableSuffixes") List tableSuffixes, + @Param("oldDevice") boolean oldDevice); + + /** + * 批量查询设备集合在一个有效时间段内的原始去重参数值列表。 + * 用于全设备 OEE 报表一次加载后在内存按设备计算,减少数据库往返次数。 + * + * @param deviceCodes 设备编码列表 + * @param paramName 参数名称 + * @param minQueryBegin 查询时间段起点(通常为窗口基线起点) + * @param maxQueryEnd 查询时间段终点 + * @param tableSuffixes 月表后缀列表 + * @param oldDevice 是否 OLD 手工设备 + * @return 去重后的参数点列表 + */ + List selectRawParamValuesBatch(@Param("deviceCodes") List deviceCodes, + @Param("paramName") String paramName, + @Param("minQueryBegin") Date minQueryBegin, + @Param("maxQueryEnd") Date maxQueryEnd, + @Param("tableSuffixes") List tableSuffixes, + @Param("oldDevice") boolean oldDevice); } 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 5300c81..069fb2b 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 @@ -18,7 +18,12 @@ public interface IInjectionOeeService { * @param beginTimeStr 开始日期 (格式: YYYY-MM-DD) * @param endTimeStr 结束日期 (格式: YYYY-MM-DD) * @param shiftType 班次类型 (ALL-全部班次, DAY-白班, NIGHT-夜班) + * @param availabilityType 利用率类型 (DAY-页面结束日, WEEK-结束日所在自然周, TOTAL-页面日期范围) * @return OEE 分析结果列表 */ - List getInjectionOeeAnalysis(String deviceCode, String beginTimeStr, String endTimeStr, String shiftType); + List getInjectionOeeAnalysis(String deviceCode, + String beginTimeStr, + String endTimeStr, + String shiftType, + String availabilityType); } 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 3e48006..bf3b281 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 @@ -2,6 +2,7 @@ package com.aucma.report.service.impl; import com.aucma.base.support.DeviceParamTableRouter; import com.aucma.report.domain.vo.InjectionOeeAnalysisVo; +import com.aucma.report.domain.vo.ParamRawPoint; import com.aucma.report.mapper.InjectionOeeMapper; import com.aucma.report.service.IInjectionOeeService; import org.slf4j.Logger; @@ -20,6 +21,7 @@ import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,7 +29,7 @@ import java.util.Map; * 注塑车间 OEE 业务逻辑层实现。 * *

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

+ * 页面在展示完整 OEE 的同时,按日利用率、自然周利用率、总利用率单选查询,方便按设备定位时间利用不足。

* * @author Antigravity */ @@ -49,6 +51,10 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { private static final String SHIFT_DAY = "DAY"; private static final String SHIFT_NIGHT = "NIGHT"; + private static final String AVAILABILITY_DAY = "DAY"; + private static final String AVAILABILITY_WEEK = "WEEK"; + private static final String AVAILABILITY_TOTAL = "TOTAL"; + 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; @@ -64,8 +70,8 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { /** OLD 手工设备采集稀疏,基线窗口放宽到 24 小时。 */ private static final int OLD_BASELINE_HOURS = -24; - /** 周利用率固定看最近 7 个生产日。 */ - private static final int WEEK_DAYS = 6; + /** 周期时间有效样本下限,样本过少时用 1.0 兜底,避免少量异常点放大 P。 */ + private static final int MIN_CYCLE_SAMPLE_COUNT = 10; @Autowired private InjectionOeeMapper injectionOeeMapper; @@ -74,7 +80,11 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { private DeviceParamTableRouter deviceParamTableRouter; @Override - public List getInjectionOeeAnalysis(String deviceCode, String beginTimeStr, String endTimeStr, String shiftType) { + public List getInjectionOeeAnalysis(String deviceCode, + String beginTimeStr, + String endTimeStr, + String shiftType, + String availabilityType) { List targetDevices = injectionOeeMapper.selectTargetDevices(deviceCode); if (CollectionUtils.isEmpty(targetDevices)) { return Collections.emptyList(); @@ -84,47 +94,49 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { sdf.setLenient(false); String normalizedShiftType = normalizeShiftType(shiftType); + String normalizedAvailabilityType = normalizeAvailabilityType(availabilityType); Date todayDate = truncateToDay(new Date()); - Date weekBeginDate = addDays(todayDate, -WEEK_DAYS); - DateRange totalRange = resolveTotalRange(beginTimeStr, endTimeStr, sdf, todayDate); + Date defaultReportDate = addDays(todayDate, -1); + DateRange selectedRange = resolveSelectedRange(beginTimeStr, endTimeStr, sdf, defaultReportDate, todayDate); + DateRange availabilityRange = resolveAvailabilityRange(selectedRange, normalizedAvailabilityType, todayDate); - List totalWindows = buildShiftWindows(totalRange.beginDate, totalRange.endDate, normalizedShiftType); - List weekWindows = buildShiftWindows(weekBeginDate, todayDate, normalizedShiftType); - List todayWindows = buildShiftWindows(todayDate, todayDate, normalizedShiftType); + List metricWindows = buildShiftWindows(availabilityRange.beginDate, + availabilityRange.endDate, normalizedShiftType); + Date queryBeginTime = firstWindowBegin(metricWindows); + Date queryEndTime = lastWindowEnd(metricWindows); + QualityResult quality = calculateQuality(queryBeginTime, queryEndTime); + MetricCache metricCache = loadMetricCache(targetDevices, metricWindows); List results = new ArrayList<>(); for (InjectionOeeAnalysisVo device : targetDevices) { - InjectionOeeAnalysisVo vo = buildOeeRow(device, normalizedShiftType, totalWindows, weekWindows, todayWindows); + InjectionOeeAnalysisVo vo = buildOeeRow(device, normalizedShiftType, normalizedAvailabilityType, + metricWindows, quality, metricCache); results.add(vo); } - fillMissingOldDeviceOeeByReference(results); + fillMissingOldDeviceOeeByReference(results, normalizedAvailabilityType); - results.sort(Comparator.comparing(InjectionOeeAnalysisVo::getDEVICE_CODE)); + results.sort(this::compareByDeviceDisplayOrder); return results; } private InjectionOeeAnalysisVo buildOeeRow(InjectionOeeAnalysisVo device, String shiftType, - List totalWindows, - List weekWindows, - List todayWindows) { + String availabilityType, + List metricWindows, + QualityResult quality, + MetricCache metricCache) { String deviceCode = device.getDEVICE_CODE(); - 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); + List runRawPoints = metricCache.getRunRawPoints(deviceCode); + List shotsRawPoints = metricCache.getShotRawPoints(deviceCode); - Date queryBeginTime = firstWindowBegin(totalWindows); - Date queryEndTime = lastWindowEnd(totalWindows); - List tableSuffixes = resolveTableSuffixes(queryBeginTime, queryEndTime); + CounterSummary totalRun = calculateCounterSummary(deviceCode, metricWindows, runRawPoints); + CounterSummary totalShots = calculateCounterSummary(deviceCode, metricWindows, shotsRawPoints); - BigDecimal cavities = resolveCavities(deviceCode, queryEndTime, tableSuffixes); + BigDecimal cavities = resolveCavities(deviceCode, metricCache); boolean cavitiesEstimated = cavities.compareTo(BigDecimal.ONE) == 0; - CycleResult cycle = calculateStandardCycle(injectionOeeMapper.selectCycleTimeSamples( - deviceCode, queryBeginTime, queryEndTime, tableSuffixes)); - QualityResult quality = calculateQuality(queryBeginTime, queryEndTime); + CycleResult cycle = calculateStandardCycle(metricCache.getCycleStats(deviceCode)); BigDecimal plannedSeconds = BigDecimal.valueOf(totalRun.planSeconds); BigDecimal runSeconds = BigDecimal.valueOf(totalRun.value); @@ -149,9 +161,7 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { 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))))); + applyAvailabilityTypeValue(vo, availabilityType, capRate(availability)); vo.setPLANNED_TIME_MINUTES(totalRun.planSeconds / 60); vo.setDOWNTIME_MINUTES(Math.max(0L, (totalRun.planSeconds - totalRun.value) / 60)); @@ -175,41 +185,308 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { return vo; } - private CounterSummary selectCounterSummary(String deviceCode, String paramName, List windows) { + /** + * 一次性加载本次 OEE 页面需要的设备参数缓存,避免按设备循环触发多次相同结构的 SQL。 + */ + private MetricCache loadMetricCache(List devices, + List metricWindows) { + MetricCache metricCache = new MetricCache(); + + loadRawPointCache(metricCache.runRawPointsByDevice, devices, PARAM_RUN_SECONDS, metricWindows); + loadRawPointCache(metricCache.shotRawPointsByDevice, devices, PARAM_SHOTS, metricWindows); + + Date queryBeginTime = firstWindowBegin(metricWindows); + Date queryEndTime = lastWindowEnd(metricWindows); + List tableSuffixes = resolveTableSuffixes(queryBeginTime, queryEndTime); + loadCavityCache(metricCache.cavitiesByDevice, devices, queryEndTime, tableSuffixes); + loadCycleStatsCache(metricCache.cycleStatsByDevice, devices, queryBeginTime, queryEndTime, tableSuffixes); + return metricCache; + } + + private void loadRawPointCache(Map> targetMap, + List devices, + String paramName, + List windows) { + loadRawPointCache(targetMap, extractDeviceCodes(devices, false), paramName, windows, + AUTO_BASELINE_HOURS, false); + loadRawPointCache(targetMap, extractDeviceCodes(devices, true), paramName, windows, + OLD_BASELINE_HOURS, true); + for (List rawPoints : targetMap.values()) { + rawPoints.sort(Comparator.comparing(ParamRawPoint::getCollectTime)); + } + } + + private void loadRawPointCache(Map> targetMap, + List deviceCodes, + String paramName, + List windows, + int baselineHours, + boolean oldDevice) { + if (CollectionUtils.isEmpty(deviceCodes) || CollectionUtils.isEmpty(windows)) { + return; + } + + List queryRanges = buildMergedQueryRanges(windows, baselineHours); + for (QueryRange queryRange : queryRanges) { + List tableSuffixes = resolveTableSuffixes(queryRange.beginTime, queryRange.endTime); + if (!oldDevice && CollectionUtils.isEmpty(tableSuffixes)) { + continue; + } + List rawPoints = injectionOeeMapper.selectRawParamValuesBatch( + deviceCodes, + paramName, + queryRange.beginTime, + queryRange.endTime, + tableSuffixes, + oldDevice + ); + mergeRawPointCache(targetMap, rawPoints); + } + } + + private void mergeRawPointCache(Map> targetMap, List rawPoints) { + if (CollectionUtils.isEmpty(rawPoints)) { + return; + } + for (ParamRawPoint rawPoint : rawPoints) { + if (rawPoint == null || !StringUtils.hasText(rawPoint.getDeviceCode())) { + continue; + } + List deviceRawPoints = targetMap.get(rawPoint.getDeviceCode()); + if (deviceRawPoints == null) { + deviceRawPoints = new ArrayList<>(); + targetMap.put(rawPoint.getDeviceCode(), deviceRawPoints); + } + deviceRawPoints.add(rawPoint); + } + } + + private void loadCavityCache(Map targetMap, + List devices, + Date endTime, + List tableSuffixes) { + loadCavityCache(targetMap, extractDeviceCodes(devices, false), endTime, tableSuffixes, false); + loadCavityCache(targetMap, extractDeviceCodes(devices, true), endTime, tableSuffixes, true); + } + + private void loadCavityCache(Map targetMap, + List deviceCodes, + Date endTime, + List tableSuffixes, + boolean oldDevice) { + if (CollectionUtils.isEmpty(deviceCodes)) { + return; + } + if (!oldDevice && CollectionUtils.isEmpty(tableSuffixes)) { + return; + } + List> rows = injectionOeeMapper.selectLatestCavitiesBatch( + deviceCodes, endTime, tableSuffixes, oldDevice); + if (CollectionUtils.isEmpty(rows)) { + return; + } + for (Map row : rows) { + String deviceCode = toMapString(row, "DEVICE_CODE"); + if (!StringUtils.hasText(deviceCode)) { + continue; + } + BigDecimal cavities = toBigDecimal(getMapValue(row, "CAVITIES")); + if (cavities.compareTo(BigDecimal.ZERO) > 0) { + targetMap.put(deviceCode, cavities); + } + } + } + + private void loadCycleStatsCache(Map> targetMap, + List devices, + Date beginTime, + Date endTime, + List tableSuffixes) { + loadCycleStatsCache(targetMap, extractDeviceCodes(devices, false), beginTime, endTime, + tableSuffixes, false); + loadCycleStatsCache(targetMap, extractDeviceCodes(devices, true), beginTime, endTime, + tableSuffixes, true); + } + + private void loadCycleStatsCache(Map> targetMap, + List deviceCodes, + Date beginTime, + Date endTime, + List tableSuffixes, + boolean oldDevice) { + if (CollectionUtils.isEmpty(deviceCodes)) { + return; + } + if (!oldDevice && CollectionUtils.isEmpty(tableSuffixes)) { + return; + } + List> rows = injectionOeeMapper.selectCycleTimeStatsBatch( + deviceCodes, beginTime, endTime, tableSuffixes, oldDevice); + if (CollectionUtils.isEmpty(rows)) { + return; + } + for (Map row : rows) { + String deviceCode = toMapString(row, "DEVICE_CODE"); + if (StringUtils.hasText(deviceCode)) { + targetMap.put(deviceCode, row); + } + } + } + + private List extractDeviceCodes(List devices, boolean oldDevice) { + List deviceCodes = new ArrayList<>(); + for (InjectionOeeAnalysisVo device : devices) { + if (device == null || !StringUtils.hasText(device.getDEVICE_CODE())) { + continue; + } + if (isOldDeviceCode(device.getDEVICE_CODE()) == oldDevice) { + deviceCodes.add(device.getDEVICE_CODE()); + } + } + return deviceCodes; + } + + private Object getMapValue(Map map, String key) { + if (map == null || key == null) { + return null; + } + if (map.containsKey(key)) { + return map.get(key); + } + return map.get(key.toLowerCase()); + } + + private String toMapString(Map map, String key) { + Object value = getMapValue(map, key); + return value == null ? null : value.toString(); + } + + /** + * 在内存中过滤出指定窗口的前序 baseline 点和窗口数据,根据单调累加器 reset 规则计算增量。 + * 此算法在逻辑上与原有 SQL 规约(selectCounterDeltaByWindow)完全等价。 + */ + private long calculateCounterDeltaInMemory(String deviceCode, MetricWindow window, List rawPoints) { + if (CollectionUtils.isEmpty(rawPoints)) { + return 0L; + } + boolean isOld = isOldDeviceCode(deviceCode); + int baselineHours = isOld ? OLD_BASELINE_HOURS : AUTO_BASELINE_HOURS; + Date baselineBeginTime = addHours(window.beginTime, baselineHours); + + BigDecimal deltaSum = BigDecimal.ZERO; + BigDecimal prevValue = null; + + for (ParamRawPoint point : rawPoints) { + Date collectTime = point.getCollectTime(); + if (collectTime.before(baselineBeginTime)) { + continue; + } + if (!collectTime.before(window.endTime)) { + // rawPoints 已按采集时间升序排列,越过窗口后无需继续扫描。 + break; + } + + BigDecimal currValue = point.getParamValue(); + + if (collectTime.before(window.beginTime)) { + // 仅作为对比的基线,不累加增量本身 + prevValue = currValue; + } else { + if (prevValue == null) { + // 没有 baseline,且这是窗口内第一个点,设为参考点,不计算增量(对应 SQL 中的 NULL) + prevValue = currValue; + } else { + if (currValue.compareTo(prevValue) > 0) { + deltaSum = deltaSum.add(currValue.subtract(prevValue)); + } else if (currValue.compareTo(prevValue) < 0) { + // 计数器在两次采集中间发生了重置,直接加当前值 + deltaSum = deltaSum.add(currValue); + } + prevValue = currValue; + } + } + } + + return safeLong(deltaSum); + } + + /** + * 计算指定窗口集合内参数的累计值和计划时间,并在内存中进行增量计算。 + */ + private CounterSummary calculateCounterSummary(String deviceCode, List windows, List rawPoints) { long value = 0L; long planSeconds = 0L; for (MetricWindow window : windows) { - value += selectCounterDelta(deviceCode, paramName, window); + value += calculateCounterDeltaInMemory(deviceCode, window, rawPoints); planSeconds += window.planSeconds; } return new CounterSummary(value, planSeconds); } /** - * 回源月分表按窗口 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); + private List selectRawPoints(String deviceCode, String paramName, List windows) { + if (CollectionUtils.isEmpty(windows)) { + return Collections.emptyList(); + } + boolean oldDevice = isOldDeviceCode(deviceCode); + int baselineHours = oldDevice ? OLD_BASELINE_HOURS : AUTO_BASELINE_HOURS; + + List queryRanges = buildMergedQueryRanges(windows, baselineHours); + if (CollectionUtils.isEmpty(queryRanges)) { + return Collections.emptyList(); + } + + List rawPoints = new ArrayList<>(); + for (QueryRange queryRange : queryRanges) { + List tableSuffixes = resolveTableSuffixes(queryRange.beginTime, queryRange.endTime); + rawPoints.addAll(injectionOeeMapper.selectRawParamValues( + deviceCode, + paramName, + queryRange.beginTime, + queryRange.endTime, + tableSuffixes, + oldDevice + )); + } + rawPoints.sort(Comparator.comparing(ParamRawPoint::getCollectTime)); + return rawPoints; + } + + /** + * 将多个统计窗口扩展出基线区间后做合并,避免历史日期和本周/今日区间不连续时扫穿中间月份。 + */ + private List buildMergedQueryRanges(List windows, int baselineHours) { + List ranges = new ArrayList<>(); + for (MetricWindow window : windows) { + ranges.add(new QueryRange(addHours(window.beginTime, baselineHours), window.endTime)); + } + if (ranges.isEmpty()) { + return Collections.emptyList(); + } + + ranges.sort(Comparator.comparing(QueryRange::getBeginTime)); + List mergedRanges = new ArrayList<>(); + for (QueryRange range : ranges) { + if (mergedRanges.isEmpty()) { + mergedRanges.add(range); + continue; + } + QueryRange lastRange = mergedRanges.get(mergedRanges.size() - 1); + if (range.beginTime.after(lastRange.endTime)) { + mergedRanges.add(range); + } else if (range.endTime.after(lastRange.endTime)) { + lastRange.endTime = range.endTime; + } + } + return mergedRanges; } /** * 使用新设备有效 OEE 结果的均值和中位数,为 OLD 设备缺失或无有效指标时临时补估算值。 */ - private void fillMissingOldDeviceOeeByReference(List list) { + private void fillMissingOldDeviceOeeByReference(List list, String availabilityType) { if (CollectionUtils.isEmpty(list)) { return; } @@ -219,8 +496,6 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { 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<>(); @@ -233,12 +508,10 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { 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); + applyOldDeviceEstimate(item, availabilityType, availability, performance, shots); usedEstimatedShots.add(shots); missingOldDeviceIndex++; } @@ -257,8 +530,69 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { private boolean isOldDevice(InjectionOeeAnalysisVo item) { return item != null - && item.getDEVICE_CODE() != null - && item.getDEVICE_CODE().trim().startsWith(OLD_DEVICE_CODE_PREFIX); + && isOldDeviceCode(item.getDEVICE_CODE()); + } + + private boolean isOldDeviceCode(String deviceCode) { + return deviceCode != null + && deviceCode.trim().startsWith(OLD_DEVICE_CODE_PREFIX); + } + + private void applyAvailabilityTypeValue(InjectionOeeAnalysisVo vo, String availabilityType, BigDecimal availability) { + if (AVAILABILITY_WEEK.equals(availabilityType)) { + vo.setWEEK_AVAILABILITY(toDouble(availability)); + return; + } + if (AVAILABILITY_TOTAL.equals(availabilityType)) { + vo.setTOTAL_AVAILABILITY(toDouble(availability)); + return; + } + vo.setTODAY_AVAILABILITY(toDouble(availability)); + } + + private int compareByDeviceDisplayOrder(InjectionOeeAnalysisVo left, InjectionOeeAnalysisVo right) { + String leftCode = left == null ? null : left.getDEVICE_CODE(); + String rightCode = right == null ? null : right.getDEVICE_CODE(); + int priorityCompare = Integer.compare(resolveDeviceDisplayPriority(leftCode), resolveDeviceDisplayPriority(rightCode)); + if (priorityCompare != 0) { + return priorityCompare; + } + int numberCompare = Integer.compare(resolveDeviceDisplayNumber(leftCode), resolveDeviceDisplayNumber(rightCode)); + if (numberCompare != 0) { + return numberCompare; + } + return String.valueOf(leftCode).compareTo(String.valueOf(rightCode)); + } + + private int resolveDeviceDisplayPriority(String deviceCode) { + if (deviceCode == null) { + return 1; + } + String normalizedCode = deviceCode.trim(); + if (normalizedCode.startsWith("YZM-")) { + return 0; + } + if (normalizedCode.startsWith(OLD_DEVICE_CODE_PREFIX)) { + return 2; + } + return 1; + } + + private int resolveDeviceDisplayNumber(String deviceCode) { + if (deviceCode == null) { + return Integer.MAX_VALUE; + } + String normalizedCode = deviceCode.trim(); + int dashIndex = normalizedCode.lastIndexOf('-'); + if (dashIndex < 0 || dashIndex == normalizedCode.length() - 1) { + return Integer.MAX_VALUE; + } + try { + // 为什么按编号排序:现场设备编码 YZM-02 / OLD-02 比字典序更符合车间页面阅读习惯。 + return Integer.parseInt(normalizedCode.substring(dashIndex + 1)); + } catch (NumberFormatException e) { + return Integer.MAX_VALUE; + } } private List buildReferenceRates(List list, RateAccessor accessor) { @@ -325,9 +659,8 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { } private void applyOldDeviceEstimate(InjectionOeeAnalysisVo item, + String availabilityType, BigDecimal availability, - BigDecimal weekAvailability, - BigDecimal todayAvailability, BigDecimal performance, Long shots) { BigDecimal quality = BigDecimal.valueOf(item.getQUALITY() == null ? 1D : item.getQUALITY().doubleValue()); @@ -338,9 +671,7 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { 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)); + applyAvailabilityTypeValue(item, availabilityType, availability); item.setPERFORMANCE(toDouble(performance)); item.setDIAGNOSTIC_PERFORMANCE(toDouble(performance)); item.setOEE(toDouble(oee)); @@ -482,8 +813,8 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { 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); + private BigDecimal resolveCavities(String deviceCode, MetricCache metricCache) { + BigDecimal cavities = metricCache.cavitiesByDevice.get(deviceCode); if (cavities == null || cavities.compareTo(BigDecimal.ZERO) <= 0) { // 为什么默认 1:模腔数缺失时仍可计算开模数对应件数,但必须通过降级说明暴露给业务。 return BigDecimal.ONE; @@ -491,34 +822,24 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { return cavities; } - private CycleResult calculateStandardCycle(List cycleSamples) { + private CycleResult calculateStandardCycle(Map cycleStats) { CycleResult result = new CycleResult(); - List validSamples = new ArrayList<>(); - if (cycleSamples != null) { - for (BigDecimal sample : cycleSamples) { - if (sample != null && sample.compareTo(BigDecimal.ZERO) > 0) { - validSamples.add(sample); - } - } - } - - if (validSamples.size() < 20) { + if (cycleStats == null || cycleStats.isEmpty()) { result.cycleSeconds = BigDecimal.ZERO; result.estimated = true; result.source = "标准周期兜底"; return result; } - List secondSamples = new ArrayList<>(); - BigDecimal sum = BigDecimal.ZERO; - BigDecimal microsecondDivisor = new BigDecimal("1000000"); - for (BigDecimal sample : validSamples) { - BigDecimal second = sample.divide(microsecondDivisor, 6, RoundingMode.HALF_UP); - secondSamples.add(second); - sum = sum.add(second); + int sampleCount = toBigDecimal(cycleStats.get("SAMPLE_COUNT")).intValue(); + if (sampleCount < MIN_CYCLE_SAMPLE_COUNT) { + result.cycleSeconds = BigDecimal.ZERO; + result.estimated = true; + result.source = "标准周期兜底"; + return result; } - BigDecimal mean = sum.divide(BigDecimal.valueOf(secondSamples.size()), 6, RoundingMode.HALF_UP); + BigDecimal mean = toBigDecimal(cycleStats.get("AVG_SECONDS")); if (mean.compareTo(BigDecimal.ZERO) <= 0) { result.cycleSeconds = BigDecimal.ZERO; result.estimated = true; @@ -526,32 +847,17 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { return result; } - BigDecimal varianceSum = BigDecimal.ZERO; - for (BigDecimal second : secondSamples) { - BigDecimal diff = second.subtract(mean); - varianceSum = varianceSum.add(diff.multiply(diff)); - } - 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) { + BigDecimal median = toBigDecimal(cycleStats.get("MEDIAN_SECONDS")); + if (median.compareTo(BigDecimal.ZERO) <= 0) { result.cycleSeconds = BigDecimal.ZERO; result.estimated = true; result.source = "标准周期兜底"; return result; } - secondSamples.sort(Comparator.naturalOrder()); - int count = secondSamples.size(); - BigDecimal median; - if (count % 2 == 1) { - median = secondSamples.get(count / 2); - } else { - median = secondSamples.get(count / 2 - 1).add(secondSamples.get(count / 2)) - .divide(new BigDecimal("2"), 6, RoundingMode.HALF_UP); - } - - result.cycleSeconds = median; + result.cycleSeconds = median.setScale(6, RoundingMode.HALF_UP); result.estimated = false; + // 为什么不用离散系数直接否决:注塑换模、空循环会拉高波动,中位数已经承担抗极端值职责。 result.source = "设备采集周期中位数"; return result; } @@ -649,6 +955,16 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { return SHIFT_ALL; } + private String normalizeAvailabilityType(String availabilityType) { + if (AVAILABILITY_WEEK.equalsIgnoreCase(availabilityType)) { + return AVAILABILITY_WEEK; + } + if (AVAILABILITY_TOTAL.equalsIgnoreCase(availabilityType)) { + return AVAILABILITY_TOTAL; + } + return AVAILABILITY_DAY; + } + private String resolveShiftName(String shiftType) { if (SHIFT_DAY.equals(shiftType)) { return "白班"; @@ -659,23 +975,60 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { return "全部班次"; } - private DateRange resolveTotalRange(String beginTimeStr, String endTimeStr, SimpleDateFormat sdf, Date todayDate) { - Date beginDate = parseDay(beginTimeStr, sdf, todayDate); - Date endDate = parseDay(endTimeStr, sdf, todayDate); + private DateRange resolveSelectedRange(String beginTimeStr, + String endTimeStr, + SimpleDateFormat sdf, + Date defaultDate, + Date maxDate) { + Date beginDate = parseDay(beginTimeStr, sdf, defaultDate); + Date endDate = parseDay(endTimeStr, sdf, defaultDate); if (beginDate.after(endDate)) { Date tmp = beginDate; beginDate = endDate; endDate = tmp; } - if (beginDate.after(todayDate)) { - return new DateRange(todayDate, todayDate); + if (beginDate.after(maxDate)) { + return new DateRange(maxDate, maxDate); } - if (endDate.after(todayDate)) { - endDate = todayDate; + if (endDate.after(maxDate)) { + endDate = maxDate; } return new DateRange(beginDate, endDate); } + private DateRange resolveAvailabilityRange(DateRange selectedRange, String availabilityType, Date maxDate) { + if (AVAILABILITY_WEEK.equals(availabilityType)) { + Date weekBeginDate = beginOfNaturalWeek(selectedRange.endDate); + Date weekEndDate = endOfNaturalWeek(selectedRange.endDate); + if (weekEndDate.after(maxDate)) { + // 为什么截到当前日期:自然周未结束时,不能把未来班次计入计划时间分母。 + weekEndDate = maxDate; + } + return new DateRange(weekBeginDate, weekEndDate); + } + if (AVAILABILITY_TOTAL.equals(availabilityType)) { + return selectedRange; + } + // 为什么用页面结束日:日利用率是用户选择日期的生产日,不再固定取服务器“今天”。 + return new DateRange(selectedRange.endDate, selectedRange.endDate); + } + + private Date beginOfNaturalWeek(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(truncateToDay(date)); + int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); + int mondayOffset = (dayOfWeek + 5) % 7; + cal.add(Calendar.DATE, -mondayOffset); + return cal.getTime(); + } + + private Date endOfNaturalWeek(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(beginOfNaturalWeek(date)); + cal.add(Calendar.DATE, 6); + return cal.getTime(); + } + private Date parseDay(String value, SimpleDateFormat sdf, Date defaultDate) { if (!StringUtils.hasText(value)) { return defaultDate; @@ -802,6 +1155,45 @@ public class InjectionOeeServiceImpl implements IInjectionOeeService { } } + private static class QueryRange { + private final Date beginTime; + private Date endTime; + + private QueryRange(Date beginTime, Date endTime) { + this.beginTime = beginTime; + this.endTime = endTime; + } + + private Date getBeginTime() { + return beginTime; + } + } + + private static class MetricCache { + private final Map> runRawPointsByDevice = new HashMap<>(); + private final Map> shotRawPointsByDevice = new HashMap<>(); + private final Map cavitiesByDevice = new HashMap<>(); + private final Map> cycleStatsByDevice = new HashMap<>(); + + private List getRunRawPoints(String deviceCode) { + return getRawPoints(runRawPointsByDevice, deviceCode); + } + + private List getShotRawPoints(String deviceCode) { + return getRawPoints(shotRawPointsByDevice, deviceCode); + } + + private List getRawPoints(Map> rawPointsByDevice, + String deviceCode) { + List rawPoints = rawPointsByDevice.get(deviceCode); + return rawPoints == null ? Collections.emptyList() : rawPoints; + } + + private Map getCycleStats(String deviceCode) { + return cycleStatsByDevice.get(deviceCode); + } + } + private static class CycleResult { private BigDecimal cycleSeconds = BigDecimal.ZERO; private boolean estimated = true; diff --git a/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml b/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml index 598dcce..f5b63d2 100644 --- a/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml +++ b/aucma-report/src/main/resources/mapper/report/InjectionOeeMapper.xml @@ -4,6 +4,12 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + + + + + + + + + + + + + + + + + + + +