|
|
@@ -33,6 +33,12 @@ import com.platform.dao.mapper.upms.SysUserMapper;
|
|
|
import com.platform.dao.util.MessageTemplateUtil;
|
|
|
import com.platform.dao.vo.SysUserVO;
|
|
|
import com.platform.dao.vo.repair.RepairApplicationFormVO;
|
|
|
+import com.platform.dao.vo.repair.RepairUncompletedOrderVO;
|
|
|
+import com.platform.dao.vo.report.MtbfFaultOrderVO;
|
|
|
+import com.platform.dao.vo.report.MtbfMonthVO;
|
|
|
+import com.platform.dao.vo.report.MtbfReportVO;
|
|
|
+import com.platform.dao.vo.report.MtbfResultVO;
|
|
|
+import com.platform.dao.vo.report.MttrMonthVO;
|
|
|
import com.platform.dao.vo.report.RepairReport24VO;
|
|
|
import com.platform.dao.vo.report.RepairReportMttr;
|
|
|
import com.platform.dao.vo.report.RepairSbInfoReport;
|
|
|
@@ -61,6 +67,9 @@ import javax.annotation.Resource;
|
|
|
import java.math.BigDecimal;
|
|
|
import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.time.YearMonth;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
import java.util.*;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
@@ -1091,12 +1100,17 @@ public class RepairApplicationFormServiceImpl
|
|
|
super.modModelByPrimaryKey(applicationForm);
|
|
|
|
|
|
// 更新设备状态为使用中(走带状态变更日志的链路)
|
|
|
- SbInfoDTO sbInfoDTO = new SbInfoDTO();
|
|
|
- sbInfoDTO.setId(applicationForm.getSbId());
|
|
|
- sbInfoDTO.setStatus(SbInfoStatusEnum.IN_USE.getValue());
|
|
|
- sbInfoDTO.setChangeReason("维修完成恢复使用");
|
|
|
- sbInfoDTO.setActualUser(applicationForm.getRepairUserName());
|
|
|
- sbInfoService.updateStatus(sbInfoDTO);
|
|
|
+ // 仅停机单需要恢复,且该设备没有其他未完结的停机报修单时才恢复,避免多个停机单只修完一张就把设备置为在用
|
|
|
+ if (applicationForm.getNeedStop() != null
|
|
|
+ && applicationForm.getNeedStop().intValue() == RepairApplicationFormStopEnum.STOP.getValue()
|
|
|
+ && isSbStopClear(applicationForm.getSbId(), applicationForm.getId())) {
|
|
|
+ SbInfoDTO sbInfoDTO = new SbInfoDTO();
|
|
|
+ sbInfoDTO.setId(applicationForm.getSbId());
|
|
|
+ sbInfoDTO.setStatus(SbInfoStatusEnum.IN_USE.getValue());
|
|
|
+ sbInfoDTO.setChangeReason("维修完成恢复使用");
|
|
|
+ sbInfoDTO.setActualUser(applicationForm.getRepairUserName());
|
|
|
+ sbInfoService.updateStatus(sbInfoDTO);
|
|
|
+ }
|
|
|
|
|
|
// 维修完成通知报修人:邮件+站内信+微信,任何异常不影响完成主流程
|
|
|
// 已调整:改到审核通过(approve)后发送,点“完成”时不再通知
|
|
|
@@ -1802,10 +1816,19 @@ public class RepairApplicationFormServiceImpl
|
|
|
public void approve(RepairApplicationFormDTO dto) {
|
|
|
// 1.修改验收单状态和验收验收时间
|
|
|
RepairApplicationForm applicationForm = mapper.selectById(dto.getId());
|
|
|
- // 如果选择了设备停机,则要修改设备状态
|
|
|
SbInfo info = sbInfoService.getModelById(applicationForm.getSbId());
|
|
|
- info.setStatus(SbInfoStatusEnum.IN_USE.getValue());
|
|
|
- sbInfoService.modModelByPrimaryKey(info);
|
|
|
+ // 仅停机单需要恢复,且该设备没有其他未完结的停机报修单时才恢复为使用中(走带状态变更日志和停机记录闭合的链路)
|
|
|
+ if (applicationForm.getNeedStop() != null
|
|
|
+ && applicationForm.getNeedStop().intValue() == RepairApplicationFormStopEnum.STOP.getValue()
|
|
|
+ && isSbStopClear(applicationForm.getSbId(), applicationForm.getId())) {
|
|
|
+ SbInfoDTO sbInfoDTO = new SbInfoDTO();
|
|
|
+ sbInfoDTO.setId(applicationForm.getSbId());
|
|
|
+ sbInfoDTO.setStatus(SbInfoStatusEnum.IN_USE.getValue());
|
|
|
+ sbInfoDTO.setChangeReason("维修验收通过恢复使用");
|
|
|
+ UserInfo approveUser = SecurityUtils.getUserInfo();
|
|
|
+ sbInfoDTO.setActualUser(approveUser != null ? approveUser.getRealName() : applicationForm.getRepairUserName());
|
|
|
+ sbInfoService.updateStatus(sbInfoDTO);
|
|
|
+ }
|
|
|
applicationForm.setCheckEndTime(LocalDateTime.now());
|
|
|
applicationForm.setStatus(RepairApplicationFormStatusEnum.FINISHED.getValue());
|
|
|
applicationForm.setUpdateTime(applicationForm.getCheckEndTime());
|
|
|
@@ -2011,6 +2034,77 @@ public class RepairApplicationFormServiceImpl
|
|
|
return mapper.selectCountByExample(weekend);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 查询设备未完结的报修单(排除已完成/已拒单),用于提交报修前的重复报修提醒
|
|
|
+ *
|
|
|
+ * @param sbId 设备id
|
|
|
+ * @return 未完结报修单列表,按报修时间倒序
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<RepairUncompletedOrderVO> getUncompletedRepairs(String sbId) {
|
|
|
+ List<RepairUncompletedOrderVO> result = new ArrayList<>();
|
|
|
+ if (StringUtils.isBlank(sbId)) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ Weekend<RepairApplicationForm> weekend = new Weekend<>(RepairApplicationForm.class);
|
|
|
+ weekend.weekendCriteria()
|
|
|
+ .andEqualTo(RepairApplicationForm::getSbId, sbId)
|
|
|
+ .andNotIn(RepairApplicationForm::getStatus, Arrays.asList(
|
|
|
+ RepairApplicationFormStatusEnum.FINISHED.getValue(),
|
|
|
+ RepairApplicationFormStatusEnum.REFUSED.getValue()));
|
|
|
+ weekend.orderBy("applyTime").desc();
|
|
|
+ List<RepairApplicationForm> list = mapper.selectByExample(weekend);
|
|
|
+ if (CollectionUtil.isEmpty(list)) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ for (RepairApplicationForm form : list) {
|
|
|
+ RepairUncompletedOrderVO vo = new RepairUncompletedOrderVO();
|
|
|
+ vo.setId(form.getId());
|
|
|
+ vo.setNo(form.getNo());
|
|
|
+ vo.setApplyTime(form.getApplyTime());
|
|
|
+ vo.setStatus(form.getStatus());
|
|
|
+ vo.setStatusLabel(getStatusLabel(form.getStatus()));
|
|
|
+ vo.setContent(form.getContent());
|
|
|
+ vo.setActualUser(form.getActualUser());
|
|
|
+ vo.setRepairUserName(form.getRepairUserName());
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断设备是否可以恢复为在用:不存在其他未完结(排除已完成/已拒单)且选择停机的报修单
|
|
|
+ * 一台设备可能同时有多张停机报修单,需全部完结后才允许设备恢复使用
|
|
|
+ *
|
|
|
+ * @param sbId 设备id
|
|
|
+ * @param excludeId 排除的报修单id(当前正在完结的单据,状态未落库时传入)
|
|
|
+ * @return true-可以恢复在用
|
|
|
+ */
|
|
|
+ private boolean isSbStopClear(String sbId, String excludeId) {
|
|
|
+ if (StringUtils.isBlank(sbId)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ Weekend<RepairApplicationForm> weekend = new Weekend<>(RepairApplicationForm.class);
|
|
|
+ weekend.weekendCriteria()
|
|
|
+ .andEqualTo(RepairApplicationForm::getSbId, sbId)
|
|
|
+ .andEqualTo(RepairApplicationForm::getNeedStop,
|
|
|
+ RepairApplicationFormStopEnum.STOP.getValue())
|
|
|
+ .andNotIn(RepairApplicationForm::getStatus, Arrays.asList(
|
|
|
+ RepairApplicationFormStatusEnum.FINISHED.getValue(),
|
|
|
+ RepairApplicationFormStatusEnum.REFUSED.getValue()));
|
|
|
+ if (StringUtils.isNotBlank(excludeId)) {
|
|
|
+ weekend.weekendCriteria().andNotEqualTo(RepairApplicationForm::getId, excludeId);
|
|
|
+ }
|
|
|
+ return mapper.selectCountByExample(weekend) == 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 报修单状态名称,统一取自 RepairApplicationFormStatusEnum
|
|
|
+ */
|
|
|
+ private String getStatusLabel(Integer status) {
|
|
|
+ return RepairApplicationFormStatusEnum.descriptionOf(status);
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public void autoAssignTaskForTimer() {
|
|
|
LocalDateTime minutesAgo = DateUtils.minus(LocalDateTime.now(), 10, ChronoUnit.MINUTES);
|
|
|
@@ -2295,4 +2389,138 @@ public class RepairApplicationFormServiceImpl
|
|
|
model.setSearchRepairEndTime(endTime);
|
|
|
model.setSearchRepairStartTime(startTime);
|
|
|
}
|
|
|
+
|
|
|
+ /** MTBF 月趋势分组 key 格式,同时用于解析 MTTR/MTBF 统计月份参数 */
|
|
|
+ private static final DateTimeFormatter MTBF_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
|
|
|
+
|
|
|
+ /** MTTR/MTBF 统计时间段跨度上限(月),防止前端传入过大区间拖垮查询 */
|
|
|
+ private static final int MTTR_MAX_MONTH_SPAN = 36;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 MTTR/MTBF 统计时间段:优先 mttrStartMonth/mttrEndMonth(yyyy-MM),
|
|
|
+ * 未传时兼容旧版 mttrYear(按该年 1-12 月)
|
|
|
+ *
|
|
|
+ * @return [开始月份, 结束月份]
|
|
|
+ */
|
|
|
+ private YearMonth[] resolveMttrMonthRange(RepairApplicationFormDTO model) {
|
|
|
+ YearMonth start;
|
|
|
+ YearMonth end;
|
|
|
+ if (StringUtils.isNotBlank(model.getMttrStartMonth()) && StringUtils.isNotBlank(model.getMttrEndMonth())) {
|
|
|
+ start = parseMttrMonth(model.getMttrStartMonth());
|
|
|
+ end = parseMttrMonth(model.getMttrEndMonth());
|
|
|
+ if (start.isAfter(end)) {
|
|
|
+ throw new DeniedException("开始月份不能晚于结束月份");
|
|
|
+ }
|
|
|
+ } else if (model.getMttrYear() != null) {
|
|
|
+ start = YearMonth.of(model.getMttrYear(), 1);
|
|
|
+ end = YearMonth.of(model.getMttrYear(), 12);
|
|
|
+ } else {
|
|
|
+ throw new DeniedException("统计时间段不可为空");
|
|
|
+ }
|
|
|
+ if (start.plusMonths(MTTR_MAX_MONTH_SPAN - 1L).isBefore(end)) {
|
|
|
+ throw new DeniedException("统计时间段跨度不能超过" + MTTR_MAX_MONTH_SPAN + "个月");
|
|
|
+ }
|
|
|
+ return new YearMonth[]{start, end};
|
|
|
+ }
|
|
|
+
|
|
|
+ private YearMonth parseMttrMonth(String month) {
|
|
|
+ try {
|
|
|
+ return YearMonth.parse(month, MTBF_MONTH_FORMATTER);
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
+ throw new DeniedException("统计月份格式错误,应为 yyyy-MM:" + month);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<MttrMonthVO> getMttrMonthTrend(RepairApplicationFormDTO model) {
|
|
|
+ YearMonth[] range = resolveMttrMonthRange(model);
|
|
|
+ YearMonth start = range[0];
|
|
|
+ YearMonth end = range[1];
|
|
|
+ // 仅统计非计划性维修(故障)
|
|
|
+ model.setCategory(RepairApplicationFormCategoryEnum.PLAN_NOT.getValue());
|
|
|
+ model.setSearchRepairStartTime(start.atDay(1).atStartOfDay());
|
|
|
+ model.setSearchRepairEndTime(end.atEndOfMonth().atTime(23, 59, 59));
|
|
|
+ List<MttrMonthVO> dbList = mapper.statisticsMttrByMonth(model);
|
|
|
+ Map<String, MttrMonthVO> monthMap = new HashMap<>();
|
|
|
+ for (MttrMonthVO item : dbList) {
|
|
|
+ monthMap.put(item.getMonth(), item);
|
|
|
+ }
|
|
|
+ // 按选择时间段逐月返回,无数据月份补 0,保证趋势图横轴完整
|
|
|
+ List<MttrMonthVO> result = new ArrayList<>();
|
|
|
+ for (YearMonth m = start; !m.isAfter(end); m = m.plusMonths(1)) {
|
|
|
+ String key = m.format(MTBF_MONTH_FORMATTER);
|
|
|
+ MttrMonthVO vo = monthMap.get(key);
|
|
|
+ if (vo == null) {
|
|
|
+ vo = new MttrMonthVO().setMonth(key).setNum(0).setTotalRepairHours(0.0).setMttrMinutes(0.0);
|
|
|
+ }
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public MtbfResultVO getMtbfReport(RepairApplicationFormDTO model) {
|
|
|
+ YearMonth[] range = resolveMttrMonthRange(model);
|
|
|
+ YearMonth start = range[0];
|
|
|
+ YearMonth end = range[1];
|
|
|
+ // 仅统计非计划性维修;不限时间段拉全量明细,保证跨年故障间隔计算正确,月趋势再按时间段截取
|
|
|
+ model.setCategory(RepairApplicationFormCategoryEnum.PLAN_NOT.getValue());
|
|
|
+ List<MtbfFaultOrderVO> orders = mapper.selectFaultOrdersForMtbf(model);
|
|
|
+ // SQL 已按 sb_id、apply_time 升序,这里保持分组内顺序
|
|
|
+ Map<String, List<MtbfFaultOrderVO>> sbGroups = orders.stream()
|
|
|
+ .collect(Collectors.groupingBy(MtbfFaultOrderVO::getSbId, LinkedHashMap::new, Collectors.toList()));
|
|
|
+ // 月趋势聚合:key=yyyy-MM(下一次故障报修时间所在月),value=[间隔总小时, 间隔次数]
|
|
|
+ Map<String, double[]> monthAgg = new HashMap<>();
|
|
|
+ List<MtbfReportVO> sbList = new ArrayList<>(sbGroups.size());
|
|
|
+ for (List<MtbfFaultOrderVO> group : sbGroups.values()) {
|
|
|
+ MtbfFaultOrderVO first = group.get(0);
|
|
|
+ MtbfReportVO vo = new MtbfReportVO()
|
|
|
+ .setSbId(first.getSbId())
|
|
|
+ .setSbNo(first.getSbNo())
|
|
|
+ .setSbName(first.getSbName())
|
|
|
+ .setLevel(first.getLevel())
|
|
|
+ .setSbTypeName(first.getSbTypeName())
|
|
|
+ .setFaultNum(group.size());
|
|
|
+ double totalHours = 0.0;
|
|
|
+ int intervals = 0;
|
|
|
+ LocalDateTime prevRepairEnd = null;
|
|
|
+ for (MtbfFaultOrderVO order : group) {
|
|
|
+ if (prevRepairEnd != null) {
|
|
|
+ // 故障间隔 = 上次维修结束 -> 下次报修
|
|
|
+ double hours = ChronoUnit.MINUTES.between(prevRepairEnd, order.getApplyTime()) / 60.0;
|
|
|
+ if (hours < 0) {
|
|
|
+ // 数据异常兑底(报修时间早于上次维修结束),间隔计 0
|
|
|
+ hours = 0;
|
|
|
+ }
|
|
|
+ totalHours += hours;
|
|
|
+ intervals++;
|
|
|
+ String monthKey = order.getApplyTime().format(MTBF_MONTH_FORMATTER);
|
|
|
+ double[] agg = monthAgg.computeIfAbsent(monthKey, k -> new double[2]);
|
|
|
+ agg[0] += hours;
|
|
|
+ agg[1] += 1;
|
|
|
+ }
|
|
|
+ prevRepairEnd = order.getRepairEndTime();
|
|
|
+ }
|
|
|
+ // 故障不足 2 次无法计算间隔,返回 null 前端显示 "-"
|
|
|
+ vo.setMtbfHours(intervals > 0 ? BigDecimalUtil.decimal(totalHours / intervals, 2) : null);
|
|
|
+ sbList.add(vo);
|
|
|
+ }
|
|
|
+ // 按选择时间段逐月返回趋势
|
|
|
+ List<MtbfMonthVO> monthList = new ArrayList<>();
|
|
|
+ for (YearMonth m = start; !m.isAfter(end); m = m.plusMonths(1)) {
|
|
|
+ String key = m.format(MTBF_MONTH_FORMATTER);
|
|
|
+ double[] agg = monthAgg.get(key);
|
|
|
+ MtbfMonthVO monthVO = new MtbfMonthVO().setMonth(key);
|
|
|
+ if (agg == null || agg[1] == 0) {
|
|
|
+ monthVO.setNum(0).setMtbfHours(null);
|
|
|
+ } else {
|
|
|
+ monthVO.setNum((int) agg[1]).setMtbfHours(BigDecimalUtil.decimal(agg[0] / agg[1], 2));
|
|
|
+ }
|
|
|
+ monthList.add(monthVO);
|
|
|
+ }
|
|
|
+ // 按设备列表:MTBF 升序(间隔越短越不稳定排前),无值排最后
|
|
|
+ sbList.sort(Comparator.comparing(MtbfReportVO::getMtbfHours,
|
|
|
+ Comparator.nullsLast(Comparator.naturalOrder())));
|
|
|
+ return new MtbfResultVO().setSbList(sbList).setMonthList(monthList);
|
|
|
+ }
|
|
|
}
|