From 3d397976d08f0359b8cfeb4e4f22403ae5d0c04f Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期六, 24 十月 2026 20:44:14 +0800
Subject: [PATCH] feat(device): 设备核查计划附件与设备使用记录联动改造

---
 cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceRecord.java                           |    3 
 cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java             |    3 
 cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml                        |   10 
 cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java        |  524 +++++++++++++++++++++++----
 cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExamineAttachmentMapper.java        |    9 
 cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml                           |    1 
 cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java   |  252 +++++++++++-
 cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java            |   32 +
 /dev/null                                                                                   |    0 
 cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java                   |    2 
 cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java      |   40 ++
 cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml                    |    8 
 inspect-server/src/main/java/com/ruoyi/inspect/mapper/InsOrderTemplateDeviceMapper.java     |    3 
 cnas-device/src/main/java/com/ruoyi/device/dto/DeviceRecordDto.java                         |    4 
 cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java |   49 ++
 cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExamineAttachment.java                |   28 +
 cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExaminePlanDetails.java               |   15 
 inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java    |  105 ++--
 inspect-server/src/main/resources/mapper/InsOrderTemplateDeviceMapper.xml                   |   10 
 19 files changed, 919 insertions(+), 179 deletions(-)

diff --git a/cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java b/cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java
index 208271a..d5aab47 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java
+++ b/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
diff --git a/cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java b/cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java
index 16e0ac2..933ecb1 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java
+++ b/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;
+
 }
diff --git a/cnas-device/src/main/java/com/ruoyi/device/dto/DeviceRecordDto.java b/cnas-device/src/main/java/com/ruoyi/device/dto/DeviceRecordDto.java
index 57c90a5..e144de5 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/dto/DeviceRecordDto.java
+++ b/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;
 }
diff --git a/cnas-device/src/main/java/com/ruoyi/device/excel/DeviceRecordExportWord.java b/cnas-device/src/main/java/com/ruoyi/device/excel/DeviceRecordExportWord.java
deleted file mode 100644
index db56f63..0000000
--- a/cnas-device/src/main/java/com/ruoyi/device/excel/DeviceRecordExportWord.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.ruoyi.device.excel;
-
-import com.ruoyi.device.pojo.DeviceRecord;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-/**
- * Author: yuan
- * Date: 2024-12-10 鏄熸湡浜� 17:19:22
- * Description:
- */
-@Data
-public class DeviceRecordExportWord extends DeviceRecord {
-    @Schema(description = "浣跨敤鍓�0浠h〃涓嶆甯�1浠h〃姝e父")
-    private String useBeforeString;
-
-    @Schema(description = "浣跨敤鍚�0浠h〃涓嶆甯�1浠h〃姝e父")
-    private String useAfterString;
-
-    @Schema(description = "鎿嶄綔鏃堕棿 String yyyy-MM-dd")
-    private String operationDate;
-
-    @Schema(description = "浣跨敤寮�濮嬫棩鏈� String yyyy-MM-dd \n HH:mm:ss")
-    private String useStartDateString;
-
-    @Schema(description = "浣跨敤缁撴潫鏃ユ湡 String yyyy-MM-dd \n HH:mm:ss")
-    private String useEndDateString;
-}
diff --git a/cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExamineAttachmentMapper.java b/cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExamineAttachmentMapper.java
new file mode 100644
index 0000000..d67bd9c
--- /dev/null
+++ b/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> {
+}
diff --git a/cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java b/cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java
index cce6942..214de38 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java
+++ b/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);
 }
diff --git a/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExamineAttachment.java b/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExamineAttachment.java
new file mode 100644
index 0000000..1c46d36
--- /dev/null
+++ b/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;
+}
diff --git a/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExaminePlanDetails.java b/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExaminePlanDetails.java
index 6a3bcf6..62d2a0c 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExaminePlanDetails.java
+++ b/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 = "鏍告煡璐d换浜�")
     private String checkChargerUser;
 
+    @Schema(description = "鐜娓╂箍搴�")
+    private String environment;
+
+    @Schema(description = "鏍告煡渚濇嵁")
+    private String checkBasis;
+
+    @Schema(description = "澶栬鎯呭喌")
+    private String appearanceStatus;
+
+    @Schema(description = "杩愯鏄惁姝e父")
+    private String runningStatus;
+
     @Schema(description = "澶囨敞")
     private String remark;
 
diff --git a/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceRecord.java b/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceRecord.java
index 72d568e..09930d3 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceRecord.java
+++ b/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;
 }
diff --git a/cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java b/cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java
index 50cb2ad..aa4c52d 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java
+++ b/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 鏍告煡鏈堜唤锛坹yyy-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);
 }
diff --git a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java b/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
index a8b3ace..da709e6 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
+++ b/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;
+    }
+
+    /**
+     * 鍒犻櫎鏈鏍哥殑璁″垝鍚庯紝鎶婅澶囨。妗堟仮澶嶅埌鏈�杩戜竴鏉″鏍搁�氳繃鐨勮褰曪紱
+     * 娌℃湁宸插畬鎴愯褰曟椂锛屾墠娓呯┖妗f鐨勬牳鏌ユ棩鏈熷拰鍛ㄦ湡
+     */
+    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());
+        // 鍒犻櫎鐨勪笉鏄。妗堝綋鍓嶇櫥璁扮殑閭d竴杞紝涓旇澶囪繕鏈夊叾浠栬鍒掓椂锛屼笉鍔ㄦ。妗�
+        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")));
+    }
+
+    /**
+     * 瑙f瀽鏄庣粏鏍告煡鏈堜唤涓哄綋鏈堥鏃ワ紝鏍煎紡涓嶅悎娉曟椂杩斿洖 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;
     }
 }
diff --git a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java b/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java
index 9e882f2..6124241 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java
+++ b/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);
+        }
+
         // 澶嶆牳閫氳繃鍚庯紝璁惧妗f鐨勬渶杩�/涓嬫鏍告煡鏃ユ湡杩涘叆涓嬩竴杞�
-        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;
diff --git a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java b/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
index d49ccbb..d4fc412 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
+++ b/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;
+    }
 }
diff --git a/cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml b/cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml
index cd28058..edd6bbd 100644
--- a/cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml
+++ b/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
diff --git a/cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml b/cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml
index cf4b0ae..35b561f 100644
--- a/cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml
+++ b/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}
diff --git a/cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml b/cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml
index 26dc79b..571542b 100644
--- a/cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml
+++ b/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
diff --git a/cnas-device/src/main/resources/static/word/use-record.docx b/cnas-device/src/main/resources/static/word/use-record.docx
deleted file mode 100644
index 00632d7..0000000
--- a/cnas-device/src/main/resources/static/word/use-record.docx
+++ /dev/null
Binary files differ
diff --git a/inspect-server/src/main/java/com/ruoyi/inspect/mapper/InsOrderTemplateDeviceMapper.java b/inspect-server/src/main/java/com/ruoyi/inspect/mapper/InsOrderTemplateDeviceMapper.java
index 9a3951a..e334df9 100644
--- a/inspect-server/src/main/java/com/ruoyi/inspect/mapper/InsOrderTemplateDeviceMapper.java
+++ b/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);
 }
diff --git a/inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java b/inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
index 9df50e7..3dde4b3 100644
--- a/inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
+++ b/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());
     }
 
 
diff --git a/inspect-server/src/main/resources/mapper/InsOrderTemplateDeviceMapper.xml b/inspect-server/src/main/resources/mapper/InsOrderTemplateDeviceMapper.xml
index 3ccd014..c6f4ee2 100644
--- a/inspect-server/src/main/resources/mapper/InsOrderTemplateDeviceMapper.xml
+++ b/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>

--
Gitblit v1.9.3