修复人员管理接口校验与导出,完善设备保养计划与文件受控功能

- 人员管理:补齐新增、修改、删除、提交、确认等校验,修复导出与消息通知,任职授权记录按 JL04 上岗证模板导出
- 设备保养计划:支持明细附件,完善提醒与导出
- 文件受控:增加发布日期、实施日期及批量导入,调整变更、作废、发放回收逻辑
- 原始记录与设备使用记录联动:环境条件、使用时间回填
- lims_delta.sql 补充设备保养附件表及文件受控日期字段,新增数据修复脚本
已添加8个文件
已修改42个文件
1974 ■■■■■ 文件已修改
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceMaintenancePlanController.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceRecordController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceMaintenancePlanDetailsDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceMaintenanceAttachmentMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceMaintenanceAttachment.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/DeviceMaintenancePlanService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/DeviceRecordService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceMaintenancePlanServiceImpl.java 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceMaintenancePlanDetailsMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/maintenance-plan.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/controller/ManageDocumentControlledController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/excel/ManageDocumentControlledImportDto.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/excel/ManageDocumentControlledImportListener.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/mapper/ManageDocumentControlledMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/pojo/ManageDocumentControlled.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/ManageDocumentControlledService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentAlterServiceImpl.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentCancelServiceImpl.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentControlledServiceImpl.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentIssueRecycleServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/resources/mapper/ManageDocumentControlledMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonBasicInfoController.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonPersonnelCapacityController.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonPostAuthorizationRecordController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/dto/UserPageDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonBasicInfo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonJobResponsibilities.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonPersonnelCapacity.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonPostAuthorizationRecord.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonTraining.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonBasicInfoServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonJobResponsibilitiesServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonPersonnelCapacityServiceImpl.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonPostAuthorizationRecordServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingRecordServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingServiceImpl.java 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/resources/mapper/PersonBasicInfoMapper.xml 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/resources/mapper/PersonPersonnelCapacityMapper.xml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/resources/mapper/PersonTrainingRecordMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/resources/static/credentials-deal.docx 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsProductServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/fix_device_maintenance_plan_duplicates.sql 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/fix_device_record_backfill.sql 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/fix_inspection_order_detail_backfill.sql 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/fix_inspection_order_orphans.sql 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/lims_delta.sql 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceMaintenancePlanController.java
@@ -5,6 +5,7 @@
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.device.dto.DeviceMaintenancePlanDetailsDto;
import com.ruoyi.device.dto.DeviceMaintenancePlanDto;
import com.ruoyi.device.pojo.DeviceMaintenanceAttachment;
import com.ruoyi.device.pojo.DeviceMaintenancePlan;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import com.ruoyi.device.service.DeviceMaintenancePlanService;
@@ -12,6 +13,7 @@
import io.swagger.v3.oas.annotations.Operation;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@@ -132,7 +134,8 @@
     */
    @Operation(summary = "导出设备保养计划")
    @GetMapping("/exportDeviceMaintenancePlan")
    public Result exportDeviceMaintenancePlan(@RequestParam("maintenancePlanId") Integer maintenancePlanId, HttpServletResponse response) {
    public Result exportDeviceMaintenancePlan(@RequestParam("maintenancePlanId") Integer maintenancePlanId,
                                              HttpServletResponse response) {
        return deviceMaintenancePlanService.exportDeviceMaintenancePlanDto(maintenancePlanId, response);
    }
@@ -153,4 +156,32 @@
    public Result reviewMaintenancePlanStatus(@RequestBody DeviceMaintenancePlanDto deviceMaintenancePlanDto) {
        return deviceMaintenancePlanService.reviewMaintenancePlanStatus(deviceMaintenancePlanDto);
    }
    /**
     * æŸ¥è¯¢ä¿å…»è®¡åˆ’明细附件
     */
    @Operation(summary = "查询保养计划明细附件")
    @GetMapping("/selectAttachments")
    public Result<List<DeviceMaintenanceAttachment>> selectAttachments(Integer maintenancePlanDetailId) {
        return Result.success(deviceMaintenancePlanService.selectAttachments(maintenancePlanDetailId));
    }
    /**
     * ä¸Šä¼ ä¿å…»è®¡åˆ’明细附件
     */
    @Operation(summary = "上传保养计划明细附件")
    @PostMapping("/uploadAttachment")
    public Result uploadAttachment(@RequestParam("maintenancePlanDetailId") Integer maintenancePlanDetailId,
                                   @RequestParam("file") MultipartFile file) {
        return Result.success(deviceMaintenancePlanService.uploadAttachment(maintenancePlanDetailId, file));
    }
    /**
     * åˆ é™¤ä¿å…»è®¡åˆ’明细附件
     */
    @Operation(summary = "删除保养计划明细附件")
    @DeleteMapping("/deleteAttachment")
    public Result deleteAttachment(Long id) {
        return Result.success(deviceMaintenancePlanService.deleteAttachment(id));
    }
}
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceRecordController.java
@@ -29,8 +29,10 @@
    @Operation(summary = "备使用记录查询")
    @GetMapping("/deviceRecordPage")
    public Result deviceRecordPage(Integer deviceId, Page page, String sampleCode, String managementNumber) {
        return Result.success(deviceRecordService.deviceRecordPage(deviceId, page, sampleCode, managementNumber));
    public Result deviceRecordPage(Integer deviceId, Page page, String deviceName,
                                   String specificationModel, String sampleCode, String managementNumber) {
        return Result.success(deviceRecordService.deviceRecordPage(
                deviceId, page, deviceName, specificationModel, sampleCode, managementNumber));
    }
    @Operation(summary = "新增")
@@ -57,7 +59,9 @@
    @Operation(summary = "设备使用记录导出")
    @GetMapping("/exportUseRecord")
    public void exportUseRecord(Integer deviceId, String sampleCode, String managementNumber, String exportDate, HttpServletResponse response){
        deviceRecordService.exportUseRecord(deviceId, sampleCode, managementNumber, exportDate, response);
    public void exportUseRecord(Integer deviceId, String deviceName, String specificationModel,
                                String managementNumber, String sampleCode, HttpServletResponse response) {
        deviceRecordService.exportUseRecord(
                deviceId, deviceName, specificationModel, managementNumber, sampleCode, response);
    }
}
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceMaintenancePlanDetailsDto.java
@@ -98,4 +98,7 @@
    @Schema(description = "审核时间")
    private LocalDateTime reviewTime;
    @Schema(description = "附件数量")
    private Integer attachmentCount;
}
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceMaintenanceAttachmentMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package com.ruoyi.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.device.pojo.DeviceMaintenanceAttachment;
public interface DeviceMaintenanceAttachmentMapper extends BaseMapper<DeviceMaintenanceAttachment> {
}
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceRecordMapper.java
@@ -19,7 +19,12 @@
 */
public interface DeviceRecordMapper extends BaseMapper<DeviceRecord> {
    IPage<DeviceRecordDto> deviceRecordPage(@Param("deviceId") Integer deviceId, @Param("page") Page page, @Param("sampleCode") String sampleCode, @Param("managementNumber") String managementNumber, @Param("userId") Integer userId);
    IPage<DeviceRecordDto> deviceRecordPage(@Param("deviceId") Integer deviceId, @Param("page") Page page,
                                            @Param("deviceName") String deviceName,
                                            @Param("specificationModel") String specificationModel,
                                            @Param("sampleCode") String sampleCode,
                                            @Param("managementNumber") String managementNumber,
                                            @Param("userId") Integer userId);
    /**
@@ -31,10 +36,15 @@
    /**
     * æŸ¥è¯¢å¯¼å‡ºè®¾å¤‡ä½¿ç”¨è®°å½•
     * @param deviceId
     * @param deviceName è®¾å¤‡åç§°ç­›é€‰
     * @param specificationModel è®¾å¤‡åž‹å·ç­›é€‰
     * @param managementNumber è®¾å¤‡ç¼–号筛选
     * @param sampleCode æ ·å“ç¼–号筛选
     * @param managementNumber ç®¡ç†ç¼–号筛选
     * @param exportDate
     * @return
     */
    List<DeviceRecordDto> 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("deviceName") String deviceName,
                                           @Param("specificationModel") String specificationModel,
                                           @Param("managementNumber") String managementNumber,
                                           @Param("sampleCode") String sampleCode);
}
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceMaintenanceAttachment.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_maintenance_attachment")
public class DeviceMaintenanceAttachment {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Integer maintenancePlanDetailId;
    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/service/DeviceMaintenancePlanService.java
@@ -7,8 +7,10 @@
import com.ruoyi.device.dto.DeviceMaintenancePlanDetailsDto;
import com.ruoyi.device.dto.DeviceMaintenancePlanDto;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceMaintenanceAttachment;
import com.ruoyi.device.pojo.DeviceMaintenancePlan;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDate;
@@ -136,4 +138,19 @@
     * åˆ é™¤è®¾å¤‡ä¿å…»è®¡åˆ’详情;已出审核结果的记录随历史留档,未审核的记录随明细一起删除
     */
    boolean removeMaintenancePlanDetail(Integer maintenancePlanDetailId);
    /**
     * æŸ¥è¯¢è®¡åˆ’明细附件列表
     */
    List<DeviceMaintenanceAttachment> selectAttachments(Integer maintenancePlanDetailId);
    /**
     * ä¸Šä¼ è®¡åˆ’明细附件
     */
    DeviceMaintenanceAttachment uploadAttachment(Integer maintenancePlanDetailId, MultipartFile file);
    /**
     * åˆ é™¤è®¡åˆ’明细附件
     */
    boolean deleteAttachment(Long id);
}
cnas-device/src/main/java/com/ruoyi/device/service/DeviceRecordService.java
@@ -18,17 +18,20 @@
 */
public interface DeviceRecordService extends IService<DeviceRecord> {
    IPage<DeviceRecordDto> deviceRecordPage(Integer deviceId, Page page, String sampleCode, String managementNumber);
    IPage<DeviceRecordDto> deviceRecordPage(Integer deviceId, Page page, String deviceName,
                                            String specificationModel, String sampleCode, String managementNumber);
    /**
     * å¯¼å‡ºè®¾å¤‡ä½¿ç”¨è®°å½•
     *
     * @param deviceId
     * @param deviceName è®¾å¤‡åç§°ç­›é€‰
     * @param specificationModel è®¾å¤‡åž‹å·ç­›é€‰
     * @param managementNumber è®¾å¤‡ç¼–号筛选
     * @param sampleCode æ ·å“ç¼–号筛选
     * @param managementNumber ç®¡ç†ç¼–号筛选
     * @param exportDate å¯¼å‡ºæœˆä»½
     * @param response
     */
    void exportUseRecord(Integer deviceId, String sampleCode, String managementNumber, String exportDate, HttpServletResponse response);
    void exportUseRecord(Integer deviceId, String deviceName, String specificationModel,
                         String managementNumber, String sampleCode, HttpServletResponse response);
}
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceMaintenancePlanServiceImpl.java
@@ -17,12 +17,14 @@
import com.ruoyi.common.utils.WxCpUtils;
import com.ruoyi.device.dto.DeviceMaintenancePlanDetailsDto;
import com.ruoyi.device.dto.DeviceMaintenancePlanDto;
import com.ruoyi.device.mapper.DeviceMaintenanceAttachmentMapper;
import com.ruoyi.device.mapper.DeviceMaintenanceMapper;
import com.ruoyi.device.mapper.DeviceMaintenancePlanDetailsMapper;
import com.ruoyi.device.mapper.DeviceMaintenancePlanMapper;
import com.ruoyi.device.mapper.DeviceMapper;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.pojo.DeviceMaintenanceAttachment;
import com.ruoyi.device.pojo.DeviceMaintenancePlan;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import com.ruoyi.device.service.DeviceMaintenancePlanDetailsService;
@@ -34,12 +36,16 @@
import com.ruoyi.system.service.InformationNotificationService;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
@@ -48,7 +54,9 @@
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -79,6 +87,14 @@
    private DeviceMapper deviceMapper;
    @Resource
    private DeviceMaintenanceMapper deviceMaintenanceMapper;
    @Resource
    private DeviceMaintenanceAttachmentMapper deviceMaintenanceAttachmentMapper;
    @Value("${file.path}")
    private String filePath;
    @Value("${wordUrl}")
    private String wordUrl;
    /**
     * åˆ†é¡µæŸ¥è¯¢è®¾å¤‡ä¿å…»è®¡åˆ’
@@ -141,13 +157,22 @@
        }
        this.saveOrUpdate(deviceMaintenancePlanDto);
        // åˆ é™¤åŽŸæœ¬çš„è¯¦æƒ…
        // åˆ é™¤åŽŸæœ¬çš„è¯¦æƒ…ï¼›æ˜Žç»†é‡å»ºåŽä»ä¿ç•™åŽŸæ˜Žç»†id的附件不清理,id丢失的才清理
        List<DeviceMaintenancePlanDetails> oldDetails = deviceMaintenancePlanDetailsService.list(
                Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                        .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        deviceMaintenancePlanDetailsService.remove(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery().eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        List<Integer> lostDetailIds = oldDetails.stream().map(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId)
                .collect(Collectors.toList());
        // è¯¦æƒ…赋值并保存
        List<DeviceMaintenancePlanDetailsDto> deviceMaintenancePlanDetails = deviceMaintenancePlanDto.getDeviceMaintenancePlanDetails();
        if (CollectionUtils.isNotEmpty(deviceMaintenancePlanDetails)) { // è¯¦æƒ…不为空
            List<Integer> keptDetailIds = deviceMaintenancePlanDetails.stream()
                    .map(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId)
                    .filter(id -> id != null)
                    .collect(Collectors.toList());
            lostDetailIds.removeAll(keptDetailIds);
            List<DeviceMaintenancePlanDetails> collect = deviceMaintenancePlanDetails.stream().map(deviceMaintenancePlanDetail -> { // éåŽ†è¯¦æƒ…
                deviceMaintenancePlanDetail.setDeviceId(deviceMaintenancePlanDto.getDeviceId()); // è®¾å¤‡ID
                deviceMaintenancePlanDetail.setMaintenancePlanId(deviceMaintenancePlanDto.getMaintenancePlanId()); // ä¿å…»è®¡åˆ’ID
                DeviceMaintenancePlanDetails planDetails = new DeviceMaintenancePlanDetails();
                BeanUtils.copyProperties(deviceMaintenancePlanDetail, planDetails);
@@ -155,6 +180,7 @@
            }).collect(Collectors.toList());
            deviceMaintenancePlanDetailsService.saveBatch(collect);
        }
        removeAttachmentsByDetailIds(lostDetailIds);
        return Result.success();
    }
@@ -165,8 +191,12 @@
     */
    @Override
    public Result deleteMaintenancePlan(DeviceMaintenancePlanDto deviceMaintenancePlanDto) {
        List<DeviceMaintenancePlanDetails> details = deviceMaintenancePlanDetailsService.list(
                Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                        .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        this.removeById(deviceMaintenancePlanDto);
        deviceMaintenancePlanDetailsService.remove(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery().eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, deviceMaintenancePlanDto.getMaintenancePlanId()));
        removeAttachmentsByDetailIds(details.stream().map(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId).collect(Collectors.toList()));
        return Result.success();
    }
@@ -181,27 +211,27 @@
        // æŸ¥è¯¢è®¾å¤‡ä¿å…»è®¡åˆ’
        DeviceMaintenancePlanDto deviceMaintenancePlan = baseMapper.selectMaintenancePlanById(maintenancePlanId);
        // æŸ¥è¯¢è®¾å¤‡ä¿å…»è®¡åˆ’详情
        List<DeviceMaintenancePlanDetailsDto> deviceMaintenancePlanDetailsDtoList = deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId);
        // æŸ¥è¯¢è®¾å¤‡ä¿å…»è®¡åˆ’详情,按“设备 + ä¿å…»æœˆä»½â€å…œåº•去重
        List<DeviceMaintenancePlanDetailsDto> deviceMaintenancePlanDetailsDtoList = dedupeMaintenanceDetails(
                deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId));
        // è®¾ç½®åºå·
        deviceMaintenancePlanDetailsDtoList.forEach(deviceInspectionRecordDetails -> {
            deviceInspectionRecordDetails.setIndex(deviceMaintenancePlanDetailsDtoList.indexOf(deviceInspectionRecordDetails) + 1);
        });
        for (int i = 0; i < deviceMaintenancePlanDetailsDtoList.size(); i++) {
            deviceMaintenancePlanDetailsDtoList.get(i).setIndex(i + 1);
        }
        // èŽ·å–è·¯å¾„
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/maintenance-plan.docx");
        Configure configure = Configure.builder()
                .bind("deviceMaintenancePlanDetailsDtoList", new HackLoopTableRenderPolicy())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("deviceMaintenancePlan", deviceMaintenancePlan);
                    put("deviceMaintenancePlanDetailsDtoList", deviceMaintenancePlanDetailsDtoList);
                    // ç¼–制人签名地址
                    put("compilerUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceMaintenancePlan.getCompilerId()));
                    // å®¡æ ¸äººç­¾ååœ°å€
                    put("auditUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceMaintenancePlan.getAuditId()));
                }});
        Map<String, Object> renderData = new HashMap<>();
        renderData.put("deviceMaintenancePlan", deviceMaintenancePlan);
        renderData.put("deviceMaintenancePlanDetailsDtoList", deviceMaintenancePlanDetailsDtoList);
        // ç¼–制人签名地址
        renderData.put("compilerUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceMaintenancePlan.getCompilerId()));
        // å®¡æ ¸äººç­¾ååœ°å€
        renderData.put("auditUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceMaintenancePlan.getAuditId()));
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(renderData);
        try {
            response.setContentType("application/msword");
@@ -233,7 +263,8 @@
        // æŸ¥è¯¢è¯¦æƒ…
        DeviceMaintenancePlanDto deviceMaintenancePlanDto = new DeviceMaintenancePlanDto();
        BeanUtils.copyProperties(deviceMaintenancePlan, deviceMaintenancePlanDto);
        deviceMaintenancePlanDto.setDeviceMaintenancePlanDetails(deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId));
        deviceMaintenancePlanDto.setDeviceMaintenancePlanDetails(dedupeMaintenanceDetails(
                deviceMaintenancePlanDetailsMapper.deviceInspectionRecordDetailsList(maintenancePlanId)));
        return Result.success(deviceMaintenancePlanDto);
    }
@@ -659,9 +690,94 @@
            List<Integer> recordIds = records.stream().map(DeviceMaintenance::getId).collect(Collectors.toList());
            deviceMaintenanceMapper.deleteBatchIds(recordIds);
        }
        removeAttachmentsByDetailIds(List.of(maintenancePlanDetailId));
        return deviceMaintenancePlanDetailsMapper.deleteById(maintenancePlanDetailId) > 0;
    }
    @Override
    public List<DeviceMaintenanceAttachment> selectAttachments(Integer maintenancePlanDetailId) {
        if (maintenancePlanDetailId == null) {
            return new ArrayList<>();
        }
        return deviceMaintenanceAttachmentMapper.selectList(Wrappers.<DeviceMaintenanceAttachment>lambdaQuery()
                .eq(DeviceMaintenanceAttachment::getMaintenancePlanDetailId, maintenancePlanDetailId)
                .orderByAsc(DeviceMaintenanceAttachment::getSort, DeviceMaintenanceAttachment::getId));
    }
    @Override
    public DeviceMaintenanceAttachment uploadAttachment(Integer maintenancePlanDetailId, MultipartFile file) {
        if (maintenancePlanDetailId == null) {
            throw new ErrorException("缺少保养计划明细id");
        }
        if (deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId) == null) {
            throw new ErrorException("保养计划明细不存在");
        }
        if (file == null || file.isEmpty()) {
            throw new ErrorException("附件不能为空");
        }
        // ç³»ç»Ÿå…¨å±€ä¸Šä¼ ä¸Šé™ä¸º200MB,这里提前给出友好提示
        if (file.getSize() > 200L * 1024 * 1024) {
            throw new ErrorException("附件不能超过200MB");
        }
        String originalName = file.getOriginalFilename();
        if (originalName == null || originalName.trim().isEmpty()) {
            originalName = "未命名文件";
        }
        // ä¸ªåˆ«æµè§ˆå™¨å¯èƒ½å¸¦ä¸Šè·¯å¾„前缀,只保留文件名本身
        originalName = originalName.replaceAll(".*[/\\\\]", "");
        try {
            // å›¾ç‰‡æ”¾å›¾ç‰‡ç›®å½•(/img é™æ€è·¯å¾„),其余任意格式放 word ç›®å½•(/word é™æ€è·¯å¾„)
            boolean image = originalName.matches("(?i).+\\.(jpg|jpeg|png|gif)$");
            File directory = new File(image ? filePath : wordUrl);
            if (!directory.exists()) {
                directory.mkdirs();
            }
            String fileUrl = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")) + "-" + originalName;
            file.transferTo(new File(directory, fileUrl));
            DeviceMaintenanceAttachment attachment = new DeviceMaintenanceAttachment();
            attachment.setMaintenancePlanDetailId(maintenancePlanDetailId);
            attachment.setFileName(originalName);
            attachment.setFileUrl(fileUrl);
            attachment.setSort(selectAttachments(maintenancePlanDetailId).size());
            deviceMaintenanceAttachmentMapper.insert(attachment);
            return attachment;
        } catch (IOException e) {
            throw new ErrorException("附件上传失败");
        }
    }
    @Override
    public boolean deleteAttachment(Long id) {
        return deviceMaintenanceAttachmentMapper.deleteById(id) > 0;
    }
    /**
     * åˆ é™¤è®¡åˆ’明细对应的附件记录
     */
    private void removeAttachmentsByDetailIds(List<Integer> maintenancePlanDetailIds) {
        if (CollectionUtils.isEmpty(maintenancePlanDetailIds)) {
            return;
        }
        deviceMaintenanceAttachmentMapper.delete(Wrappers.<DeviceMaintenanceAttachment>lambdaQuery()
                .in(DeviceMaintenanceAttachment::getMaintenancePlanDetailId, maintenancePlanDetailIds));
    }
    /**
     * åŒä¸€è®¡åˆ’、同一设备只保留一条;SQL å·²æŒ‰â€œå·²æœ‰ä¿å…»è®°å½•优先、审核通过优先、最新优先”排序,这里取首条
     */
    private List<DeviceMaintenancePlanDetailsDto> dedupeMaintenanceDetails(List<DeviceMaintenancePlanDetailsDto> details) {
        if (CollectionUtils.isEmpty(details)) {
            return details;
        }
        Map<String, DeviceMaintenancePlanDetailsDto> unique = new LinkedHashMap<>();
        for (DeviceMaintenancePlanDetailsDto detail : details) {
            String key = detail.getDeviceId() != null ? String.valueOf(detail.getDeviceId())
                    : "name:" + detail.getDeviceName();
            unique.putIfAbsent(key, detail);
        }
        return new ArrayList<>(unique.values());
    }
    /**
     * ä¸¤ä¸ªä¿å…»æ—¥æœŸä¹‹é—´ç›¸å·®çš„æœˆæ•°
     */
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
@@ -11,6 +11,7 @@
import com.ruoyi.device.service.DeviceRecordService;
import com.ruoyi.system.domain.vo.SysRoleVo;
import com.ruoyi.system.mapper.SysRoleMenuMapper;
import org.apache.poi.xwpf.usermodel.BreakType;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.TableRowAlign;
import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
@@ -71,11 +72,9 @@
    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_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 String FOOTNOTE = "注:设备使用前后一栏,正常划“√”;不正常划“×”并写明情况。";
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
@@ -83,7 +82,9 @@
    private SysRoleMenuMapper sysRoleMenuMapper;
    @Override
    public IPage<DeviceRecordDto> deviceRecordPage(Integer deviceId, Page page, String sampleCode, String managementNumber) {
    public IPage<DeviceRecordDto> deviceRecordPage(Integer deviceId, Page page, String deviceName,
                                                   String specificationModel, String sampleCode,
                                                   String managementNumber) {
        // todo:仅看自己
        Long userId = SecurityUtils.getUserId();
        // èŽ·å–æƒé™åˆ¤æ–­æ˜¯å¦æ˜¯ä»…çœ‹æˆ‘
@@ -95,19 +96,20 @@
            testUserId = SecurityUtils.getUserId().intValue();
        }
        return baseMapper.deviceRecordPage(deviceId, page, sampleCode, managementNumber, testUserId);
        return baseMapper.deviceRecordPage(deviceId, page, deviceName, specificationModel,
                sampleCode, managementNumber, testUserId);
    }
    @Override
    public void exportUseRecord(Integer deviceId, String sampleCode, String managementNumber, String exportDate, HttpServletResponse response) {
        List<DeviceRecordDto> records = baseMapper.selectExportList(deviceId, sampleCode, managementNumber, exportDate);
    public void exportUseRecord(Integer deviceId, String deviceName, String specificationModel,
                                String managementNumber, String sampleCode, HttpServletResponse response) {
        List<DeviceRecordDto> records = baseMapper.selectExportList(deviceId, deviceName,
                specificationModel, managementNumber, sampleCode);
        if (records == null || records.isEmpty()) {
            throw new ServiceException(exportDate == null || exportDate.isEmpty()
                    ? "没有可导出的使用记录,无法导出"
                    : exportDate + " æ²¡æœ‰å¯å¯¼å‡ºçš„使用记录,无法导出");
            throw new ServiceException("没有可导出的使用记录,无法导出");
        }
        // å•台设备沿用截图表头; å¤šå°è®¾å¤‡åˆå¹¶åˆ°ä¸€å¼ è¡¨, æ¯è¡Œå¸¦è®¾å¤‡ä¿¡æ¯
        // ä¸€å°è®¾å¤‡ä¸€æœ¬è¿è¡Œè®°å½•,多台设备在同一 Word å†…按设备分页
        Map<Integer, List<DeviceRecordDto>> deviceRecords = new LinkedHashMap<>();
        for (DeviceRecordDto record : records) {
            deviceRecords.computeIfAbsent(record.getDeviceId(), key -> new ArrayList<>()).add(record);
@@ -115,11 +117,13 @@
        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);
            boolean first = true;
            for (List<DeviceRecordDto> group : deviceRecords.values()) {
                if (!first) {
                    addPageBreak(document);
                }
                renderForm(document, group.get(0), mergeSameConditionRecords(group));
                first = false;
            }
            response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
@@ -132,6 +136,49 @@
        } catch (IOException e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    /**
     * åŒä¸€è®¾å¤‡ã€åŒä¸€å¤©ã€èµ·æ­¢æ—¶é—´/温湿度/使用人/使用前后状况相同的记录合并一行,样品编号用“、”拼接。
     */
    private List<DeviceRecordDto> mergeSameConditionRecords(List<DeviceRecordDto> records) {
        Map<String, DeviceRecordDto> merged = new LinkedHashMap<>();
        for (DeviceRecordDto record : records) {
            String key = String.join("|",
                    formatDate(record.getUseStartDate()),
                    formatTime(record.getUseStartDate()),
                    formatTime(record.getUseEndDate()),
                    valueOrEmpty(record.getTemperature()),
                    valueOrEmpty(record.getHumidity()),
                    valueOrEmpty(record.getUsePerson()),
                    String.valueOf(record.getUseBefore()),
                    String.valueOf(record.getUseAfter()));
            DeviceRecordDto existing = merged.get(key);
            if (existing == null) {
                merged.put(key, record);
            } else {
                existing.setSampleCode(appendDistinct(existing.getSampleCode(), record.getSampleCode()));
                existing.setRemark(appendDistinct(existing.getRemark(), record.getRemark()));
            }
        }
        return new ArrayList<>(merged.values());
    }
    private String appendDistinct(String existing, String addition) {
        if (addition == null || addition.trim().isEmpty()) {
            return existing;
        }
        if (existing == null || existing.trim().isEmpty()) {
            return addition;
        }
        List<String> parts = java.util.Arrays.asList(existing.split("、"));
        return parts.contains(addition) ? existing : existing + "、" + addition;
    }
    private void addPageBreak(XWPFDocument document) {
        XWPFParagraph paragraph = document.createParagraph();
        paragraph.setSpacingAfter(0);
        paragraph.createRun().addBreak(BreakType.PAGE);
    }
    private void setupPage(XWPFDocument document) {
@@ -198,54 +245,10 @@
                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]);
            }
        }
        XWPFParagraph footnote = addParagraph(document, FOOTNOTE, 10.5, false, ParagraphAlignment.LEFT);
        footnote.setSpacingBefore(120);
        footnote.setSpacingAfter(0);
    }
    private XWPFParagraph addParagraph(XWPFDocument document, String text, double fontSize, boolean bold, ParagraphAlignment alignment) {
@@ -461,24 +464,6 @@
        };
    }
    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);
    }
@@ -491,7 +476,7 @@
        if (value == null) {
            return "";
        }
        return value == 0 ? "异常" : "良好";
        return value == 0 ? "异常" : "正常";
    }
    private String valueOrEmpty(String value) {
cnas-device/src/main/resources/mapper/DeviceMaintenancePlanDetailsMapper.xml
@@ -11,10 +11,17 @@
            d.management_number device_number,
            d.specification_model,
            d.management_number,
            d.storage_point
            d.storage_point,
            (select count(*) from device_maintenance dm
             where dm.maintenance_plan_detail_id = dmpd.maintenance_plan_detail_id) has_record
        from device_maintenance_plan_details dmpd
        left join device d on dmpd.device_id = d.id
        where dmpd.maintenance_plan_id = #{maintenancePlanId}
        order by has_record desc,
                 (select count(*) from device_maintenance dm2
                  where dm2.maintenance_plan_detail_id = dmpd.maintenance_plan_detail_id
                    and dm2.review_status = 1) desc,
                 dmpd.maintenance_plan_detail_id desc
    </select>
    <!-- è®¾å¤‡ä¿å…»è®¡åˆ’详情列表(含到点标红、关联的保养记录与审核状态) -->
@@ -53,7 +60,9 @@
            case when d.next_maintenance_date is not null
                and d.next_maintenance_date &lt;= date_add(curdate(), interval 1 month)
                and (d.last_maintenance_date is null or d.last_maintenance_date &lt; dmpd.maintenance_date)
                then 1 else 0 end as red_flag
                then 1 else 0 end as red_flag,
            (select count(*) from device_maintenance_attachment a
             where a.maintenance_plan_detail_id = dmpd.maintenance_plan_detail_id) attachment_count
        from device_maintenance_plan_details dmpd
        left join device d on dmpd.device_id = d.id
        left join device_maintenance dm on dm.maintenance_plan_detail_id = dmpd.maintenance_plan_detail_id
cnas-device/src/main/resources/mapper/InsOrderDeviceRecordMapper.xml
@@ -15,6 +15,12 @@
        <if test="deviceId != null">
            and dr.device_id = #{deviceId}
        </if>
        <if test="deviceName != '' and deviceName != null">
            and d.device_name like concat('%', #{deviceName}, '%')
        </if>
        <if test="specificationModel != '' and specificationModel != null">
            and d.specification_model like concat('%', #{specificationModel}, '%')
        </if>
        <if test="sampleCode != '' and sampleCode != null">
            and dr.sample_code like concat('%', #{sampleCode}, '%')
        </if>
@@ -50,15 +56,18 @@
        <if test="deviceId != null">
            and dr.device_id = #{deviceId}
        </if>
        <if test="sampleCode != '' and sampleCode != null">
            and dr.sample_code like concat('%', #{sampleCode}, '%')
        <if test="deviceName != '' and deviceName != null">
            and d.device_name like concat('%', #{deviceName}, '%')
        </if>
        <if test="specificationModel != '' and specificationModel != null">
            and d.specification_model like concat('%', #{specificationModel}, '%')
        </if>
        <if test="managementNumber != '' and managementNumber != null">
            and d.management_number like concat('%', #{managementNumber}, '%')
        </if>
        <if test="exportDate != '' and exportDate != null">
            and DATE_FORMAT(dr.use_start_date, '%Y-%m') = #{exportDate}
        <if test="sampleCode != '' and sampleCode != null">
            and dr.sample_code like concat('%', #{sampleCode}, '%')
        </if>
        ORDER BY dr.use_start_date asc
        ORDER BY d.device_name asc, d.id asc, dr.use_start_date asc
    </select>
</mapper>
cnas-device/src/main/resources/static/word/maintenance-plan.docx
Binary files differ
cnas-manage/src/main/java/com/ruoyi/manage/controller/ManageDocumentControlledController.java
@@ -1,20 +1,26 @@
package com.ruoyi.manage.controller;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.deepoove.poi.data.style.*;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.utils.JackSonUtil;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.manage.excel.ManageDocumentControlledImportDto;
import com.ruoyi.manage.excel.ManageDocumentControlledImportListener;
import com.ruoyi.manage.pojo.ManageDocumentControlled;
import com.ruoyi.manage.service.ManageDocumentControlledService;
import com.deepoove.poi.data.style.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
@@ -45,6 +51,21 @@
        return Result.success(manageDocumentControlledService.addManageDocumentControlled(manageDocumentControlled));
    }
    @Operation(summary = "批量导入文件受控申请")
    @PostMapping("/importManageDocumentControlled")
    public Result importManageDocumentControlled(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new ErrorException("请选择导入文件");
        }
        try {
            EasyExcel.read(file.getInputStream(), ManageDocumentControlledImportDto.class,
                    new ManageDocumentControlledImportListener(manageDocumentControlledService)).sheet().doRead();
        } catch (IOException e) {
            throw new ErrorException("读取导入文件失败");
        }
        return Result.success();
    }
    @Operation(summary = "查看文件受控")
    @GetMapping("/getManageDocumentControlled")
cnas-manage/src/main/java/com/ruoyi/manage/excel/ManageDocumentControlledImportDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.ruoyi.manage.excel;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
/**
 * æ–‡ä»¶å—控申请导入行
 */
@Getter
@Setter
@ExcelIgnoreUnannotated
public class ManageDocumentControlledImportDto {
    @ExcelProperty("文件名称")
    private String name;
    @ExcelProperty("文件版本")
    private String version;
    @ExcelProperty("发布日期")
    private String publishDate;
    @ExcelProperty("实施日期")
    private String implementDate;
    @ExcelIgnore
    private LocalDate publishDateValue;
    @ExcelIgnore
    private LocalDate implementDateValue;
}
cnas-manage/src/main/java/com/ruoyi/manage/excel/ManageDocumentControlledImportListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
package com.ruoyi.manage.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.manage.service.ManageDocumentControlledService;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
 * æ–‡ä»¶å—控申请批量导入
 */
public class ManageDocumentControlledImportListener extends AnalysisEventListener<ManageDocumentControlledImportDto> {
    private static final List<String> HEADERS = Arrays.asList("文件名称", "文件版本", "发布日期", "实施日期");
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-M-d");
    private final ManageDocumentControlledService manageDocumentControlledService;
    private final List<ManageDocumentControlledImportDto> rows = new ArrayList<>();
    public ManageDocumentControlledImportListener(ManageDocumentControlledService manageDocumentControlledService) {
        this.manageDocumentControlledService = manageDocumentControlledService;
    }
    @Override
    public void invoke(ManageDocumentControlledImportDto data, AnalysisContext context) {
        if (isBlankRow(data)) {
            return;
        }
        int rowNum = context.readRowHolder().getRowIndex() + 1;
        if (!StringUtils.hasText(data.getName())) {
            throw new BaseException("第" + rowNum + "行:文件名称不能为空");
        }
        data.setPublishDateValue(parseDate(data.getPublishDate(), rowNum, "发布日期"));
        data.setImplementDateValue(parseDate(data.getImplementDate(), rowNum, "实施日期"));
        rows.add(data);
    }
    private LocalDate parseDate(String value, int rowNum, String column) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        String text = value.trim();
        int space = text.indexOf(' ');
        if (space > 0) {
            text = text.substring(0, space);
        }
        text = text.replace("å¹´", "-").replace("月", "-").replace("日", "")
                .replace("/", "-").replace(".", "-");
        try {
            // Excel é‡Œæ—¥æœŸå•元格可能被读成数字序列(1899-12-30 èµ·ç®—)
            if (text.matches("\\d{5}(\\.\\d+)?")) {
                return LocalDate.of(1899, 12, 30).plusDays((long) Double.parseDouble(text));
            }
            return LocalDate.parse(text, DATE_FORMAT);
        } catch (Exception e) {
            throw new BaseException("第" + rowNum + "行:" + column + "格式不正确,请按 yyyy-MM-dd å¡«å†™");
        }
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (rows.isEmpty()) {
            throw new BaseException("导入数据不得为空");
        }
        manageDocumentControlledService.importManageDocumentControlled(rows);
    }
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        if (CollectionUtils.isEmpty(headMap) || headMap.size() != HEADERS.size()) {
            throw new BaseException("导入模板有误,请使用最新模板");
        }
        for (int i = 0; i < HEADERS.size(); i++) {
            String head = headMap.get(i);
            if (head == null || !HEADERS.get(i).equals(head.trim())) {
                throw new BaseException("导入模板有误,表头必须为:文件名称、文件版本、发布日期、实施日期");
            }
        }
    }
    private boolean isBlankRow(ManageDocumentControlledImportDto data) {
        return data == null
                || (!StringUtils.hasText(data.getName())
                && !StringUtils.hasText(data.getVersion())
                && !StringUtils.hasText(data.getPublishDate())
                && !StringUtils.hasText(data.getImplementDate()));
    }
}
cnas-manage/src/main/java/com/ruoyi/manage/mapper/ManageDocumentControlledMapper.java
@@ -7,12 +7,14 @@
import com.ruoyi.manage.pojo.ManageDocumentControlled;
import org.apache.ibatis.annotations.Param;
import java.util.Map;
/**
 * <p>
 * æ–‡ä»¶å—控 Mapper æŽ¥å£
 * </p>
 *
 * @author
 * @author
 * @since 2024-11-08 02:54:44
 */
public interface ManageDocumentControlledMapper extends BaseMapper<ManageDocumentControlled> {
@@ -20,4 +22,10 @@
    IPage<ManageDocumentControlled> pageManageDocumentControlled(Page page, @Param("ew") QueryWrapper<ManageDocumentControlled> queryWrappers);
    ManageDocumentControlled getManageDocumentControlled(Long id);
    /**
     * å¯¼å…¥è‰ç¨¿ï¼šç”¨ Map ä¼ å‚,绕过 MyBatis-Plus çš„ createUser è‡ªåŠ¨å¡«å……ï¼Œ
     * è®©å¯¼å…¥æ•°æ®çš„申请人为空。
     */
    int insertImportedDraft(Map<String, Object> row);
}
cnas-manage/src/main/java/com/ruoyi/manage/pojo/ManageDocumentControlled.java
@@ -52,6 +52,16 @@
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate submitDate;
    @Schema(description = "发布日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate publishDate;
    @Schema(description = "实施日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate implementDate;
    @Schema(description = "说明")
    private String instructions;
cnas-manage/src/main/java/com/ruoyi/manage/service/ManageDocumentControlledService.java
@@ -3,9 +3,11 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.manage.excel.ManageDocumentControlledImportDto;
import com.ruoyi.manage.pojo.ManageDocumentControlled;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
@@ -30,5 +32,7 @@
    int checkManageDocumentControlled(ManageDocumentControlled manageDocumentControlled);
    int importManageDocumentControlled(List<ManageDocumentControlledImportDto> rows);
    void checkManageDocumentControlledPdf(Long id, HttpServletResponse response) throws Exception ;
}
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentAlterServiceImpl.java
@@ -99,6 +99,7 @@
        manageRecordAudit.setReason(manageDocumentAlter.getAlterNote());
        manageRecordAudit.setAlterUser(SecurityUtils.getUserId().intValue());
        manageRecordAudit.setMethod("修订");
        manageRecordAudit.setCreateTime(LocalDate.now());
        if (ObjectUtils.isNotEmpty(manageDocumentAlter.getFile())) {
            String urlString;
            String pathName;
@@ -156,7 +157,44 @@
                return 0;
            }
        }
        return manageDocumentAlterMapper.updateById(manageDocumentAlter);
        int result = manageDocumentAlterMapper.updateById(manageDocumentAlter);
        // å˜æ›´å†…容修改后同步到自动生成的修订记录,避免两个页面内容对不上
        ManageRecordAudit linkedAudit = findLinkedAudit(manageDocumentAlter1);
        if (linkedAudit != null) {
            if (ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterBeforeCode())) {
                linkedAudit.setDocumentCode(manageDocumentAlter.getAlterBeforeCode());
            }
            if (ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterBeforeName())) {
                linkedAudit.setDocumentName(manageDocumentAlter.getAlterBeforeName());
            }
            if (ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterBeforeVersion())) {
                linkedAudit.setBeforeVersion(manageDocumentAlter.getAlterBeforeVersion());
            }
            if (ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterAfterVersion())) {
                linkedAudit.setAfterVersion(manageDocumentAlter.getAlterAfterVersion());
            }
            if (ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterNote())) {
                linkedAudit.setReason(manageDocumentAlter.getAlterNote());
            }
            if (ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterAfterUrl())) {
                linkedAudit.setAfterUrl(manageDocumentAlter.getAlterAfterUrl());
            }
            manageRecordAuditMapper.updateById(linkedAudit);
        }
        return result;
    }
    // æŒ‰å˜æ›´å‰ä¿¡æ¯å®šä½è‡ªåŠ¨ç”Ÿæˆçš„é‚£æ¡ä¿®è®¢è®°å½•
    private ManageRecordAudit findLinkedAudit(ManageDocumentAlter manageDocumentAlter) {
        return manageRecordAuditMapper.selectOne(Wrappers.<ManageRecordAudit>lambdaQuery()
                .eq(ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterBeforeCode()), ManageRecordAudit::getDocumentCode, manageDocumentAlter.getAlterBeforeCode())
                .eq(ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterBeforeName()), ManageRecordAudit::getDocumentName, manageDocumentAlter.getAlterBeforeName())
                .eq(ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterBeforeVersion()), ManageRecordAudit::getBeforeVersion, manageDocumentAlter.getAlterBeforeVersion())
                .eq(ObjectUtils.isNotEmpty(manageDocumentAlter.getAlterAfterVersion()), ManageRecordAudit::getAfterVersion, manageDocumentAlter.getAlterAfterVersion())
                .eq(ObjectUtils.isNotEmpty(manageDocumentAlter.getCreateUser()), ManageRecordAudit::getAlterUser, manageDocumentAlter.getCreateUser())
                .eq(ManageRecordAudit::getMethod, "修订")
                .orderByDesc(ManageRecordAudit::getId)
                .last("limit 1"));
    }
    @Override
@@ -217,6 +255,8 @@
                manageRecordAudit.setDocumentName(manageDocumentCancel.getName());
                manageRecordAudit.setAlterThing("作废");
                manageRecordAudit.setMethod("作废");
                manageRecordAudit.setAlterUser(oldManageDocumentAlter.getCreateUser());
                manageRecordAudit.setCreateTime(LocalDate.now());
                manageRecordAuditMapper.insert(manageRecordAudit);
            }else {
                //如果是存档不可用,那就将文件清单的状态改为无效
@@ -235,6 +275,15 @@
        manageRecordCheck.setCheckState(manageDocumentAlter.getState());
        manageRecordCheck.setRemark(manageDocumentAlter.getAlterNote());
        manageRecordCheckMapper.insert(manageRecordCheck);
        // å®¡æ ¸é€šè¿‡åŽå›žå†™æ‰¹å‡†äººï¼Œæ–‡ä»¶ä¿®è®¢ç”³è¯·å®¡æ‰¹è®°å½•里能直接看到审批结果
        ManageRecordAudit linkedAudit = findLinkedAudit(oldManageDocumentAlter);
        if (linkedAudit != null
                && "通过".equals(manageDocumentAlter.getState())
                && linkedAudit.getRatifyUser() == null
                && oldManageDocumentAlter.getCheckUser() != null) {
            linkedAudit.setRatifyUser(oldManageDocumentAlter.getCheckUser());
            manageRecordAuditMapper.updateById(linkedAudit);
        }
        return manageDocumentAlterMapper.updateById(manageDocumentAlter);
    }
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentCancelServiceImpl.java
@@ -5,20 +5,25 @@
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.manage.mapper.ManageDocumentCancelMapper;
import com.ruoyi.manage.mapper.ManageDocumentListMapper;
import com.ruoyi.manage.mapper.ManageRecordAuditMapper;
import com.ruoyi.manage.mapper.ManageRecordCancelMapper;
import com.ruoyi.manage.mapper.ManageRecordCheckMapper;
import com.ruoyi.manage.pojo.ManageDocumentCancel;
import com.ruoyi.manage.pojo.ManageDocumentList;
import com.ruoyi.manage.pojo.ManageRecordAudit;
import com.ruoyi.manage.pojo.ManageRecordCancel;
import com.ruoyi.manage.pojo.ManageRecordCheck;
import com.ruoyi.manage.service.ManageDocumentCancelService;
@@ -29,6 +34,7 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -54,7 +60,8 @@
    @Resource
    private ManageRecordAuditMapper manageRecordAuditMapper;
    @Resource
    private ManageRecordCancelMapper manageRecordCancelMapper;
    @Resource
    private ManageDocumentListMapper manageDocumentListMapper;
@@ -76,6 +83,8 @@
        manageRecordAudit.setDocumentName(manageDocumentCancel.getName());
        manageRecordAudit.setAlterThing("作废");
        manageRecordAudit.setMethod("作废");
        manageRecordAudit.setAlterUser(SecurityUtils.getUserId().intValue());
        manageRecordAudit.setCreateTime(LocalDate.now());
        manageRecordAuditMapper.insert(manageRecordAudit);
        return manageDocumentCancelMapper.insert(manageDocumentCancel);
    }
@@ -83,9 +92,14 @@
    @Override
    public int checkManageDocumentCancel(Integer id, String state) {
        ManageDocumentCancel manageDocumentCancel = manageDocumentCancelMapper.selectById(id);
        if (state.equals("通过")){
        if (manageDocumentCancel == null) {
            throw new ErrorException("申请记录不存在");
        }
        if ("通过".equals(state)){
            //删除文件清单对应数据
            manageDocumentListMapper.delete(Wrappers.<ManageDocumentList>lambdaQuery().eq(ManageDocumentList::getDocumentCode,manageDocumentCancel.getDocumentCode()));
            // ä½œåºŸé€šè¿‡åŽè‡ªåŠ¨ç”Ÿæˆé”€æ¯å°è´¦ï¼Œä¿è¯â€œæ–‡ä»¶ä½œåºŸç”³è¯·â€å’Œâ€œä½œåºŸæ–‡ä»¶é”€æ¯è®°å½•â€çš„æ•°æ®è”åŠ¨
            createRecordCancelIfAbsent(manageDocumentCancel);
        }
        manageDocumentCancel.setState(state);
        /*新增8.4的文件审批记录*/
@@ -143,4 +157,24 @@
    public int doManageDocumentCancel(ManageDocumentCancel manageDocumentCancel) {
        return manageDocumentCancelMapper.updateById(manageDocumentCancel);
    }
    // ä½œåºŸç”³è¯·é€šè¿‡åŽï¼Œåœ¨â€œä½œåºŸæ–‡ä»¶é”€æ¯è®°å½•”生成一条待补全的台账记录
    private void createRecordCancelIfAbsent(ManageDocumentCancel manageDocumentCancel) {
        ManageRecordCancel existing = manageRecordCancelMapper.selectOne(Wrappers.<ManageRecordCancel>lambdaQuery()
                .eq(ObjectUtils.isNotEmpty(manageDocumentCancel.getDocumentCode()), ManageRecordCancel::getDocumentCode, manageDocumentCancel.getDocumentCode())
                .eq(ObjectUtils.isNotEmpty(manageDocumentCancel.getName()), ManageRecordCancel::getDocumentName, manageDocumentCancel.getName())
                .orderByDesc(ManageRecordCancel::getId)
                .last("limit 1"));
        if (existing != null) {
            return;
        }
        ManageRecordCancel manageRecordCancel = new ManageRecordCancel();
        manageRecordCancel.setDocumentCode(manageDocumentCancel.getDocumentCode());
        manageRecordCancel.setDocumentName(manageDocumentCancel.getName());
        manageRecordCancel.setReason(ObjectUtils.isNotEmpty(manageDocumentCancel.getCancelNote())
                ? manageDocumentCancel.getCancelNote() : "文件作废");
        manageRecordCancel.setRemark("由文件作废申请自动生成,请补充销毁数量后批准");
        manageRecordCancel.setCreateUser(manageDocumentCancel.getCreateUser());
        manageRecordCancelMapper.insert(manageRecordCancel);
    }
}
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentControlledServiceImpl.java
@@ -7,8 +7,10 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.common.numgen.NumberGenerator;
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.manage.excel.ManageDocumentControlledImportDto;
import com.ruoyi.manage.mapper.ManageDocumentControlledMapper;
import com.ruoyi.manage.mapper.ManageDocumentListMapper;
import com.ruoyi.manage.mapper.ManageRecordCheckMapper;
@@ -28,7 +30,10 @@
import java.io.FileInputStream;
import java.io.OutputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@@ -60,6 +65,11 @@
    @Resource
    private UserMapper userMapper;
    @Resource
    private NumberGenerator<ManageDocumentControlled> numberGenerator;
    private static final DateTimeFormatter CODE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd");
    @Override
    public IPage<ManageDocumentControlled> pageManageDocumentControlled(Page page, ManageDocumentControlled manageDocumentControlled) {
        return manageDocumentControlledMapper.pageManageDocumentControlled(page, QueryWrappers.queryWrappers(manageDocumentControlled));
@@ -68,6 +78,10 @@
    @Override
    public int addManageDocumentControlled(ManageDocumentControlled manageDocumentControlled) {
        manageDocumentControlled.setState("待审核");
        // æäº¤æ—¥æœŸä¸å†åœ¨ç•Œé¢å¡«å†™ï¼Œå…¥åº“时自动写当天,保留原有字段含义
        if (manageDocumentControlled.getSubmitDate() == null) {
            manageDocumentControlled.setSubmitDate(LocalDate.now());
        }
        String urlString;
        String pathName;
        String path;
@@ -149,6 +163,13 @@
    @Override
    public int checkManageDocumentControlled(ManageDocumentControlled manageDocumentControlled) {
        ManageDocumentControlled documentControlled = manageDocumentControlledMapper.selectById(manageDocumentControlled.getId());
        if (documentControlled == null) {
            throw new ErrorException("申请记录不存在");
        }
        // å·²é€šè¿‡çš„申请不能再审核,否则会重复生成文件清单和审批记录
        if ("通过".equals(documentControlled.getState())) {
            throw new ErrorException("该申请已审核通过,不能重复审核");
        }
        if (manageDocumentControlled.getState().equals("通过")) {
            // åˆ é™¤æ—§æ–‡ä»¶
            File oldFile = new File(wordUrl + "/" + documentControlled.getUrl());
@@ -180,7 +201,9 @@
            manageDocumentList.setName(documentControlled.getName());
            manageDocumentList.setVersion(documentControlled.getVersion());
            manageDocumentList.setWriter(documentControlled.getWriter());
            manageDocumentList.setEffectiveDate(LocalDate.now());
            // æ–‡ä»¶æ¸…单的生效日期取申请的“实施日期”,没填才用当天
            manageDocumentList.setEffectiveDate(documentControlled.getImplementDate() != null
                    ? documentControlled.getImplementDate() : LocalDate.now());
            manageDocumentList.setState("有效");
            manageDocumentList.setUrl(manageDocumentControlled.getUrl());
            manageDocumentListMapper.insert(manageDocumentList);
@@ -190,7 +213,14 @@
        manageRecordCheck.setDocumentCode(documentControlled.getDocumentCode());
        manageRecordCheck.setDocumentName(documentControlled.getName());
        manageRecordCheck.setDocumentVersion(documentControlled.getVersion());
        manageRecordCheck.setWriteUser(userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getName,documentControlled.getWriter())).getId());
        // ä½œè€…可能不是系统用户(历史数据/手填),查不到时不再空指针
        User writerUser = null;
        if (documentControlled.getWriter() != null && !documentControlled.getWriter().trim().isEmpty()) {
            writerUser = userMapper.selectOne(Wrappers.<User>lambdaQuery()
                    .eq(User::getName, documentControlled.getWriter())
                    .last("limit 1"));
        }
        manageRecordCheck.setWriteUser(writerUser == null ? null : writerUser.getId());
        manageRecordCheck.setCheckUser(documentControlled.getDutyUser());
        manageRecordCheck.setCheckState(manageDocumentControlled.getState());
        manageRecordCheck.setRemark(documentControlled.getInstructions());
@@ -199,6 +229,38 @@
    }
    @Override
    public int importManageDocumentControlled(List<ManageDocumentControlledImportDto> rows) {
        LocalDate today = LocalDate.now();
        LocalDateTime now = LocalDateTime.now();
        String codePrefix = "WT" + CODE_DATE_FORMAT.format(today);
        int count = 0;
        for (ManageDocumentControlledImportDto dto : rows) {
            Map<String, Object> row = new HashMap<>();
            row.put("documentCode", numberGenerator.generateNumberWithPrefix(
                    3, codePrefix, ManageDocumentControlled::getDocumentCode));
            row.put("type", null);
            row.put("name", dto.getName().trim());
            row.put("version", trimToNull(dto.getVersion()));
            row.put("writer", null);
            row.put("submitDate", today);
            row.put("publishDate", dto.getPublishDateValue());
            row.put("implementDate", dto.getImplementDateValue());
            row.put("instructions", null);
            row.put("state", "草稿");
            row.put("url", null);
            row.put("dutyUser", null);
            row.put("createTime", now);
            row.put("updateTime", now);
            count += manageDocumentControlledMapper.insertImportedDraft(row);
        }
        return count;
    }
    private String trimToNull(String value) {
        return value == null || value.trim().isEmpty() ? null : value.trim();
    }
    @Override
    public void checkManageDocumentControlledPdf(Long id, HttpServletResponse response) throws Exception {
        ManageDocumentControlled manageDocumentControlled = manageDocumentControlledMapper.selectById(id);
        if (manageDocumentControlled == null || ObjectUtils.isEmpty(manageDocumentControlled.getUrl())) {
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentIssueRecycleServiceImpl.java
@@ -149,14 +149,35 @@
        if (ObjectUtils.isNotEmpty(manageDocumentIssueRecycle.getRecycleUser())){
            /*新增8.3的发放与回收记录*/
            ManageRecordIssueRecycle manageRecordIssueRecycle = manageRecordIssueRecycleMapper.selectOne(Wrappers.<ManageRecordIssueRecycle>lambdaQuery()
                    .eq(ManageRecordIssueRecycle::getDocumentCode, documentIssueRecycle.getDocumentCode())
                    .eq(ManageRecordIssueRecycle::getDocumentName, documentIssueRecycle.getName())
                    .eq(ManageRecordIssueRecycle::getDocumentVersion, documentIssueRecycle.getVersion())
                    .eq(ManageRecordIssueRecycle::getReceiveUser, documentIssueRecycle.getIssueUser())
                    .eq(ManageRecordIssueRecycle::getReceiveDate, documentIssueRecycle.getIssueDate()));
            manageRecordIssueRecycle.setSignedUser(manageDocumentIssueRecycle.getRecycleUser());
            manageRecordIssueRecycle.setSignedDate(manageDocumentIssueRecycle.getRecycleDate());
            manageRecordIssueRecycleMapper.updateById(manageRecordIssueRecycle);
                    .eq(ObjectUtils.isNotEmpty(documentIssueRecycle.getDocumentCode()), ManageRecordIssueRecycle::getDocumentCode, documentIssueRecycle.getDocumentCode())
                    .eq(ObjectUtils.isNotEmpty(documentIssueRecycle.getName()), ManageRecordIssueRecycle::getDocumentName, documentIssueRecycle.getName())
                    .eq(ObjectUtils.isNotEmpty(documentIssueRecycle.getVersion()), ManageRecordIssueRecycle::getDocumentVersion, documentIssueRecycle.getVersion())
                    .eq(ObjectUtils.isNotEmpty(documentIssueRecycle.getIssueUser()), ManageRecordIssueRecycle::getReceiveUser, documentIssueRecycle.getIssueUser())
                    .eq(ObjectUtils.isNotEmpty(documentIssueRecycle.getIssueDate()), ManageRecordIssueRecycle::getReceiveDate, documentIssueRecycle.getIssueDate())
                    .orderByDesc(ManageRecordIssueRecycle::getId)
                    .last("limit 1"));
            if (manageRecordIssueRecycle == null) {
                // åŽ†å²å‘æ”¾è®°å½•åœ¨å°è´¦é‡Œå¯èƒ½æ²¡æœ‰å¯¹åº”è¡Œï¼Œå›žæ”¶æ—¶è¡¥å»ºï¼Œä¿è¯ä¸¤ä¸ªé¡µé¢å§‹ç»ˆèƒ½å¯¹ä¸Š
                manageRecordIssueRecycle = new ManageRecordIssueRecycle();
                manageRecordIssueRecycle.setDocumentCode(documentIssueRecycle.getDocumentCode());
                manageRecordIssueRecycle.setDocumentName(documentIssueRecycle.getName());
                manageRecordIssueRecycle.setDocumentVersion(documentIssueRecycle.getVersion());
                manageRecordIssueRecycle.setReceiveUser(documentIssueRecycle.getIssueUser());
                manageRecordIssueRecycle.setReceiveDate(documentIssueRecycle.getIssueDate());
                manageRecordIssueRecycle.setSignedUser(manageDocumentIssueRecycle.getRecycleUser());
                manageRecordIssueRecycle.setSignedDate(manageDocumentIssueRecycle.getRecycleDate());
                manageRecordIssueRecycleMapper.insert(manageRecordIssueRecycle);
            } else {
                // å‘放信息跟着变更,回收信息回写签收人/签收日期
                manageRecordIssueRecycle.setDocumentCode(manageDocumentIssueRecycle.getDocumentCode());
                manageRecordIssueRecycle.setDocumentName(manageDocumentIssueRecycle.getName());
                manageRecordIssueRecycle.setDocumentVersion(manageDocumentIssueRecycle.getVersion());
                manageRecordIssueRecycle.setReceiveUser(manageDocumentIssueRecycle.getIssueUser());
                manageRecordIssueRecycle.setReceiveDate(manageDocumentIssueRecycle.getIssueDate());
                manageRecordIssueRecycle.setSignedUser(manageDocumentIssueRecycle.getRecycleUser());
                manageRecordIssueRecycle.setSignedDate(manageDocumentIssueRecycle.getRecycleDate());
                manageRecordIssueRecycleMapper.updateById(manageRecordIssueRecycle);
            }
        }
        if (ObjectUtils.isNotEmpty(manageDocumentIssueRecycle.getFile())) {
            if (ObjectUtils.isNotEmpty(documentIssueRecycle.getUrl())) {
cnas-manage/src/main/resources/mapper/ManageDocumentControlledMapper.xml
@@ -10,6 +10,8 @@
        <result column="version" property="version"/>
        <result column="writer" property="writer"/>
        <result column="submit_date" property="submitDate"/>
        <result column="publish_date" property="publishDate"/>
        <result column="implement_date" property="implementDate"/>
        <result column="instructions" property="instructions"/>
        <result column="state" property="state"/>
        <result column="url" property="url"/>
@@ -39,4 +41,14 @@
                 left join user u1 on cmdc.create_user = u1.id
                 left join user u2 on cmdc.duty_user = u2.id
    </select>
    <!-- å¯¼å…¥è‰ç¨¿ï¼šcreate_user æ˜¾å¼å†™ NULL,申请人留空 -->
    <insert id="insertImportedDraft" parameterType="java.util.Map">
        insert into cnas_manage_document_controlled
        (document_code, type, name, version, writer, submit_date, publish_date, implement_date,
         instructions, state, url, duty_user, create_user, update_user, create_time, update_time)
        values
        (#{documentCode}, #{type}, #{name}, #{version}, #{writer}, #{submitDate}, #{publishDate}, #{implementDate},
         #{instructions}, #{state}, #{url}, #{dutyUser}, null, null, #{createTime}, #{updateTime})
    </insert>
</mapper>
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonBasicInfoController.java
@@ -96,9 +96,20 @@
    // åˆ é™¤æ–‡ä»¶
    @DeleteMapping("/deleteCNASFile")
    public Result saveFile(String fileName) {
        if (fileName == null || fileName.trim().isEmpty()) {
            throw new ErrorException("缺少文件名");
        }
        String[] split = fileName.split(",");
        for (String s : split) {
            FileSaveUtil.DeleteFile(s);
            String name = s.trim();
            if (name.isEmpty()) {
                continue;
            }
            // åªå…è®¸åˆ é™¤æ–‡ä»¶åï¼Œç¦æ­¢é€šè¿‡è·¯å¾„穿越删除其他目录
            if (name.contains("/") || name.contains("\\") || name.contains("..")) {
                throw new ErrorException("文件名不合法");
            }
            FileSaveUtil.DeleteFile(name);
        }
        return Result.success();
@@ -144,8 +155,8 @@
    @Operation(summary = "下载人员档案卡")
    @GetMapping("/exportPersonBasicInfoById")
    public Result exportPersonBasicInfoById(Integer id, HttpServletResponse response) {
        return Result.success(personBasicInfoService.exportPersonBasicInfoById(id,response));
    public void exportPersonBasicInfoById(Integer id, HttpServletResponse response) {
        personBasicInfoService.exportPersonBasicInfoById(id, response);
    }
    /**
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonPersonnelCapacityController.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.personnel.dto.PersonPersonnelCapacityDto;
import com.ruoyi.personnel.pojo.PersonPersonnelCapacity;
import com.ruoyi.personnel.service.PersonPersonnelCapacityService;
@@ -36,6 +37,18 @@
    @Operation(summary = "新增/更新 äººå‘˜èƒ½åŠ›")
    @PostMapping("addOrUpdatePersonPersonnelCapacity")
    public Result<?> addOrUpdatePersonPersonnelCapacity(@RequestBody PersonPersonnelCapacity personPersonnelCapacity) {
        if (personPersonnelCapacity == null || personPersonnelCapacity.getUserId() == null) {
            throw new ErrorException("缺少人员");
        }
        if (personPersonnelCapacity.getId() != null) {
            PersonPersonnelCapacity existing = personPersonnelCapacityService.getById(personPersonnelCapacity.getId());
            if (existing == null) {
                throw new ErrorException("人员能力记录不存在或已被删除");
            }
            if (existing.getConfirmDate() != null) {
                throw new ErrorException("该记录已确认,不能修改");
            }
        }
        personPersonnelCapacityService.saveOrUpdate(personPersonnelCapacity);
        return Result.success();
    }
@@ -43,7 +56,16 @@
    @Operation(summary = "删除 äººå‘˜èƒ½åŠ›")
    @DeleteMapping("deletePersonPersonnelCapacity")
    public Result<?> deletePersonPersonnelCapacity(@RequestParam("id") Integer id) {
        // åˆ é™¤æ•°æ®
        if (id == null) {
            throw new ErrorException("缺少人员能力记录id");
        }
        PersonPersonnelCapacity existing = personPersonnelCapacityService.getById(id);
        if (existing == null) {
            throw new ErrorException("人员能力记录不存在或已被删除");
        }
        if (existing.getConfirmDate() != null) {
            throw new ErrorException("该记录已确认,不能删除");
        }
        personPersonnelCapacityService.removeById(id);
        return Result.success();
    }
@@ -60,11 +82,24 @@
    @Operation(summary = "确认 äººå‘˜èƒ½åŠ›")
    @PostMapping("confirmPersonnelCapability")
    public Result<?> confirmPersonnelCapability(@RequestBody PersonPersonnelCapacity personPersonnelCapacity) {
        if (personPersonnelCapacity == null || personPersonnelCapacity.getId() == null) {
            throw new ErrorException("缺少人员能力记录id");
        }
        PersonPersonnelCapacity record = personPersonnelCapacityService.getById(personPersonnelCapacity.getId());
        if (record == null) {
            throw new ErrorException("人员能力记录不存在或已被删除");
        }
        if (record.getConfirmDate() != null) {
            throw new ErrorException("该记录已确认,请勿重复确认");
        }
        Integer userId = SecurityUtils.getUserId().intValue();
        if (record.getConfirmOperatingPersonnelId() == null
                || !record.getConfirmOperatingPersonnelId().equals(userId)) {
            throw new ErrorException("您不是该记录指定的确认人,无法确认");
        }
        personPersonnelCapacityService.update(Wrappers.<PersonPersonnelCapacity>lambdaUpdate()
                .eq(PersonPersonnelCapacity::getId, personPersonnelCapacity.getId())
                .set(PersonPersonnelCapacity::getConfirmOperatingPersonnelId, userId)
                .set(PersonPersonnelCapacity::getConfirmDate, personPersonnelCapacity.getConfirmDate()));
                .eq(PersonPersonnelCapacity::getId, record.getId())
                .set(PersonPersonnelCapacity::getConfirmDate, LocalDateTime.now()));
        return Result.success();
    }
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonPostAuthorizationRecordController.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.common.utils.FileSaveUtil;
import com.ruoyi.personnel.dto.PersonPostAuthorizationRecordDto;
import com.ruoyi.personnel.pojo.PersonPostAuthorizationRecord;
@@ -33,6 +34,16 @@
    @Operation(summary = "新增/更新 ä»»èŒæŽˆæƒè®°å½•")
    @PostMapping("/addOrUpdatePersonPostAuthorizationRecord")
    public Result<?> addOrUpdatePersonPostAuthorizationRecord(@RequestBody PersonPostAuthorizationRecord personRewardPunishmentRecord) {
        if (personRewardPunishmentRecord == null || personRewardPunishmentRecord.getUserId() == null) {
            throw new ErrorException("请选择被任职人员");
        }
        if (personRewardPunishmentRecord.getPost() == null || personRewardPunishmentRecord.getPost().trim().isEmpty()) {
            throw new ErrorException("请输入任职岗位");
        }
        if (personRewardPunishmentRecord.getId() != null
                && personPostAuthorizationRecordService.getById(personRewardPunishmentRecord.getId()) == null) {
            throw new ErrorException("任职授权记录不存在或已被删除");
        }
        personPostAuthorizationRecordService.saveOrUpdate(personRewardPunishmentRecord);
        return Result.success();
    }
@@ -40,8 +51,11 @@
    @Operation(summary = "删除 ä»»èŒæŽˆæƒè®°å½•")
    @DeleteMapping("/deletePersonPostAuthorizationRecord")
    public Result<?> deletePersonPostAuthorizationRecord(@RequestParam("id") Integer id) {
        // åˆ é™¤æ–‡ä»¶
        PersonPostAuthorizationRecord postAuthorizationRecord = personPostAuthorizationRecordService.getById(id);
        if (postAuthorizationRecord == null) {
            throw new ErrorException("任职授权记录不存在或已被删除");
        }
        // åˆ é™¤æ–‡ä»¶
        FileSaveUtil.DeleteFile(postAuthorizationRecord.getSystemFileName());
        // åˆ é™¤æ•°æ®
        personPostAuthorizationRecordService.removeById(id);
cnas-personnel/src/main/java/com/ruoyi/personnel/dto/UserPageDto.java
@@ -20,5 +20,7 @@
    @Schema(description = "角色")
    private String roleName;
    @Schema(description = "组织架构id")
    private Integer departmentId;
}
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonBasicInfo.java
@@ -44,6 +44,7 @@
    private String personnelClassification;
    @Schema(description = "出生日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    @TableField(updateStrategy = FieldStrategy.ALWAYS) // ä¸ºç©ºå¯ä»¥æ›´æ–°
    private LocalDateTime dateBirth;
@@ -62,6 +63,7 @@
    private String officialAcademicRedentials;
    @Schema(description = "毕业时间1")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    @ExcelProperty("毕业时间")
    private LocalDateTime graduationTime1;
@@ -74,6 +76,7 @@
    private String major1;
    @Schema(description = "毕业时间2")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    @TableField(updateStrategy = FieldStrategy.ALWAYS) // ä¸ºç©ºå¯ä»¥æ›´æ–°
    private LocalDateTime graduationTime2;
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonJobResponsibilities.java
@@ -1,6 +1,7 @@
package com.ruoyi.personnel.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@@ -44,6 +45,7 @@
    private String submittingOperator;
    @Schema(description = "1提交日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime submittingDate;
    @Schema(description = "2 ä»»èŒäºº ä¸»ç®¡id")
@@ -53,12 +55,14 @@
    private String incumbentOperator;
    @Schema(description = "2 ä»»èŒäºº æ—¥æœŸ")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime incumbentDate;
    @Schema(description = "3 ä¸»ç®¡ æ“ä½œäºº")
    private String supervisorOperator;
    @Schema(description = "3 ä¸»ç®¡ æ—¥æœŸ")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime supervisorDate;
    @Schema(description = "创建人id", hidden = true)
@@ -70,6 +74,7 @@
    @Schema(description = "创建日期 / æäº¤æ—¥æœŸ", hidden = true)
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @Schema(description = "当前负责人")
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonPersonnelCapacity.java
@@ -1,6 +1,7 @@
package com.ruoyi.personnel.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@@ -89,14 +90,17 @@
    private Integer confirmOperatingPersonnelId;
    @Schema(description = "2 ç¡®è®¤äºº æ—¥æœŸ")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime confirmDate;
    @Schema(description = "创建日期", hidden = true)
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @Schema(description = "更新日期", hidden = true)
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @Schema(description = "创建人", hidden = true)
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonPostAuthorizationRecord.java
@@ -1,6 +1,7 @@
package com.ruoyi.personnel.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@@ -51,10 +52,12 @@
    @Schema(description = "创建时间", hidden = true)
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    @Schema(description = "更新时间", hidden = true)
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    @Schema(description = "更新人id", hidden = true)
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonTraining.java
@@ -1,6 +1,7 @@
package com.ruoyi.personnel.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@@ -38,12 +39,14 @@
    private Integer compilerId;
    @Schema(description = "编制日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime compilationDate;
    @Schema(description = "审核人id")
    private Integer reviewerId;
    @Schema(description = "审核日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime auditDate;
    @Schema(description = "审核状态")
@@ -62,10 +65,12 @@
    private Integer approvalStatus;
    @Schema(description = "批准日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime approvalDate;
    @Schema(description = "创建日期")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    @Schema(description = "创建人id")
@@ -74,6 +79,7 @@
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    @Schema(description = "更新人id")
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonBasicInfoServiceImpl.java
@@ -163,7 +163,18 @@
    @Override
    public void exportPersonBasicInfo(UserPageDto userPageDto, HttpServletResponse response) throws Exception {
        ArrayList<PersonBasicInfoDto> data = new ArrayList<>();
        List<User> list = userMapper.selectList(null);
        LambdaQueryWrapper<User> userWrapper = Wrappers.<User>lambdaQuery()
                .and(w -> w.eq(User::getDelFlag, "0").or().isNull(User::getDelFlag))
                .and(w -> w.eq(User::getIsCustom, 0).or().isNull(User::getIsCustom));
        if (userPageDto != null) {
            if (ObjectUtils.isNotEmpty(userPageDto.getName())) {
                userWrapper.like(User::getName, userPageDto.getName().trim());
            }
            if (userPageDto.getDepartmentId() != null) {
                userWrapper.apply("FIND_IN_SET({0}, depart_lims_id)", userPageDto.getDepartmentId());
            }
        }
        List<User> list = userMapper.selectList(userWrapper);
        for (User user : list) {
            PersonBasicInfoDto personBasicInfoDto = new PersonBasicInfoDto();
            PersonBasicInfo personBasicInfo = baseMapper.selectOne(Wrappers.<PersonBasicInfo>lambdaQuery().eq(PersonBasicInfo::getUserId, user.getId()));
@@ -193,12 +204,18 @@
    @Override
    public String exportPersonBasicInfoById(Integer id, HttpServletResponse response) {
        Map<String, Object> userMap = baseMapper.selectexportPersonBasic(id);
        if (id == null) {
            throw new ErrorException("缺少人员id");
        }
        User user = userMapper.selectById(id);
        PersonBasicInfo personBasicInfo = baseMapper.selectOne(Wrappers.<PersonBasicInfo>lambdaQuery().eq(PersonBasicInfo::getUserId, user.getId()));
        if (user == null) {
            throw new ErrorException("人员不存在或已被删除");
        }
        PersonBasicInfo personBasicInfo = baseMapper.selectOne(Wrappers.<PersonBasicInfo>lambdaQuery().eq(PersonBasicInfo::getUserId, id));
        if (ObjectUtils.isEmpty(personBasicInfo)){
            throw new ErrorException("该用户的基本信息没有录入,暂无法导出");
        }
        Map<String, Object> userMap = baseMapper.selectexportPersonBasic(id);
        // è¯ä»¶
        List<Annex> annexList = annexMapper.selectList(Wrappers.<Annex>lambdaQuery()
                .eq(Annex::getUserId, id));
@@ -320,27 +337,26 @@
     */
    @Override
    public boolean delDepartment(Integer id) {
        //判断是否有子类,直到没有为止
        List<DepartmentLims> department = getDepartment(id);
        List<Integer> ids = department.stream().map(DepartmentLims::getId).collect(Collectors.toList());
        departmentMapper.delete(Wrappers.<DepartmentLims>lambdaQuery()
                .in(DepartmentLims::getId, ids));
        return true;
    }
    //判断是否有子类,直到没有为止
    public List<DepartmentLims> getDepartment(Integer id) {
        List<DepartmentLims> list = new ArrayList<>();
        DepartmentLims depart = departmentMapper.selectById(id);
        list.add(depart);
        List<DepartmentLims> departments = departmentMapper.selectList(Wrappers.<DepartmentLims>lambdaQuery().eq(DepartmentLims::getFatherId, id));
        if (ObjectUtils.isNotEmpty(departments)) {
            list.addAll(departments);
            for (DepartmentLims department : departments) {
                list.addAll(getDepartment(department.getId()));
            }
        if (id == null) {
            throw new ErrorException("缺少架构id");
        }
        return list;
        DepartmentLims depart = departmentMapper.selectById(id);
        if (depart == null) {
            throw new ErrorException("该架构不存在或已被删除");
        }
        Long childCount = departmentMapper.selectCount(Wrappers.<DepartmentLims>lambdaQuery()
                .eq(DepartmentLims::getFatherId, id));
        if (childCount != null && childCount > 0) {
            throw new ErrorException("该架构下存在下级架构,请先删除下级架构");
        }
        Long userCount = userMapper.selectCount(Wrappers.<User>lambdaQuery()
                .and(w -> w.eq(User::getDelFlag, "0").or().isNull(User::getDelFlag))
                .apply("FIND_IN_SET({0}, depart_lims_id)", id));
        if (userCount != null && userCount > 0) {
            throw new ErrorException("该架构下还有人员,请先将人员移出该架构");
        }
        departmentMapper.deleteById(id);
        return true;
    }
    /**
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonJobResponsibilitiesServiceImpl.java
@@ -8,6 +8,7 @@
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.config.ConfigureBuilder;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.personnel.dto.PersonJobResponsibilitiesDto;
import com.ruoyi.personnel.mapper.PersonJobResponsibilitiesMapper;
import com.ruoyi.personnel.pojo.PersonJobResponsibilities;
@@ -47,18 +48,31 @@
    @Override
    public void exportPersonJobResponsibilities(Integer id, HttpServletResponse response) {
        if (id == null) {
            throw new ErrorException("缺少岗位职责id");
        }
        PersonJobResponsibilities personJobResponsibilities = baseMapper.selectById(id);
        if (personJobResponsibilities == null) {
            throw new ErrorException("岗位职责不存在或已被删除");
        }
        if (ObjectUtils.isEmpty(personJobResponsibilities.getIncumbentId())) {
            throw new ErrorException("该岗位职责还没有选择任职人,暂无法导出");
        }
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日");
        //岗位名称
        String name = personJobResponsibilities.getPostName();
        Integer incumbentUserId = Integer.parseInt(personJobResponsibilities.getIncumbentId());
        //所属部门
        String departLims = userMapper.selectUserDepartmentLimsName(Integer.parseInt(personJobResponsibilities.getIncumbentId()));
        String departLims = userMapper.selectUserDepartmentLimsName(incumbentUserId);
        //工作目标
        String objective = personJobResponsibilities.getJobObjective();
        //岗位职责
        String responsibilities = personJobResponsibilities.getJobResponsibilities();
        //任职人
        User user = userMapper.selectById(Integer.parseInt(personJobResponsibilities.getIncumbentId()));
        User user = userMapper.selectById(incumbentUserId);
        if (user == null) {
            throw new ErrorException("任职人不存在或已被删除,请重新选择任职人");
        }
        String incumbent = user.getName();
        //任职时间
        String incumbentDate = "";
@@ -68,7 +82,8 @@
        //主管
        String supervisor = "";
        if (ObjectUtils.isNotEmpty(personJobResponsibilities.getSupervisorId())) {
            supervisor = userMapper.selectById(personJobResponsibilities.getSupervisorId()).getName();
            User supervisorUser = userMapper.selectById(personJobResponsibilities.getSupervisorId());
            supervisor = supervisorUser != null ? supervisorUser.getName() : "";
        }
        //主管时间
        String supervisorDate = "";
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonPersonnelCapacityServiceImpl.java
@@ -29,6 +29,7 @@
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
@@ -69,19 +70,35 @@
     */
    @Override
    public void exportPersonnelCapacity(Integer id, HttpServletResponse response) {
        if (id == null) {
            throw new ErrorException("缺少人员能力记录id");
        }
        PersonPersonnelCapacityExportDto capacityExportDto = baseMapper.selectExportPersonnelCapacity(id);
        if (capacityExportDto == null) {
            throw new ErrorException("人员能力记录不存在或已被删除");
        }
        // ç¡®è®¤äºº
        String confirmUrl = null;
        if (capacityExportDto.getConfirmOperatingPersonnelId() != null) {
            confirmUrl = userMapper.selectById(capacityExportDto.getConfirmOperatingPersonnelId()).getSignatureUrl();
            User confirmUser = userMapper.selectById(capacityExportDto.getConfirmOperatingPersonnelId());
            if (confirmUser == null) {
                throw new ErrorException("确认人不存在或已被删除,无法导出");
            }
            confirmUrl = confirmUser.getSignatureUrl();
            if (StringUtils.isBlank(confirmUrl)) {
                throw new ErrorException("缺少确认人签名");
                throw new ErrorException("确认人还没有上传签名,无法导出");
            }
            if (!new File(imgUrl + "/" + confirmUrl).exists()) {
                throw new ErrorException("确认人签名文件不存在,请让确认人重新上传");
            }
        }
        // èŽ·å–è·¯å¾„
        InputStream inputStream = this.getClass().getResourceAsStream("/static/personnel-capacity.docx");
        if (inputStream == null) {
            throw new ErrorException("人员能力导出模板不存在");
        }
        ConfigureBuilder builder = Configure.builder();
        builder.useSpringEL(true);
        String finalConfirmUrl = confirmUrl;
@@ -95,8 +112,8 @@
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "人员能力", "UTF-8");
            String userName = StringUtils.isBlank(capacityExportDto.getUserName()) ? "人员能力" : capacityExportDto.getUserName() + "人员能力认定";
            String fileName = URLEncoder.encode(userName, "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
@@ -117,17 +134,40 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitConfirmPersonnelCapability(PersonPersonnelCapacity personPersonnelCapacity) {
        if (personPersonnelCapacity.getUserId() == null) {
            throw new ErrorException("缺少人员");
        }
        if (personPersonnelCapacity.getConfirmOperatingPersonnelId() == null) {
            throw new ErrorException("缺少确认人");
        }
        User formUser = userMapper.selectById(personPersonnelCapacity.getUserId());
        if (formUser == null) {
            throw new ErrorException("人员不存在或已被删除,无法提交");
        }
        User confirmUser = userMapper.selectById(personPersonnelCapacity.getConfirmOperatingPersonnelId());
        if (confirmUser == null) {
            throw new ErrorException("确认人不存在或已被删除,无法提交");
        }
        if (personPersonnelCapacity.getId() != null) {
            PersonPersonnelCapacity existing = this.getById(personPersonnelCapacity.getId());
            if (existing == null) {
                throw new ErrorException("人员能力记录不存在或已被删除");
            }
            if (existing.getConfirmDate() != null) {
                throw new ErrorException("该记录已确认,不能重复提交");
            }
        }
        Integer userId = SecurityUtils.getUserId().intValue();
        User user = userMapper.selectById(userId);
        // æäº¤å’Œç¡®è®¤æ—¥æœŸåˆ†å¼€ï¼šç¡®è®¤æ—¥æœŸåªåœ¨ç¡®è®¤æ“ä½œæ—¶ç”±ç³»ç»Ÿå†™å…¥
        personPersonnelCapacity.setConfirmDate(null);
        this.saveOrUpdate(personPersonnelCapacity);
        // æ¶ˆæ¯å‘送
        InformationNotification info = new InformationNotification();
        // å‘送人
        info.setCreateUser(user.getName());
        info.setCreateUser(user != null ? user.getName() : "");
        info.setMessageType("6");
        info.setTheme("CNAS人员能力确认通知");
        info.setContent(formUser.getName() + "的人员能力待确认");
@@ -136,22 +176,21 @@
        info.setConsigneeId(personPersonnelCapacity.getConfirmOperatingPersonnelId());
        info.setJumpPath(MenuJumpPathConstants.PERSONNEL);
        informationNotificationService.addInformationNotification(info);
        this.saveOrUpdate(personPersonnelCapacity);
        // å‘送企业微信通知
        threadPoolTaskExecutor.execute(() -> {
            // æŸ¥è¯¢æŽ¥æ”¶äºº
            User personnel = userMapper.selectById(personPersonnelCapacity.getConfirmOperatingPersonnelId());
            if (confirmUser.getAccount() == null || confirmUser.getAccount().trim().isEmpty()) {
                return;
            }
            String message = "";
            message += "CNAS人员能力确认通知";
            message += "\n请去资源管理-人员-人员能力填写";
            message += "\n请去资源要求-人员-人员管理-人员能力填写";
            message += "\n" + formUser.getName() + "的人员能力待确认";
            //发送企业微信消息通知
            try {
                WxCpUtils.inform(personnel.getAccount(), message, null);
                WxCpUtils.inform(confirmUser.getAccount(), message, null);
            } catch (Exception e) {
                throw new RuntimeException(e);
                e.printStackTrace();
            }
        });
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonPostAuthorizationRecordServiceImpl.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
@@ -11,7 +13,9 @@
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.personnel.dto.PersonPostAuthorizationRecordDto;
import com.ruoyi.personnel.mapper.PersonBasicInfoMapper;
import com.ruoyi.personnel.mapper.PersonPostAuthorizationRecordMapper;
import com.ruoyi.personnel.pojo.PersonBasicInfo;
import com.ruoyi.personnel.pojo.PersonPostAuthorizationRecord;
import com.ruoyi.personnel.service.PersonPostAuthorizationRecordService;
import com.ruoyi.system.mapper.UserMapper;
@@ -20,6 +24,7 @@
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
@@ -39,6 +44,8 @@
    @Resource
    private UserMapper userMapper;
    @Resource
    private PersonBasicInfoMapper personBasicInfoMapper;
    @Value("${file.path}")
    private String imgUrl;
@@ -50,73 +57,80 @@
    @Override
    public void exportPersonPostAuthorizationRecord(Integer id, HttpServletResponse response) {
        if (id == null) {
            throw new ErrorException("缺少任职授权记录id");
        }
        PersonPostAuthorizationRecord personPostAuthorizationRecord = baseMapper.selectById(id);
        if (personPostAuthorizationRecord == null) {
            throw new ErrorException("任职授权记录不存在或已被删除");
        }
        Integer incumbentUserId;
        try {
            incumbentUserId = Integer.valueOf(personPostAuthorizationRecord.getUserId());
        } catch (Exception e) {
            throw new ErrorException("该记录没有选择被任职人员,无法导出上岗证");
        }
        //姓名
        User user = userMapper.selectById(personPostAuthorizationRecord.getUserId());
        User user = userMapper.selectById(incumbentUserId);
        if (user == null) {
            throw new ErrorException("被任职人员不存在或已被删除,无法导出上岗证");
        }
        String name = user.getName();
        //todo: äººå‘˜ä»»èŒæŽˆæƒè®°å½•姓名英文
//        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
//        format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
//        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
//        StringBuilder pinyinBuilder = new StringBuilder();
//        for (int i = 0; i < name.length(); i++) {
//            char c = name.charAt(i);
//            // åˆ¤æ–­æ˜¯å¦æ˜¯æ±‰å­—
//            if (Character.toString(c).matches("[\\u4E00-\\u9FFF]")) {
//                // èŽ·å–æ±‰å­—çš„æ‹¼éŸ³æ•°ç»„
//                String[] pinyinArray = new String[0];
//                try {
//                    pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
//                } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
//
//                }
//                if (pinyinArray!= null && pinyinArray.length > 0) {
//                    pinyinBuilder.append(pinyinArray[0]);
//                }
//            } else {
//                pinyinBuilder.append(c);
//            }
//        }
//        String nameEn = pinyinBuilder.toString();
        String nameEn = user.getNameEn();
        //理论知识考试成绩
        String num1=personPostAuthorizationRecord.getNum1();
        //操作技能考试成绩
        String num2=personPostAuthorizationRecord.getNum2();
        //身份证号
        PersonBasicInfo basicInfo = personBasicInfoMapper.selectOne(Wrappers.<PersonBasicInfo>lambdaQuery()
                .eq(PersonBasicInfo::getUserId, incumbentUserId)
                .last("limit 1"));
        if (basicInfo == null || StringUtils.isBlank(basicInfo.getIdentityCard())) {
            throw new ErrorException(name + "还没有在人员基本信息中填写身份证号,无法导出上岗证");
        }
        String identityCard = basicInfo.getIdentityCard();
        //证书编号
        String code = personPostAuthorizationRecord.getCertificateNumber();
        if (StringUtils.isBlank(code)) {
            throw new ErrorException("该记录还没有填写证书编号,无法导出上岗证");
        }
        //任职岗位
        String post = personPostAuthorizationRecord.getPost();
        String finalPost = StringUtils.isBlank(post) ? "检测" : post;
        //发证时间
        LocalDateTime createTime = personPostAuthorizationRecord.getCreateTime();
        if (createTime == null) {
            createTime = LocalDateTime.now();
        }
        String year = createTime.getYear() + "";
        String mon = createTime.getMonth().getValue() + "";
        String mon = createTime.getMonthValue() + "";
        String day = createTime.getDayOfMonth() + "";
        //个人照片
        if (ObjectUtils.isEmpty(user.getPictureUrl())) {
            throw new ErrorException(name+"的个人照片没有上传");
            throw new ErrorException(name + "的个人照片没有上传,无法导出上岗证");
        }
        String pictureUrl = user.getPictureUrl();
        if (!new File(imgUrl + "/" + pictureUrl).exists()) {
            throw new ErrorException(name + "的个人照片文件不存在,请重新上传");
        }
        InputStream inputStream = this.getClass().getResourceAsStream("/static/credentials-deal.docx");
        if (inputStream == null) {
            throw new ErrorException("上岗证导出模板不存在");
        }
        ConfigureBuilder builder = Configure.builder();
        builder.useSpringEL(true);
        XWPFTemplate template = XWPFTemplate.compile(inputStream, builder.build()).render(
                new HashMap<String, Object>() {{
                    put("name", name);
                    put("nameEn", nameEn);
                    put("num1", num1);
                    put("num2", num2);
                    put("identityCard", identityCard);
                    put("code", code);
                    put("post", finalPost);
                    put("year", year);
                    put("mon", mon);
                    put("day", day);
                    put("writeUrl", new FilePictureRenderData(100,50,imgUrl + "/" + pictureUrl));
                    put("writeUrl", new FilePictureRenderData(100, 140, imgUrl + "/" + pictureUrl));
                }});
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    name+"的岗位职业资格正式", "UTF-8");
                    name + "的检测人员上岗证", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingRecordServiceImpl.java
@@ -11,6 +11,7 @@
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.WxCpUtils;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.personnel.dto.PersonTrainingRecordDto;
import com.ruoyi.personnel.dto.PersonTrainingRecordListDto;
@@ -90,8 +91,14 @@
    @Override
    public void exportTrainingRecordAddTrainingDate(Integer userId, Integer trainingDate, HttpServletResponse response) {
        if (userId == null) {
            throw new ErrorException("缺少人员id");
        }
        // æŸ¥è¯¢äººå‘˜äººä¿¡æ¯
        PersonTrainingRecordListDto trainingRecordListDto = baseMapper.selectUserTraining(userId);
        if (trainingRecordListDto == null) {
            throw new ErrorException("人员不存在或已被删除,无法导出");
        }
        // æŸ¥è¯¢åŸ¹è®­è®°å½•
        List<TrainingRecordPersonDetailedDto> personDetailedDtos = baseMapper.selectPersonDetailedDtosByTrainingDate(userId, trainingDate);
@@ -99,6 +106,9 @@
        // èŽ·å–è·¯å¾„
        InputStream inputStream = this.getClass().getResourceAsStream("/static/training-record.docx");
        if (inputStream == null) {
            throw new ErrorException("培训记录导出模板不存在");
        }
        Configure configure = Configure.builder()
                .bind("personnelDetailsLisat", new HackLoopTableRenderPolicy())
                .build();
@@ -130,6 +140,21 @@
     */
    @Override
    public void trainingAndAssessmentRecordsAdded(PersonTrainingRecordSubmitDto personTrainingRecordSubmitDto) {
        if (personTrainingRecordSubmitDto == null || personTrainingRecordSubmitDto.getTrainingDetailedId() == null) {
            throw new ErrorException("缺少培训明细id");
        }
        if (personTrainingRecordSubmitDto.getState() == null) {
            throw new ErrorException("缺少培训状态");
        }
        PersonTrainingDetailed personPersonnelCapacity = personTrainingDetailedService.getById(personTrainingRecordSubmitDto.getTrainingDetailedId());
        if (personPersonnelCapacity == null) {
            throw new ErrorException("培训记录不存在或已被删除");
        }
        boolean waitEvaluate = Integer.valueOf(2).equals(personTrainingRecordSubmitDto.getState());
        if (waitEvaluate && personTrainingRecordSubmitDto.getAssessmentUserId() == null) {
            throw new ErrorException("请选择评价人");
        }
        personTrainingDetailedService.update(Wrappers.<PersonTrainingDetailed>lambdaUpdate()
                .eq(PersonTrainingDetailed::getId, personTrainingRecordSubmitDto.getTrainingDetailedId())
                .set(PersonTrainingDetailed::getAssessmentMethod, personTrainingRecordSubmitDto.getAssessmentMethod())
@@ -138,42 +163,44 @@
                .set(PersonTrainingDetailed::getAssessmentUserId, personTrainingRecordSubmitDto.getAssessmentUserId())
                .set(PersonTrainingDetailed::getState, personTrainingRecordSubmitDto.getState()));
        // å‘送消息通知
        if (personTrainingRecordSubmitDto.getState().equals(2)) {
            PersonTrainingDetailed personPersonnelCapacity = personTrainingDetailedService.getById(personTrainingRecordSubmitDto.getTrainingDetailedId());
            Integer userId = SecurityUtils.getUserId().intValue();
            User user = userMapper.selectById(userId);
            // æ¶ˆæ¯å‘送
            InformationNotification info = new InformationNotification();
            // å‘送人
            info.setCreateUser(user.getName());
            info.setMessageType("6");
            info.setTheme("CNAS人员培训计划待评价");
            info.setContent("培训内容:" + personPersonnelCapacity.getTrainingContent() + "的人员培训待评价");
            info.setSenderId(userId);
            // æŽ¥æ”¶äºº
            info.setConsigneeId(personTrainingRecordSubmitDto.getAssessmentUserId());
            info.setJumpPath(MenuJumpPathConstants.PERSONNEL);
            informationNotificationService.addInformationNotification(info);
            // å‘送企业微信通知
            threadPoolTaskExecutor.execute(() -> {
                // æŸ¥è¯¢æŽ¥æ”¶äºº
                User personnel = userMapper.selectById(personTrainingRecordSubmitDto.getAssessmentUserId());
                String message = "";
                message += "CNAS人员培训计划待评价";
                message += "\n请去资源管理-人员-人员培训计划";
                message += "\n" + "培训内容:" + personPersonnelCapacity.getTrainingContent() + "的人员培训待评价";
                //发送企业微信消息通知
                try {
                    WxCpUtils.inform(personnel.getAccount(), message, null);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
        // åªæœ‰æäº¤å¾…评价时才通知评价人
        if (!waitEvaluate) {
            return;
        }
        Integer userId = SecurityUtils.getUserId().intValue();
        User user = userMapper.selectById(userId);
        // æ¶ˆæ¯å‘送
        InformationNotification info = new InformationNotification();
        // å‘送人
        info.setCreateUser(user != null ? user.getName() : "");
        info.setMessageType("6");
        info.setTheme("CNAS人员培训计划待评价");
        info.setContent("培训内容:" + personPersonnelCapacity.getTrainingContent() + "的人员培训待评价");
        info.setSenderId(userId);
        // æŽ¥æ”¶äºº
        info.setConsigneeId(personTrainingRecordSubmitDto.getAssessmentUserId());
        info.setJumpPath(MenuJumpPathConstants.PERSONNEL);
        informationNotificationService.addInformationNotification(info);
        // å‘送企业微信通知
        threadPoolTaskExecutor.execute(() -> {
            // æŸ¥è¯¢æŽ¥æ”¶äºº
            User personnel = userMapper.selectById(personTrainingRecordSubmitDto.getAssessmentUserId());
            if (personnel == null || personnel.getAccount() == null || personnel.getAccount().trim().isEmpty()) {
                return;
            }
            String message = "";
            message += "CNAS人员培训计划待评价";
            message += "\n请去资源管理-人员-人员培训计划";
            message += "\n" + "培训内容:" + personPersonnelCapacity.getTrainingContent() + "的人员培训待评价";
            //发送企业微信消息通知
            try {
                WxCpUtils.inform(personnel.getAccount(), message, null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingServiceImpl.java
@@ -33,6 +33,7 @@
import com.ruoyi.personnel.pojo.PersonTraining;
import com.ruoyi.personnel.pojo.PersonTrainingDetailed;
import com.ruoyi.personnel.pojo.PersonTrainingDetailedFile;
import com.ruoyi.personnel.pojo.PersonTrainingRecord;
import com.ruoyi.personnel.service.PersonTrainingDetailedService;
import com.ruoyi.personnel.service.PersonTrainingService;
import com.ruoyi.system.mapper.UserMapper;
@@ -57,6 +58,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
 * <p>
@@ -98,50 +100,32 @@
    @Override
    public void personTrainingImport(MultipartFile file, PersonTraining training) {
        if (file == null || file.isEmpty()) {
            throw new ErrorException("请选择导入文件");
        }
        if (training == null || StringUtils.isBlank(training.getPlanYear())) {
            throw new ErrorException("请选择计划年份");
        }
        if (training.getReviewerId() == null) {
            throw new ErrorException("请选择审核人");
        }
        String originalFilename = file.getOriginalFilename();
        if (StringUtils.isBlank(originalFilename)) {
            throw new ErrorException("文件名不能为空");
        }
        Integer userId = SecurityUtils.getUserId().intValue();
        // å¹´åº¦è®¡åˆ’父级新增数据
        PersonTraining personSupervisePlan = new PersonTraining();
        String fileName = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf("."));
        String fileName = originalFilename.contains(".")
                ? originalFilename.substring(0, originalFilename.lastIndexOf("."))
                : originalFilename;
        personSupervisePlan.setFileName(fileName);
        personSupervisePlan.setPlanYear(training.getPlanYear());
        personSupervisePlan.setCompilerId(userId);
        personSupervisePlan.setReviewerId(training.getReviewerId());
        personSupervisePlan.setCompilationDate(LocalDateTime.now());
        baseMapper.insert(personSupervisePlan);
        User user = userMapper.selectById(userId);
        // æ¶ˆæ¯å‘送
        InformationNotification info = new InformationNotification();
        // å‘送人
        info.setCreateUser(user.getName());
        info.setMessageType("6");
        info.setTheme("CNAS培训计划审核通知");
        info.setContent("您有一条培训计划待审核");
        info.setSenderId(userId);
        // æŽ¥æ”¶äºº
        info.setConsigneeId(training.getApproverId());
        info.setJumpPath(MenuJumpPathConstants.PERSONNEL);
        informationNotificationService.addInformationNotification(info);
        // å‘送企业微信通知
        threadPoolTaskExecutor.execute(() -> {
            // æŸ¥è¯¢æŽ¥æ”¶äºº
            User personnel = userMapper.selectById(training.getApproverId());
            String message = "";
            message += "CNAS培训计划审核通知";
            message += "\n请去资源管理-人员-培训计划填写";
            message += "\n" + fileName + "的培训计划待审核";
            //发送企业微信消息通知(接收人不存在或发送失败不影响导入流程)
            if (personnel == null || StringUtils.isBlank(personnel.getAccount())) {
                return;
            }
            try {
                WxCpUtils.inform(personnel.getAccount(), message, null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        // å¹´åº¦è®¡åˆ’详情 æ–°å¢ž
        try {
@@ -149,65 +133,138 @@
            personSupervisePlanDetailsListener.setPlanId(personSupervisePlan.getId());
            EasyExcel.read(file.getInputStream(), PersonTrainingDetailedUpload.class, personSupervisePlanDetailsListener).sheet().doRead();
        } catch (IOException e) {
            e.printStackTrace();
            throw new ErrorException("读取导入文件失败,请检查文件格式");
        }
        User user = userMapper.selectById(userId);
        // æ¶ˆæ¯å‘送(接收人为选定的审核人)
        InformationNotification info = new InformationNotification();
        info.setCreateUser(user != null ? user.getName() : "");
        info.setMessageType("6");
        info.setTheme("CNAS培训计划审核通知");
        info.setContent("您有一条培训计划待审核");
        info.setSenderId(userId);
        info.setConsigneeId(training.getReviewerId());
        info.setJumpPath(MenuJumpPathConstants.PERSONNEL);
        informationNotificationService.addInformationNotification(info);
        // å‘送企业微信通知(接收人不存在或发送失败不影响导入流程)
        threadPoolTaskExecutor.execute(() -> {
            User personnel = userMapper.selectById(training.getReviewerId());
            if (personnel == null || StringUtils.isBlank(personnel.getAccount())) {
                return;
            }
            String message = "";
            message += "CNAS培训计划审核通知";
            message += "\n请去资源管理-人员-培训计划填写";
            message += "\n" + fileName + "的培训计划待审核";
            try {
                WxCpUtils.inform(personnel.getAccount(), message, null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
    @Override
    public void personTrainingDelete(Integer id) {
        personTrainingDetailedService.remove(Wrappers.<PersonTrainingDetailed>lambdaQuery()
        if (id == null) {
            throw new ErrorException("缺少计划id");
        }
        List<PersonTrainingDetailed> details = personTrainingDetailedService.list(Wrappers.<PersonTrainingDetailed>lambdaQuery()
                .eq(PersonTrainingDetailed::getPlanId, id));
        if (!details.isEmpty()) {
            List<Integer> detailIds = details.stream().map(PersonTrainingDetailed::getId).collect(Collectors.toList());
            // è¿žåŒæ˜Žç»†ä¸‹çš„培训记录、附件一起删除,避免留下孤儿数据
            personTrainingRecordMapper.delete(Wrappers.<PersonTrainingRecord>lambdaQuery()
                    .in(PersonTrainingRecord::getCourseId, detailIds));
            personTrainingDetailedFileMapper.delete(Wrappers.<PersonTrainingDetailedFile>lambdaQuery()
                    .in(PersonTrainingDetailedFile::getTrainingDetailedId, detailIds));
            personTrainingDetailedService.remove(Wrappers.<PersonTrainingDetailed>lambdaQuery()
                    .eq(PersonTrainingDetailed::getPlanId, id));
        }
        baseMapper.deleteById(id);
    }
    @Override
    public void reviewAnnualPersonnelTraining(PersonTraining training) {
        PersonTraining personTraining = new PersonTraining();
        personTraining.setId(training.getId());
        personTraining.setApproverId(training.getApproverId());// æ·»åŠ æ‰¹å‡†äºº
        personTraining.setAuditDate(LocalDateTime.now());
        personTraining.setAuditRemarks(training.getAuditRemarks());
        personTraining.setReviewerStatus(training.getReviewerStatus());
        if (training == null || training.getId() == null) {
            throw new ErrorException("缺少计划id");
        }
        if (!Integer.valueOf(1).equals(training.getReviewerStatus()) && !Integer.valueOf(2).equals(training.getReviewerStatus())) {
            throw new ErrorException("审核结果不正确");
        }
        PersonTraining old = baseMapper.selectById(training.getId());
        if (old == null) {
            throw new ErrorException("培训计划不存在或已被删除");
        }
        if (Integer.valueOf(1).equals(old.getReviewerStatus())) {
            throw new ErrorException("该计划已审核通过,请勿重复审核");
        }
        boolean passed = Integer.valueOf(1).equals(training.getReviewerStatus());
        if (passed && training.getApproverId() == null) {
            throw new ErrorException("请选择批准人");
        }
        // æ¶ˆæ¯å‘送
        baseMapper.update(null, Wrappers.<PersonTraining>lambdaUpdate()
                .eq(PersonTraining::getId, training.getId())
                .set(PersonTraining::getApproverId, passed ? training.getApproverId() : null)
                .set(PersonTraining::getAuditDate, LocalDateTime.now())
                .set(PersonTraining::getAuditRemarks, training.getAuditRemarks())
                .set(PersonTraining::getReviewerStatus, training.getReviewerStatus()));
        // åªæœ‰å®¡æ ¸é€šè¿‡æ‰é€šçŸ¥æ‰¹å‡†äºº
        if (!passed) {
            return;
        }
        Integer userId = SecurityUtils.getUserId().intValue();
        User user = userMapper.selectById(userId);
        InformationNotification info = new InformationNotification();
        // å‘送人
        info.setCreateUser(user.getName());
        info.setCreateUser(user != null ? user.getName() : "");
        info.setMessageType("6");
        info.setTheme("CNAS培训计划审核通知");
        info.setContent("您有一条培训计划待批准");
        info.setSenderId(userId);
        // æŽ¥æ”¶äºº
        info.setConsigneeId(training.getApproverId());
        info.setJumpPath(MenuJumpPathConstants.PERSONNEL);
        informationNotificationService.addInformationNotification(info);
        // å‘送企业微信通知
        threadPoolTaskExecutor.execute(() -> {
            // æŸ¥è¯¢æŽ¥æ”¶äºº
            User personnel = userMapper.selectById(training.getApproverId());
            if (personnel == null || StringUtils.isBlank(personnel.getAccount())) {
                return;
            }
            String message = "";
            message += "CNAS培训计划批准通知";
            message += "\n请去资源管理-人员-培训计划填写";
            message += "\n" + old.getFileName() + "的培训计划待批准";
            //发送企业微信消息通知
            try {
                WxCpUtils.inform(personnel.getAccount(), message, null);
            } catch (Exception e) {
                throw new RuntimeException(e);
                e.printStackTrace();
            }
        });
        baseMapper.updateById(personTraining);
    }
    @Override
    public void approveAnnualPersonnelTraining(PersonTraining training) {
        if (training == null || training.getId() == null) {
            throw new ErrorException("缺少计划id");
        }
        if (!Integer.valueOf(1).equals(training.getApprovalStatus()) && !Integer.valueOf(2).equals(training.getApprovalStatus())) {
            throw new ErrorException("批准结果不正确");
        }
        PersonTraining old = baseMapper.selectById(training.getId());
        if (old == null) {
            throw new ErrorException("培训计划不存在或已被删除");
        }
        if (!Integer.valueOf(1).equals(old.getReviewerStatus())) {
            throw new ErrorException("该计划尚未审核通过,不能批准");
        }
        if (Integer.valueOf(1).equals(old.getApprovalStatus())) {
            throw new ErrorException("该计划已批准,请勿重复操作");
        }
        LambdaUpdateWrapper<PersonTraining> wrapper = Wrappers.<PersonTraining>lambdaUpdate()
                .eq(PersonTraining::getId, training.getId())
                .set(PersonTraining::getApprovalDate, LocalDateTime.now())
@@ -224,30 +281,48 @@
    @Override
    public void exportPersonTraining(Integer id, HttpServletResponse response) {
        if (id == null) {
            throw new ErrorException("缺少计划id");
        }
        // æŸ¥è¯¢è¯¦æƒ…
        PersonTraining personTraining = baseMapper.selectById(id);
        //获取提交人的签名地址
        String writeUrl = userMapper.selectById(personTraining.getCompilerId()).getSignatureUrl();
        if (ObjectUtils.isEmpty(writeUrl) || writeUrl.equals("")) {
            throw new ErrorException("找不到检验人的签名");
        if (personTraining == null) {
            throw new ErrorException("培训计划不存在或已被删除");
        }
        //获取复核人的签名地址
        //获取编制人的签名地址
        User compiler = userMapper.selectById(personTraining.getCompilerId());
        String writeUrl = compiler != null ? compiler.getSignatureUrl() : null;
        if (StringUtils.isBlank(writeUrl)) {
            throw new ErrorException("编制人未上传签名,暂无法导出");
        }
        if (!new File(imgUrl + "/" + writeUrl).exists()) {
            throw new ErrorException("编制人的签名图片不存在,请重新上传签名");
        }
        //获取审核人的签名地址
        String examineUrl = null;
        if (personTraining.getReviewerId() != null) {
            examineUrl = userMapper.selectById(personTraining.getReviewerId()).getSignatureUrl();
            User reviewer = userMapper.selectById(personTraining.getReviewerId());
            examineUrl = reviewer != null ? reviewer.getSignatureUrl() : null;
            if (StringUtils.isBlank(examineUrl)) {
                throw new ErrorException("找不到复核人的签名");
                throw new ErrorException("审核人未上传签名,暂无法导出");
            }
            if (!new File(imgUrl + "/" + examineUrl).exists()) {
                throw new ErrorException("审核人的签名图片不存在,请重新上传签名");
            }
        }
        //获取批准人的签名地址
        String ratifyUrl = null;
        if (personTraining.getApproverId() != null) {
            ratifyUrl = userMapper.selectById(personTraining.getApproverId()).getSignatureUrl();
            User approver = userMapper.selectById(personTraining.getApproverId());
            ratifyUrl = approver != null ? approver.getSignatureUrl() : null;
            if (StringUtils.isBlank(ratifyUrl)) {
                throw new ErrorException("找不到复核人的签名");
                throw new ErrorException("批准人未上传签名,暂无法导出");
            }
            if (!new File(imgUrl + "/" + ratifyUrl).exists()) {
                throw new ErrorException("批准人的签名图片不存在,请重新上传签名");
            }
        }
@@ -307,8 +382,14 @@
     */
    @Override
    public void exportPersonTrainingRecord(Integer id, HttpServletResponse response) {
        if (id == null) {
            throw new ErrorException("缺少培训明细id");
        }
        // æŸ¥è¯¢äººå‘˜åŸ¹è®­æ˜Žç»†
        PersonTrainingDetailedDto detailedDto = personTrainingDetailedMapper.selectTrainingDetail(id);
        if (detailedDto == null) {
            throw new ErrorException("培训记录不存在或已被删除");
        }
        // ç”ŸæˆåŸ¹è®­è€å¸ˆç­¾åï¼ˆæœªé…ç½®ç­¾åæ—¶ç”¨é€æ˜Žå›¾ç‰‡å…œåº•,模板里的图片占位不能为 null)
        detailedDto.setTrainingLecturerRender(UserUtils.orTransparentPicture(
cnas-personnel/src/main/resources/mapper/PersonBasicInfoMapper.xml
@@ -5,13 +5,55 @@
    <select id="selectLimsUser" resultType="com.ruoyi.common.core.domain.entity.DepartmentDto">
        SELECT u.id userId, u.name, SUBSTRING_INDEX(SUBSTRING_INDEX(u.depart_lims_id, ',', -2), ',', 1) AS fatherId
        FROM user u
        where u.is_custom = 0
        where COALESCE(u.is_custom, 0) = 0
          and COALESCE(u.del_flag, '0') = '0'
          and u.depart_lims_id is not null
          and u.depart_lims_id != ''
    </select>
    <select id="getCNASPersonnelInfo" resultType="com.ruoyi.personnel.dto.PersonBasicInfoDto">
        SELECT *
        SELECT cpbi.id                 AS id,
               u.id                    AS userId,
               u.account               AS account,
               u.name                  AS name,
               u.name_en               AS nameEn,
               u.age                   AS age,
               u.phone                 AS phone,
               u.email                 AS email,
               u.signature_url         AS signatureUrl,
               u.picture_url           AS pictureUrl,
               u.depart_lims_id        AS departLimsId,
               cpbi.current_state      AS currentState,
               cpbi.professional_title AS professionalTitle,
               cpbi.sex                AS sex,
               cpbi.personnel_classification AS personnelClassification,
               cpbi.date_birth         AS dateBirth,
               cpbi.identity_card      AS identityCard,
               cpbi.nation             AS nation,
               cpbi.political_status   AS politicalStatus,
               cpbi.official_academic_redentials AS officialAcademicRedentials,
               cpbi.graduation_time1   AS graduationTime1,
               cpbi.graduated_institutions1 AS graduatedInstitutions1,
               cpbi.major1             AS major1,
               cpbi.graduation_time2   AS graduationTime2,
               cpbi.graduated_institutions2 AS graduatedInstitutions2,
               cpbi.major2             AS major2,
               cpbi.telephone          AS telephone,
               cpbi.remarks            AS remarks,
               cpbi.corporate_name     AS corporateName,
               cpbi.post_name          AS postName,
               cpbi.group_time         AS groupTime,
               cpbi.labor_relations    AS laborRelations,
               cpbi.working_time       AS workingTime,
               cpbi.contract_life_time AS contractLifeTime,
               cpbi.native_place       AS nativePlace,
               cpbi.validity_period    AS validityPeriod,
               cpbi.marital_status     AS maritalStatus,
               cpbi.id_address         AS idAddress,
               cpbi.id_detail_address  AS idDetailAddress,
               cpbi.dumpling_time      AS dumplingTime,
               cpbi.highest_degree     AS highestDegree,
               cpbi.last_update_time   AS lastUpdateTime
        FROM user u
                 left join cnas_person_basic_info cpbi on cpbi.user_id = u.id
        where u.id = #{userId}
@@ -36,10 +78,14 @@
        from user u
                 left join cnas_person_basic_info cpbi on cpbi.user_id = u.id
        <where>
            FIND_IN_SET(#{departmentId},u.depart_lims_id)
            <if test="departmentId != null">
                FIND_IN_SET(#{departmentId},u.depart_lims_id)
            </if>
            <if test="name != null and name != ''">
                and u.name like concat('%',#{name},'%')
            </if>
            and COALESCE(u.is_custom, 0) = 0
            and COALESCE(u.del_flag, '0') = '0'
        </where>
    </select>
cnas-personnel/src/main/resources/mapper/PersonPersonnelCapacityMapper.xml
@@ -32,6 +32,7 @@
                and u3.name like concat('%', #{userName}, '%')
            </if>
        </where>
        order by cppc.id desc
    </select>
    <!-- æŸ¥è¯¢äººå‘˜èƒ½åŠ›æŽ¥å£ -->
cnas-personnel/src/main/resources/mapper/PersonTrainingRecordMapper.xml
@@ -14,11 +14,13 @@
    <select id="personnelTrainingPersonnel" resultType="com.ruoyi.personnel.dto.PersonTrainingRecordListDto">
        select u.name, u.account, dl.name depart_lims_name, cpbi.professional_title,
        cpbi.official_academic_redentials, u.id user_id
        cpbi.official_academic_redentials, u.id user_id,
        DATE_FORMAT(cpbi.group_time, '%Y-%m-%d') AS unitTimeSting
        from user u
        left join cnas_person_basic_info cpbi on cpbi.user_id = u.id
        left join department_lims dl on dl.id = SUBSTRING_INDEX(SUBSTRING_INDEX(u.depart_lims_id, ',', -2), ',', 1)
        where u.is_custom = 0
        where COALESCE(u.is_custom, 0) = 0
          and COALESCE(u.del_flag, '0') = '0'
        <if test="userName != '' and userName != null and userName != 'null'">
            and u.name like concat('%', #{userName}, '%')
        </if>
@@ -66,7 +68,7 @@
        from user u
                 left join cnas_person_basic_info cpbi on cpbi.user_id = u.id
                 left join department_lims dl on dl.id = SUBSTRING_INDEX(SUBSTRING_INDEX(u.depart_lims_id, ',', -2), ',', 1)
        where u.is_custom = 0
        where COALESCE(u.is_custom, 0) = 0
        and u.id = #{userId}
    </select>
@@ -83,7 +85,7 @@
            and cptr.user_id = #{userId}
        <where>
            <if test="year!= null and year!= ''">
                and cptd.training_date like concat('%', #{year}, '%')}
                and cptd.training_date like concat('%', #{year}, '%')
            </if>
        </where>
    </select>
@@ -121,7 +123,7 @@
                and cptd.training_date like concat('%', #{year}, '%')
            </if>
        </where>
        order by STR_TO_DATE(CONCAT(cptd.training_date, '.01'), '%Y.%m.%d')
        order by cptd.training_date
    </select>
</mapper>
cnas-personnel/src/main/resources/static/credentials-deal.docx
Binary files differ
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -1035,7 +1035,8 @@
    /**
     * åŽŸå§‹è®°å½•ä¿å­˜åŽï¼ŒæŒ‰å½“å‰æ¨¡æ¿é€‰ç”¨çš„è®¾å¤‡ç”Ÿæˆå¾…å¡«å†™çš„ä½¿ç”¨è®°å½•ã€‚
     * åŒä¸€è®¢å•、样品、设备只生成一条;已经填写过的记录不会被覆盖。
     * åŒä¸€è®¢å•、样品、设备只生成一条;温湿度和使用日期取原始记录快照,
     * æ—¶é—´é»˜è®¤ 10:00-19:30;已经手工填写过的记录不会被覆盖。
     */
    private synchronized void syncDeviceRecordsFromTemplate(Integer orderId, Integer sampleId,
                                                            Integer templateId, Integer userId) {
@@ -1052,6 +1053,24 @@
        if (CollectionUtils.isEmpty(deviceIds)) {
            return;
        }
        InsOrderStandardTemplate snapshot = insOrderStandardTemplateService.getOne(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .eq(InsOrderStandardTemplate::getTemplateId, templateId)
                        .last("limit 1"));
        String temperature = snapshot == null || StringUtils.isBlank(snapshot.getTemperature())
                ? order.getTemperature() : snapshot.getTemperature();
        String humidity = snapshot == null || StringUtils.isBlank(snapshot.getHumidity())
                ? order.getHumidity() : snapshot.getHumidity();
        // åŽŸå§‹è®°å½•æ²¡æœ‰æ£€æµ‹æ—¥æœŸæ—¶ç•™ç©ºï¼Œä¸é»˜è®¤å½“å¤©
        LocalDateTime useStartDate = null;
        LocalDateTime useEndDate = null;
        if (snapshot != null && snapshot.getDetectionTime() != null) {
            useStartDate = snapshot.getDetectionTime().atTime(10, 0);
            LocalDate endDate = snapshot.getDetectionEndTime() != null
                    ? snapshot.getDetectionEndTime() : snapshot.getDetectionTime();
            useEndDate = endDate.atTime(19, 30);
        }
        User user = userMapper.selectById(userId);
        String usePerson = user == null ? "" : user.getName();
        String sampleCode = StringUtils.isNotBlank(sample.getSampleCode())
@@ -1060,19 +1079,46 @@
            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) {
            InsOrderDeviceRecord existing = insOrderDeviceRecordMapper.selectOne(
                    Wrappers.<InsOrderDeviceRecord>lambdaQuery()
                            .eq(InsOrderDeviceRecord::getInsOrderId, orderId)
                            .eq(InsOrderDeviceRecord::getDeviceId, deviceId)
                            .eq(InsOrderDeviceRecord::getSampleCode, sampleCode)
                            .last("limit 1"));
            if (existing != null) {
                // åªè¡¥è‡ªåŠ¨ç”Ÿæˆä½†å°šæœªå¡«å†™çš„è®°å½•
                if (existing.getUseStartDate() != null) {
                    continue;
                }
                InsOrderDeviceRecord update = new InsOrderDeviceRecord();
                update.setId(existing.getId());
                if (StringUtils.isBlank(existing.getTemperature())) {
                    update.setTemperature(temperature);
                }
                if (StringUtils.isBlank(existing.getHumidity())) {
                    update.setHumidity(humidity);
                }
                if (useStartDate != null) {
                    update.setUseStartDate(useStartDate);
                    update.setUseEndDate(useEndDate);
                }
                if (existing.getUsePersonId() == null) {
                    update.setUsePersonId(userId);
                }
                if (StringUtils.isBlank(existing.getUsePerson())) {
                    update.setUsePerson(usePerson);
                }
                insOrderDeviceRecordMapper.updateById(update);
                continue;
            }
            InsOrderDeviceRecord record = new InsOrderDeviceRecord();
            record.setInsOrderId(orderId);
            record.setDeviceId(deviceId);
            record.setSampleCode(sampleCode);
            record.setTemperature(order.getTemperature());
            record.setHumidity(order.getHumidity());
            record.setTemperature(temperature);
            record.setHumidity(humidity);
            record.setUseStartDate(useStartDate);
            record.setUseEndDate(useEndDate);
            record.setUseBefore(1);
            record.setUseAfter(1);
            record.setUsePersonId(userId);
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsProductServiceImpl.java
@@ -11,12 +11,14 @@
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.dto.InsProductBindingDto;
import com.ruoyi.inspect.dto.WriteInspectionConditionDto;
import com.ruoyi.inspect.mapper.InsOrderDeviceRecordMapper;
import com.ruoyi.inspect.mapper.InsOrderMapper;
import com.ruoyi.inspect.mapper.InsOrderStandardTemplateMapper;
import com.ruoyi.inspect.mapper.InsOrderTemplateDeviceMapper;
import com.ruoyi.inspect.mapper.InsProductMapper;
import com.ruoyi.inspect.mapper.InsSampleMapper;
import com.ruoyi.inspect.pojo.InsOrder;
import com.ruoyi.inspect.pojo.InsOrderDeviceRecord;
import com.ruoyi.inspect.pojo.InsOrderStandardTemplate;
import com.ruoyi.inspect.pojo.InsOrderTemplateDevice;
import com.ruoyi.inspect.pojo.InsProduct;
@@ -28,6 +30,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@@ -48,6 +52,8 @@
    private InsOrderStandardTemplateMapper insOrderStandardTemplateMapper;
    private InsOrderTemplateDeviceMapper insOrderTemplateDeviceMapper;
    private InsOrderDeviceRecordMapper insOrderDeviceRecordMapper;
    @Override
    public int selectOrderManDay(Integer orderId) {
@@ -144,6 +150,7 @@
            if (!hasSnapshotField && !dto.isDeviceIdsPresent()) {
                throw new ErrorException("没有需要保存的模板检验条件");
            }
            refreshPendingDeviceRecords(dto);
            return true;
        }
        InsOrder insOrder = new InsOrder();
@@ -153,6 +160,38 @@
        return true;
    }
    /**
     * åŽŸå§‹è®°å½•çš„çŽ¯å¢ƒæ¡ä»¶åŽè¡¥æ—¶ï¼ŒåŒæ­¥åˆ°å°šæœªå¡«å†™çš„ä½¿ç”¨è®°å½•ã€‚
     * å·²æ‰‹å·¥æ”¹åŠ¨è¿‡çš„è®°å½•ï¼ˆå·²æœ‰å¼€å§‹æ—¥æœŸï¼‰ä¸è¦†ç›–ã€‚
     */
    private void refreshPendingDeviceRecords(WriteInspectionConditionDto dto) {
        boolean hasTemperature = dto.isTemperaturePresent()
                && dto.getTemperature() != null && !dto.getTemperature().trim().isEmpty();
        boolean hasDetectionRange = dto.isDetectionTimePresent() && dto.getDetectionTime() != null;
        if (!hasTemperature && !hasDetectionRange) {
            return;
        }
        List<Integer> deviceIds = insOrderTemplateDeviceMapper
                .selectDeviceIdsByOrderAndTemplate(dto.getId(), dto.getTemplateId());
        if (deviceIds == null || deviceIds.isEmpty()) {
            return;
        }
        LocalDateTime start = null;
        LocalDateTime end = null;
        if (hasDetectionRange) {
            start = dto.getDetectionTime().atTime(10, 0);
            LocalDate endDate = dto.getDetectionEndTime() != null ? dto.getDetectionEndTime() : dto.getDetectionTime();
            end = endDate.atTime(19, 30);
        }
        insOrderDeviceRecordMapper.update(null, Wrappers.<InsOrderDeviceRecord>lambdaUpdate()
                .eq(InsOrderDeviceRecord::getInsOrderId, dto.getId())
                .in(InsOrderDeviceRecord::getDeviceId, deviceIds)
                .isNull(InsOrderDeviceRecord::getUseStartDate)
                .set(hasTemperature, InsOrderDeviceRecord::getTemperature, dto.getTemperature())
                .set(hasDetectionRange, InsOrderDeviceRecord::getUseStartDate, start)
                .set(hasDetectionRange, InsOrderDeviceRecord::getUseEndDate, end));
    }
    //查询待检项目
    @Override
    public IPage<InsProduct> selectNoProducts(Page page, InsProduct insProduct, Integer orderId, String ids) {
sql/fix_device_maintenance_plan_duplicates.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
-- ============================================================
-- è®¾å¤‡ä¿å…»è®¡åˆ’明细去重(数据修复,非结构变更)
-- ç”Ÿæˆæ—¥æœŸï¼š2026-11-06
--
-- èƒŒæ™¯ï¼šåŽ†å²ç‰ˆæœ¬è¡¥è®¾å¤‡/改日期时,可能给同一计划、同一设备、
--   åŒä¸€ä¿å…»æœˆä»½ç•™ä¸‹å¤šæ¡æ˜Žç»†ï¼Œå¯¼è‡´å¯¼å‡ºæ–‡æ¡£é‡å¤ã€‚
--
-- åŽ»é‡å£å¾„ï¼šåŒä¸€ maintenance_plan_id + device_id + ä¿å…»æœˆä»½
--   åªä¿ç•™ä¸€æ¡ã€‚
--   ä¼˜å…ˆä¿ç•™å·²æœ‰ä¿å…»è®°å½•的明细;都没有记录时保留最新一条。
--   ä¸ºä¿è¯æ•°æ®å®‰å…¨ï¼Œåªåˆ é™¤â€œæ²¡æœ‰ä»»ä½•保养记录”的重复行;
--   è‹¥åŒä¸€ç»„里有多条都已有保养记录,不自动删除,需人工确认。
--
-- æ‰§è¡Œå‰å¿…须先备份(脚本会自动创建备份表,已存在则不覆盖)。
-- è„šæœ¬å¯é‡å¤æ‰§è¡Œï¼ˆå¹‚ç­‰ï¼‰ã€‚
-- ============================================================
-- 1. å¤‡ä»½æ˜Žç»†è¡¨ï¼ˆé¦–次执行创建,重复执行不覆盖)
CREATE TABLE IF NOT EXISTS `device_maintenance_plan_details_bak_20261106` AS
SELECT * FROM `device_maintenance_plan_details`;
-- 2. æŸ¥çœ‹åŽ»é‡å‰çš„é‡å¤æƒ…å†µ
SELECT maintenance_plan_id, device_id, DATE_FORMAT(maintenance_date, '%Y-%m') AS maintenance_month,
       COUNT(*) AS duplicate_count,
       GROUP_CONCAT(maintenance_plan_detail_id ORDER BY maintenance_plan_detail_id) AS detail_ids
FROM device_maintenance_plan_details
GROUP BY maintenance_plan_id, device_id, DATE_FORMAT(maintenance_date, '%Y-%m')
HAVING COUNT(*) > 1;
-- 3. åˆ é™¤é‡å¤è¡Œï¼šæŒ‰â€œæœ‰ä¿å…»è®°å½•优先、最新优先”排序,仅删除无保养记录的重复行
START TRANSACTION;
DELETE d
FROM device_maintenance_plan_details d
JOIN (
    SELECT maintenance_plan_detail_id
    FROM (
        SELECT dmpd.maintenance_plan_detail_id,
               ROW_NUMBER() OVER (
                   PARTITION BY dmpd.maintenance_plan_id, dmpd.device_id, DATE_FORMAT(dmpd.maintenance_date, '%Y-%m')
                   ORDER BY (SELECT COUNT(*) FROM device_maintenance dm
                             WHERE dm.maintenance_plan_detail_id = dmpd.maintenance_plan_detail_id) DESC,
                            dmpd.maintenance_plan_detail_id DESC
               ) AS rn
        FROM device_maintenance_plan_details dmpd
    ) ranked
    WHERE ranked.rn > 1
) dup ON d.maintenance_plan_detail_id = dup.maintenance_plan_detail_id
WHERE NOT EXISTS (
    SELECT 1 FROM device_maintenance dm
    WHERE dm.maintenance_plan_detail_id = d.maintenance_plan_detail_id
);
COMMIT;
-- 4. æ ¡éªŒï¼ˆæ‰§è¡Œå®Œå¯äººå·¥æ ¸å¯¹ï¼‰
-- 4.1 åº”为 0 è¡Œï¼šåŒä¸€è®¡åˆ’ + åŒä¸€è®¾å¤‡ + åŒä¸€ä¿å…»æœˆä»½ä»æœ‰é‡å¤
-- SELECT maintenance_plan_id, device_id, DATE_FORMAT(maintenance_date, '%Y-%m') AS maintenance_month, COUNT(*) AS c
-- FROM device_maintenance_plan_details
-- GROUP BY maintenance_plan_id, device_id, DATE_FORMAT(maintenance_date, '%Y-%m')
-- HAVING c > 1;
--
-- 4.2 åº”为 0 è¡Œï¼šä¿å…»è®°å½•关联的计划明细已不存在(防止误删导致记录孤儿)
-- SELECT dm.id, dm.maintenance_plan_detail_id
-- FROM device_maintenance dm
-- LEFT JOIN device_maintenance_plan_details dmpd
--   ON dmpd.maintenance_plan_detail_id = dm.maintenance_plan_detail_id
-- WHERE dmpd.maintenance_plan_detail_id IS NULL;
--
-- 4.3 æ¸…理前后数量对比:
-- SELECT COUNT(*) FROM device_maintenance_plan_details;
-- SELECT COUNT(*) FROM device_maintenance_plan_details_bak_20261106;
sql/fix_device_record_backfill.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
-- ============================================================
-- è®¾å¤‡ä½¿ç”¨è®°å½•历史数据回填(数据修复,非结构变更)
-- ç”Ÿæˆæ—¥æœŸï¼š2026-11-06
--
-- èƒŒæ™¯ï¼šæ—§ç‰ˆæœ¬è‡ªåŠ¨ç”Ÿæˆä½¿ç”¨è®°å½•æ—¶åªå†™è®¾å¤‡ã€æ ·å“å’Œä½¿ç”¨äººï¼Œ
--   æ²¡æœ‰å†™ä½¿ç”¨æ—¥æœŸã€èµ·æ­¢æ—¶é—´å’Œæ¸©æ¹¿åº¦ï¼Œå¯¼è‡´ä½¿ç”¨è®°å½•页面这些列为空。
--   æ–°ç‰ˆæœ¬åœ¨ä¿å­˜åŽŸå§‹è®°å½•æ—¶ä¼šè‡ªåŠ¨å¸¦å‡ºï¼Œä½†ä¸ä¼šå›žå¤´ä¿®æ”¹åŽ†å²æ•°æ®ã€‚
--
-- å›žå¡«å£å¾„(按客户确认的规则):
--   1. åªå¤„理 use_start_date ä¸ºç©ºçš„历史记录,已手工填写的不动;
--   2. é€šè¿‡ ins_order_template_device æ‰¾åˆ°è¯¥è®¢å•、该设备对应的
--      åŽŸå§‹è®°å½•å¿«ç…§ ins_order_standard_template;
--   3. åŒä¸€è®¾å¤‡åœ¨åŒä¸€è®¢å•对应多个模板时,取检测日期最早的一条;
--   4. ä½¿ç”¨å¼€å§‹æ—¥æœŸ = æ£€æµ‹å¼€å§‹æ—¥æœŸ 10:00:00;
--      ä½¿ç”¨ç»“束日期 = æ£€æµ‹ç»“束日期 19:30:00;
--      æ²¡æœ‰æ£€æµ‹ç»“束日期时按开始日期当天 19:30:00;
--   5. æ¸©åº¦å–原始记录快照温度,已有的不覆盖;
--   6. åŽŸå§‹è®°å½•æ²¡æœ‰æ¹¿åº¦å­—æ®µï¼Œæ¹¿åº¦ä¸å›žå¡«ï¼›
--   7. æ‰¾ä¸åˆ°åŽŸå§‹è®°å½•è®¾å¤‡å…³è”çš„æ‰‹å·¥è®°å½•ä¿æŒä¸ºç©ºï¼Œç”±å®žéªŒäººå‘˜è¡¥å¡«ã€‚
--
-- æ•°æ®æ¥æºå‡ä¸ºåº“中已存在的原始记录快照,仅 10:00 / 19:30
-- ä¸ºæŒ‰å®¢æˆ·è§„则填入的默认时间。
--
-- è„šæœ¬å¯é‡å¤æ‰§è¡Œï¼ˆå¹‚ç­‰ï¼‰ã€‚
-- ============================================================
-- 1. å¤‡ä»½ä½¿ç”¨è®°å½•表(首次执行创建,重复执行不覆盖)
CREATE TABLE IF NOT EXISTS `device_record_bak_20261106` AS
SELECT * FROM `device_record`;
-- 2. å›žå¡«å‰ç»Ÿè®¡
-- 2.1 ç©ºè®°å½•总数
SELECT COUNT(*) AS blank_total
FROM device_record
WHERE use_start_date IS NULL;
-- 2.2 èƒ½åŒ¹é…åˆ°åŽŸå§‹è®°å½•æ¡ä»¶çš„ç©ºè®°å½•æ•°
SELECT COUNT(*) AS matchable_total
FROM device_record dr
WHERE dr.use_start_date IS NULL
  AND EXISTS (
      SELECT 1
      FROM ins_order_template_device t
      JOIN ins_order_standard_template s
        ON s.ins_order_id = t.ins_order_id AND s.template_id = t.template_id
      WHERE t.ins_order_id = dr.ins_order_id
        AND t.device_id = dr.device_id
        AND s.detection_time IS NOT NULL
  );
-- 2.3 åŒ¹é…ä¸åˆ°ã€å°†ä¿æŒä¸ºç©ºçš„记录(人工补填)
SELECT dr.id, dr.ins_order_id, dr.device_id, dr.sample_code
FROM device_record dr
WHERE dr.use_start_date IS NULL
  AND NOT EXISTS (
      SELECT 1
      FROM ins_order_template_device t
      JOIN ins_order_standard_template s
        ON s.ins_order_id = t.ins_order_id AND s.template_id = t.template_id
      WHERE t.ins_order_id = dr.ins_order_id
        AND t.device_id = dr.device_id
        AND s.detection_time IS NOT NULL
  );
-- 3. å›žå¡«
START TRANSACTION;
UPDATE device_record dr
JOIN (
    SELECT t.ins_order_id, t.device_id,
           s.temperature, s.detection_time, s.detection_end_time
    FROM ins_order_template_device t
    JOIN ins_order_standard_template s
      ON s.ins_order_id = t.ins_order_id AND s.template_id = t.template_id
    WHERE s.detection_time IS NOT NULL
      AND s.id = (
          SELECT s2.id
          FROM ins_order_template_device t2
          JOIN ins_order_standard_template s2
            ON s2.ins_order_id = t2.ins_order_id AND s2.template_id = t2.template_id
          WHERE t2.ins_order_id = t.ins_order_id
            AND t2.device_id = t.device_id
            AND s2.detection_time IS NOT NULL
          ORDER BY s2.detection_time ASC, s2.id ASC
          LIMIT 1
      )
) picked ON picked.ins_order_id = dr.ins_order_id AND picked.device_id = dr.device_id
SET dr.use_start_date = TIMESTAMP(picked.detection_time, '10:00:00'),
    dr.use_end_date = TIMESTAMP(COALESCE(picked.detection_end_time, picked.detection_time), '19:30:00'),
    dr.temperature = COALESCE(NULLIF(dr.temperature, ''), picked.temperature)
WHERE dr.use_start_date IS NULL;
COMMIT;
-- 4. æ ¡éªŒï¼ˆæ‰§è¡Œå®Œå¯äººå·¥æ ¸å¯¹ï¼‰
-- 4.1 å›žå¡«å‰åŽå¯¹æ¯”:备份表空格数 - çŽ°è¡¨ç©ºæ ¼æ•° = æœ¬æ¬¡å›žå¡«æ¡æ•°
-- SELECT
--     (SELECT COUNT(*) FROM device_record_bak_20261106 WHERE use_start_date IS NULL) AS blank_before,
--     (SELECT COUNT(*) FROM device_record WHERE use_start_date IS NULL) AS blank_after;
--
-- 4.2 æŠ½æŸ¥æŸè®¢å•的回填结果
-- SELECT id, device_id, ins_order_id, sample_code, temperature, use_start_date, use_end_date, use_person
-- FROM device_record
-- WHERE ins_order_id = ?;
sql/fix_inspection_order_detail_backfill.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
-- ============================================================
-- æ£€éªŒå§”托单旧数据明细回填(数据修复,非结构变更)
-- ç”Ÿæˆæ—¥æœŸï¼š2026-11-06
--
-- èƒŒæ™¯ï¼šæ—§ç‰ˆè‡ªåŠ¨ç”Ÿæˆæ£€éªŒå§”æ‰˜å•æ—¶åªå†™äº†è¡¨å¤´ï¼Œæ²¡æœ‰å†™å…¥
--   cnas_inspection_order_detail æ˜Žç»†è¡Œï¼Œå¯¼è‡´æŸ¥çœ‹/编辑弹窗里
--   æ ·å“åç§°ã€æ ·å“ç¼–号、检测项目、数量、状态、检测依据、备注、
--   å§”托编号全部为空。新版监听器已会写明细,仅需回填历史数据。
--
-- å›žå¡«æ¥æºï¼š
--   æ ·å“åç§°   cnas_inspection_order.sample_name(空时取 ins_order)
--   æ ·å“ç¼–号   ins_sample ç¬¬ä¸€æ¡æ ·å“ç¼–号
--   æ£€æµ‹é¡¹ç›®   ins_order_standard_template.name åŽ»é‡åŽâ€œã€â€æ‹¼æŽ¥
--   æ•°é‡/状态  cnas_inspection_order è¡¨å¤´ï¼ˆç©ºæ—¶å– ins_order)
--   æ£€æµ‹ä¾æ®   standard_tree -> standard_method çš„“编号 åç§°â€ï¼Œ
--              å–不到时回退到模板快照编号
--   å¤‡æ³¨       ins_order.remark
--
-- ä»…处理“已有下单记录但没有任何明细行”的委托单;
-- å·²æœ‰æ˜Žç»†çš„记录不改、不重复插入。可重复执行(幂等)。
--
-- æ‰§è¡Œå‰å»ºè®®å¤‡ä»½ï¼ˆåœ¨ LIMS æ•°æ®åº“内执行):
--   CREATE TABLE cnas_inspection_order_detail_bak_20261106 AS
--   SELECT * FROM cnas_inspection_order_detail;
-- ============================================================
START TRANSACTION;
INSERT INTO cnas_inspection_order_detail
    (inspection_order_id, sample_number, sample_name, test_item,
     quantity, sample_status, test_standard, remark)
SELECT
    io.inspection_order_id,
    (SELECT s.sample_code
     FROM ins_sample s
     WHERE s.ins_order_id = o.id
     ORDER BY s.id
     LIMIT 1),
    COALESCE(NULLIF(io.sample_name, ''), NULLIF(o.sample_view, ''), o.sample),
    (SELECT GROUP_CONCAT(DISTINCT t.name ORDER BY t.sort, t.id SEPARATOR '、')
     FROM ins_order_standard_template t
     WHERE t.ins_order_id = o.id),
    COALESCE(NULLIF(io.quantity, ''), o.test_quantity),
    COALESCE(NULLIF(io.sample_status, ''), o.sample_status),
    COALESCE(
        NULLIF(TRIM(CONCAT(COALESCE(sm.code, ''), ' ', COALESCE(sm.name, ''))), ''),
        (SELECT GROUP_CONCAT(DISTINCT t.number ORDER BY t.sort, t.id SEPARATOR '、')
         FROM ins_order_standard_template t
         WHERE t.ins_order_id = o.id)
    ),
    o.remark
FROM cnas_inspection_order io
JOIN ins_order o ON o.id = io.ins_order_id
LEFT JOIN standard_tree st ON st.id = o.standard_tree_id
LEFT JOIN standard_method sm ON sm.id = st.standard_method_id
WHERE NOT EXISTS (
    SELECT 1 FROM cnas_inspection_order_detail d
    WHERE d.inspection_order_id = io.inspection_order_id
);
COMMIT;
-- æ ¡éªŒï¼ˆæ‰§è¡Œå®Œå¯äººå·¥æ ¸å¯¹ï¼Œåº”返回 0 è¡Œï¼‰ï¼š
-- SELECT io.inspection_order_id, io.entrust_code
-- FROM cnas_inspection_order io
-- JOIN ins_order o ON o.id = io.ins_order_id
-- WHERE NOT EXISTS (
--     SELECT 1 FROM cnas_inspection_order_detail d
--     WHERE d.inspection_order_id = io.inspection_order_id
-- );
--
-- æŠ½æŸ¥å›žå¡«ç»“果:
-- SELECT d.inspection_order_detail_id, d.inspection_order_id, d.sample_name,
--        d.sample_number, d.test_item, d.quantity, d.sample_status,
--        d.test_standard, d.remark
-- FROM cnas_inspection_order_detail d
-- JOIN cnas_inspection_order io ON io.inspection_order_id = d.inspection_order_id
-- ORDER BY d.inspection_order_id;
sql/fix_inspection_order_orphans.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
-- ============================================================
-- æ£€éªŒå§”托单孤儿数据清理(数据修复,非结构变更)
-- ç”Ÿæˆæ—¥æœŸï¼š2026-11-06
--
-- èƒŒæ™¯ï¼šå®¢æˆ·åº“共有 24 æ¡æ£€éªŒå§”托单,但委托下单只有 22 æ¡ã€‚
--   å¤šå‡ºçš„ 2 æ¡å§”托单(LMSY-20260922-01、LMSY-20260929-试样)
--   å¯¹åº”的下单记录(ins_order 10908、10938)已被删除,属于
--   åŽ†å²é—ç•™çš„å­¤å„¿æ•°æ®ã€‚å½“å‰åˆ é™¤ä¸‹å•çš„é€»è¾‘å·²ä¼šçº§è”æ¸…ç†å§”æ‰˜å•ï¼Œ
--   è¿™ä¸¤æ¡æ˜¯æ—§ç‰ˆæœ¬åˆ é™¤æ—¶ç•™ä¸‹çš„。
--
-- å·²æ ¸å®žï¼šè¿™ä¸¤æ¡å§”托单没有明细、样品接收、报告、设备使用等
--   ä»»ä½•关联记录,删除不影响其他数据。
--
-- ä»…按委托编号精确匹配,且再次校验“下单不存在 + æ— ä»»ä½•关联记录”
-- åŽæ‰åˆ é™¤ï¼Œå¯é‡å¤æ‰§è¡Œï¼ˆå¹‚等)。
--
-- æ‰§è¡Œå‰å»ºè®®å¤‡ä»½ï¼ˆåœ¨ LIMS æ•°æ®åº“内执行):
--   CREATE TABLE cnas_inspection_order_bak_20261106 AS
--   SELECT * FROM cnas_inspection_order;
-- ============================================================
START TRANSACTION;
DELETE io
FROM cnas_inspection_order io
LEFT JOIN ins_order o ON o.id = io.ins_order_id
WHERE o.id IS NULL
  AND io.entrust_code IN ('LMSY-20260922-01', 'LMSY-20260929-试样')
  AND NOT EXISTS (
      SELECT 1 FROM cnas_inspection_order_detail d
      WHERE d.inspection_order_id = io.inspection_order_id)
  AND NOT EXISTS (
      SELECT 1 FROM cnas_process_sample ps
      WHERE ps.inspection_order_id = io.inspection_order_id)
  AND NOT EXISTS (
      SELECT 1 FROM cnas_process_report pr
      WHERE pr.inspection_order_id = io.inspection_order_id)
  AND NOT EXISTS (
      SELECT 1 FROM cnas_process_order_device pod
      WHERE pod.inspection_order_id = io.inspection_order_id);
COMMIT;
-- æ ¡éªŒï¼ˆæ‰§è¡Œå®Œå¯äººå·¥æ ¸å¯¹ï¼‰ï¼š
-- å§”托单总数应与委托下单数一致(客户库应为 22):
-- SELECT COUNT(*) AS inspection_order_total FROM cnas_inspection_order;
-- SELECT COUNT(*) AS ins_order_total FROM ins_order;
--
-- åº”返回 0 è¡Œï¼š
-- SELECT io.inspection_order_id, io.entrust_code, io.ins_order_id
-- FROM cnas_inspection_order io
-- LEFT JOIN ins_order o ON o.id = io.ins_order_id
-- WHERE o.id IS NULL;
sql/lims_delta.sql
@@ -1,3 +1,22 @@
-- æ‰¹é‡ä¸‹å•批次号:同一次批量下单的样品共享一个批次号
ALTER TABLE ins_order ADD COLUMN batch_no VARCHAR(64) NULL COMMENT '批量下单批次号' AFTER entrust_code;
ALTER TABLE cnas_inspection_order ADD COLUMN batch_no VARCHAR(64) NULL COMMENT '批量下单批次号' AFTER entrust_code;
-- è®¾å¤‡ä¿å…»è®¡åˆ’明细附件表:附件按计划明细(设备行)挂载
CREATE TABLE IF NOT EXISTS `device_maintenance_attachment` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '附件id',
  `maintenance_plan_detail_id` int NOT NULL COMMENT '设备保养计划明细id',
  `file_name` varchar(255) NOT NULL COMMENT '文件名',
  `file_url` varchar(500) NOT NULL COMMENT '文件地址',
  `sort` int DEFAULT '0' COMMENT '排序',
  `create_user` int DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` int DEFAULT NULL COMMENT '修改人',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  KEY `idx_dmpa_detail_id` (`maintenance_plan_detail_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备保养计划明细附件表';
-- æ–‡ä»¶å—控申请:增加发布日期、实施日期(提交日期列保留,但界面不再显示)
ALTER TABLE cnas_manage_document_controlled ADD COLUMN publish_date DATE NULL COMMENT '发布日期' AFTER submit_date;
ALTER TABLE cnas_manage_document_controlled ADD COLUMN implement_date DATE NULL COMMENT '实施日期' AFTER publish_date;