|
|
|
@ -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-N),Service 只做转发。
|
|
|
|
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),
|
|
|
|
|
|
|
|
// 并组装最终的 OEE(OEE = 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.0,OEE 数值就退化为可用率,等接通后再接真数据。
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|