吴羡
2026-10-24 3d397976d08f0359b8cfeb4e4f22403ae5d0c04f
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
@@ -20,12 +20,18 @@
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;
@@ -34,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;
@@ -41,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;
@@ -49,6 +57,7 @@
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;
@@ -77,6 +86,18 @@
    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;
    /**
     * 新增设备核查计划
@@ -381,15 +402,15 @@
    }
    /**
     * 设备档案变更后,把设备同步到对应年份的设备核查计划明细
     * 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细
     */
    @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);
    }
@@ -406,11 +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;
@@ -423,6 +441,8 @@
            }
        }
        deviceMapper.updateById(device);
        // 本轮核查完成后立即登记下一轮,历史明细和记录保持不动
        syncDeviceExamineDetail(device);
    }
    /**
@@ -445,7 +465,7 @@
    }
    /**
     * 扫描所有设备,把已到点(下次核查日期前1个月起,含已过期)的设备补进本年度计划
     * 扫描所有设备,把本年度及往年逾期未核查的设备补进计划
     */
    private void ensureExamineDetails(DeviceExaminePlan plan) {
        List<Device> devices = deviceMapper.selectList(null);
@@ -455,9 +475,9 @@
    }
    /**
     * 设备提醒起点:下次核查日期前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 {
@@ -465,21 +485,21 @@
            } 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())
@@ -501,9 +521,256 @@
    }
    /**
     * 计划明细的核查时间可能是 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) {
@@ -513,6 +780,63 @@
                }
            }
        }
        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;
    }
}