feat(report): 新增18台设备开模数统计接口

1. 新增设备开模数统计VO类,封装设备名称、开模数及内部标记字段
2. 新增报表服务接口、控制器方法,提供对外统计接口
3. 编写Mapper接口及XML实现,支持按昨日07:00-今日07:00窗口统计
4. 实现服务层逻辑,包含基准时间计算、分表路由、缺数设备临时估算
5. 补充测试依赖及Mapper空行规范
master
zch 1 month ago
parent 1c9d71f221
commit 4ac38319b0

@ -12,6 +12,7 @@ import org.apache.ibatis.annotations.Param;
* @date 2026-03-18
*/
public interface ProdOrderNoteMapper {
ProdOrderNote selectProdOrderNoteByObjId(@Param("objId") Long objId);
List<ProdOrderNote> selectProdOrderNoteList(ProdOrderNote prodOrderNote);

@ -13,6 +13,7 @@ import org.apache.ibatis.annotations.Param;
* @date 2026-03-18
*/
public interface ProdRouteMapper {
ProdRoute selectProdRouteByObjId(@Param("objId") Long objId);
ProdRoute selectProdRouteByRouteCode(@Param("routeCode") String routeCode);

@ -111,4 +111,13 @@ public class Board4Controller extends BaseController {
public AjaxResult getDeviceProductionList() {
return AjaxResult.success(board4Service.getDeviceProductionList());
}
/**
* 1807:00-07:00
*/
@Anonymous
@GetMapping("/deviceOpeningCountList")
public AjaxResult getDeviceOpeningCountList() {
return AjaxResult.success(board4Service.getDeviceOpeningCountList());
}
}

@ -0,0 +1,71 @@
package com.aucma.report.domain.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
/**
* Board4 VO
*
* @author YinQ
*/
public class Board4DeviceOpeningCountVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 设备名称 */
private String deviceName;
/** 开模数统计口径为昨日07:00到今日07:00的“机台状态-实际产出数量”增量 */
private Long openingCount;
/** 设备编码仅用于后端识别OLD设备接口不返回 */
@JsonIgnore
private String deviceCode;
/** 窗口内源表是否存在采集数据仅用于判断OLD设备是否需要临时估算接口不返回 */
@JsonIgnore
private Integer windowDataFlag;
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public Long getOpeningCount() {
return openingCount;
}
public void setOpeningCount(Long openingCount) {
this.openingCount = openingCount;
}
@JsonIgnore
public String getDeviceCode() {
return deviceCode;
}
public void setDeviceCode(String deviceCode) {
this.deviceCode = deviceCode;
}
@JsonIgnore
public Integer getWindowDataFlag() {
return windowDataFlag;
}
public void setWindowDataFlag(Integer windowDataFlag) {
this.windowDataFlag = windowDataFlag;
}
/**
*
*/
@JsonIgnore
public boolean hasWindowData() {
return windowDataFlag != null && windowDataFlag.intValue() > 0;
}
}

@ -2,7 +2,9 @@ package com.aucma.report.mapper;
import com.aucma.report.domain.vo.*;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@ -92,4 +94,13 @@ public interface Board4Mapper {
* BASE_DEVICE_PARAM_VAL + BASE_DEVICELEDGER
*/
List<Board4DeviceProductionVo> selectDeviceProductionAnalysis();
/**
* 1807:00-07:00
* BASE_DEVICE_PARAM_VAL_YYYYMM + BASE_DEVICELEDGER
*/
List<Board4DeviceOpeningCountVo> selectDeviceOpeningCountList(@Param("beginTime") Date beginTime,
@Param("endTime") Date endTime,
@Param("baselineBeginTime") Date baselineBeginTime,
@Param("tableSuffixes") List<String> tableSuffixes);
}

@ -61,4 +61,9 @@ public interface IBoard4Service {
* /
*/
List<Board4DeviceProductionVo> getDeviceProductionList();
/**
* 1807:00-07:00
*/
List<Board4DeviceOpeningCountVo> getDeviceOpeningCountList();
}

@ -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;
/**
*
* 11123112
@ -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:0007: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();
}
}

@ -173,4 +173,168 @@
ORDER BY production DESC, d.DEVICE_CODE
</select>
<!-- 查询18台设备开模数昨日07:00-今日07:00 -->
<select id="selectDeviceOpeningCountList" resultType="com.aucma.report.domain.vo.Board4DeviceOpeningCountVo">
WITH device_base AS (
SELECT d.DEVICE_CODE AS device_code,
d.DEVICE_NAME AS device_name,
NVL(
TO_NUMBER(REGEXP_SUBSTR(d.DEVICE_NAME, '[0-9]+')),
TO_NUMBER(REGEXP_SUBSTR(d.DEVICE_CODE, '[0-9]+'))
) AS device_no
FROM BASE_DEVICELEDGER d
WHERE d.IS_FLAG = 1
AND d.DEVICE_NAME IS NOT NULL
AND NVL(d.DEVICE_TYPE, '1') = '1'
),
target_device AS (
SELECT device_code,
device_name,
device_no,
sort_no
FROM (
SELECT b.device_code,
b.device_name,
b.device_no,
ROW_NUMBER() OVER (
ORDER BY NVL(b.device_no, 9999), b.device_code
) AS sort_no
FROM device_base b
)
WHERE sort_no &lt;= 18
),
source_param AS (
<choose>
<when test="tableSuffixes != null and tableSuffixes.size() > 0">
<!-- 为什么这样做DeviceParamTableRouter 约定非OLD设备读月分表OLD设备仍保留在主表。 -->
<foreach item="suffix" collection="tableSuffixes" separator=" UNION ALL ">
SELECT v.DEVICE_CODE AS device_code,
v.COLLECT_TIME AS collect_time,
TO_NUMBER(TRIM(v.PARAM_VALUE)) AS param_value
FROM BASE_DEVICE_PARAM_VAL_${suffix} v
WHERE v.DEVICE_CODE NOT LIKE 'OLD-%'
AND v.PARAM_NAME = '机台状态-实际产出数量'
AND v.COLLECT_TIME &gt;= #{baselineBeginTime}
AND v.COLLECT_TIME &lt; #{endTime}
AND REGEXP_LIKE(TRIM(v.PARAM_VALUE), '^[+-]?[0-9]+([.][0-9]+)?$')
AND EXISTS (
SELECT 1
FROM target_device td
WHERE td.device_code = v.DEVICE_CODE
)
</foreach>
UNION ALL
SELECT v.DEVICE_CODE AS device_code,
v.COLLECT_TIME AS collect_time,
TO_NUMBER(TRIM(v.PARAM_VALUE)) AS param_value
FROM BASE_DEVICE_PARAM_VAL v
WHERE v.DEVICE_CODE LIKE 'OLD-%'
AND v.PARAM_NAME = '机台状态-实际产出数量'
AND v.COLLECT_TIME &gt;= #{baselineBeginTime}
AND v.COLLECT_TIME &lt; #{endTime}
AND REGEXP_LIKE(TRIM(v.PARAM_VALUE), '^[+-]?[0-9]+([.][0-9]+)?$')
AND EXISTS (
SELECT 1
FROM target_device td
WHERE td.device_code = v.DEVICE_CODE
)
</when>
<otherwise>
<!-- 为什么这样做即使当前月份分表不存在OLD设备仍可从BASE_DEVICE_PARAM_VAL主表回源取数。 -->
SELECT v.DEVICE_CODE AS device_code,
v.COLLECT_TIME AS collect_time,
TO_NUMBER(TRIM(v.PARAM_VALUE)) AS param_value
FROM BASE_DEVICE_PARAM_VAL v
WHERE v.DEVICE_CODE LIKE 'OLD-%'
AND v.PARAM_NAME = '机台状态-实际产出数量'
AND v.COLLECT_TIME &gt;= #{baselineBeginTime}
AND v.COLLECT_TIME &lt; #{endTime}
AND REGEXP_LIKE(TRIM(v.PARAM_VALUE), '^[+-]?[0-9]+([.][0-9]+)?$')
AND EXISTS (
SELECT 1
FROM target_device td
WHERE td.device_code = v.DEVICE_CODE
)
</otherwise>
</choose>
),
baseline_param AS (
SELECT device_code,
collect_time,
param_value
FROM (
SELECT p.device_code,
p.collect_time,
p.param_value,
ROW_NUMBER() OVER (
PARTITION BY p.device_code
ORDER BY p.collect_time DESC
) AS rn
FROM source_param p
WHERE p.collect_time &lt; #{beginTime}
)
WHERE rn = 1
),
window_param AS (
SELECT p.device_code,
p.collect_time,
p.param_value
FROM source_param p
WHERE p.collect_time &gt;= #{beginTime}
AND p.collect_time &lt; #{endTime}
UNION ALL
SELECT p.device_code,
p.collect_time,
p.param_value
FROM baseline_param p
),
ordered_param AS (
SELECT p.device_code,
p.collect_time,
p.param_value,
LAG(p.param_value) OVER (
PARTITION BY p.device_code
ORDER BY p.collect_time
) AS previous_param_value
FROM window_param p
),
actual_prod AS (
SELECT p.device_code,
ROUND(SUM(
CASE
WHEN p.collect_time &lt; #{beginTime} THEN 0
WHEN p.previous_param_value IS NULL THEN 0
WHEN p.param_value &gt; p.previous_param_value THEN p.param_value - p.previous_param_value
WHEN p.param_value &lt; p.previous_param_value THEN p.param_value
ELSE 0
END
)) AS opening_count
FROM ordered_param p
GROUP BY p.device_code
),
window_data_device AS (
SELECT p.device_code,
COUNT(1) AS window_data_count
FROM source_param p
WHERE p.collect_time &gt;= #{beginTime}
AND p.collect_time &lt; #{endTime}
GROUP BY p.device_code
)
SELECT td.device_code AS deviceCode,
td.device_name AS deviceName,
NVL(ap.opening_count, 0) AS openingCount,
CASE
WHEN wd.window_data_count &gt; 0 THEN 1
ELSE 0
END AS windowDataFlag
FROM target_device td
LEFT JOIN actual_prod ap
ON td.device_code = ap.device_code
LEFT JOIN window_data_device wd
ON td.device_code = wd.device_code
<!-- 为什么这样做mapper只负责回源表取真实采集和标记是否有数据OLD设备缺数时的临时估算放在Service独立方法里
后续关闭模拟数据时只需要注释掉方法调用不影响真实取数SQL。 -->
ORDER BY NVL(td.device_no, 9999), td.device_code
</select>
</mapper>

Loading…
Cancel
Save