| | |
| | | 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.device.pojo.DeviceExamineRecordContrast; |
| | | import com.ruoyi.device.service.DeviceExaminePlanDetailsService; |
| | | import com.ruoyi.device.service.DeviceExaminePlanService; |
| | | import com.ruoyi.framework.exception.ErrorException; |
| | |
| | | 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; |
| | |
| | | import java.time.LocalDateTime; |
| | | import java.time.YearMonth; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.temporal.ChronoUnit; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | |
| | | private DeviceMapper deviceMapper; |
| | | @Resource |
| | | private UserMapper userMapper; |
| | | @Resource |
| | | private DeviceExamineRecordMapper deviceExamineRecordMapper; |
| | | @Resource |
| | | private DeviceExamineRecordContrastMapper deviceExamineRecordContrastMapper; |
| | | @Resource |
| | | private DeviceExamineAttachmentMapper deviceExamineAttachmentMapper; |
| | | |
| | | @Value("${file.path}") |
| | | private String filePath; |
| | | |
| | | @Value("${wordUrl}") |
| | | private String wordUrl; |
| | | |
| | | /** |
| | | * 新增设备核查计划 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 设备档案变更后,把设备同步到对应年份的设备核查计划明细 |
| | | * 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细 |
| | | */ |
| | | @Override |
| | | public void syncDeviceExamineDetail(Device device) { |
| | | LocalDate remind = calcExamineRemindDate(device); |
| | | if (remind == null || remind.isAfter(LocalDate.now())) { |
| | | LocalDate due = calcExamineDueDate(device); |
| | | if (due == null) { |
| | | return; |
| | | } |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(String.valueOf(remind.getYear())); |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(String.valueOf(due.getYear())); |
| | | fillExamineDetailIfAbsent(plan, device); |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | deviceMapper.updateById(device); |
| | | // 本轮核查完成后立即登记下一轮,历史明细和记录保持不动 |
| | | syncDeviceExamineDetail(device); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 扫描所有设备,把已到点(下次核查日期前1个月起,含已过期)的设备补进本年度计划 |
| | | * 扫描所有设备,把本年度及往年逾期未核查的设备补进计划 |
| | | */ |
| | | private void ensureExamineDetails(DeviceExaminePlan plan) { |
| | | List<Device> devices = deviceMapper.selectList(null); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 设备提醒起点:下次核查日期前1个月(没填下次日期时用最近日期+核查周期兜底) |
| | | * 设备本轮应核查日期(没填下次日期时用最近日期+核查周期兜底) |
| | | */ |
| | | private LocalDate calcExamineRemindDate(Device device) { |
| | | private LocalDate calcExamineDueDate(Device device) { |
| | | LocalDateTime base = device.getNextExamineDate(); |
| | | if (base == null && device.getLastExamineDate() != null && StringUtils.isNotBlank(device.getExamineCycle())) { |
| | | try { |
| | |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | return base == null ? null : base.toLocalDate().minusMonths(1); |
| | | return base == null ? null : base.toLocalDate(); |
| | | } |
| | | |
| | | /** |
| | | * 已到点且计划中尚无该设备本轮明细时自动补一条 |
| | | * 计划中尚无该设备本轮明细时自动补一条;核查时间取应核查月份 |
| | | */ |
| | | private void fillExamineDetailIfAbsent(DeviceExaminePlan plan, Device device) { |
| | | LocalDate remind = calcExamineRemindDate(device); |
| | | if (remind == null || remind.isAfter(LocalDate.now())) { |
| | | LocalDate due = calcExamineDueDate(device); |
| | | if (due == null) { |
| | | return; |
| | | } |
| | | if (plan.getPlanYear() != null && String.valueOf(remind.getYear()).compareTo(plan.getPlanYear()) > 0) { |
| | | if (plan.getPlanYear() != null && String.valueOf(due.getYear()).compareTo(plan.getPlanYear()) > 0) { |
| | | return; |
| | | } |
| | | String checkTime = remind.format(DateTimeFormatter.ofPattern("yyyy-MM")); |
| | | String checkTime = due.format(DateTimeFormatter.ofPattern("yyyy-MM")); |
| | | Long exist = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery() |
| | | .eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId()) |
| | | .eq(DeviceExaminePlanDetails::getDeviceId, device.getId()) |
| | |
| | | } |
| | | |
| | | /** |
| | | * 计划明细的核查时间可能是 yyyy-MM 或 yyyy-MM-dd,统一取月份首日 |
| | | * 为指定设备找一条可录入核查记录的计划明细;没有可用明细时在当前年度计划新增本轮明细 |
| | | */ |
| | | private LocalDate parseCheckDate(String checkTime) { |
| | | @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("请选择核查设备"); |
| | | } |
| | | Device device = deviceMapper.selectById(deviceId); |
| | | if (device == null) { |
| | | throw new ErrorException("核查设备不存在"); |
| | | } |
| | | String checkMonth = normalizeCheckMonth(checkTime); |
| | | 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) |
| | | .orderByAsc(DeviceExaminePlanDetails::getCheckTime)); |
| | | DeviceExaminePlanDetails nearest = null; |
| | | long nearestDistance = Long.MAX_VALUE; |
| | | for (DeviceExaminePlanDetails detail : details) { |
| | | // 已有核查记录的明细代表已完成的一轮,后续核查永远不复用、不覆盖 |
| | | Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery() |
| | | .eq(DeviceExamineRecord::getPlanDetailsId, detail.getPlanDetailsId())); |
| | | if (recordCount != null && recordCount > 0) { |
| | | continue; |
| | | } |
| | | if (plan != null && detail.getPlanId() != null && detail.getPlanId().equals(plan.getPlanId())) { |
| | | return detail; |
| | | } |
| | | long distance = monthDistance(detail.getCheckTime(), checkMonth); |
| | | if (distance < nearestDistance) { |
| | | nearest = detail; |
| | | nearestDistance = distance; |
| | | } |
| | | } |
| | | // 提前核查时优先复用系统登记、尚未核查的这一轮(可能在别的年份计划里) |
| | | if (nearest != null) { |
| | | return nearest; |
| | | } |
| | | if (!createIfAbsent) { |
| | | return null; |
| | | } |
| | | DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails(); |
| | | detail.setPlanId(plan.getPlanId()); |
| | | detail.setDeviceId(device.getId()); |
| | | String deviceName = device.getDeviceName() == null ? "" : device.getDeviceName(); |
| | | if (StringUtils.isNotBlank(device.getSpecificationModel())) { |
| | | deviceName = deviceName + " " + device.getSpecificationModel(); |
| | | } |
| | | detail.setDeviceName(deviceName); |
| | | detail.setDeviceNumber(device.getManagementNumber()); |
| | | String detailCheckTime = normalizeCheckDate(checkTime); |
| | | detail.setCheckTime(StringUtils.isBlank(detailCheckTime) ? checkMonth : detailCheckTime); |
| | | deviceExaminePlanDetailsMapper.insert(detail); |
| | | return detail; |
| | | } |
| | | |
| | | /** |
| | | * 规范核查月份为 yyyy-MM,非法或为空时取当前月份 |
| | | */ |
| | | private String normalizeCheckMonth(String checkTime) { |
| | | if (StringUtils.isNotBlank(checkTime)) { |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | if (normalized.length() >= 7) { |
| | | try { |
| | | return YearMonth.parse(normalized.substring(0, 7)).toString(); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | } |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | | * 两个月份之间相差的月数 |
| | | */ |
| | | private long monthDistance(String fromCheckTime, String targetMonth) { |
| | | try { |
| | | YearMonth from = YearMonth.parse(normalizeCheckMonth(fromCheckTime)); |
| | | YearMonth target = YearMonth.parse(targetMonth); |
| | | return Math.abs(ChronoUnit.MONTHS.between(from, target)); |
| | | } catch (Exception e) { |
| | | return Long.MAX_VALUE; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 修改计划明细;核查月份跨年时,把明细归到核查月份所属年份的计划下 |
| | | */ |
| | | @Override |
| | | public boolean updateExaminePlanDetail(DeviceExaminePlanDetails detail) { |
| | | if (detail == null || detail.getPlanDetailsId() == null) { |
| | | throw new ErrorException("缺少设备核查计划明细id"); |
| | | } |
| | | if (StringUtils.isNotBlank(detail.getCheckTime())) { |
| | | String checkTime = normalizeCheckDate(detail.getCheckTime()); |
| | | detail.setCheckTime(checkTime); |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(checkTime.substring(0, 4)); |
| | | detail.setPlanId(plan.getPlanId()); |
| | | } |
| | | return deviceExaminePlanDetailsMapper.updateById(detail) > 0; |
| | | } |
| | | |
| | | /** |
| | | * 删除计划明细;已有核查/比对记录的不允许删除,保证历史留档 |
| | | */ |
| | | @Override |
| | | public boolean removeExaminePlanDetail(Integer planDetailsId) { |
| | | 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() |
| | | .eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId)); |
| | | if ((recordCount != null && recordCount > 0) || (contrastCount != null && contrastCount > 0)) { |
| | | throw new ErrorException("该计划已存在核查记录,为保留历史记录不允许删除"); |
| | | } |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | } |
| | | return LocalDate.now(); |
| | | 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; |
| | | } |
| | | } |