| | |
| | | import com.ruoyi.device.dto.DeviceExaminePlanDetailsDto; |
| | | import com.ruoyi.device.dto.DeviceExaminePlanDto; |
| | | import com.ruoyi.device.excel.upload.DeviceExaminePlanUpload; |
| | | import com.ruoyi.device.mapper.DeviceExamineAttachmentMapper; |
| | | import com.ruoyi.device.mapper.DeviceExaminePlanDetailsMapper; |
| | | import com.ruoyi.device.mapper.DeviceExaminePlanMapper; |
| | | import com.ruoyi.device.mapper.DeviceExamineRecordContrastMapper; |
| | | import com.ruoyi.device.mapper.DeviceExamineRecordMapper; |
| | | import com.ruoyi.device.mapper.DeviceMapper; |
| | | import com.ruoyi.device.pojo.Device; |
| | | import com.ruoyi.device.pojo.DeviceExamineAttachment; |
| | | import com.ruoyi.device.pojo.DeviceExaminePlan; |
| | | import com.ruoyi.device.pojo.DeviceExaminePlanDetails; |
| | | import com.ruoyi.device.pojo.DeviceExamineRecord; |
| | |
| | | import com.ruoyi.system.mapper.UserMapper; |
| | | import com.ruoyi.system.service.InformationNotificationService; |
| | | import org.springframework.beans.BeanUtils; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | |
| | | import jakarta.annotation.Resource; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | |
| | | private DeviceExamineRecordMapper deviceExamineRecordMapper; |
| | | @Resource |
| | | private DeviceExamineRecordContrastMapper deviceExamineRecordContrastMapper; |
| | | @Resource |
| | | private DeviceExamineAttachmentMapper deviceExamineAttachmentMapper; |
| | | |
| | | @Value("${file.path}") |
| | | private String filePath; |
| | | |
| | | @Value("${wordUrl}") |
| | | private String wordUrl; |
| | | |
| | | /** |
| | | * 新增设备核查计划 |
| | |
| | | if (device == null) { |
| | | return; |
| | | } |
| | | // 提前核查时计划月份可能还没到,取实际完成当天;否则按本轮计划核查月份 |
| | | LocalDate checkDate = parseCheckDate(detail.getCheckTime()); |
| | | if (checkDate.isAfter(LocalDate.now())) { |
| | | checkDate = LocalDate.now(); |
| | | } |
| | | LocalDateTime checkDateTime = checkDate.atStartOfDay(); |
| | | // 审核通过当天即视为实际核查完成时间 |
| | | LocalDateTime checkDateTime = LocalDate.now().atStartOfDay(); |
| | | // 只允许向前滚动,避免重复复核旧记录时把日期改回去 |
| | | if (device.getLastExamineDate() != null && !checkDateTime.isAfter(device.getLastExamineDate())) { |
| | | return; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 计划明细的核查时间可能是 yyyy-MM 或 yyyy-MM-dd,统一取月份首日 |
| | | */ |
| | | private LocalDate parseCheckDate(String checkTime) { |
| | | if (StringUtils.isNotBlank(checkTime)) { |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | if (normalized.length() >= 7) { |
| | | try { |
| | | return YearMonth.parse(normalized.substring(0, 7)).atDay(1); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | } |
| | | return LocalDate.now(); |
| | | } |
| | | |
| | | /** |
| | | * 为指定设备找一条可录入核查记录的计划明细;没有可用明细时在当前年度计划新增本轮明细 |
| | | */ |
| | | @Override |
| | | public DeviceExaminePlanDetails resolveExaminePlanDetail(Integer deviceId, String checkTime) { |
| | | return findExaminePlanDetail(deviceId, checkTime, true); |
| | | } |
| | | |
| | | /** |
| | | * 查找指定设备可复用的计划明细;只查询,不新增计划或明细 |
| | | */ |
| | | @Override |
| | | public DeviceExaminePlanDetails findExaminePlanDetail(Integer deviceId, String checkTime) { |
| | | return findExaminePlanDetail(deviceId, checkTime, false); |
| | | } |
| | | |
| | | private DeviceExaminePlanDetails findExaminePlanDetail(Integer deviceId, String checkTime, boolean createIfAbsent) { |
| | | if (deviceId == null) { |
| | | throw new ErrorException("请选择核查设备"); |
| | | } |
| | |
| | | throw new ErrorException("核查设备不存在"); |
| | | } |
| | | String checkMonth = normalizeCheckMonth(checkTime); |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(checkMonth.substring(0, 4)); |
| | | String planYear = checkMonth.substring(0, 4); |
| | | DeviceExaminePlan plan; |
| | | if (createIfAbsent) { |
| | | plan = getOrCreatePlanEntity(planYear); |
| | | } else { |
| | | plan = baseMapper.selectOne(Wrappers.<DeviceExaminePlan>lambdaQuery() |
| | | .eq(DeviceExaminePlan::getPlanYear, planYear) |
| | | .orderByDesc(DeviceExaminePlan::getPlanId) |
| | | .last("limit 1")); |
| | | } |
| | | List<DeviceExaminePlanDetails> details = deviceExaminePlanDetailsMapper.selectList( |
| | | Wrappers.<DeviceExaminePlanDetails>lambdaQuery() |
| | | .eq(DeviceExaminePlanDetails::getDeviceId, deviceId) |
| | |
| | | if (recordCount != null && recordCount > 0) { |
| | | continue; |
| | | } |
| | | if (detail.getPlanId() != null && detail.getPlanId().equals(plan.getPlanId())) { |
| | | if (plan != null && detail.getPlanId() != null && detail.getPlanId().equals(plan.getPlanId())) { |
| | | return detail; |
| | | } |
| | | long distance = monthDistance(detail.getCheckTime(), checkMonth); |
| | |
| | | if (nearest != null) { |
| | | return nearest; |
| | | } |
| | | if (!createIfAbsent) { |
| | | return null; |
| | | } |
| | | DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails(); |
| | | detail.setPlanId(plan.getPlanId()); |
| | | detail.setDeviceId(device.getId()); |
| | |
| | | } |
| | | detail.setDeviceName(deviceName); |
| | | detail.setDeviceNumber(device.getManagementNumber()); |
| | | detail.setCheckTime(checkMonth); |
| | | String detailCheckTime = normalizeCheckDate(checkTime); |
| | | detail.setCheckTime(StringUtils.isBlank(detailCheckTime) ? checkMonth : detailCheckTime); |
| | | deviceExaminePlanDetailsMapper.insert(detail); |
| | | return detail; |
| | | } |
| | |
| | | } |
| | | } |
| | | return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); |
| | | } |
| | | |
| | | /** |
| | | * 规范核查日期:带具体日期的保留到日,只填月份的规范为 yyyy-MM |
| | | */ |
| | | private String normalizeCheckDate(String checkTime) { |
| | | if (StringUtils.isBlank(checkTime)) { |
| | | return null; |
| | | } |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | if (normalized.length() >= 10) { |
| | | try { |
| | | return LocalDate.parse(normalized.substring(0, 10)).toString(); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | return normalizeCheckMonth(normalized); |
| | | } |
| | | |
| | | /** |
| | |
| | | throw new ErrorException("缺少设备核查计划明细id"); |
| | | } |
| | | if (StringUtils.isNotBlank(detail.getCheckTime())) { |
| | | String checkMonth = normalizeCheckMonth(detail.getCheckTime()); |
| | | detail.setCheckTime(checkMonth); |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(checkMonth.substring(0, 4)); |
| | | String checkTime = normalizeCheckDate(detail.getCheckTime()); |
| | | detail.setCheckTime(checkTime); |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(checkTime.substring(0, 4)); |
| | | detail.setPlanId(plan.getPlanId()); |
| | | } |
| | | return deviceExaminePlanDetailsMapper.updateById(detail) > 0; |
| | |
| | | if (planDetailsId == null) { |
| | | throw new ErrorException("缺少设备核查计划明细id"); |
| | | } |
| | | DeviceExaminePlanDetails detail = deviceExaminePlanDetailsMapper.selectById(planDetailsId); |
| | | if (detail == null) { |
| | | return false; |
| | | } |
| | | Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery() |
| | | .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId)); |
| | | Long contrastCount = deviceExamineRecordContrastMapper.selectCount(Wrappers.<DeviceExamineRecordContrast>lambdaQuery() |
| | |
| | | if ((recordCount != null && recordCount > 0) || (contrastCount != null && contrastCount > 0)) { |
| | | throw new ErrorException("该计划已存在核查记录,为保留历史记录不允许删除"); |
| | | } |
| | | return deviceExaminePlanDetailsService.removeById(planDetailsId); |
| | | boolean removed = deviceExaminePlanDetailsService.removeById(planDetailsId); |
| | | if (removed && detail.getDeviceId() != null) { |
| | | rollbackDeviceExamineDate(detail); |
| | | } |
| | | return removed; |
| | | } |
| | | |
| | | /** |
| | | * 删除未审核的计划后,把设备档案恢复到最近一条审核通过的记录; |
| | | * 没有已完成记录时,才清空档案的核查日期和周期 |
| | | */ |
| | | private void rollbackDeviceExamineDate(DeviceExaminePlanDetails removedDetail) { |
| | | Device device = deviceMapper.selectById(removedDetail.getDeviceId()); |
| | | if (device == null) { |
| | | return; |
| | | } |
| | | Long remainCount = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery() |
| | | .eq(DeviceExaminePlanDetails::getDeviceId, removedDetail.getDeviceId())); |
| | | boolean isCurrentRound = sameExamineMonth(removedDetail.getCheckTime(), device.getNextExamineDate()); |
| | | // 删除的不是档案当前登记的那一轮,且设备还有其他计划时,不动档案 |
| | | if (!isCurrentRound && remainCount != null && remainCount > 0) { |
| | | return; |
| | | } |
| | | LocalDate lastDone = null; |
| | | List<DeviceExaminePlanDetails> details = deviceExaminePlanDetailsMapper.selectList( |
| | | Wrappers.<DeviceExaminePlanDetails>lambdaQuery() |
| | | .eq(DeviceExaminePlanDetails::getDeviceId, removedDetail.getDeviceId())); |
| | | for (DeviceExaminePlanDetails item : details) { |
| | | if (!hasPassedExamineRecord(item.getPlanDetailsId())) { |
| | | continue; |
| | | } |
| | | LocalDate date = parseCheckMonth(item.getCheckTime()); |
| | | if (date != null && (lastDone == null || date.isAfter(lastDone))) { |
| | | lastDone = date; |
| | | } |
| | | } |
| | | if (lastDone == null) { |
| | | deviceMapper.update(null, Wrappers.<Device>lambdaUpdate() |
| | | .eq(Device::getId, removedDetail.getDeviceId()) |
| | | .set(Device::getLastExamineDate, null) |
| | | .set(Device::getNextExamineDate, null) |
| | | .set(Device::getExamineCycle, null)); |
| | | return; |
| | | } |
| | | LocalDateTime last = lastDone.atStartOfDay(); |
| | | LocalDateTime next = null; |
| | | if (StringUtils.isNotBlank(device.getExamineCycle())) { |
| | | try { |
| | | next = last.plusMonths(Long.parseLong(device.getExamineCycle().trim())); |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | deviceMapper.update(null, Wrappers.<Device>lambdaUpdate() |
| | | .eq(Device::getId, removedDetail.getDeviceId()) |
| | | .set(Device::getLastExamineDate, last) |
| | | .set(Device::getNextExamineDate, next)); |
| | | } |
| | | |
| | | /** |
| | | * 明细下是否存在审核通过的核查记录或比对记录 |
| | | */ |
| | | private boolean hasPassedExamineRecord(Integer planDetailsId) { |
| | | Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery() |
| | | .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId) |
| | | .eq(DeviceExamineRecord::getReviewStatus, 1)); |
| | | if (recordCount != null && recordCount > 0) { |
| | | return true; |
| | | } |
| | | Long contrastCount = deviceExamineRecordContrastMapper.selectCount(Wrappers.<DeviceExamineRecordContrast>lambdaQuery() |
| | | .eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId) |
| | | .eq(DeviceExamineRecordContrast::getReviewStatus, 1)); |
| | | return contrastCount != null && contrastCount > 0; |
| | | } |
| | | |
| | | /** |
| | | * 计划明细的核查月份是否就是档案下次核查日期所在月份 |
| | | */ |
| | | private boolean sameExamineMonth(String checkTime, LocalDateTime nextExamineDate) { |
| | | if (StringUtils.isBlank(checkTime) || nextExamineDate == null) { |
| | | return false; |
| | | } |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | return normalized.length() >= 7 |
| | | && normalized.substring(0, 7).equals(nextExamineDate.format(DateTimeFormatter.ofPattern("yyyy-MM"))); |
| | | } |
| | | |
| | | /** |
| | | * 解析明细核查月份为当月首日,格式不合法时返回 null |
| | | */ |
| | | private LocalDate parseCheckMonth(String checkTime) { |
| | | if (StringUtils.isNotBlank(checkTime)) { |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | if (normalized.length() >= 7) { |
| | | try { |
| | | return YearMonth.parse(normalized.substring(0, 7)).atDay(1); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public List<DeviceExamineAttachment> selectAttachments(Integer planDetailsId) { |
| | | if (planDetailsId == null) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return deviceExamineAttachmentMapper.selectList(Wrappers.<DeviceExamineAttachment>lambdaQuery() |
| | | .eq(DeviceExamineAttachment::getPlanDetailsId, planDetailsId) |
| | | .orderByAsc(DeviceExamineAttachment::getSort, DeviceExamineAttachment::getId)); |
| | | } |
| | | |
| | | @Override |
| | | public DeviceExamineAttachment uploadAttachment(Integer planDetailsId, MultipartFile file) { |
| | | if (planDetailsId == null) { |
| | | throw new ErrorException("缺少核查计划明细id"); |
| | | } |
| | | if (deviceExaminePlanDetailsMapper.selectById(planDetailsId) == null) { |
| | | throw new ErrorException("核查计划明细不存在"); |
| | | } |
| | | if (file == null || file.isEmpty()) { |
| | | throw new ErrorException("附件不能为空"); |
| | | } |
| | | // 系统全局上传上限为200MB,这里提前给出友好提示 |
| | | if (file.getSize() > 200L * 1024 * 1024) { |
| | | throw new ErrorException("附件不能超过200MB"); |
| | | } |
| | | String originalName = file.getOriginalFilename(); |
| | | if (originalName == null || originalName.trim().isEmpty()) { |
| | | originalName = "未命名文件"; |
| | | } |
| | | // 个别浏览器可能带上路径前缀,只保留文件名本身 |
| | | originalName = originalName.replaceAll(".*[/\\\\]", ""); |
| | | try { |
| | | // 图片放图片目录(/img 静态路径),其余任意格式放 word 目录(/word 静态路径) |
| | | boolean image = originalName.matches("(?i).+\\.(jpg|jpeg|png|gif)$"); |
| | | File directory = new File(image ? filePath : wordUrl); |
| | | if (!directory.exists()) { |
| | | directory.mkdirs(); |
| | | } |
| | | String fileUrl = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")) + "-" + originalName; |
| | | file.transferTo(new File(directory, fileUrl)); |
| | | DeviceExamineAttachment attachment = new DeviceExamineAttachment(); |
| | | attachment.setPlanDetailsId(planDetailsId); |
| | | attachment.setFileName(originalName); |
| | | attachment.setFileUrl(fileUrl); |
| | | attachment.setSort(selectAttachments(planDetailsId).size()); |
| | | deviceExamineAttachmentMapper.insert(attachment); |
| | | return attachment; |
| | | } catch (IOException e) { |
| | | throw new ErrorException("附件上传失败"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public boolean deleteAttachment(Long id) { |
| | | return deviceExamineAttachmentMapper.deleteById(id) > 0; |
| | | } |
| | | } |