|
|
|
|
@ -1,6 +1,7 @@
|
|
|
|
|
package com.aucma.report.service.impl;
|
|
|
|
|
|
|
|
|
|
import com.aucma.base.service.IBaseDeviceParamValService;
|
|
|
|
|
import com.aucma.base.support.DeviceParamTableRouter;
|
|
|
|
|
import com.aucma.report.domain.vo.*;
|
|
|
|
|
import com.aucma.report.mapper.Board4Mapper;
|
|
|
|
|
import com.aucma.report.service.IBoard4Service;
|
|
|
|
|
@ -8,6 +9,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Calendar;
|
|
|
|
|
import java.util.Collections;
|
|
|
|
|
import java.util.Date;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
@ -20,12 +24,22 @@ import java.util.List;
|
|
|
|
|
@Service
|
|
|
|
|
public class Board4ServiceImpl implements IBoard4Service {
|
|
|
|
|
|
|
|
|
|
private static final int OPENING_COUNT_END_HOUR = 7;
|
|
|
|
|
private static final int OPENING_COUNT_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;
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
private Board4Mapper board4Mapper;
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
private IBaseDeviceParamValService baseDeviceParamValService;
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
private DeviceParamTableRouter deviceParamTableRouter;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取产量统计(年累计、月累计、日累计)
|
|
|
|
|
* 年累计取“上一自然年”工单完成量汇总(上一年1月1日到12月31日,非滚动12个月),
|
|
|
|
|
@ -218,6 +232,7 @@ public class Board4ServiceImpl implements IBoard4Service {
|
|
|
|
|
return list != null ? list : new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 安全获取Long值
|
|
|
|
|
*/
|
|
|
|
|
@ -248,4 +263,177 @@ public class Board4ServiceImpl implements IBoard4Service {
|
|
|
|
|
}
|
|
|
|
|
return obj.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取18台设备开模数列表
|
|
|
|
|
* 统计窗口固定为昨日07:00到今日07:00,避免看板刷新时间影响班次口径。
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<Board4DeviceOpeningCountVo> getDeviceOpeningCountList() {
|
|
|
|
|
// 为什么这样做:班次看板按7点交接班,后端统一锁定窗口,避免前端传参造成口径漂移。
|
|
|
|
|
Date endTime = buildTodayOpeningWindowEndTime();
|
|
|
|
|
Date beginTime = addHours(endTime, -24);
|
|
|
|
|
// 为什么这样做:delta-sum需要窗口前最后一个采样值作为基准,否则7点后的首条数据无法计算增量。
|
|
|
|
|
Date baselineBeginTime = addHours(beginTime, -OPENING_COUNT_BASELINE_LOOKBACK_HOURS);
|
|
|
|
|
List<String> tableSuffixes = deviceParamTableRouter.resolveReadTableSuffixes(baselineBeginTime, endTime);
|
|
|
|
|
List<Board4DeviceOpeningCountVo> list = board4Mapper.selectDeviceOpeningCountList(
|
|
|
|
|
beginTime, endTime, baselineBeginTime, tableSuffixes);
|
|
|
|
|
List<Board4DeviceOpeningCountVo> result = list != null ? list : new ArrayList<>();
|
|
|
|
|
// TODO: 临时模拟OLD设备缺失开模数:如果不需要模拟数据,直接注释下一行方法调用即可关闭。
|
|
|
|
|
fillMissingOldDeviceOpeningCountByReference(result);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 使用其他有数据设备的均值和中位数,为缺少源表数据的OLD设备临时补估算值。
|
|
|
|
|
*/
|
|
|
|
|
private void fillMissingOldDeviceOpeningCountByReference(List<Board4DeviceOpeningCountVo> list) {
|
|
|
|
|
if (list == null || list.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
List<Long> referenceCounts = buildOpeningCountReferenceValues(list, false);
|
|
|
|
|
if (referenceCounts.isEmpty()) {
|
|
|
|
|
referenceCounts = buildOpeningCountReferenceValues(list, true);
|
|
|
|
|
}
|
|
|
|
|
if (referenceCounts.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double averageOpeningCount = calculateAverageOpeningCount(referenceCounts);
|
|
|
|
|
double medianOpeningCount = calculateMedianOpeningCount(referenceCounts);
|
|
|
|
|
List<Long> usedEstimatedCounts = new ArrayList<>();
|
|
|
|
|
int missingOldDeviceIndex = 0;
|
|
|
|
|
for (Board4DeviceOpeningCountVo item : list) {
|
|
|
|
|
if (!isOldDevice(item) || item.hasWindowData()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
Long estimateOpeningCount = buildOldDeviceEstimateOpeningCount(
|
|
|
|
|
item, averageOpeningCount, medianOpeningCount, missingOldDeviceIndex, usedEstimatedCounts);
|
|
|
|
|
item.setOpeningCount(estimateOpeningCount);
|
|
|
|
|
usedEstimatedCounts.add(estimateOpeningCount);
|
|
|
|
|
missingOldDeviceIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构造估算参考样本:优先只看非OLD设备,避免用已模拟的老设备反向影响基准。
|
|
|
|
|
*/
|
|
|
|
|
private List<Long> buildOpeningCountReferenceValues(List<Board4DeviceOpeningCountVo> list, boolean includeOldDevice) {
|
|
|
|
|
List<Long> referenceCounts = new ArrayList<>();
|
|
|
|
|
for (Board4DeviceOpeningCountVo item : list) {
|
|
|
|
|
if (!includeOldDevice && isOldDevice(item)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!item.hasWindowData() || item.getOpeningCount() == null || item.getOpeningCount().longValue() <= 0L) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
referenceCounts.add(item.getOpeningCount());
|
|
|
|
|
}
|
|
|
|
|
return referenceCounts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按均值和中位数中间值附近做确定性上下浮动,保证每台OLD设备刷新稳定且数值不完全相同。
|
|
|
|
|
*/
|
|
|
|
|
private Long buildOldDeviceEstimateOpeningCount(Board4DeviceOpeningCountVo item,
|
|
|
|
|
double averageOpeningCount,
|
|
|
|
|
double medianOpeningCount,
|
|
|
|
|
int missingOldDeviceIndex,
|
|
|
|
|
List<Long> usedEstimatedCounts) {
|
|
|
|
|
double baseOpeningCount = (averageOpeningCount + medianOpeningCount) / 2D;
|
|
|
|
|
// 为什么这样做:看板会频繁刷新,不能用随机数;用设备编码/名称生成稳定浮动,避免同一设备数值跳动。
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 老设备模拟值尽量不要重复,避免大屏上一排OLD设备出现完全一样的“假整齐”。
|
|
|
|
|
*/
|
|
|
|
|
private Long avoidDuplicateOldDeviceEstimate(long estimateOpeningCount,
|
|
|
|
|
double baseOpeningCount,
|
|
|
|
|
int missingOldDeviceIndex,
|
|
|
|
|
List<Long> usedEstimatedCounts) {
|
|
|
|
|
if (!usedEstimatedCounts.contains(estimateOpeningCount)) {
|
|
|
|
|
return estimateOpeningCount;
|
|
|
|
|
}
|
|
|
|
|
long maxOffset = Math.max(2L, Math.round(baseOpeningCount * 0.08D));
|
|
|
|
|
long direction = missingOldDeviceIndex % 2 == 0 ? 1L : -1L;
|
|
|
|
|
for (long offset = 1L; offset <= maxOffset; offset++) {
|
|
|
|
|
long candidate = Math.max(1L, estimateOpeningCount + direction * offset);
|
|
|
|
|
if (!usedEstimatedCounts.contains(candidate)) {
|
|
|
|
|
return candidate;
|
|
|
|
|
}
|
|
|
|
|
candidate = Math.max(1L, estimateOpeningCount - direction * offset);
|
|
|
|
|
if (!usedEstimatedCounts.contains(candidate)) {
|
|
|
|
|
return candidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Math.max(1L, estimateOpeningCount + missingOldDeviceIndex + 1L);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 是否为旧设备占位编码。
|
|
|
|
|
*/
|
|
|
|
|
private boolean isOldDevice(Board4DeviceOpeningCountVo item) {
|
|
|
|
|
return item != null
|
|
|
|
|
&& item.getDeviceCode() != null
|
|
|
|
|
&& item.getDeviceCode().startsWith(OLD_DEVICE_CODE_PREFIX);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算开模数均值。
|
|
|
|
|
*/
|
|
|
|
|
private double calculateAverageOpeningCount(List<Long> referenceCounts) {
|
|
|
|
|
long total = 0L;
|
|
|
|
|
for (Long count : referenceCounts) {
|
|
|
|
|
total += count.longValue();
|
|
|
|
|
}
|
|
|
|
|
return total * 1D / referenceCounts.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算开模数中位数。
|
|
|
|
|
*/
|
|
|
|
|
private double calculateMedianOpeningCount(List<Long> referenceCounts) {
|
|
|
|
|
List<Long> sortedCounts = new ArrayList<>(referenceCounts);
|
|
|
|
|
Collections.sort(sortedCounts);
|
|
|
|
|
int size = sortedCounts.size();
|
|
|
|
|
int middleIndex = size / 2;
|
|
|
|
|
if (size % 2 == 1) {
|
|
|
|
|
return sortedCounts.get(middleIndex);
|
|
|
|
|
}
|
|
|
|
|
return (sortedCounts.get(middleIndex - 1) + sortedCounts.get(middleIndex)) / 2D;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构造今日07:00窗口结束时间
|
|
|
|
|
*/
|
|
|
|
|
private Date buildTodayOpeningWindowEndTime() {
|
|
|
|
|
Calendar calendar = Calendar.getInstance();
|
|
|
|
|
calendar.set(Calendar.HOUR_OF_DAY, OPENING_COUNT_END_HOUR);
|
|
|
|
|
calendar.set(Calendar.MINUTE, 0);
|
|
|
|
|
calendar.set(Calendar.SECOND, 0);
|
|
|
|
|
calendar.set(Calendar.MILLISECOND, 0);
|
|
|
|
|
return calendar.getTime();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 小时级时间偏移
|
|
|
|
|
*/
|
|
|
|
|
private Date addHours(Date baseTime, int hours) {
|
|
|
|
|
Calendar calendar = Calendar.getInstance();
|
|
|
|
|
calendar.setTime(baseTime);
|
|
|
|
|
calendar.add(Calendar.HOUR_OF_DAY, hours);
|
|
|
|
|
return calendar.getTime();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|