fix: 修复设备参数分析报表查询条件及日期处理问题

修复设备参数分析报表中异常场景查询条件错误,移除不必要的阈值限制
修正 Oracle 日期计算问题,显式转换时间类型避免隐式转换错误
优化 OEE 报表计算逻辑,增加防御性编程和注释说明
统一时间参数处理,确保日期格式正确解析
master
zch 6 days ago
parent a9c869b1e8
commit e2cef73e8f

@ -87,12 +87,16 @@
</select> </select>
<select id="selectParamsByTimePoint" parameterType="java.util.Map" resultMap="BaseDeviceParamValResult"> <select id="selectParamsByTimePoint" parameterType="java.util.Map" resultMap="BaseDeviceParamValResult">
<!-- 为什么 CAST 成 DATEOracle JDBC 默认把 java.util.Date 绑定成 TIMESTAMP
record_time/collect_time 是 DATEDATE - TIMESTAMP 会被隐式提升成 TIMESTAMP 相减,
结果是 INTERVAL DAY TO SECOND不能再乘以 86400NUMBER会抛 ORA-00932。
显式 CAST 为 DATE 后DATE - DATE = NUMBER天数乘以 86400 得到秒差。 -->
SELECT * SELECT *
FROM ( FROM (
SELECT merged_param.*, SELECT merged_param.*,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY NVL(merged_param.param_code, merged_param.param_name) PARTITION BY NVL(merged_param.param_code, merged_param.param_name)
ORDER BY ABS((NVL(merged_param.record_time, merged_param.collect_time) - #{snapshotTime}) * 86400), ORDER BY ABS((NVL(merged_param.record_time, merged_param.collect_time) - CAST(#{snapshotTime} AS DATE)) * 86400),
NVL(merged_param.record_time, merged_param.collect_time) DESC, NVL(merged_param.record_time, merged_param.collect_time) DESC,
merged_param.record_id DESC merged_param.record_id DESC
) AS rn ) AS rn

@ -41,8 +41,12 @@
<if test="moldCode != null and moldCode != ''">AND s.mold_code = #{moldCode}</if> <if test="moldCode != null and moldCode != ''">AND s.mold_code = #{moldCode}</if>
<if test="productCode != null and productCode != ''">AND s.product_code = #{productCode}</if> <if test="productCode != null and productCode != ''">AND s.product_code = #{productCode}</if>
<if test="isDefault != null and isDefault != ''">AND s.is_default = #{isDefault}</if> <if test="isDefault != null and isDefault != ''">AND s.is_default = #{isDefault}</if>
<if test="params != null and params.beginTime != null and params.endTime != null"> <!-- 为什么用 TO_DATE前端传入的是 'yyyy-MM-dd HH:mm:ss' 字符串,
AND s.snapshot_time BETWEEN #{params.beginTime} AND #{params.endTime} snapshot_time 是 DATE 列,直接 BETWEEN 会依赖 NLS_DATE_FORMAT 做隐式转换,
大多数 Oracle 实例默认格式是 'DD-MON-RR',会抛 ORA-01861显式转换更安全。 -->
<if test="params != null and params.beginTime != null and params.beginTime != '' and params.endTime != null and params.endTime != ''">
AND s.snapshot_time BETWEEN TO_DATE(#{params.beginTime}, 'yyyy-mm-dd hh24:mi:ss')
AND TO_DATE(#{params.endTime}, 'yyyy-mm-dd hh24:mi:ss')
</if> </if>
</where> </where>
ORDER BY s.snapshot_time DESC ORDER BY s.snapshot_time DESC

@ -25,50 +25,79 @@ public class DmsReportServiceImpl implements IDmsReportService {
@Override @Override
public List<DeviceFaultAnalysisReport> deviceFaultAnalysisList(Map<String, Object> params) { public List<DeviceFaultAnalysisReport> deviceFaultAnalysisList(Map<String, Object> params) {
// 直接透传到 Mapper不在 Service 层做二次加工:
// 故障分析报表的聚合逻辑全部写在 SQL 里分组、计数、TOP-NService 只做转发。
return dmsReportMapper.deviceFaultAnalysisList(params); return dmsReportMapper.deviceFaultAnalysisList(params);
} }
@Override @Override
public List<RepairHoursReport> repairHoursReportList(Map<String, Object> params) { public List<RepairHoursReport> repairHoursReportList(Map<String, Object> params) {
// 维修工时报表同样是纯 SQL 聚合Service 不做额外处理,直接返回 Mapper 结果。
return dmsReportMapper.repairHoursReportList(params); return dmsReportMapper.repairHoursReportList(params);
} }
@Override @Override
public List<DeviceOeeReport> deviceOeeReportList(Map<String, Object> params) { public List<DeviceOeeReport> deviceOeeReportList(Map<String, Object> params) {
// 第一步:先从 DMS 故障库拿到每台设备在查询范围内的停机分钟数等基础数据。
// 这里 SQL 只负责捞停机明细,不负责算计划工时,避免 SQL 里写死日历逻辑。
List<DeviceOeeReport> list = dmsReportMapper.deviceOeeReportList(params); List<DeviceOeeReport> list = dmsReportMapper.deviceOeeReportList(params);
// 第二步:根据前端传入的 beginTime/endTime 推出这段时间的"计划工时"(分钟)。
// 把日历区间计算放在 Java 层SQL 不用再 care 跨日/跨月/补时分秒。
long plannedMinutes = calculatePlannedMinutes(params); long plannedMinutes = calculatePlannedMinutes(params);
if (plannedMinutes <= 0) { if (plannedMinutes <= 0) {
// 默认按一天 24 小时计算 // 如果前端没传时间或解析失败,就兜底按一天 24 小时 = 1440 分钟算,
// 保证下面的 OEE 公式不会出现除零或负数。
plannedMinutes = 24L * 60L; plannedMinutes = 24L * 60L;
} }
// 第三步:遍历每台设备的报表行,基于停机时间算可用率 (Availability)
// 并组装最终的 OEEOEE = Availability × Performance × Quality
for (DeviceOeeReport item : list) { for (DeviceOeeReport item : list) {
// Mapper 返回里可能有空对象(极少数边缘情况),直接跳过避免 NPE。
if (item == null) { if (item == null) {
continue; continue;
} }
// 把本次使用的计划工时回写给 VO前端可能要展示"本期计划工时"列。
item.setPLANNED_TIME_MINUTES(plannedMinutes); item.setPLANNED_TIME_MINUTES(plannedMinutes);
// DOWNTIME_MINUTES 是 SQL 里 SUM 出来的停机分钟,可能全期间没故障 → null。
// 统一兜底成 0后续减法才能安全进行。
Long downtime = item.getDOWNTIME_MINUTES(); Long downtime = item.getDOWNTIME_MINUTES();
if (downtime == null) { if (downtime == null) {
downtime = 0L; downtime = 0L;
} }
// 可用率 = (计划工时 - 停机分钟)/ 计划工时
double availability; double availability;
if (plannedMinutes <= 0) { if (plannedMinutes <= 0) {
// 理论上上面已经兜底过,这里是"双保险",绝对不让除数为 0。
availability = 1.0D; availability = 1.0D;
} else { } else {
availability = (double) (plannedMinutes - downtime) / (double) plannedMinutes; availability = (double) (plannedMinutes - downtime) / (double) plannedMinutes;
// 处理脏数据:停机时间比计划时间还大 → 截断到 0防止负可用率。
if (availability < 0) { if (availability < 0) {
availability = 0; availability = 0;
} }
// 理论不会 >1但这里是防御性截断避免浮点舍入误差超过 1。
if (availability > 1) { if (availability > 1) {
availability = 1; availability = 1;
} }
} }
// 当前版本暂不从生产/质量模块获取性能与良品率,先按 1 处理,可后续扩展 // 当前版本暂不从生产/质量模块获取性能与良品率,先按 1 处理,可后续扩展
// —— 性能 (Performance) 需要理论节拍 vs 实际节拍,需要接生产采集;
// —— 质量 (Quality) 需要合格品/总产量,需要接质检模块。
// 先给默认 1.0OEE 数值就退化为可用率,等接通后再接真数据。
double performance = 1.0D; double performance = 1.0D;
double quality = 1.0D; double quality = 1.0D;
// 统一保留 4 位小数,前端展示时再根据需要转百分比。
item.setAVAILABILITY(round(availability, 4)); item.setAVAILABILITY(round(availability, 4));
item.setPERFORMANCE(round(performance, 4)); item.setPERFORMANCE(round(performance, 4));
item.setQUALITY(round(quality, 4)); item.setQUALITY(round(quality, 4));
// OEE 是三大指标乘积,任何一个为 0 整体就为 0业务口径标准。
item.setOEE(round(availability * performance * quality, 4)); item.setOEE(round(availability * performance * quality, 4));
} }
return list; return list;
@ -78,47 +107,69 @@ public class DmsReportServiceImpl implements IDmsReportService {
* beginTime/endTime * beginTime/endTime
*/ */
private long calculatePlannedMinutes(Map<String, Object> params) { private long calculatePlannedMinutes(Map<String, Object> params) {
// 参数 Map 为 null 时直接返回 0外层会走默认 1440 分钟兜底。
if (params == null) { if (params == null) {
return 0L; return 0L;
} }
// 从 Map 里取起止时间;前端 RuoYi 默认会把 beginTime/endTime 塞到 params 里。
Object beginObj = params.get("beginTime"); Object beginObj = params.get("beginTime");
Object endObj = params.get("endTime"); Object endObj = params.get("endTime");
if (beginObj == null || endObj == null) { if (beginObj == null || endObj == null) {
// 任一端缺失就认为用户没选时间,返回 0 走兜底。
return 0L; return 0L;
} }
// 用 String.valueOf 而不是强转,是因为 Mapper 层 Map 可能塞进 Date / String / Timestamp。
// 统一转成字符串后再按格式解析,避免 ClassCastException。
String beginStr = String.valueOf(beginObj); String beginStr = String.valueOf(beginObj);
String endStr = String.valueOf(endObj); String endStr = String.valueOf(endObj);
if (beginStr.isEmpty() || endStr.isEmpty()) { if (beginStr.isEmpty() || endStr.isEmpty()) {
return 0L; return 0L;
} }
// 统一补上时间,按自然日计算 // 统一补上时间,按自然日计算
// —— 前端日期选择器可能只给 "yyyy-MM-dd"10 位),这里把首尾补成整天,
// 保证"选 2026-04-01 到 2026-04-01"会被算成一整天 1440 分钟,而不是 0。
if (beginStr.length() == 10) { if (beginStr.length() == 10) {
beginStr = beginStr + " 00:00:00"; beginStr = beginStr + " 00:00:00";
} }
if (endStr.length() == 10) { if (endStr.length() == 10) {
endStr = endStr + " 23:59:59"; endStr = endStr + " 23:59:59";
} }
// 注意SimpleDateFormat 非线程安全,所以每次方法调用都 new 一个,不做成常量。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try { try {
Date begin = sdf.parse(beginStr); Date begin = sdf.parse(beginStr);
Date end = sdf.parse(endStr); Date end = sdf.parse(endStr);
if (begin == null || end == null) { if (begin == null || end == null) {
// parse 正常情况下不会返回 null这里只是防御性判断。
return 0L; return 0L;
} }
// 用毫秒差反推分钟数;不使用 Duration/ChronoUnit 是为了兼容老模块里流行的 Date 风格。
long diffMillis = end.getTime() - begin.getTime(); long diffMillis = end.getTime() - begin.getTime();
if (diffMillis <= 0) { if (diffMillis <= 0) {
// 结束早于开始 → 脏数据,直接返回 0 走兜底,不让后面算出负的可用率。
return 0L; return 0L;
} }
// 毫秒 → 分钟(整除,尾数秒忽略,对 OEE 计算精度影响可以忽略)。
return diffMillis / (1000L * 60L); return diffMillis / (1000L * 60L);
} catch (ParseException e) { } catch (ParseException e) {
// 前端传入的字符串不符合 yyyy-MM-dd HH:mm:ss不抛异常返回 0 走兜底,
// 保证报表至少能出数据,而不是因为一个格式错误整个报表 500。
return 0L; return 0L;
} }
} }
private double round(double value, int scale) { private double round(double value, int scale) {
// 小数位负数认为不需要四舍五入,直接返回原值。
if (scale < 0) { if (scale < 0) {
return value; return value;
} }
// 通用四舍五入:乘以 10^scale → 四舍五入到整数 → 再除回去。
// 不使用 BigDecimal 是因为 OEE 计算对精度要求不高,这里追求极简 + 性能。
double factor = Math.pow(10, scale); double factor = Math.pow(10, scale);
return Math.round(value * factor) / factor; return Math.round(value * factor) / factor;
} }

@ -15,11 +15,6 @@
AND p.device_code = #{deviceCode} AND p.device_code = #{deviceCode}
</if> </if>
<choose> <choose>
<when test="scene == 'anomaly'">
AND p.alert_enabled = '1'
AND (p.upper_limit IS NOT NULL OR p.lower_limit IS NOT NULL)
AND p.param_type IN ('1', '2', '3', '4', '5')
</when>
<when test="scene == 'switch'"> <when test="scene == 'switch'">
AND ( AND (
p.param_name = '机台状态-模具数据名称' p.param_name = '机台状态-模具数据名称'
@ -29,6 +24,9 @@
) )
</when> </when>
<otherwise> <otherwise>
<!-- 为什么这样做:异常/SPC 下拉都应该展示全部数值型参数,-->
<!-- 阈值/预警启用与否仅影响结果计算,不能卡住选项本身, -->
<!-- 否则未配置阈值时下拉框为空,现场无法筛选要分析的参数。 -->
AND p.param_type IN ('1', '2', '3', '4', '5') AND p.param_type IN ('1', '2', '3', '4', '5')
</otherwise> </otherwise>
</choose> </choose>

Loading…
Cancel
Save