cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
@@ -22,10 +22,14 @@
import com.ruoyi.device.excel.upload.DeviceExaminePlanUpload;
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.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;
@@ -45,7 +49,11 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDate;
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;
@@ -74,6 +82,10 @@
    private DeviceMapper deviceMapper;
    @Resource
    private UserMapper userMapper;
    @Resource
    private DeviceExamineRecordMapper deviceExamineRecordMapper;
    @Resource
    private DeviceExamineRecordContrastMapper deviceExamineRecordContrastMapper;
    /**
     * 新增设备核查计划
@@ -346,4 +358,291 @@
        return deviceExaminePlanDto;
    }
    /**
     * 按年份查询设备核查计划(不存在则自动创建),并自动补全已到点的设备明细
     */
    @Override
    public DeviceExaminePlanDto getOrCreatePlanByYear(String planYear) {
        if (StringUtils.isBlank(planYear)) {
            planYear = String.valueOf(LocalDate.now().getYear());
        }
        DeviceExaminePlan plan = getOrCreatePlanEntity(planYear);
        ensureExamineDetails(plan);
        DeviceExaminePlanDto planDto = new DeviceExaminePlanDto();
        BeanUtils.copyProperties(plan, planDto);
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        planDto.setWriteTimeStr(plan.getWriteTime() == null ? null : plan.getWriteTime().format(dateFormatter));
        planDto.setRatifyTimeStr(plan.getRatifyTime() == null ? null : plan.getRatifyTime().format(dateFormatter));
        if (plan.getWriteUserId() != null) {
            User writeUser = userMapper.selectById(plan.getWriteUserId());
            if (writeUser != null) {
                planDto.setWriteName(writeUser.getName());
            }
        }
        if (plan.getRatifyUserId() != null) {
            User ratifyUser = userMapper.selectById(plan.getRatifyUserId());
            if (ratifyUser != null) {
                planDto.setRatifyName(ratifyUser.getName());
            }
        }
        return planDto;
    }
    /**
     * 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细
     */
    @Override
    public void syncDeviceExamineDetail(Device device) {
        LocalDate due = calcExamineDueDate(device);
        if (due == null) {
            return;
        }
        DeviceExaminePlan plan = getOrCreatePlanEntity(String.valueOf(due.getYear()));
        fillExamineDetailIfAbsent(plan, device);
    }
    /**
     * 复核通过后,把设备档案的最近/下次核查日期滚动到本轮
     */
    @Override
    public void updateDeviceExamineDate(Integer planDetailsId) {
        DeviceExaminePlanDetails detail = deviceExaminePlanDetailsMapper.selectById(planDetailsId);
        if (detail == null || detail.getDeviceId() == null) {
            return;
        }
        Device device = deviceMapper.selectById(detail.getDeviceId());
        if (device == null) {
            return;
        }
        // 提前核查时计划月份可能还没到,取实际完成当天;否则按本轮计划核查月份
        LocalDate checkDate = parseCheckDate(detail.getCheckTime());
        if (checkDate.isAfter(LocalDate.now())) {
            checkDate = LocalDate.now();
        }
        LocalDateTime checkDateTime = checkDate.atStartOfDay();
        // 只允许向前滚动,避免重复复核旧记录时把日期改回去
        if (device.getLastExamineDate() != null && !checkDateTime.isAfter(device.getLastExamineDate())) {
            return;
        }
        device.setLastExamineDate(checkDateTime);
        if (StringUtils.isNotBlank(device.getExamineCycle())) {
            try {
                device.setNextExamineDate(checkDateTime.plusMonths(Long.parseLong(device.getExamineCycle().trim())));
            } catch (NumberFormatException ignored) {
            }
        }
        deviceMapper.updateById(device);
        // 本轮核查完成后立即登记下一轮,历史明细和记录保持不动
        syncDeviceExamineDetail(device);
    }
    /**
     * 查询年份对应的计划,不存在则创建
     */
    private DeviceExaminePlan getOrCreatePlanEntity(String planYear) {
        DeviceExaminePlan plan = baseMapper.selectOne(Wrappers.<DeviceExaminePlan>lambdaQuery()
                .eq(DeviceExaminePlan::getPlanYear, planYear)
                .orderByDesc(DeviceExaminePlan::getPlanId)
                .last("limit 1"));
        if (plan == null) {
            plan = new DeviceExaminePlan();
            plan.setPlanName(planYear + "年度设备核查计划");
            plan.setPlanYear(planYear);
            plan.setWriteUserId(SecurityUtils.getUserId().intValue());
            plan.setWriteTime(LocalDateTime.now());
            baseMapper.insert(plan);
        }
        return plan;
    }
    /**
     * 扫描所有设备,把本年度及往年逾期未核查的设备补进计划
     */
    private void ensureExamineDetails(DeviceExaminePlan plan) {
        List<Device> devices = deviceMapper.selectList(null);
        for (Device device : devices) {
            fillExamineDetailIfAbsent(plan, device);
        }
    }
    /**
     * 设备本轮应核查日期(没填下次日期时用最近日期+核查周期兜底)
     */
    private LocalDate calcExamineDueDate(Device device) {
        LocalDateTime base = device.getNextExamineDate();
        if (base == null && device.getLastExamineDate() != null && StringUtils.isNotBlank(device.getExamineCycle())) {
            try {
                base = device.getLastExamineDate().plusMonths(Long.parseLong(device.getExamineCycle().trim()));
            } catch (NumberFormatException ignored) {
            }
        }
        return base == null ? null : base.toLocalDate();
    }
    /**
     * 计划中尚无该设备本轮明细时自动补一条;核查时间取应核查月份
     */
    private void fillExamineDetailIfAbsent(DeviceExaminePlan plan, Device device) {
        LocalDate due = calcExamineDueDate(device);
        if (due == null) {
            return;
        }
        if (plan.getPlanYear() != null && String.valueOf(due.getYear()).compareTo(plan.getPlanYear()) > 0) {
            return;
        }
        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())
                .likeRight(DeviceExaminePlanDetails::getCheckTime, checkTime));
        if (exist != null && exist > 0) {
            return;
        }
        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());
        detail.setCheckTime(checkTime);
        deviceExaminePlanDetailsMapper.insert(detail);
    }
    /**
     * 计划明细的核查时间可能是 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) {
        if (deviceId == null) {
            throw new ErrorException("请选择核查设备");
        }
        Device device = deviceMapper.selectById(deviceId);
        if (device == null) {
            throw new ErrorException("核查设备不存在");
        }
        String checkMonth = normalizeCheckMonth(checkTime);
        DeviceExaminePlan plan = getOrCreatePlanEntity(checkMonth.substring(0, 4));
        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 (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;
        }
        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());
        detail.setCheckTime(checkMonth);
        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"));
    }
    /**
     * 两个月份之间相差的月数
     */
    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 checkMonth = normalizeCheckMonth(detail.getCheckTime());
            detail.setCheckTime(checkMonth);
            DeviceExaminePlan plan = getOrCreatePlanEntity(checkMonth.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");
        }
        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("该计划已存在核查记录,为保留历史记录不允许删除");
        }
        return deviceExaminePlanDetailsService.removeById(planDetailsId);
    }
}