吴羡
2026-10-24 3d397976d08f0359b8cfeb4e4f22403ae5d0c04f
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
@@ -20,12 +20,14 @@
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;
@@ -38,6 +40,7 @@
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;
@@ -45,6 +48,7 @@
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;
@@ -86,6 +90,14 @@
    private DeviceExamineRecordMapper deviceExamineRecordMapper;
    @Resource
    private DeviceExamineRecordContrastMapper deviceExamineRecordContrastMapper;
    @Resource
    private DeviceExamineAttachmentMapper deviceExamineAttachmentMapper;
    @Value("${file.path}")
    private String filePath;
    @Value("${wordUrl}")
    private String wordUrl;
    /**
     * 新增设备核查计划
@@ -415,12 +427,8 @@
        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;
@@ -513,26 +521,22 @@
    }
    /**
     * 计划明细的核查时间可能是 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("请选择核查设备");
        }
@@ -541,7 +545,16 @@
            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)
@@ -555,7 +568,7 @@
            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);
@@ -568,6 +581,9 @@
        if (nearest != null) {
            return nearest;
        }
        if (!createIfAbsent) {
            return null;
        }
        DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails();
        detail.setPlanId(plan.getPlanId());
        detail.setDeviceId(device.getId());
@@ -577,7 +593,8 @@
        }
        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;
    }
@@ -596,6 +613,23 @@
            }
        }
        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);
    }
    /**
@@ -620,9 +654,9 @@
            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;
@@ -636,6 +670,10 @@
        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()
@@ -643,6 +681,162 @@
        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;
    }
}