吴羡
2026-10-24 3d397976d08f0359b8cfeb4e4f22403ae5d0c04f
feat(device): 设备核查计划附件与设备使用记录联动改造

- 设备核查计划支持附件上传与查看
- 保存原始记录时按模板设备自动生成设备使用记录
- 仪器设备运行记录改为多设备合并导出,无记录时给出提示
已添加2个文件
已删除2个文件
已修改16个文件
1126 ■■■■■ 文件已修改
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceRecordDto.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/excel/DeviceRecordExportWord.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExamineAttachmentMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExamineAttachment.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExaminePlanDetails.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceRecord.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java 524 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/use-record.docx 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/mapper/InsOrderTemplateDeviceMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/mapper/InsOrderTemplateDeviceMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java
@@ -7,6 +7,7 @@
import com.ruoyi.device.dto.DeviceExaminePlanDto;
import com.ruoyi.device.dto.DeviceExamineRecordContrastDto;
import com.ruoyi.device.dto.DeviceExamineRecordDto;
import com.ruoyi.device.pojo.DeviceExamineAttachment;
import com.ruoyi.device.pojo.DeviceExaminePlan;
import com.ruoyi.device.pojo.DeviceExaminePlanDetails;
import com.ruoyi.device.service.DeviceExaminePlanDetailsService;
@@ -21,6 +22,7 @@
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
@@ -192,6 +194,44 @@
    }
    /**
     * æŸ¥æ‰¾æŒ‡å®šè®¾å¤‡å¯å¤ç”¨çš„计划明细(只查询,不新增)
     * @return
     */
    @Operation(summary = "查找可复用的核查计划明细")
    @GetMapping("/findExaminePlanDetail")
    public Result<DeviceExaminePlanDetails> findExaminePlanDetail(Integer deviceId, String checkTime){
        return Result.success(deviceExaminePlanService.findExaminePlanDetail(deviceId, checkTime));
    }
    /**
     * æŸ¥è¯¢æ ¸æŸ¥è®°å½•附件
     */
    @Operation(summary = "查询核查记录附件")
    @GetMapping("/selectAttachments")
    public Result<List<DeviceExamineAttachment>> selectAttachments(Integer planDetailsId) {
        return Result.success(deviceExaminePlanService.selectAttachments(planDetailsId));
    }
    /**
     * ä¸Šä¼ æ ¸æŸ¥è®°å½•附件(任意格式,支持多个)
     */
    @Operation(summary = "上传核查记录附件")
    @PostMapping("/uploadAttachment")
    public Result uploadAttachment(@RequestParam("planDetailsId") Integer planDetailsId,
                                   @RequestParam("file") MultipartFile file) {
        return Result.success(deviceExaminePlanService.uploadAttachment(planDetailsId, file));
    }
    /**
     * åˆ é™¤æ ¸æŸ¥è®°å½•附件
     */
    @Operation(summary = "删除核查记录附件")
    @DeleteMapping("/deleteAttachment")
    public Result deleteAttachment(Long id) {
        return Result.success(deviceExaminePlanService.deleteAttachment(id));
    }
    /**
     * å¯¼å‡ºè®¾å¤‡æ ¸æŸ¥è®¡åˆ’
     * @param planId è®¾å¤‡æ ¸æŸ¥è®¡åˆ’id
     * @return
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java
@@ -23,4 +23,7 @@
    @Schema(description = "到点标红, 1:需核查")
    private Integer redFlag;
    @Schema(description = "附件数量")
    private Integer attachmentCount;
}
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceRecordDto.java
@@ -9,6 +9,10 @@
    @Schema(description = "设备名称")
    private String deviceName;
    @Schema(description = "设备型号")
    private String specificationModel;
    @Schema(description = "设备编号")
    private String managementNumber;
    @Schema(description = "关联原始记录编号")
    private String orderCode;
}
cnas-device/src/main/java/com/ruoyi/device/excel/DeviceRecordExportWord.java
ÎļþÒÑɾ³ý
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExamineAttachmentMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.device.pojo.DeviceExamineAttachment;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeviceExamineAttachmentMapper extends BaseMapper<DeviceExamineAttachment> {
}
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java
@@ -36,5 +36,5 @@
     * @param exportDate
     * @return
     */
    List<DeviceRecord> selectExportList(@Param("deviceId") Integer deviceId, @Param("sampleCode") String sampleCode, @Param("managementNumber") String managementNumber, @Param("exportDate") String exportDate);
    List<DeviceRecordDto> selectExportList(@Param("deviceId") Integer deviceId, @Param("sampleCode") String sampleCode, @Param("managementNumber") String managementNumber, @Param("exportDate") String exportDate);
}
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExamineAttachment.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.device.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("device_examine_attachment")
public class DeviceExamineAttachment {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Integer planDetailsId;
    private String fileName;
    private String fileUrl;
    private Integer sort;
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExaminePlanDetails.java
@@ -36,6 +36,9 @@
    @Schema(description = "计划名称")
    private String deviceName;
    @Schema(description = "设备型号")
    private String deviceModel;
    @Schema(description = "核查时间")
    private String checkTime;
@@ -54,6 +57,18 @@
    @Schema(description = "核查责任人")
    private String checkChargerUser;
    @Schema(description = "环境温湿度")
    private String environment;
    @Schema(description = "核查依据")
    private String checkBasis;
    @Schema(description = "外观情况")
    private String appearanceStatus;
    @Schema(description = "运行是否正常")
    private String runningStatus;
    @Schema(description = "备注")
    private String remark;
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceRecord.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@@ -62,8 +63,10 @@
    private String remark;
    @Schema(description = "使用开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime useStartDate;
    @Schema(description = "使用结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime useEndDate;
}
cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java
@@ -6,11 +6,13 @@
import com.ruoyi.device.dto.DeviceExaminePlanDetailsDto;
import com.ruoyi.device.dto.DeviceExaminePlanDto;
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 org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
@@ -116,6 +118,14 @@
    DeviceExaminePlanDetails resolveExaminePlanDetail(Integer deviceId, String checkTime);
    /**
     * æŸ¥æ‰¾æŒ‡å®šè®¾å¤‡å¯å¤ç”¨çš„æ ¸æŸ¥è®°å½•计划明细(只查询,不新增计划或明细)
     * @param deviceId è®¾å¤‡id
     * @param checkTime æ ¸æŸ¥æœˆä»½ï¼ˆyyyy-MM)
     * @return å¯å¤ç”¨çš„计划明细,没有时返回 null
     */
    DeviceExaminePlanDetails findExaminePlanDetail(Integer deviceId, String checkTime);
    /**
     * ä¿®æ”¹è®¡åˆ’明细(核查月份跨年时归到对应年份的计划)
     * @param detail è®¡åˆ’明细
     * @return æ˜¯å¦ä¿®æ”¹æˆåŠŸ
@@ -128,4 +138,26 @@
     * @return æ˜¯å¦åˆ é™¤æˆåŠŸ
     */
    boolean removeExaminePlanDetail(Integer planDetailsId);
    /**
     * æŸ¥è¯¢æ ¸æŸ¥è®°å½•的附件列表
     * @param planDetailsId è®¡åˆ’明细id
     * @return é™„件列表
     */
    List<DeviceExamineAttachment> selectAttachments(Integer planDetailsId);
    /**
     * ä¸Šä¼ æ ¸æŸ¥è®°å½•的附件(支持任意格式,可多个)
     * @param planDetailsId è®¡åˆ’明细id
     * @param file æ–‡ä»¶
     * @return é™„件信息
     */
    DeviceExamineAttachment uploadAttachment(Integer planDetailsId, MultipartFile file);
    /**
     * åˆ é™¤æ ¸æŸ¥è®°å½•的附件
     * @param id é™„ä»¶id
     * @return æ˜¯å¦åˆ é™¤æˆåŠŸ
     */
    boolean deleteAttachment(Long id);
}
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;
    }
}
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java
@@ -37,6 +37,8 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
@@ -177,20 +179,59 @@
        if (dto.getPlanDetailsId() == null) {
            throw new ErrorException("缺少计划详细信息id");
        }
        DeviceExaminePlanDetails planDetails = deviceExaminePlanDetailsMapper.selectById(dto.getPlanDetailsId());
        if (planDetails == null) {
            throw new ErrorException("核查计划明细不存在");
        }
        if (planDetails.getCheckChargerUserId() == null && StringUtils.isBlank(planDetails.getCheckChargerUser())) {
            throw new ErrorException("请先在编辑中填写核查负责人");
        }
        Integer reviewStatus = dto.getReviewStatus();
        Integer reviewerId = SecurityUtils.getUserId().intValue();
        User reviewer = userMapper.selectById(reviewerId);
        LambdaUpdateWrapper<DeviceExamineRecord> wrapper = Wrappers.<DeviceExamineRecord>lambdaUpdate()
                .eq(DeviceExamineRecord::getPlanDetailsId, dto.getPlanDetailsId())
                .set(DeviceExamineRecord::getReviewStatus, dto.getReviewStatus())
                .set(DeviceExamineRecord::getReviewStatus, reviewStatus)
                .set(DeviceExamineRecord::getReviewRemark, dto.getReviewRemark());
        // ä¸º0清除审核人
        if (dto.getReviewStatus().equals(0)) {
        if (reviewStatus != null && reviewStatus == 0) {
            wrapper.set(DeviceExamineRecord::getReviewUserId, null)
                    .set(DeviceExamineRecord::getReviewUser, null);
        } else {
            wrapper.set(DeviceExamineRecord::getReviewUserId, reviewerId)
                    .set(DeviceExamineRecord::getReviewUser, reviewer == null ? null : reviewer.getName());
        }
        this.update(wrapper);
        boolean updated = this.update(wrapper);
        // è®¡åˆ’行尚未录入核查记录时,补一条审核结果记录,保证列表能显示通过/不通过状态
        if (!updated) {
            DeviceExamineRecord record = new DeviceExamineRecord();
            record.setPlanDetailsId(dto.getPlanDetailsId());
            record.setReviewStatus(reviewStatus);
            record.setReviewRemark(dto.getReviewRemark());
            record.setCheckerUserId(planDetails.getCheckChargerUserId());
            record.setCheckerUser(planDetails.getCheckChargerUser());
            if (reviewStatus != null && reviewStatus == 1) {
                record.setReviewUserId(reviewerId);
                record.setReviewUser(reviewer == null ? null : reviewer.getName());
            }
            this.save(record);
        }
        // å¤æ ¸é€šè¿‡åŽï¼Œè®¾å¤‡æ¡£æ¡ˆçš„æœ€è¿‘/下次核查日期进入下一轮
        if (dto.getReviewStatus() != null && dto.getReviewStatus() == 1) {
        if (reviewStatus != null && reviewStatus == 1) {
            // æå‰å®¡æ ¸æ—¶æŠŠæœ¬è½®è®¡åˆ’归到实际审核月份,便于系统登记下一轮;手动录入到日的日期保持原样
            String currentCheckTime = planDetails.getCheckTime();
            boolean hasCheckDate = currentCheckTime != null && currentCheckTime.trim().length() >= 10;
            if (!hasCheckDate) {
                DeviceExaminePlanDetails updateDetail = new DeviceExaminePlanDetails();
                updateDetail.setPlanDetailsId(dto.getPlanDetailsId());
                updateDetail.setCheckTime(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
                deviceExaminePlanService.updateExaminePlanDetail(updateDetail);
            }
            deviceExaminePlanService.updateDeviceExamineDate(dto.getPlanDetailsId());
        }
        return true;
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
@@ -3,30 +3,56 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.dto.DeviceRecordDto;
import com.ruoyi.device.excel.DeviceRecordExportWord;
import com.ruoyi.device.mapper.DeviceMapper;
import com.ruoyi.device.mapper.DeviceRecordMapper;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceRecord;
import com.ruoyi.device.service.DeviceRecordService;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.system.domain.vo.SysRoleVo;
import com.ruoyi.system.mapper.SysRoleMenuMapper;
import org.springframework.beans.BeanUtils;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.TableRowAlign;
import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblCellMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblLayoutType;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTrPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblLayoutType;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -40,8 +66,18 @@
 */
@Service
public class DeviceRecordServiceImpl extends ServiceImpl<DeviceRecordMapper, DeviceRecord> implements DeviceRecordService {
    @Resource
    private DeviceMapper deviceMapper;
    private static final String FORM_TITLE = "仪器设备运行记录";
    private static final String RECORD_NUMBER = "LMSY-CX07-JL03";
    private static final String FONT_NAME = "宋体";
    private static final int[] COLUMN_WIDTHS = {1622, 4168, 1288, 1107, 1137, 1076, 940, 1182, 1288, 1350};
    private static final String[] HEADER_FIRST_ROW = {"使用日期", "样品编号", "时间 24 å°æ—¶åˆ¶", "", "设备状况", "", "环境条件", "", "使用人", "备注"};
    private static final String[] HEADER_SECOND_ROW = {"", "", "开始", "结束", "使用前", "使用后", "℃", "%RH", "", ""};
    private static final int[] COMBINED_COLUMN_WIDTHS = {1300, 2000, 1500, 1600, 2000, 900, 900, 800, 800, 650, 750, 950, 1008};
    private static final String[] COMBINED_HEADER_FIRST_ROW = {"使用日期", "设备名称", "设备型号", "设备编号", "样品编号", "时间 24 å°æ—¶åˆ¶", "", "设备状况", "", "环境条件", "", "使用人", "备注"};
    private static final String[] COMBINED_HEADER_SECOND_ROW = {"", "", "", "", "", "开始", "结束", "使用前", "使用后", "℃", "%RH", "", ""};
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
    @Resource
    private SysRoleMenuMapper sysRoleMenuMapper;
@@ -62,87 +98,403 @@
        return baseMapper.deviceRecordPage(deviceId, page, sampleCode, managementNumber, testUserId);
    }
    @Override
    public void exportUseRecord(Integer deviceId, String sampleCode, String managementNumber, String exportDate, HttpServletResponse response) {
        // æŸ¥è¯¢cnas设备使用记录(按当前筛选条件, æ¡ä»¶ä¸ºç©ºæ—¶å¯¼å‡ºå…¨éƒ¨)
        List<DeviceRecord> deviceList = baseMapper.selectExportList(deviceId, sampleCode, managementNumber, exportDate);
        // è®¾å¤‡ä¿¡æ¯(未选中具体设备时导出全部设备的记录, è®¾å¤‡ä¿¡æ¯ç•™ç©º)
        Device device = deviceId == null ? null : deviceMapper.selectById(deviceId);
        // æŸ¥è¯¢è®¾å¤‡å±žäºŽå“ªä¸ªå®žéªŒå®¤
        String laboratoryName = "装备线缆实验室";
        // è¦æ˜ å°„到word数据
        List<DeviceRecordExportWord> deviceExportList = new ArrayList<>();
        // deviceExportList èµ‹å€¼
        for (DeviceRecord deviceRecord : deviceList) {
            // å¤„理设备 å¼€å§‹ä½¿ç”¨æ—¶é—´ å’Œ ç»“束时间
            String startTime = "";
            String endTime = "";
            String operationDate = "";
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
            if (deviceRecord.getUseStartDate() != null) {
                startTime = deviceRecord.getUseStartDate().format(formatter);
                endTime = deviceRecord.getUseEndDate().format(formatter);
                String[] startTimeSplit = startTime.split(" ");
                String[] endTimeSplit = endTime.split(" ");
                if (startTimeSplit[0].equals(endTimeSplit[0])) {
                    startTime = startTimeSplit[1];
                    endTime = endTimeSplit[1];
                } else {
                    endTime = endTimeSplit[0] + '\n' + endTimeSplit[1];
                    startTime = startTimeSplit[0] + '\n' + startTimeSplit[1];
                }
                DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy.M.d");
                operationDate = deviceRecord.getUseStartDate().format(formatter2);
            }
            // å¤„理使用前和使用后
            String useBeforeString = deviceRecord.getUseBefore() == 0? "异常" : "良好";
            String useAfterString = deviceRecord.getUseAfter() == 0 ? "异常" : "良好";
            DeviceRecordExportWord deviceRecordExportWord = new DeviceRecordExportWord();
            // è¿›è¡Œèµ‹å€¼
            BeanUtils.copyProperties(deviceRecord, deviceRecordExportWord);
            deviceRecordExportWord.setUseBeforeString(useBeforeString); // ä½¿ç”¨å‰
            deviceRecordExportWord.setUseAfterString(useAfterString); // ä½¿ç”¨åŽ
            deviceRecordExportWord.setOperationDate(operationDate); // è®¾å¤‡æ“ä½œæ—¥æœŸ
            deviceRecordExportWord.setUseStartDateString(startTime); // å¼€å§‹æ—¶é—´
            deviceRecordExportWord.setUseEndDateString(endTime); // ç»“束时间
            deviceExportList.add(deviceRecordExportWord);
        List<DeviceRecordDto> records = baseMapper.selectExportList(deviceId, sampleCode, managementNumber, exportDate);
        if (records == null || records.isEmpty()) {
            throw new ServiceException(exportDate == null || exportDate.isEmpty()
                    ? "没有可导出的使用记录,无法导出"
                    : exportDate + " æ²¡æœ‰å¯å¯¼å‡ºçš„使用记录,无法导出");
        }
        // èŽ·å–è·¯å¾„
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/use-record.docx");
        Configure configure = Configure.builder()
                .bind("useRecord", new HackLoopTableRenderPolicy())
                .build();
        String deviceName = device == null ? "" : device.getDeviceName();
        String deviceManagementNumber = device == null ? "" : device.getManagementNumber();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("useRecord", deviceExportList);
                    put("deviceName", deviceName);
                    put("managementNumber", deviceManagementNumber);
                    put("laboratory", laboratoryName);
                }});
        // å•台设备沿用截图表头; å¤šå°è®¾å¤‡åˆå¹¶åˆ°ä¸€å¼ è¡¨, æ¯è¡Œå¸¦è®¾å¤‡ä¿¡æ¯
        Map<Integer, List<DeviceRecordDto>> deviceRecords = new LinkedHashMap<>();
        for (DeviceRecordDto record : records) {
            deviceRecords.computeIfAbsent(record.getDeviceId(), key -> new ArrayList<>()).add(record);
        }
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "仪器使用记录表", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
            template.write(os);
            os.flush();
            os.close();
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        try (XWPFDocument document = new XWPFDocument()) {
            setupPage(document);
            if (deviceRecords.size() == 1) {
                List<DeviceRecordDto> group = deviceRecords.values().iterator().next();
                renderForm(document, group.get(0), group);
            } else {
                renderCombinedForm(document, records);
            }
            response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
            String fileName = URLEncoder.encode(FORM_TITLE, StandardCharsets.UTF_8).replace("+", "%20");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".docx");
            try (OutputStream os = response.getOutputStream()) {
                document.write(os);
                os.flush();
            }
        } catch (IOException e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    private void setupPage(XWPFDocument document) {
        CTBody body = document.getDocument().getBody();
        CTSectPr sectPr = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
        CTPageSz pageSz = sectPr.isSetPgSz() ? sectPr.getPgSz() : sectPr.addNewPgSz();
        pageSz.setW(BigInteger.valueOf(16838));
        pageSz.setH(BigInteger.valueOf(11906));
        pageSz.setOrient(STPageOrientation.LANDSCAPE);
        CTPageMar pageMar = sectPr.isSetPgMar() ? sectPr.getPgMar() : sectPr.addNewPgMar();
        pageMar.setTop(BigInteger.valueOf(680));
        pageMar.setBottom(BigInteger.valueOf(680));
        pageMar.setLeft(BigInteger.valueOf(794));
        pageMar.setRight(BigInteger.valueOf(794));
        pageMar.setHeader(BigInteger.valueOf(851));
        pageMar.setFooter(BigInteger.valueOf(992));
        pageMar.setGutter(BigInteger.ZERO);
    }
    private void renderForm(XWPFDocument document, DeviceRecordDto device, List<DeviceRecordDto> records) {
        XWPFParagraph recordNumber = addParagraph(document, "记录编号:" + RECORD_NUMBER, 10.5, false, ParagraphAlignment.LEFT);
        recordNumber.setSpacingAfter(240);
        XWPFParagraph title = addParagraph(document, FORM_TITLE, 22, true, ParagraphAlignment.CENTER);
        title.setSpacingAfter(240);
        renderDeviceInfo(document, device);
        document.createParagraph();
        XWPFTable table = document.createTable(2 + records.size(), COLUMN_WIDTHS.length);
        configureTable(table, COLUMN_WIDTHS);
        for (int i = 0; i < table.getNumberOfRows(); i++) {
            XWPFTableRow row = table.getRow(i);
            row.setHeight(i < 2 ? 560 : 520);
            for (int j = 0; j < COLUMN_WIDTHS.length; j++) {
                setCellWidth(row.getCell(j), COLUMN_WIDTHS[j]);
            }
            setRowProperties(row, i < 2);
        }
        XWPFTableRow firstHeader = table.getRow(0);
        for (int i = 0; i < HEADER_FIRST_ROW.length; i++) {
            setCellText(firstHeader.getCell(i), HEADER_FIRST_ROW[i]);
        }
        XWPFTableRow secondHeader = table.getRow(1);
        for (int i = 0; i < HEADER_SECOND_ROW.length; i++) {
            setCellText(secondHeader.getCell(i), HEADER_SECOND_ROW[i]);
        }
        mergeHorizontal(firstHeader, COLUMN_WIDTHS, 6, 2);
        mergeHorizontal(firstHeader, COLUMN_WIDTHS, 4, 2);
        mergeHorizontal(firstHeader, COLUMN_WIDTHS, 2, 2);
        mergeVertical(firstHeader.getCell(0), secondHeader.getCell(0));
        mergeVertical(firstHeader.getCell(1), secondHeader.getCell(1));
        mergeVertical(firstHeader.getCell(5), secondHeader.getCell(8));
        mergeVertical(firstHeader.getCell(6), secondHeader.getCell(9));
        for (int i = 0; i < records.size(); i++) {
            XWPFTableRow row = table.getRow(2 + i);
            String[] values = buildRowValues(records.get(i));
            for (int j = 0; j < values.length; j++) {
                setCellText(row.getCell(j), values[j]);
            }
        }
    }
    private void renderCombinedForm(XWPFDocument document, List<DeviceRecordDto> records) {
        XWPFParagraph recordNumber = addParagraph(document, "记录编号:" + RECORD_NUMBER, 10.5, false, ParagraphAlignment.LEFT);
        recordNumber.setSpacingAfter(240);
        XWPFParagraph title = addParagraph(document, FORM_TITLE, 22, true, ParagraphAlignment.CENTER);
        title.setSpacingAfter(240);
        XWPFTable table = document.createTable(2 + records.size(), COMBINED_COLUMN_WIDTHS.length);
        configureTable(table, COMBINED_COLUMN_WIDTHS);
        for (int i = 0; i < table.getNumberOfRows(); i++) {
            XWPFTableRow row = table.getRow(i);
            row.setHeight(i < 2 ? 560 : 520);
            for (int j = 0; j < COMBINED_COLUMN_WIDTHS.length; j++) {
                setCellWidth(row.getCell(j), COMBINED_COLUMN_WIDTHS[j]);
            }
            setRowProperties(row, i < 2);
        }
        XWPFTableRow firstHeader = table.getRow(0);
        for (int i = 0; i < COMBINED_HEADER_FIRST_ROW.length; i++) {
            setCellText(firstHeader.getCell(i), COMBINED_HEADER_FIRST_ROW[i]);
        }
        XWPFTableRow secondHeader = table.getRow(1);
        for (int i = 0; i < COMBINED_HEADER_SECOND_ROW.length; i++) {
            setCellText(secondHeader.getCell(i), COMBINED_HEADER_SECOND_ROW[i]);
        }
        mergeHorizontal(firstHeader, COMBINED_COLUMN_WIDTHS, 9, 2);
        mergeHorizontal(firstHeader, COMBINED_COLUMN_WIDTHS, 7, 2);
        mergeHorizontal(firstHeader, COMBINED_COLUMN_WIDTHS, 5, 2);
        mergeVertical(firstHeader.getCell(0), secondHeader.getCell(0));
        mergeVertical(firstHeader.getCell(1), secondHeader.getCell(1));
        mergeVertical(firstHeader.getCell(2), secondHeader.getCell(2));
        mergeVertical(firstHeader.getCell(3), secondHeader.getCell(3));
        mergeVertical(firstHeader.getCell(4), secondHeader.getCell(4));
        mergeVertical(firstHeader.getCell(8), secondHeader.getCell(11));
        mergeVertical(firstHeader.getCell(9), secondHeader.getCell(12));
        for (int i = 0; i < records.size(); i++) {
            XWPFTableRow row = table.getRow(2 + i);
            String[] values = buildCombinedRowValues(records.get(i));
            for (int j = 0; j < values.length; j++) {
                setCellText(row.getCell(j), values[j]);
            }
        }
    }
    private XWPFParagraph addParagraph(XWPFDocument document, String text, double fontSize, boolean bold, ParagraphAlignment alignment) {
        XWPFParagraph paragraph = document.createParagraph();
        paragraph.setAlignment(alignment);
        addRun(paragraph, text, fontSize, bold);
        return paragraph;
    }
    private void addDeviceField(XWPFParagraph paragraph, String label, String value) {
        addRun(paragraph, label, 10.5, false);
        XWPFRun valueRun = addRun(paragraph, padUnderline(value), 10.5, false);
        valueRun.setUnderline(UnderlinePatterns.SINGLE);
    }
    private String padUnderline(String value) {
        String content = value == null ? "" : value.trim();
        int width = 0;
        for (int i = 0; i < content.length(); i++) {
            width += content.charAt(i) >= 0x2E80 ? 2 : 1;
        }
        StringBuilder text = new StringBuilder(content);
        for (int i = width; i < 20; i++) {
            text.append(' ');
        }
        return text.toString();
    }
    private XWPFRun addRun(XWPFParagraph paragraph, String text, double fontSize, boolean bold) {
        XWPFRun run = paragraph.createRun();
        run.setFontFamily(FONT_NAME);
        run.setFontFamily(FONT_NAME, XWPFRun.FontCharRange.eastAsia);
        run.setFontSize(fontSize);
        run.setBold(bold);
        run.setText(text);
        return run;
    }
    private void renderDeviceInfo(XWPFDocument document, DeviceRecordDto device) {
        String[] labels = {"设备名称:", "设备型号:", "设备编号:"};
        String[] values = {
                device == null ? "" : device.getDeviceName(),
                device == null ? "" : device.getSpecificationModel(),
                device == null ? "" : device.getManagementNumber()
        };
        int[] widths = {5052, 5053, 5053};
        XWPFTable table = document.createTable(1, labels.length);
        CTTbl ctTbl = table.getCTTbl();
        CTTblPr tblPr = ctTbl.getTblPr() != null ? ctTbl.getTblPr() : ctTbl.addNewTblPr();
        CTTblWidth tblWidth = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.DXA);
        tblWidth.setW(BigInteger.valueOf(totalWidth(COLUMN_WIDTHS)));
        CTTblLayoutType layoutType = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
        layoutType.setType(STTblLayoutType.FIXED);
        setNoBorders(tblPr);
        CTTblCellMar cellMar = tblPr.isSetTblCellMar() ? tblPr.getTblCellMar() : tblPr.addNewTblCellMar();
        setZeroWidth(cellMar.isSetLeft() ? cellMar.getLeft() : cellMar.addNewLeft());
        setZeroWidth(cellMar.isSetRight() ? cellMar.getRight() : cellMar.addNewRight());
        CTTblGrid grid = ctTbl.getTblGrid() != null ? ctTbl.getTblGrid() : ctTbl.addNewTblGrid();
        while (grid.sizeOfGridColArray() > 0) {
            grid.removeGridCol(0);
        }
        for (int width : widths) {
            grid.addNewGridCol().setW(BigInteger.valueOf(width));
        }
        XWPFTableRow row = table.getRow(0);
        for (int i = 0; i < labels.length; i++) {
            XWPFTableCell cell = row.getCell(i);
            setCellWidth(cell, widths[i]);
            cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
            XWPFParagraph paragraph = cell.getParagraphs().isEmpty() ? cell.addParagraph() : cell.getParagraphs().get(0);
            paragraph.setAlignment(ParagraphAlignment.LEFT);
            paragraph.setSpacingBefore(0);
            paragraph.setSpacingAfter(0);
            addDeviceField(paragraph, labels[i], values[i]);
        }
    }
    private void setNoBorders(CTTblPr tblPr) {
        CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
        setNoBorder(borders.isSetTop() ? borders.getTop() : borders.addNewTop());
        setNoBorder(borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft());
        setNoBorder(borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom());
        setNoBorder(borders.isSetRight() ? borders.getRight() : borders.addNewRight());
        setNoBorder(borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH());
        setNoBorder(borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV());
    }
    private void setNoBorder(CTBorder border) {
        border.setVal(STBorder.NIL);
        border.setSz(BigInteger.ZERO);
        border.setSpace(BigInteger.ZERO);
        border.setColor("auto");
    }
    private void setZeroWidth(CTTblWidth width) {
        width.setType(STTblWidth.DXA);
        width.setW(BigInteger.ZERO);
    }
    private void configureTable(XWPFTable table, int[] widths) {
        table.setTableAlignment(TableRowAlign.CENTER);
        CTTbl ctTbl = table.getCTTbl();
        CTTblPr tblPr = ctTbl.getTblPr() != null ? ctTbl.getTblPr() : ctTbl.addNewTblPr();
        CTTblWidth tblWidth = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.DXA);
        tblWidth.setW(BigInteger.valueOf(totalWidth(widths)));
        CTTblLayoutType layoutType = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
        layoutType.setType(STTblLayoutType.FIXED);
        CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
        setBorder(borders.isSetTop() ? borders.getTop() : borders.addNewTop());
        setBorder(borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft());
        setBorder(borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom());
        setBorder(borders.isSetRight() ? borders.getRight() : borders.addNewRight());
        setBorder(borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH());
        setBorder(borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV());
        CTTblGrid grid = ctTbl.getTblGrid() != null ? ctTbl.getTblGrid() : ctTbl.addNewTblGrid();
        while (grid.sizeOfGridColArray() > 0) {
            grid.removeGridCol(0);
        }
        for (int width : widths) {
            grid.addNewGridCol().setW(BigInteger.valueOf(width));
        }
    }
    private int totalWidth(int[] widths) {
        int total = 0;
        for (int width : widths) {
            total += width;
        }
        return total;
    }
    private void setBorder(CTBorder border) {
        border.setVal(STBorder.SINGLE);
        border.setSz(BigInteger.valueOf(4));
        border.setSpace(BigInteger.ZERO);
        border.setColor("000000");
    }
    private void setCellWidth(XWPFTableCell cell, int width) {
        CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
        CTTblWidth tcW = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
        tcW.setType(STTblWidth.DXA);
        tcW.setW(BigInteger.valueOf(width));
    }
    private void setCellText(XWPFTableCell cell, String text) {
        cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
        XWPFParagraph paragraph = cell.getParagraphs().isEmpty() ? cell.addParagraph() : cell.getParagraphs().get(0);
        paragraph.setAlignment(ParagraphAlignment.CENTER);
        paragraph.setSpacingBefore(0);
        paragraph.setSpacingAfter(0);
        addRun(paragraph, text == null ? "" : text, 10.5, false);
    }
    private void setRowProperties(XWPFTableRow row, boolean repeatHeader) {
        CTRow ctRow = row.getCtRow();
        CTTrPr trPr = ctRow.isSetTrPr() ? ctRow.getTrPr() : ctRow.addNewTrPr();
        trPr.addNewCantSplit();
        if (repeatHeader) {
            trPr.addNewTblHeader();
        }
    }
    private void mergeHorizontal(XWPFTableRow row, int[] widths, int firstIndex, int span) {
        int width = 0;
        for (int i = firstIndex; i < firstIndex + span; i++) {
            width += widths[i];
        }
        XWPFTableCell first = row.getCell(firstIndex);
        setCellWidth(first, width);
        CTTcPr tcPr = first.getCTTc().isSetTcPr() ? first.getCTTc().getTcPr() : first.getCTTc().addNewTcPr();
        (tcPr.isSetGridSpan() ? tcPr.getGridSpan() : tcPr.addNewGridSpan()).setVal(BigInteger.valueOf(span));
        for (int i = firstIndex + span - 1; i > firstIndex; i--) {
            row.removeCell(i);
        }
    }
    private void mergeVertical(XWPFTableCell first, XWPFTableCell second) {
        setVMerge(first, STMerge.RESTART);
        setVMerge(second, STMerge.CONTINUE);
    }
    private void setVMerge(XWPFTableCell cell, STMerge.Enum value) {
        CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
        (tcPr.isSetVMerge() ? tcPr.getVMerge() : tcPr.addNewVMerge()).setVal(value);
    }
    private String[] buildRowValues(DeviceRecordDto record) {
        return new String[]{
                formatDate(record.getUseStartDate()),
                valueOrEmpty(record.getSampleCode()),
                formatTime(record.getUseStartDate()),
                formatTime(record.getUseEndDate()),
                statusText(record.getUseBefore()),
                statusText(record.getUseAfter()),
                valueOrEmpty(record.getTemperature()),
                valueOrEmpty(record.getHumidity()),
                valueOrEmpty(record.getUsePerson()),
                valueOrEmpty(record.getRemark())
        };
    }
    private String[] buildCombinedRowValues(DeviceRecordDto record) {
        return new String[]{
                formatDate(record.getUseStartDate()),
                valueOrEmpty(record.getDeviceName()),
                valueOrEmpty(record.getSpecificationModel()),
                valueOrEmpty(record.getManagementNumber()),
                valueOrEmpty(record.getSampleCode()),
                formatTime(record.getUseStartDate()),
                formatTime(record.getUseEndDate()),
                statusText(record.getUseBefore()),
                statusText(record.getUseAfter()),
                valueOrEmpty(record.getTemperature()),
                valueOrEmpty(record.getHumidity()),
                valueOrEmpty(record.getUsePerson()),
                valueOrEmpty(record.getRemark())
        };
    }
    private String formatDate(LocalDateTime value) {
        return value == null ? "" : value.format(DATE_FORMATTER);
    }
    private String formatTime(LocalDateTime value) {
        return value == null ? "" : value.format(TIME_FORMATTER);
    }
    private String statusText(Integer value) {
        if (value == null) {
            return "";
        }
        return value == 0 ? "异常" : "良好";
    }
    private String valueOrEmpty(String value) {
        return value == null ? "" : value;
    }
}
cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml
@@ -12,6 +12,7 @@
            d.device_id,
            d.device_number,
            d.device_name,
            d.device_model,
            d.check_time,
            d.check_index,
            d.check_method,
@@ -19,6 +20,10 @@
            d.check_charger_user_id,
            CASE WHEN d.check_charger_user='' or d.check_charger_user is null THEN u.name
                ELSE d.check_charger_user END AS check_charger_user,
            d.environment,
            d.check_basis,
            d.appearance_status,
            d.running_status,
            d.remark,
            d.file_name,
            d.file_url,
@@ -39,7 +44,8 @@
        CASE WHEN dev.next_examine_date IS NOT NULL
            AND dev.next_examine_date &lt;= date_add(curdate(), interval 1 month)
            AND (dev.last_examine_date IS NULL OR dev.last_examine_date &lt; str_to_date(concat(left(d.check_time, 7), '-01'), '%Y-%m-%d'))
            THEN 1 ELSE 0 END red_flag
            THEN 1 ELSE 0 END red_flag,
        (select count(*) from device_examine_attachment a where a.plan_details_id = d.plan_details_id) attachment_count
        from device_examine_plan_details d
        left join user u on u.id = d.check_charger_user_id
        left join device_examine_record dc on d.plan_details_id = dc.plan_details_id
cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml
@@ -11,6 +11,7 @@
        from device_examine_plan cqm
        left join user u1 on u1.id = cqm.write_user_id
        left join user u3 on u3.id = cqm.ratify_user_id
        where exists (select 1 from device_examine_plan_details cqmd where cqmd.plan_id = cqm.plan_id)
        order by cqm.create_time desc) a
        <if test="ew.customSqlSegment != null and ew.customSqlSegment != ''">
            ${ew.customSqlSegment}
cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml
@@ -5,9 +5,12 @@
    <select id="deviceRecordPage" resultType="com.ruoyi.device.dto.DeviceRecordDto">
        select dr.*,
               d.device_name,
               d.management_number
               d.specification_model,
               d.management_number,
               o.entrust_code as order_code
        from device_record dr
                 left join device d on d.id = dr.device_id
                 left join ins_order o on o.id = dr.ins_order_id
        where 1=1
        <if test="deviceId != null">
            and dr.device_id = #{deviceId}
@@ -37,7 +40,10 @@
    <!-- æŸ¥è¯¢å¯¼å‡ºè®¾å¤‡ä½¿ç”¨è®°å½• -->
    <select id="selectExportList" resultType="com.ruoyi.device.dto.DeviceRecordDto">
        select dr.*
        select dr.*,
               d.device_name,
               d.specification_model,
               d.management_number
        from device_record dr
                 left join device d on d.id = dr.device_id
        where dr.use_start_date is not null
cnas-device/src/main/resources/static/word/use-record.docx
Binary files differ
inspect-server/src/main/java/com/ruoyi/inspect/mapper/InsOrderTemplateDeviceMapper.java
@@ -13,4 +13,7 @@
    List<Integer> selectExistingDeviceIds(@Param("deviceIds") List<Integer> deviceIds);
    List<Integer> selectDeviceIdsByOrderId(@Param("orderId") Integer orderId);
    List<Integer> selectDeviceIdsByOrderAndTemplate(@Param("orderId") Integer orderId,
                                                    @Param("templateId") Integer templateId);
}
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -605,11 +605,9 @@
        }
        // æ·»åŠ è®¾å¤‡è®°å½•
        threadPoolTaskExecutor.execute(() -> {
            // æ·»åŠ è®¾å¤‡ä½¿ç”¨è®°å½•
            addDeviceRecord(insSample, userId);
        });
        // åŽŸå§‹è®°å½•ä¿å­˜åŽï¼ŒæŒ‰å½“å‰æ¨¡æ¿é€‰ç”¨çš„è®¾å¤‡ç”Ÿæˆä½¿ç”¨è®°å½•
        syncDeviceRecordsFromTemplate(saveInsContextDto.getOrderId(), saveInsContextDto.getSampleId(),
                saveInsContextDto.getCurrentTable(), userId);
    }
    /**
@@ -692,6 +690,9 @@
                .and(w -> w.isNull(InsProduct::getLastValue).or().eq(InsProduct::getLastValue, "")));
        sample.setInsState(unfinished == 0 ? 2 : 1);
        insSampleMapper.updateById(sample);
        // åŽŸå§‹è®°å½•ä¿å­˜åŽï¼ŒæŒ‰å½“å‰æ¨¡æ¿é€‰ç”¨çš„è®¾å¤‡ç”Ÿæˆä½¿ç”¨è®°å½•
        syncDeviceRecordsFromTemplate(dto.getOrderId(), dto.getSampleId(), dto.getTemplateId(), userId);
    }
    /**
@@ -1031,60 +1032,52 @@
        }
    }
    private synchronized void addDeviceRecord(InsSample insSample, Integer userId) {
        InsOrder order = insOrderMapper.selectById(insSample.getInsOrderId());
    /**
     * åŽŸå§‹è®°å½•ä¿å­˜åŽï¼ŒæŒ‰å½“å‰æ¨¡æ¿é€‰ç”¨çš„è®¾å¤‡ç”Ÿæˆå¾…å¡«å†™çš„ä½¿ç”¨è®°å½•ã€‚
     * åŒä¸€è®¢å•、样品、设备只生成一条;已经填写过的记录不会被覆盖。
     */
    private synchronized void syncDeviceRecordsFromTemplate(Integer orderId, Integer sampleId,
                                                            Integer templateId, Integer userId) {
        if (orderId == null || sampleId == null || templateId == null) {
            return;
        }
        InsOrder order = insOrderMapper.selectById(orderId);
        InsSample sample = insSampleMapper.selectById(sampleId);
        if (order == null || sample == null) {
            return;
        }
        List<Integer> deviceIds = insOrderTemplateDeviceMapper
                .selectDeviceIdsByOrderAndTemplate(orderId, templateId);
        if (CollectionUtils.isEmpty(deviceIds)) {
            return;
        }
        User user = userMapper.selectById(userId);
        // æŸ¥è¯¢è®¾å¤‡ä½¿ç”¨è®°å½•查询该订单的使用记录
        List<InsOrderDeviceRecordDto> deviceRecordDtoList = insOrderDeviceRecordMapper.selectDeviceNumber(insSample.getInsOrderId());
        Set<String> recordCodeset = deviceRecordDtoList.stream().map(InsOrderDeviceRecordDto::getManagementNumber).collect(Collectors.toSet());
        // èŽ·å–è®¢å•è®¾å¤‡ç¼–å·
        List<InsProductResult> resultList = insProductResultMapper.selectResultByOrderId(insSample.getInsOrderId());
        Set<String> deviceCodeSet = new HashSet<>();
        for (InsProductResult result : resultList) {
            // æ·»åŠ è®¾å¤‡ç¼–å·
            List<JSONObject> jsonObjects = JSON.parseArray(result.getEquipValue(), JSONObject.class);
            for (JSONObject jsonObject : jsonObjects) {
                if (!"".equals(jsonObject.get("v") + "")) {
                    List<String> v = StrUtil.split(jsonObject.get("v") + "", ",");
                    deviceCodeSet.addAll(v);
                }
        String usePerson = user == null ? "" : user.getName();
        String sampleCode = StringUtils.isNotBlank(sample.getSampleCode())
                ? sample.getSampleCode() : order.getEntrustCode();
        for (Integer deviceId : deviceIds) {
            if (deviceId == null) {
                continue;
            }
            Long count = insOrderDeviceRecordMapper.selectCount(Wrappers.<InsOrderDeviceRecord>lambdaQuery()
                    .eq(InsOrderDeviceRecord::getInsOrderId, orderId)
                    .eq(InsOrderDeviceRecord::getDeviceId, deviceId)
                    .eq(InsOrderDeviceRecord::getSampleCode, sampleCode));
            if (count != null && count > 0) {
                continue;
            }
            InsOrderDeviceRecord record = new InsOrderDeviceRecord();
            record.setInsOrderId(orderId);
            record.setDeviceId(deviceId);
            record.setSampleCode(sampleCode);
            record.setTemperature(order.getTemperature());
            record.setHumidity(order.getHumidity());
            record.setUseBefore(1);
            record.setUseAfter(1);
            record.setUsePersonId(userId);
            record.setUsePerson(usePerson);
            insOrderDeviceRecordMapper.insert(record);
        }
        // 1.判断是否有没有添加的使用记录
        Set<String> orderDeviceNumbers = getDeviceDifference(deviceCodeSet, recordCodeset);
        // æ·»åŠ ä½¿ç”¨è®°å½•, æ ¹æ®ç¼–号查询设备id
        if (CollectionUtils.isNotEmpty(orderDeviceNumbers)) {
            List<Integer> orderDeviceIds = insOrderDeviceRecordMapper.selectDeviceIdsByNumbers(orderDeviceNumbers);
            List<InsOrderDeviceRecord> collect = orderDeviceIds.stream().map(deviceId -> {
                InsOrderDeviceRecord insOrderDeviceRecord = new InsOrderDeviceRecord();
                insOrderDeviceRecord.setInsOrderId(insSample.getInsOrderId());
                insOrderDeviceRecord.setDeviceId(deviceId);
                insOrderDeviceRecord.setSampleCode(order.getEntrustCode());
                insOrderDeviceRecord.setUseBefore(1);
                insOrderDeviceRecord.setUseAfter(1);
                insOrderDeviceRecord.setUsePerson(user.getName());
                insOrderDeviceRecord.setUsePersonId(user.getId());
                return insOrderDeviceRecord;
            }).collect(Collectors.toList());
            insOrderDeviceRecordService.saveBatch(collect);
        }
        // 2.判断是否取消了设备使用
        Set<String> repoprNumbers = getDeviceDifference(recordCodeset, deviceCodeSet);
        if (CollectionUtils.isNotEmpty(repoprNumbers)) {
            List<Integer> reportDeviceIds = insOrderDeviceRecordMapper.selectDeviceIdsByNumbers(repoprNumbers);
            insOrderDeviceRecordMapper.delete(Wrappers.<InsOrderDeviceRecord>lambdaQuery()
                    .in(InsOrderDeviceRecord::getDeviceId, reportDeviceIds)
                    .eq(InsOrderDeviceRecord::getInsOrderId, insSample.getInsOrderId()));
        }
    }
    private static Set<String> getDeviceDifference(Set<String> number1, Set<String> number2) {
        return number1.stream().filter(s1 -> number2.stream().noneMatch(s2 -> s1.equals(s2)))
                .collect(Collectors.toSet());
    }
inspect-server/src/main/resources/mapper/InsOrderTemplateDeviceMapper.xml
@@ -20,4 +20,14 @@
        order by min(otd.sort), otd.device_id
    </select>
    <select id="selectDeviceIdsByOrderAndTemplate" resultType="java.lang.Integer">
        select otd.device_id
        from ins_order_template_device otd
        where otd.ins_order_id = #{orderId}
          and otd.template_id = #{templateId}
          and otd.device_id is not null
        group by otd.device_id
        order by min(otd.sort), otd.device_id
    </select>
</mapper>