cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceMaintenancePlanServiceImpl.java
@@ -2,6 +2,7 @@
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -16,29 +17,48 @@
import com.ruoyi.common.utils.WxCpUtils;
import com.ruoyi.device.dto.DeviceMaintenancePlanDetailsDto;
import com.ruoyi.device.dto.DeviceMaintenancePlanDto;
import com.ruoyi.device.mapper.DeviceMaintenanceAttachmentMapper;
import com.ruoyi.device.mapper.DeviceMaintenanceMapper;
import com.ruoyi.device.mapper.DeviceMaintenancePlanDetailsMapper;
import com.ruoyi.device.mapper.DeviceMaintenancePlanMapper;
import com.ruoyi.device.mapper.DeviceMapper;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.pojo.DeviceMaintenanceAttachment;
import com.ruoyi.device.pojo.DeviceMaintenancePlan;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import com.ruoyi.device.service.DeviceMaintenancePlanDetailsService;
import com.ruoyi.device.service.DeviceMaintenancePlanService;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.UserUtils;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
import org.apache.commons.collections4.CollectionUtils;
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 org.springframework.web.multipart.MultipartFile;
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.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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -50,6 +70,7 @@
 * @since 2024-12-16 06:10:52
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class DeviceMaintenancePlanServiceImpl extends ServiceImpl<DeviceMaintenancePlanMapper, DeviceMaintenancePlan> implements DeviceMaintenancePlanService {
    @Resource
@@ -62,6 +83,18 @@
    private InformationNotificationService informationNotificationService;
    @Resource
    private UserMapper userMapper;
    @Resource
    private DeviceMapper deviceMapper;
    @Resource
    private DeviceMaintenanceMapper deviceMaintenanceMapper;
    @Resource
    private DeviceMaintenanceAttachmentMapper deviceMaintenanceAttachmentMapper;
    @Value("${file.path}")
    private String filePath;
    @Value("${wordUrl}")
    private String wordUrl;
    /**
     * 分页查询设备保养计划
@@ -124,13 +157,22 @@
        }
        this.saveOrUpdate(deviceMaintenancePlanDto);
        // 删除原本的详情
        // 删除原本的详情;明细重建后仍保留原明细id的附件不清理,id丢失的才清理
        List<DeviceMaintenancePlanDetails> oldDetails = deviceMaintenancePlanDetailsService.list(
                Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                        .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        deviceMaintenancePlanDetailsService.remove(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery().eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        List<Integer> lostDetailIds = oldDetails.stream().map(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId)
                .collect(Collectors.toList());
        // 详情赋值并保存
        List<DeviceMaintenancePlanDetailsDto> deviceMaintenancePlanDetails = deviceMaintenancePlanDto.getDeviceMaintenancePlanDetails();
        if (CollectionUtils.isNotEmpty(deviceMaintenancePlanDetails)) { // 详情不为空
            List<Integer> keptDetailIds = deviceMaintenancePlanDetails.stream()
                    .map(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId)
                    .filter(id -> id != null)
                    .collect(Collectors.toList());
            lostDetailIds.removeAll(keptDetailIds);
            List<DeviceMaintenancePlanDetails> collect = deviceMaintenancePlanDetails.stream().map(deviceMaintenancePlanDetail -> { // 遍历详情
                deviceMaintenancePlanDetail.setDeviceId(deviceMaintenancePlanDto.getDeviceId()); // 设备ID
                deviceMaintenancePlanDetail.setMaintenancePlanId(deviceMaintenancePlanDto.getMaintenancePlanId()); // 保养计划ID
                DeviceMaintenancePlanDetails planDetails = new DeviceMaintenancePlanDetails();
                BeanUtils.copyProperties(deviceMaintenancePlanDetail, planDetails);
@@ -138,6 +180,7 @@
            }).collect(Collectors.toList());
            deviceMaintenancePlanDetailsService.saveBatch(collect);
        }
        removeAttachmentsByDetailIds(lostDetailIds);
        return Result.success();
    }
@@ -148,8 +191,12 @@
     */
    @Override
    public Result deleteMaintenancePlan(DeviceMaintenancePlanDto deviceMaintenancePlanDto) {
        List<DeviceMaintenancePlanDetails> details = deviceMaintenancePlanDetailsService.list(
                Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                        .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        this.removeById(deviceMaintenancePlanDto);
        deviceMaintenancePlanDetailsService.remove(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery().eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        removeAttachmentsByDetailIds(details.stream().map(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId).collect(Collectors.toList()));
        return Result.success();
    }
@@ -164,27 +211,27 @@
        // 查询设备保养计划
        DeviceMaintenancePlanDto deviceMaintenancePlan = baseMapper.selectMaintenancePlanById(maintenancePlanId);
        // 查询设备保养计划详情
        List<DeviceMaintenancePlanDetailsDto> deviceMaintenancePlanDetailsDtoList = deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId);
        // 查询设备保养计划详情,按“设备 + 保养月份”兜底去重
        List<DeviceMaintenancePlanDetailsDto> deviceMaintenancePlanDetailsDtoList = dedupeMaintenanceDetails(
                deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId));
        // 设置序号
        deviceMaintenancePlanDetailsDtoList.forEach(deviceInspectionRecordDetails -> {
            deviceInspectionRecordDetails.setIndex(deviceMaintenancePlanDetailsDtoList.indexOf(deviceInspectionRecordDetails) + 1);
        });
        for (int i = 0; i < deviceMaintenancePlanDetailsDtoList.size(); i++) {
            deviceMaintenancePlanDetailsDtoList.get(i).setIndex(i + 1);
        }
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/maintenance-plan.docx");
        Configure configure = Configure.builder()
                .bind("deviceMaintenancePlanDetailsDtoList", new HackLoopTableRenderPolicy())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("deviceMaintenancePlan", deviceMaintenancePlan);
                    put("deviceMaintenancePlanDetailsDtoList", deviceMaintenancePlanDetailsDtoList);
                    // 编制人签名地址
                    put("compilerUrl", UserUtils.getFinalUserSignatureUrl(deviceMaintenancePlan.getCompilerId()));
                    // 审核人签名地址
                    put("auditUrl", UserUtils.getFinalUserSignatureUrl(deviceMaintenancePlan.getAuditId()));
                }});
        Map<String, Object> renderData = new HashMap<>();
        renderData.put("deviceMaintenancePlan", deviceMaintenancePlan);
        renderData.put("deviceMaintenancePlanDetailsDtoList", deviceMaintenancePlanDetailsDtoList);
        // 编制人签名地址
        renderData.put("compilerUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceMaintenancePlan.getCompilerId()));
        // 审核人签名地址
        renderData.put("auditUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceMaintenancePlan.getAuditId()));
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(renderData);
        try {
            response.setContentType("application/msword");
@@ -216,7 +263,8 @@
        // 查询详情
        DeviceMaintenancePlanDto deviceMaintenancePlanDto = new DeviceMaintenancePlanDto();
        BeanUtils.copyProperties(deviceMaintenancePlan, deviceMaintenancePlanDto);
        deviceMaintenancePlanDto.setDeviceMaintenancePlanDetails(deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId));
        deviceMaintenancePlanDto.setDeviceMaintenancePlanDetails(dedupeMaintenanceDetails(
                deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId)));
        return Result.success(deviceMaintenancePlanDto);
    }
@@ -288,5 +336,456 @@
        return Result.success();
    }
    /**
     * 按年份查询设备保养计划(不存在则自动创建),并自动补全已到点的设备明细
     */
    @Override
    public DeviceMaintenancePlanDto getOrCreatePlanByYear(String planYear) {
        if (StringUtils.isBlank(planYear)) {
            planYear = String.valueOf(LocalDate.now().getYear());
        }
        DeviceMaintenancePlan plan = getOrCreatePlanEntity(planYear);
        ensureMaintenanceDetails(plan);
        DeviceMaintenancePlanDto planDto = new DeviceMaintenancePlanDto();
        BeanUtils.copyProperties(plan, planDto);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        planDto.setDatePreparationStr(plan.getDatePreparation() == null ? null : plan.getDatePreparation().format(formatter));
        planDto.setAuditDateStr(plan.getAuditDate() == null ? null : plan.getAuditDate().format(formatter));
        if (StringUtils.isBlank(planDto.getCompiler()) && plan.getCompilerId() != null) {
            User compiler = userMapper.selectById(plan.getCompilerId());
            if (compiler != null) {
                planDto.setCompiler(compiler.getName());
            }
        }
        if (StringUtils.isBlank(planDto.getAudit()) && plan.getAuditId() != null) {
            User audit = userMapper.selectById(plan.getAuditId());
            if (audit != null) {
                planDto.setAudit(audit.getName());
            }
        }
        return planDto;
    }
    /**
     * 保养计划年份列表:删除没有任何明细的空年份计划后,返回有明细的年份(倒序)
     */
    @Override
    public List<String> getMaintenancePlanYears() {
        // 历史数据自愈:先让已审核的明细按审核完成年份归位,再清理空年份计划
        reclassifyReviewedDetails();
        List<DeviceMaintenancePlan> plans = baseMapper.selectList(null);
        for (DeviceMaintenancePlan plan : plans) {
            Long detailCount = deviceMaintenancePlanDetailsMapper.selectCount(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                    .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, plan.getMaintenancePlanId()));
            if (detailCount == null || detailCount == 0) {
                removeById(plan.getMaintenancePlanId());
            }
        }
        return baseMapper.selectPlanYears();
    }
    /**
     * 设备档案变更后,立即把设备登记到下次保养日期所在年份的设备保养计划明细
     */
    @Override
    public void syncDeviceMaintenanceDetail(Device device) {
        LocalDate due = calcMaintenanceDueDate(device);
        if (due == null) {
            return;
        }
        DeviceMaintenancePlan plan = getOrCreatePlanEntity(String.valueOf(due.getYear()));
        fillMaintenanceDetailIfAbsent(plan, device);
    }
    /**
     * 审核通过后,按审核通过当天滚动设备档案的最近/下次保养日期
     */
    @Override
    public void updateDeviceMaintenanceDate(Integer deviceId, LocalDate maintenanceDate) {
        if (deviceId == null || maintenanceDate == null) {
            return;
        }
        Device device = deviceMapper.selectById(deviceId);
        if (device == null) {
            return;
        }
        LocalDateTime recordDate = maintenanceDate.atStartOfDay();
        // 只允许向前滚动,避免补录旧记录时把日期改回去
        if (device.getLastMaintenanceDate() != null && !recordDate.isAfter(device.getLastMaintenanceDate())) {
            return;
        }
        device.setLastMaintenanceDate(recordDate);
        if (StringUtils.isNotBlank(device.getMaintenanceCycle())) {
            try {
                device.setNextMaintenanceDate(recordDate.plusMonths(Long.parseLong(device.getMaintenanceCycle().trim())));
            } catch (NumberFormatException ignored) {
            }
        }
        deviceMapper.updateById(device);
        // 本轮保养完成后立即登记下一轮,历史明细保持不动
        syncDeviceMaintenanceDetail(device);
    }
    /**
     * 审核通过后,把计划明细归到审核完成年份的计划下;提前完成的明细不再留在原计划年份
     */
    @Override
    public void reclassifyMaintenanceDetailByReviewDate(Integer maintenancePlanDetailId, LocalDate reviewDate) {
        if (maintenancePlanDetailId == null || reviewDate == null) {
            return;
        }
        DeviceMaintenancePlanDetails detail = deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId);
        if (detail == null || detail.getMaintenancePlanId() == null) {
            return;
        }
        DeviceMaintenancePlan sourcePlan = baseMapper.selectById(detail.getMaintenancePlanId());
        if (sourcePlan == null) {
            return;
        }
        String targetYear = String.valueOf(reviewDate.getYear());
        if (StringUtils.isNotBlank(sourcePlan.getPlanYear()) && targetYear.equals(sourcePlan.getPlanYear().trim())) {
            return;
        }
        moveMaintenanceDetailToYear(detail, reviewDate);
    }
    /**
     * 把计划明细挪到审核完成年份的计划下,计划日期按实际完成日期记录
     */
    private void moveMaintenanceDetailToYear(DeviceMaintenancePlanDetails detail, LocalDate reviewDate) {
        DeviceMaintenancePlan targetPlan = getOrCreatePlanEntity(String.valueOf(reviewDate.getYear()));
        DeviceMaintenancePlanDetails update = new DeviceMaintenancePlanDetails();
        update.setMaintenancePlanDetailId(detail.getMaintenancePlanDetailId());
        update.setMaintenancePlanId(targetPlan.getMaintenancePlanId());
        // 计划日期按实际完成日期记录,列表按完成年份排序,不再按原计划日期标红
        update.setMaintenanceDate(reviewDate);
        deviceMaintenancePlanDetailsMapper.updateById(update);
    }
    /**
     * 历史数据自愈:已审核通过但计划年份与审核完成年份不一致的明细,按审核完成年份归位
     */
    private void reclassifyReviewedDetails() {
        List<DeviceMaintenance> records = deviceMaintenanceMapper.selectList(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getReviewStatus, 1)
                .isNotNull(DeviceMaintenance::getMaintenancePlanDetailId)
                .isNotNull(DeviceMaintenance::getReviewTime));
        if (CollectionUtils.isEmpty(records)) {
            return;
        }
        List<Integer> detailIds = records.stream().map(DeviceMaintenance::getMaintenancePlanDetailId)
                .distinct().collect(Collectors.toList());
        Map<Integer, DeviceMaintenancePlanDetails> detailMap = deviceMaintenancePlanDetailsMapper.selectBatchIds(detailIds)
                .stream().collect(Collectors.toMap(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId, item -> item, (a, b) -> a));
        Map<Integer, String> planYears = baseMapper.selectList(null).stream()
                .collect(Collectors.toMap(DeviceMaintenancePlan::getMaintenancePlanId,
                        item -> item.getPlanYear() == null ? "" : item.getPlanYear().trim(), (a, b) -> a));
        for (DeviceMaintenance record : records) {
            DeviceMaintenancePlanDetails detail = detailMap.get(record.getMaintenancePlanDetailId());
            if (detail == null || detail.getMaintenancePlanId() == null) {
                continue;
            }
            String reviewYear = String.valueOf(record.getReviewTime().getYear());
            if (reviewYear.equals(planYears.get(detail.getMaintenancePlanId()))) {
                continue;
            }
            moveMaintenanceDetailToYear(detail, record.getReviewTime().toLocalDate());
        }
    }
    /**
     * 分页查询设备保养计划详情(含到点标红)
     */
    @Override
    public IPage<DeviceMaintenancePlanDetailsDto> pageMaintenancePlanDetail(Page page, Integer planId, String deviceName, String deviceNumber) {
        if (planId == null) {
            return new Page<>();
        }
        return deviceMaintenancePlanDetailsMapper.pageMaintenancePlanDetail(page, planId, deviceName, deviceNumber);
    }
    /**
     * 查询年份对应的计划,不存在则创建
     */
    private DeviceMaintenancePlan getOrCreatePlanEntity(String planYear) {
        DeviceMaintenancePlan plan = baseMapper.selectOne(Wrappers.<DeviceMaintenancePlan>lambdaQuery()
                .eq(DeviceMaintenancePlan::getPlanYear, planYear)
                .orderByDesc(DeviceMaintenancePlan::getMaintenancePlanId)
                .last("limit 1"));
        if (plan == null) {
            plan = new DeviceMaintenancePlan();
            plan.setPlanName(planYear + "年度设备保养计划");
            plan.setPlanYear(planYear);
            Integer userId = SecurityUtils.getUserId().intValue();
            User user = userMapper.selectById(userId);
            plan.setCompilerId(userId);
            plan.setCompiler(user == null ? null : user.getName());
            plan.setDatePreparation(LocalDateTime.now());
            baseMapper.insert(plan);
        }
        return plan;
    }
    /**
     * 扫描所有设备,把应保养日期落在该计划年份的设备补进计划(当前年及以往年度包含逾期未保养的设备)
     */
    private void ensureMaintenanceDetails(DeviceMaintenancePlan plan) {
        List<Device> devices = deviceMapper.selectList(null);
        for (Device device : devices) {
            fillMaintenanceDetailIfAbsent(plan, device);
        }
    }
    /**
     * 设备本轮应保养日期(没填下次日期时用最近日期+保养周期兜底)
     */
    private LocalDate calcMaintenanceDueDate(Device device) {
        LocalDateTime base = device.getNextMaintenanceDate();
        if (base == null && device.getLastMaintenanceDate() != null && StringUtils.isNotBlank(device.getMaintenanceCycle())) {
            try {
                base = device.getLastMaintenanceDate().plusMonths(Long.parseLong(device.getMaintenanceCycle().trim()));
            } catch (NumberFormatException ignored) {
            }
        }
        return base == null ? null : base.toLocalDate();
    }
    /**
     * 计划中尚无该设备本轮明细时自动补一条;保养时间取应保养日期
     */
    private void fillMaintenanceDetailIfAbsent(DeviceMaintenancePlan plan, Device device) {
        LocalDate due = calcMaintenanceDueDate(device);
        if (due == null) {
            return;
        }
        if (StringUtils.isBlank(plan.getPlanYear())) {
            return;
        }
        int planYearValue;
        try {
            planYearValue = Integer.parseInt(plan.getPlanYear().trim());
        } catch (NumberFormatException e) {
            return;
        }
        int dueYear = due.getYear();
        // 未来年份的计划只登记应保养日期就在该年份的设备,避免把往年的旧日期补进未来的计划
        if (planYearValue > LocalDate.now().getYear()) {
            if (dueYear != planYearValue) {
                return;
            }
        } else if (dueYear > planYearValue) {
            return;
        }
        // 同一设备同一应保养月份只保留一条,避免手改过行内日期后刷新重复生成
        LocalDate monthStart = due.withDayOfMonth(1);
        Long exist = deviceMaintenancePlanDetailsMapper.selectCount(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, plan.getMaintenancePlanId())
                .eq(DeviceMaintenancePlanDetails::getDeviceId, device.getId())
                .ge(DeviceMaintenancePlanDetails::getMaintenanceDate, monthStart)
                .lt(DeviceMaintenancePlanDetails::getMaintenanceDate, monthStart.plusMonths(1)));
        if (exist != null && exist > 0) {
            return;
        }
        DeviceMaintenancePlanDetails detail = new DeviceMaintenancePlanDetails();
        detail.setMaintenancePlanId(plan.getMaintenancePlanId());
        detail.setDeviceId(device.getId());
        detail.setMaintenanceIntervals(device.getMaintenanceCycle());
        detail.setMaintenanceDate(due);
        deviceMaintenancePlanDetailsMapper.insert(detail);
    }
    /**
     * 为保养记录找一条可关联的计划明细;没有可用明细时在当前年度计划新增本轮明细
     */
    @Override
    public DeviceMaintenancePlanDetails resolveMaintenancePlanDetail(Integer deviceId, Integer maintenancePlanDetailId, LocalDate maintenanceDate) {
        if (maintenancePlanDetailId != null) {
            DeviceMaintenancePlanDetails detail = deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId);
            if (detail == null) {
                throw new ErrorException("保养计划明细不存在");
            }
            return detail;
        }
        if (deviceId == null) {
            throw new ErrorException("缺少设备id");
        }
        Device device = deviceMapper.selectById(deviceId);
        if (device == null) {
            throw new ErrorException("保养设备不存在");
        }
        LocalDate planDate = maintenanceDate == null ? LocalDate.now() : maintenanceDate;
        DeviceMaintenancePlan plan = getOrCreatePlanEntity(String.valueOf(planDate.getYear()));
        List<DeviceMaintenancePlanDetails> details = deviceMaintenancePlanDetailsMapper.selectList(
                Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                        .eq(DeviceMaintenancePlanDetails::getDeviceId, deviceId)
                        .orderByAsc(DeviceMaintenancePlanDetails::getMaintenanceDate));
        DeviceMaintenancePlanDetails nearest = null;
        long nearestDistance = Long.MAX_VALUE;
        for (DeviceMaintenancePlanDetails detail : details) {
            // 已有保养记录的明细代表已完成的一轮,后续记录永远不复用、不覆盖
            Long recordCount = deviceMaintenanceMapper.selectCount(Wrappers.<DeviceMaintenance>lambdaQuery()
                    .eq(DeviceMaintenance::getMaintenancePlanDetailId, detail.getMaintenancePlanDetailId()));
            if (recordCount != null && recordCount > 0) {
                continue;
            }
            if (detail.getMaintenancePlanId() != null && detail.getMaintenancePlanId().equals(plan.getMaintenancePlanId())) {
                return detail;
            }
            long distance = monthDistance(detail.getMaintenanceDate(), planDate);
            if (distance < nearestDistance) {
                nearest = detail;
                nearestDistance = distance;
            }
        }
        if (nearest != null) {
            return nearest;
        }
        DeviceMaintenancePlanDetails detail = new DeviceMaintenancePlanDetails();
        detail.setMaintenancePlanId(plan.getMaintenancePlanId());
        detail.setDeviceId(device.getId());
        detail.setMaintenanceIntervals(device.getMaintenanceCycle());
        detail.setMaintenanceDate(planDate);
        deviceMaintenancePlanDetailsMapper.insert(detail);
        return detail;
    }
    /**
     * 修改设备保养计划详情(已审核的明细不允许修改)
     */
    @Override
    public boolean updateMaintenancePlanDetail(DeviceMaintenancePlanDetails detail) {
        if (detail == null || detail.getMaintenancePlanDetailId() == null) {
            throw new ErrorException("缺少设备保养计划明细id");
        }
        DeviceMaintenance record = deviceMaintenanceMapper.selectOne(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getMaintenancePlanDetailId, detail.getMaintenancePlanDetailId())
                .last("limit 1"));
        if (record != null && record.getReviewStatus() != null) {
            throw new ErrorException("该计划已审核,不允许修改");
        }
        return deviceMaintenancePlanDetailsMapper.updateById(detail) > 0;
    }
    /**
     * 删除设备保养计划详情;已出审核结果的记录随历史留档,未审核的记录随明细一起删除
     */
    @Override
    public boolean removeMaintenancePlanDetail(Integer maintenancePlanDetailId) {
        if (maintenancePlanDetailId == null) {
            throw new ErrorException("缺少设备保养计划明细id");
        }
        DeviceMaintenancePlanDetails detail = deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId);
        if (detail == null) {
            return false;
        }
        List<DeviceMaintenance> records = deviceMaintenanceMapper.selectList(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getMaintenancePlanDetailId, maintenancePlanDetailId));
        // 审核不通过的记录允许连同明细一起删除;审核通过的记录为历史留档,不允许删除
        boolean hasPassedRecord = records.stream().anyMatch(item -> item.getReviewStatus() != null
                && item.getReviewStatus() == 1);
        if (hasPassedRecord) {
            throw new ErrorException("该计划已存在审核通过的保养记录,为保留历史记录不允许删除");
        }
        if (!records.isEmpty()) {
            List<Integer> recordIds = records.stream().map(DeviceMaintenance::getId).collect(Collectors.toList());
            deviceMaintenanceMapper.deleteBatchIds(recordIds);
        }
        removeAttachmentsByDetailIds(List.of(maintenancePlanDetailId));
        return deviceMaintenancePlanDetailsMapper.deleteById(maintenancePlanDetailId) > 0;
    }
    @Override
    public List<DeviceMaintenanceAttachment> selectAttachments(Integer maintenancePlanDetailId) {
        if (maintenancePlanDetailId == null) {
            return new ArrayList<>();
        }
        return deviceMaintenanceAttachmentMapper.selectList(Wrappers.<DeviceMaintenanceAttachment>lambdaQuery()
                .eq(DeviceMaintenanceAttachment::getMaintenancePlanDetailId, maintenancePlanDetailId)
                .orderByAsc(DeviceMaintenanceAttachment::getSort, DeviceMaintenanceAttachment::getId));
    }
    @Override
    public DeviceMaintenanceAttachment uploadAttachment(Integer maintenancePlanDetailId, MultipartFile file) {
        if (maintenancePlanDetailId == null) {
            throw new ErrorException("缺少保养计划明细id");
        }
        if (deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId) == 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));
            DeviceMaintenanceAttachment attachment = new DeviceMaintenanceAttachment();
            attachment.setMaintenancePlanDetailId(maintenancePlanDetailId);
            attachment.setFileName(originalName);
            attachment.setFileUrl(fileUrl);
            attachment.setSort(selectAttachments(maintenancePlanDetailId).size());
            deviceMaintenanceAttachmentMapper.insert(attachment);
            return attachment;
        } catch (IOException e) {
            throw new ErrorException("附件上传失败");
        }
    }
    @Override
    public boolean deleteAttachment(Long id) {
        return deviceMaintenanceAttachmentMapper.deleteById(id) > 0;
    }
    /**
     * 删除计划明细对应的附件记录
     */
    private void removeAttachmentsByDetailIds(List<Integer> maintenancePlanDetailIds) {
        if (CollectionUtils.isEmpty(maintenancePlanDetailIds)) {
            return;
        }
        deviceMaintenanceAttachmentMapper.delete(Wrappers.<DeviceMaintenanceAttachment>lambdaQuery()
                .in(DeviceMaintenanceAttachment::getMaintenancePlanDetailId, maintenancePlanDetailIds));
    }
    /**
     * 同一计划、同一设备只保留一条;SQL 已按“已有保养记录优先、审核通过优先、最新优先”排序,这里取首条
     */
    private List<DeviceMaintenancePlanDetailsDto> dedupeMaintenanceDetails(List<DeviceMaintenancePlanDetailsDto> details) {
        if (CollectionUtils.isEmpty(details)) {
            return details;
        }
        Map<String, DeviceMaintenancePlanDetailsDto> unique = new LinkedHashMap<>();
        for (DeviceMaintenancePlanDetailsDto detail : details) {
            String key = detail.getDeviceId() != null ? String.valueOf(detail.getDeviceId())
                    : "name:" + detail.getDeviceName();
            unique.putIfAbsent(key, detail);
        }
        return new ArrayList<>(unique.values());
    }
    /**
     * 两个保养日期之间相差的月数
     */
    private long monthDistance(LocalDate from, LocalDate target) {
        if (from == null || target == null) {
            return Long.MAX_VALUE;
        }
        return Math.abs(ChronoUnit.MONTHS.between(YearMonth.from(from), YearMonth.from(target)));
    }
}