feat(device,supply,satisfaction): 设备计划后端改造、耗材存放位置与满意度照片支持

- 设备保养计划:审核/退回接口、计划明细联动、Word导出模板与核查模板
- 设备核查计划:日期与审核口径调整、删除连带清理未审核记录及附件
- 服务和供应品采购:新增 storage_location 字段映射
- 客户满意度:新增接口返回保存后的实体,支持前端补传照片
已添加10个文件
已修改26个文件
1567 ■■■■■ 文件已修改
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceMaintenanceController.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceMaintenancePlanController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceMaintenanceDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceMaintenancePlanDetailsDto.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExaminePlanDetailsMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExaminePlanMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceMaintenancePlanMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceMaintenance.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/DeviceMaintenancePlanService.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/DeviceMaintenanceService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceImpl.java 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceMaintenancePlanServiceImpl.java 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceMaintenanceMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceMaintenancePlanDetailsMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/mapper/DeviceMaintenancePlanMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/device-maintenance.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-balance-0.1mg-al204.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-balance-0.1mg-me204e.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-balance-percent-bsa2202s.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-balance-percent.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-balance-thousandth.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-generic.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-rotary-viscosity.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-spectrophotometer.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-surface-tension.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/resources/static/word/examine/examine-viscosity-pin.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/controller/ClientSatisfactionController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-require/src/main/java/com/ruoyi/require/pojo/ProcurementSuppliesList.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-require/src/main/resources/mapper/ProcurementSuppliesListMapper.xml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java
@@ -141,6 +141,16 @@
    }
    /**
     * 核查计划年份列表(自动删除没有明细的空年份计划)
     * @return
     */
    @Operation(summary = "核查计划年份列表")
    @GetMapping("/getPlanYears")
    public Result<List<String>> getPlanYears() {
        return Result.success(deviceExaminePlanService.getExaminePlanYears());
    }
    /**
     * 设备核查计划详情列表
     * @return
     */
@@ -238,8 +248,8 @@
     */
    @Operation(summary = "导出设备核查计划")
    @GetMapping("/exportDeviceExaminePlanDetail")
    public void exportDeviceExaminePlanDetail(Integer planId, HttpServletResponse response){
        deviceExaminePlanService.exportDeviceExaminePlanDetail(planId, response);
    public void exportDeviceExaminePlanDetail(Integer planId, String deviceName, String deviceNumber, HttpServletResponse response){
        deviceExaminePlanService.exportDeviceExaminePlanDetail(planId, deviceName, deviceNumber, response);
    }
    /*********************************************** 报告 **************************************************/
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceMaintenanceController.java
@@ -2,12 +2,9 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.device.dto.DeviceMaintenanceDto;
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.service.DeviceMaintenancePlanService;
import com.ruoyi.device.service.DeviceMaintenanceService;
import com.ruoyi.system.mapper.UserMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired;
@@ -25,12 +22,6 @@
    @Autowired
    private DeviceMaintenanceService deviceMaintenanceService;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private DeviceMaintenancePlanService deviceMaintenancePlanService;
    /**
@@ -53,18 +44,16 @@
    @Operation(summary = "新增修改设备维护保养")
    @PostMapping("/addDeviceMaintenance")
    public Result addDeviceMaintenance(@RequestBody DeviceMaintenance deviceMaintenance){
        if (deviceMaintenance.getDeviceId() == null) {
            throw new RuntimeException("缺少设备id");
        }
        User user = userMapper.selectById(deviceMaintenance.getMaintenanceUserId());
        deviceMaintenance.setMaintenanceUserName(user.getName());
        return Result.success(deviceMaintenanceService.saveDeviceMaintenance(deviceMaintenance));
    }
        boolean saved = deviceMaintenanceService.saveOrUpdate(deviceMaintenance);
        // 保养完成后,把设备档案的最近/下次保养日期滚动到本轮
        if (saved && deviceMaintenance.getMaintenanceDate() != null) {
            deviceMaintenancePlanService.updateDeviceMaintenanceDate(deviceMaintenance.getDeviceId(), deviceMaintenance.getMaintenanceDate());
        }
        return Result.success(saved);
    /**
     * 审核保养记录
     */
    @Operation(summary = "审核保养记录")
    @PostMapping("/reviewDeviceMaintenance")
    public Result reviewDeviceMaintenance(@RequestBody DeviceMaintenanceDto deviceMaintenanceDto) {
        return Result.success(deviceMaintenanceService.reviewDeviceMaintenance(deviceMaintenanceDto));
    }
    /**
@@ -75,7 +64,7 @@
    @Operation(summary = "删除修改设备维护保养")
    @DeleteMapping("/deleteDeviceMaintenance")
    public Result deleteDeviceMaintenance(Integer id){
        return Result.success(deviceMaintenanceService.removeById(id));
        return Result.success(deviceMaintenanceService.deleteDeviceMaintenance(id));
    }
    /**
cnas-device/src/main/java/com/ruoyi/device/controller/DeviceMaintenancePlanController.java
@@ -7,7 +7,6 @@
import com.ruoyi.device.dto.DeviceMaintenancePlanDto;
import com.ruoyi.device.pojo.DeviceMaintenancePlan;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import com.ruoyi.device.service.DeviceMaintenancePlanDetailsService;
import com.ruoyi.device.service.DeviceMaintenancePlanService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -16,6 +15,7 @@
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
@@ -32,9 +32,6 @@
public class DeviceMaintenancePlanController {
    @Resource
    private DeviceMaintenancePlanService deviceMaintenancePlanService;
    @Resource
    private DeviceMaintenancePlanDetailsService deviceMaintenancePlanDetailsService;
    /**
     * 分页查询设备保养计划
@@ -95,6 +92,15 @@
    }
    /**
     * 保养计划年份列表(自动删除没有明细的空年份计划)
     */
    @Operation(summary = "保养计划年份列表")
    @GetMapping("/getPlanYears")
    public Result<List<String>> getPlanYears() {
        return Result.success(deviceMaintenancePlanService.getMaintenancePlanYears());
    }
    /**
     * 设备保养计划详情列表(含到点标红)
     */
    @Operation(summary = "设备保养计划详情列表")
@@ -109,7 +115,7 @@
    @Operation(summary = "修改设备保养计划详情")
    @PostMapping("/updateMaintenancePlanDetail")
    public Result updateMaintenancePlanDetail(@RequestBody DeviceMaintenancePlanDetails deviceMaintenancePlanDetails) {
        return Result.success(deviceMaintenancePlanDetailsService.updateById(deviceMaintenancePlanDetails));
        return Result.success(deviceMaintenancePlanService.updateMaintenancePlanDetail(deviceMaintenancePlanDetails));
    }
    /**
@@ -118,7 +124,7 @@
    @Operation(summary = "删除设备保养计划详情")
    @DeleteMapping("/delMaintenancePlanDetail")
    public Result delMaintenancePlanDetail(Integer maintenancePlanDetailId) {
        return Result.success(deviceMaintenancePlanDetailsService.removeById(maintenancePlanDetailId));
        return Result.success(deviceMaintenancePlanService.removeMaintenancePlanDetail(maintenancePlanDetailId));
    }
    /**
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceExaminePlanDetailsDto.java
@@ -23,6 +23,9 @@
    @Schema(description = "到点标红, 1:需核查")
    private Integer redFlag;
    @Schema(description = "核查完成日期(审核通过当天)")
    private String completeDate;
    @Schema(description = "附件数量")
    private Integer attachmentCount;
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceMaintenanceDto.java
@@ -17,4 +17,10 @@
    @Schema(description = "设备编号")
    private String managementNumber;
    @Schema(description = "记录状态, 1:待审核, 2:通过, 3:不通过, 4:历史")
    private Integer recordStatus;
    @Schema(description = "导出台账序号")
    private Integer index;
}
cnas-device/src/main/java/com/ruoyi/device/dto/DeviceMaintenancePlanDetailsDto.java
@@ -4,6 +4,9 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * Author: yuan
 * Date: 2024-12-17 星期二 9:26:48
@@ -30,6 +33,69 @@
    @Schema(description = "归属实验室")
    private String storagePoint;
    @Schema(description = "保养周期(月,取自设备档案)")
    private String maintenanceCycle;
    @Schema(description = "到点标红, 1:需保养")
    private Integer redFlag;
    @Schema(description = "记录状态, 0:未开始, 1:待审核, 2:通过, 3:不通过")
    private Integer recordStatus;
    @Schema(description = "保养记录id")
    private Integer recordId;
    @Schema(description = "实际保养日期")
    private LocalDate recordMaintenanceDate;
    @Schema(description = "维护人id")
    private Integer maintenanceUserId;
    @Schema(description = "维护人")
    private String maintenanceUserName;
    @Schema(description = "外观是否良好")
    private String appearanceGood;
    @Schema(description = "是否可以正常使用")
    private String normalUse;
    @Schema(description = "是否清洁仪器表面")
    private String surfaceClean;
    @Schema(description = "标识是否完好")
    private String labelIntact;
    @Schema(description = "显示器灯是否正常")
    private String displayLightNormal;
    @Schema(description = "仪器是否生锈")
    private String noRust;
    @Schema(description = "校准是否正常")
    private String calibrationNormal;
    @Schema(description = "配件是否需要更换")
    private String partsReplace;
    @Schema(description = "维护结果是否正常")
    private String resultNormal;
    @Schema(description = "保养记录备注")
    private String recordRemark;
    @Schema(description = "审核状态 0不通过 1通过 NULL待审核")
    private Integer reviewStatus;
    @Schema(description = "审核人id")
    private Integer reviewUserId;
    @Schema(description = "审核人")
    private String reviewUserName;
    @Schema(description = "审核备注")
    private String reviewRemark;
    @Schema(description = "审核时间")
    private LocalDateTime reviewTime;
}
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExaminePlanDetailsMapper.java
@@ -8,6 +8,8 @@
import com.ruoyi.device.pojo.DeviceExaminePlanDetails;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * 设备核查计划详情表 Mapper 接口
@@ -25,4 +27,11 @@
     * @return
     */
    IPage<DeviceExaminePlanDetailsDto> pageDeviceExaminePlanDetail(Page page, @Param("ew") QueryWrapper<DeviceExaminePlanDetails> ew);
    /**
     * 设备核查计划详情全量列表(导出用,不分页)
     * @param ew
     * @return
     */
    List<DeviceExaminePlanDetailsDto> listDeviceExaminePlanDetail(@Param("ew") QueryWrapper<DeviceExaminePlanDetails> ew);
}
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceExaminePlanMapper.java
@@ -8,6 +8,8 @@
import com.ruoyi.device.pojo.DeviceExaminePlan;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * 设备核查计划主表 Mapper 接口
@@ -32,4 +34,10 @@
     * @return
     */
    DeviceExaminePlanDto selectExamineExaminePlanDto(@Param("deviceExaminePlanId") Integer deviceExaminePlanId);
    /**
     * 有明细的核查计划年份(倒序)
     * @return 年份列表
     */
    List<String> selectPlanYears();
}
cnas-device/src/main/java/com/ruoyi/device/mapper/DeviceMaintenancePlanMapper.java
@@ -8,6 +8,8 @@
import com.ruoyi.device.pojo.DeviceMaintenancePlan;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * 设备保养计划表 Mapper 接口
@@ -32,4 +34,10 @@
     * @return
     */
    DeviceMaintenancePlanDto selectMaintenancePlanById(@Param("maintenancePlanId") Integer maintenancePlanId);
    /**
     * 查询有明细的保养计划年份(倒序)
     * @return 年份列表
     */
    List<String> selectPlanYears();
}
cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceMaintenance.java
@@ -25,11 +25,41 @@
    @Schema(description = "设备id")
    private Integer deviceId;
    @Schema(description = "保养计划明细id")
    private Integer maintenancePlanDetailId;
    @Schema(description = "维护日期")
    private LocalDate maintenanceDate;
    @Schema(description = "维护内容")
    private String maintenanceContent;
    @Schema(description = "外观是否良好")
    private String appearanceGood;
    @Schema(description = "是否可以正常使用")
    private String normalUse;
    @Schema(description = "是否清洁仪器表面")
    private String surfaceClean;
    @Schema(description = "标识是否完好")
    private String labelIntact;
    @Schema(description = "显示器灯是否正常")
    private String displayLightNormal;
    @Schema(description = "仪器是否生锈")
    private String noRust;
    @Schema(description = "校准是否正常")
    private String calibrationNormal;
    @Schema(description = "配件是否需要更换")
    private String partsReplace;
    @Schema(description = "维护结果是否正常")
    private String resultNormal;
    @Schema(description = "维护人id")
    private Integer maintenanceUserId;
@@ -40,6 +70,21 @@
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "审核状态 0不通过 1通过 NULL待审核")
    private Integer reviewStatus;
    @Schema(description = "审核人id")
    private Integer reviewUserId;
    @Schema(description = "审核人")
    private String reviewUserName;
    @Schema(description = "审核备注")
    private String reviewRemark;
    @Schema(description = "审核时间")
    private LocalDateTime reviewTime;
    @Schema(description = "创建日期")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java
@@ -70,11 +70,13 @@
    /**
     * 导出设备核查计划
     * 导出设备核查计划(Excel)
     * @param deviceExaminePlanId 设备核查计划id
     * @param deviceName 设备名称筛选(为空时导出该年份全部)
     * @param deviceNumber 设备编号筛选(为空时导出该年份全部)
     * @param response 响应
     */
    void exportDeviceExaminePlanDetail(Integer deviceExaminePlanId, HttpServletResponse response);
    void exportDeviceExaminePlanDetail(Integer deviceExaminePlanId, String deviceName, String deviceNumber, HttpServletResponse response);
    /**
     * 批量修改设备核查计划
@@ -98,6 +100,12 @@
    DeviceExaminePlanDto getOrCreatePlanByYear(String planYear);
    /**
     * 核查计划年份列表:删除没有任何明细的空年份计划后,返回有明细的年份(倒序)
     * @return 年份列表
     */
    List<String> getExaminePlanYears();
    /**
     * 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细
     * @param device 设备
     */
cnas-device/src/main/java/com/ruoyi/device/service/DeviceMaintenancePlanService.java
@@ -8,9 +8,11 @@
import com.ruoyi.device.dto.DeviceMaintenancePlanDto;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceMaintenancePlan;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDate;
import java.util.List;
/**
 * <p>
@@ -87,20 +89,51 @@
    DeviceMaintenancePlanDto getOrCreatePlanByYear(String planYear);
    /**
     * 设备档案变更后,把已到点的设备同步到对应年份的设备保养计划明细
     * 保养计划年份列表:删除没有任何明细的空年份计划后,返回有明细的年份(倒序)
     * @return 年份列表
     */
    List<String> getMaintenancePlanYears();
    /**
     * 设备档案变更后,立即把设备登记到下次保养日期所在年份的设备保养计划明细
     * @param device 设备
     */
    void syncDeviceMaintenanceDetail(Device device);
    /**
     * 设备保养记录保存后,滚动设备档案的最近/下次保养日期
     * 审核通过后,按审核通过当天滚动设备档案的最近/下次保养日期
     * @param deviceId 设备id
     * @param maintenanceDate 本次保养日期
     * @param maintenanceDate 保养完成日期(审核通过当天)
     */
    void updateDeviceMaintenanceDate(Integer deviceId, LocalDate maintenanceDate);
    /**
     * 审核通过后,把计划明细归到审核完成年份的计划下(提前完成的明细不再留在原计划年份)
     * @param maintenancePlanDetailId 保养计划明细id
     * @param reviewDate 审核完成日期
     */
    void reclassifyMaintenanceDetailByReviewDate(Integer maintenancePlanDetailId, LocalDate reviewDate);
    /**
     * 分页查询设备保养计划详情(含到点标红)
     */
    IPage<DeviceMaintenancePlanDetailsDto> pageMaintenancePlanDetail(Page page, Integer planId, String deviceName, String deviceNumber);
    /**
     * 为保养记录找一条可关联的计划明细;没有可用明细时在当前年度计划新增本轮明细
     * @param deviceId 设备id
     * @param maintenancePlanDetailId 指定的计划明细id,为空则自动匹配
     * @param maintenanceDate 保养日期
     */
    DeviceMaintenancePlanDetails resolveMaintenancePlanDetail(Integer deviceId, Integer maintenancePlanDetailId, LocalDate maintenanceDate);
    /**
     * 修改设备保养计划详情(已审核的明细不允许修改)
     */
    boolean updateMaintenancePlanDetail(DeviceMaintenancePlanDetails detail);
    /**
     * 删除设备保养计划详情;已出审核结果的记录随历史留档,未审核的记录随明细一起删除
     */
    boolean removeMaintenancePlanDetail(Integer maintenancePlanDetailId);
}
cnas-device/src/main/java/com/ruoyi/device/service/DeviceMaintenanceService.java
@@ -27,4 +27,22 @@
     * @return
     */
    void exportDeviceMaintenance(Integer deviceId, HttpServletResponse response);
    /**
     * 保存保养记录(关联保养计划明细,保存后进入待审核,不滚动设备档案日期)
     * @param deviceMaintenance 保养记录
     */
    boolean saveDeviceMaintenance(DeviceMaintenance deviceMaintenance);
    /**
     * 审核保养记录;审核通过后才滚动设备档案的最近/下次保养日期
     * @param deviceMaintenanceDto 审核内容
     */
    boolean reviewDeviceMaintenance(DeviceMaintenanceDto deviceMaintenanceDto);
    /**
     * 删除保养记录;审核通过和历史记录不允许删除
     * @param id 保养记录id
     */
    boolean deleteDeviceMaintenance(Integer id);
}
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
@@ -9,8 +9,6 @@
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;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.constant.MenuJumpPathConstants;
import com.ruoyi.common.core.domain.entity.InformationNotification;
import com.ruoyi.common.core.domain.entity.User;
@@ -39,10 +37,19 @@
import com.ruoyi.device.service.DeviceExaminePlanDetailsService;
import com.ruoyi.device.service.DeviceExaminePlanService;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.UserUtils;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -54,7 +61,6 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDate;
@@ -65,6 +71,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -292,57 +299,196 @@
    }
    /**
     * 导出设备核查计划
     * 导出设备核查计划(Excel):有筛选条件时导出筛选结果,没有筛选时导出该年份全部
     * @param deviceExaminePlanId 设备核查计划id
     * @param deviceName 设备名称筛选
     * @param deviceNumber 设备编号筛选
     * @param response 响应体
     */
    @Override
    public void exportDeviceExaminePlanDetail(Integer deviceExaminePlanId, HttpServletResponse response) {
        // 查询设备核查计划
        DeviceExaminePlanDto deviceExaminePlanDto = baseMapper.selectExamineExaminePlanDto(deviceExaminePlanId);
    public void exportDeviceExaminePlanDetail(Integer deviceExaminePlanId, String deviceName, String deviceNumber, HttpServletResponse response) {
        DeviceExaminePlan deviceExaminePlan = baseMapper.selectById(deviceExaminePlanId);
        if (deviceExaminePlan == null) {
            throw new ErrorException("设备核查计划不存在");
        }
        DeviceExaminePlanDetails query = new DeviceExaminePlanDetails();
        query.setPlanId(deviceExaminePlanId);
        query.setDeviceName(deviceName);
        query.setDeviceNumber(deviceNumber);
        List<DeviceExaminePlanDetailsDto> deviceExaminePlanDetailsDtoList =
                deviceExaminePlanDetailsMapper.listDeviceExaminePlanDetail(QueryWrappers.queryWrappers(query));
        // 查询设备核查计划详情
        List<DeviceExaminePlanDetails> deviceExaminePlanDetailsList = deviceExaminePlanDetailsMapper.selectList(Wrappers.<DeviceExaminePlanDetails>lambdaQuery().eq(DeviceExaminePlanDetails::getPlanId, deviceExaminePlanId)
                .orderByAsc(DeviceExaminePlanDetails::getCheckTime));
        // 设置序号
        ArrayList<DeviceExaminePlanDetailsDto> deviceExaminePlanDetailsDtoList = new ArrayList<>();
        deviceExaminePlanDetailsList.forEach(deviceExamineRecordContrastDetail -> {
            DeviceExaminePlanDetailsDto deviceExaminePlanDetailsDto = new DeviceExaminePlanDetailsDto();
            BeanUtils.copyProperties(deviceExamineRecordContrastDetail, deviceExaminePlanDetailsDto);
            deviceExaminePlanDetailsDto.setIndex(deviceExaminePlanDetailsList.indexOf(deviceExamineRecordContrastDetail) + 1);
            deviceExaminePlanDetailsDtoList.add(deviceExaminePlanDetailsDto);
        });
        String planName = StringUtils.isNotBlank(deviceExaminePlan.getPlanName()) ? deviceExaminePlan.getPlanName() : "设备核查计划";
        String[] columns = {"序号", "设备编号", "设备名称", "设备型号", "核查时间", "核查指标",
                "核查方法", "结果如何判定", "核查负责人", "审核状态", "备注"};
        List<String[]> dataRows = new ArrayList<>();
        int index = 1;
        for (DeviceExaminePlanDetailsDto detail : deviceExaminePlanDetailsDtoList) {
            String checkTime = StringUtils.isNotBlank(detail.getCompleteDate()) ? detail.getCompleteDate() : detail.getCheckTime();
            dataRows.add(new String[]{
                    String.valueOf(index++),
                    blankToEmpty(detail.getDeviceNumber()),
                    blankToEmpty(detail.getDeviceName()),
                    blankToEmpty(detail.getDeviceModel()),
                    blankToEmpty(checkTime),
                    blankToEmpty(detail.getCheckIndex()),
                    blankToEmpty(detail.getCheckMethod()),
                    blankToEmpty(detail.getHowResults()),
                    blankToEmpty(detail.getCheckChargerUser()),
                    recordStatusText(detail.getRecordStatus()),
                    blankToEmpty(detail.getRemark())
            });
        }
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/examine-plan-detail.docx");
        Configure configure = Configure.builder()
                .bind("deviceExaminePlanDetailsDtoList", new HackLoopTableRenderPolicy())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("deviceExaminePlan", deviceExaminePlanDto);
                    put("deviceExaminePlanDetailsDtoList", deviceExaminePlanDetailsDtoList);
                    //获取核查人的签名地址
                    put("writeUserUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceExaminePlanDto.getWriteUserId()));
                    //获取审查人的签名地址
                    put("reviewUserUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceExaminePlanDto.getRatifyUserId()));
                }});
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("UTF-8");
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "设备核查计划", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
            template.write(os);
            os.flush();
            os.close();
            inputStream.close();
                    "attachment;filename=" + URLEncoder.encode(planName, "UTF-8") + ".xlsx");
            writePlanExcel(response, planName, columns, dataRows);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        }
    }
    /**
     * 写计划表:标题行 + 列名 + 数据,整体只加最外边框,全部居中,无背景色
     */
    private void writePlanExcel(HttpServletResponse response, String planName, String[] columns,
                                List<String[]> dataRows) throws IOException {
        Workbook workbook = new XSSFWorkbook();
        Map<String, CellStyle> styleCache = new HashMap<>();
        Font titleFont = buildExcelFont(workbook, true, 16);
        Font headerFont = buildExcelFont(workbook, true, 12);
        Font contentFont = buildExcelFont(workbook, false, 12);
        Sheet sheet = workbook.createSheet("设备核查计划");
        int lastColumn = columns.length - 1;
        Row titleRow = sheet.createRow(0);
        titleRow.setHeightInPoints(30);
        for (int c = 0; c <= lastColumn; c++) {
            Cell cell = titleRow.createCell(c);
            if (c == 0) {
                cell.setCellValue(planName);
            }
            cell.setCellStyle(exportStyle(workbook, styleCache, titleFont, c == 0, c == lastColumn, true, false));
        }
        sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, lastColumn));
        Row headerRow = sheet.createRow(1);
        headerRow.setHeightInPoints(22);
        for (int c = 0; c <= lastColumn; c++) {
            Cell cell = headerRow.createCell(c);
            cell.setCellValue(columns[c]);
            cell.setCellStyle(exportStyle(workbook, styleCache, headerFont,
                    c == 0, c == lastColumn, false, dataRows.isEmpty()));
        }
        for (int r = 0; r < dataRows.size(); r++) {
            Row row = sheet.createRow(r + 2);
            boolean lastDataRow = r == dataRows.size() - 1;
            for (int c = 0; c <= lastColumn; c++) {
                Cell cell = row.createCell(c);
                cell.setCellValue(dataRows.get(r)[c]);
                cell.setCellStyle(exportStyle(workbook, styleCache, contentFont,
                        c == 0, c == lastColumn, false, lastDataRow));
            }
        }
        for (int c = 0; c <= lastColumn; c++) {
            int width = textWidth(columns[c]);
            for (String[] row : dataRows) {
                width = Math.max(width, textWidth(row[c]));
            }
            sheet.setColumnWidth(c, Math.min(255, width + 4) * 256);
        }
        OutputStream os = response.getOutputStream();
        workbook.write(os);
        os.flush();
        workbook.close();
    }
    private Font buildExcelFont(Workbook workbook, boolean bold, int size) {
        Font font = workbook.createFont();
        font.setFontName("宋体");
        font.setBold(bold);
        font.setFontHeightInPoints((short) size);
        return font;
    }
    /**
     * 单元格样式:居中、无背景色;内部细网格线,整体最外圈粗边框
     */
    private CellStyle exportStyle(Workbook workbook, Map<String, CellStyle> styleCache, Font font,
                                  boolean outerLeft, boolean outerRight, boolean outerTop, boolean outerBottom) {
        String key = font.getBold() + "-" + font.getFontHeightInPoints() + "-" + (outerLeft ? 1 : 0)
                + (outerRight ? 1 : 0) + (outerTop ? 1 : 0) + (outerBottom ? 1 : 0);
        CellStyle cached = styleCache.get(key);
        if (cached != null) {
            return cached;
        }
        CellStyle style = workbook.createCellStyle();
        style.setFont(font);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        if (outerLeft) {
            style.setBorderLeft(BorderStyle.MEDIUM);
        }
        if (outerRight) {
            style.setBorderRight(BorderStyle.MEDIUM);
        }
        if (outerTop) {
            style.setBorderTop(BorderStyle.MEDIUM);
        }
        if (outerBottom) {
            style.setBorderBottom(BorderStyle.MEDIUM);
        }
        styleCache.put(key, style);
        return style;
    }
    /**
     * 估算显示宽度(中文按两个字符宽)
     */
    private int textWidth(String text) {
        if (text == null) {
            return 0;
        }
        int width = 0;
        for (int i = 0; i < text.length(); i++) {
            width += text.charAt(i) > 0xFF ? 2 : 1;
        }
        return width;
    }
    /**
     * 记录状态文案:0未开始、1待批准、2通过、3不通过
     */
    private String recordStatusText(Integer recordStatus) {
        if (recordStatus == null) {
            return "未开始";
        }
        switch (recordStatus) {
            case 1:
                return "待批准";
            case 2:
                return "通过";
            case 3:
                return "不通过";
            default:
                return "未开始";
        }
    }
    private String blankToEmpty(String value) {
        return value == null ? "" : value;
    }
    @Override
@@ -409,6 +555,22 @@
            }
        }
        return planDto;
    }
    /**
     * 核查计划年份列表:删除没有任何明细的空年份计划后,返回有明细的年份(倒序)
     */
    @Override
    public List<String> getExaminePlanYears() {
        List<DeviceExaminePlan> plans = baseMapper.selectList(null);
        for (DeviceExaminePlan plan : plans) {
            Long detailCount = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
                    .eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId()));
            if (detailCount == null || detailCount == 0) {
                removeById(plan.getPlanId());
            }
        }
        return baseMapper.selectPlanYears();
    }
    /**
@@ -499,17 +661,32 @@
    }
    /**
     * 计划中尚无该设备本轮明细时自动补一条;核查时间取应核查月份
     * 计划中尚无该设备本轮明细时自动补一条;核查时间取档案下次核查日期(精确到日)
     */
    private void fillExamineDetailIfAbsent(DeviceExaminePlan plan, Device device) {
        LocalDate due = calcExamineDueDate(device);
        if (due == null) {
            return;
        }
        if (plan.getPlanYear() != null && String.valueOf(due.getYear()).compareTo(plan.getPlanYear()) > 0) {
        if (StringUtils.isBlank(plan.getPlanYear())) {
            return;
        }
        String checkTime = due.format(DateTimeFormatter.ofPattern("yyyy-MM"));
        int planYearValue;
        try {
            planYearValue = Integer.parseInt(plan.getPlanYear().trim());
        } catch (NumberFormatException e) {
            return;
        }
        int dueYear = due.getYear();
        // 未来年份的计划只登记应核查日期就在该年份的设备,避免把往年的旧日期补进未来的计划
        if (planYearValue > LocalDate.now().getYear()) {
            if (dueYear != planYearValue) {
                return;
            }
        } else if (dueYear > planYearValue) {
            return;
        }
        String checkTime = due.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        Long exist = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
                .eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId())
                .eq(DeviceExaminePlanDetails::getDeviceId, device.getId())
@@ -682,12 +859,13 @@
                .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId));
        List<DeviceExamineRecordContrast> contrasts = deviceExamineRecordContrastMapper.selectList(Wrappers.<DeviceExamineRecordContrast>lambdaQuery()
                .eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId));
        boolean hasReviewedRecord = records.stream().anyMatch(item -> item.getReviewStatus() != null
                && (item.getReviewStatus() == 0 || item.getReviewStatus() == 1));
        boolean hasReviewedContrast = contrasts.stream().anyMatch(item -> item.getReviewStatus() != null
                && (item.getReviewStatus() == 0 || item.getReviewStatus() == 1));
        if (hasReviewedRecord || hasReviewedContrast) {
            throw new ErrorException("该计划已存在审核完成的核查记录,为保留历史记录不允许删除");
        // 审核不通过的记录允许连同明细一起删除;审核通过的记录为历史留档,不允许删除
        boolean hasPassedRecord = records.stream().anyMatch(item -> item.getReviewStatus() != null
                && item.getReviewStatus() == 1);
        boolean hasPassedContrast = contrasts.stream().anyMatch(item -> item.getReviewStatus() != null
                && item.getReviewStatus() == 1);
        if (hasPassedRecord || hasPassedContrast) {
            throw new ErrorException("该计划已存在审核通过的核查记录,为保留历史记录不允许删除");
        }
        if (!records.isEmpty()) {
            List<Integer> recordIds = records.stream().map(DeviceExamineRecord::getRecordId).collect(Collectors.toList());
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExamineRecordServiceImpl.java
@@ -5,8 +5,6 @@
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.constant.MenuJumpPathConstants;
import com.ruoyi.common.core.domain.entity.InformationNotification;
import com.ruoyi.common.core.domain.entity.User;
@@ -24,11 +22,19 @@
import com.ruoyi.device.service.DeviceExamineRecordDetailService;
import com.ruoyi.device.service.DeviceExamineRecordService;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.UserUtils;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.apache.xmlbeans.XmlCursor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -37,9 +43,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -53,6 +57,22 @@
@Service
@Transactional(rollbackFor = Exception.class)
public class DeviceExamineRecordServiceImpl extends ServiceImpl<DeviceExamineRecordMapper, DeviceExamineRecord> implements DeviceExamineRecordService {
    /** 按设备套用的期间核查记录模板(与设备名称/型号匹配) */
    private static final String TEMPLATE_BALANCE_PERCENT = "/static/word/examine/examine-balance-percent.docx";
    private static final String TEMPLATE_BALANCE_PERCENT_BSA2202S = "/static/word/examine/examine-balance-percent-bsa2202s.docx";
    private static final String TEMPLATE_BALANCE_THOUSANDTH = "/static/word/examine/examine-balance-thousandth.docx";
    private static final String TEMPLATE_BALANCE_0_1MG_ME204E = "/static/word/examine/examine-balance-0.1mg-me204e.docx";
    private static final String TEMPLATE_BALANCE_0_1MG_AL204 = "/static/word/examine/examine-balance-0.1mg-al204.docx";
    private static final String TEMPLATE_SURFACE_TENSION = "/static/word/examine/examine-surface-tension.docx";
    private static final String TEMPLATE_ROTARY_VISCOSITY = "/static/word/examine/examine-rotary-viscosity.docx";
    private static final String TEMPLATE_VISCOSITY_PIN = "/static/word/examine/examine-viscosity-pin.docx";
    private static final String TEMPLATE_SPECTROPHOTOMETER = "/static/word/examine/examine-spectrophotometer.docx";
    /** 没有对应设备专用模板时套用的通用期间核查记录表 */
    private static final String TEMPLATE_GENERIC = "/static/word/examine/examine-generic.docx";
    private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
    private static final String RECORD_NUMBER_TEXT = "记录编号:LMSY-CX09-JL02";
    @Resource
    private DeviceExamineRecordDetailService deviceExamineRecordDetailService;
@@ -194,7 +214,9 @@
        LambdaUpdateWrapper<DeviceExamineRecord> wrapper = Wrappers.<DeviceExamineRecord>lambdaUpdate()
                .eq(DeviceExamineRecord::getPlanDetailsId, dto.getPlanDetailsId())
                .set(DeviceExamineRecord::getReviewStatus, reviewStatus)
                .set(DeviceExamineRecord::getReviewRemark, dto.getReviewRemark());
                .set(DeviceExamineRecord::getReviewRemark, dto.getReviewRemark())
                // 审核动作时间即核查完成时间,列表「核查时间」按它显示
                .set(DeviceExamineRecord::getUpdateTime, LocalDateTime.now());
        // 为0清除审核人
        if (reviewStatus != null && reviewStatus == 0) {
@@ -221,17 +243,8 @@
            this.save(record);
        }
        // 复核通过后,设备档案的最近/下次核查日期进入下一轮
        // 复核通过后,设备档案的最近/下次核查日期按审核当天滚动;计划行的检查日期是计划日期,审核不改它
        if (reviewStatus != null && reviewStatus == 1) {
            // 提前审核时把本轮计划归到实际审核月份,便于系统登记下一轮;手动录入到日的日期保持原样
            String currentCheckTime = planDetails.getCheckTime();
            boolean hasCheckDate = currentCheckTime != null && currentCheckTime.trim().length() >= 10;
            if (!hasCheckDate) {
                DeviceExaminePlanDetails updateDetail = new DeviceExaminePlanDetails();
                updateDetail.setPlanDetailsId(dto.getPlanDetailsId());
                updateDetail.setCheckTime(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
                deviceExaminePlanService.updateExaminePlanDetail(updateDetail);
            }
            deviceExaminePlanService.updateDeviceExamineDate(dto.getPlanDetailsId());
        }
        return true;
@@ -250,40 +263,320 @@
        if (deviceExamineRecordDto == null) {
            throw new ErrorException("该计划明细暂无核查记录,请先录入后再导出");
        }
        DeviceExaminePlanDetails planDetails = deviceExaminePlanDetailsMapper.selectById(planDetailsId);
        Device device = planDetails != null && planDetails.getDeviceId() != null
                ? deviceMapper.selectById(planDetails.getDeviceId()) : null;
        // 查询复核核查记录详情
        List<DeviceExamineRecordDetail> deviceExamineRecordDetailList = deviceExamineRecordDetailService.list(Wrappers.<DeviceExamineRecordDetail>lambdaQuery().eq(DeviceExamineRecordDetail::getRecordId, deviceExamineRecordDto.getRecordId()));
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/examine-record.docx");
        Configure configure = Configure.builder()
                .bind("deviceExamineRecordDetailList", new HackLoopTableRenderPolicy())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("deviceExamineRecordDto", deviceExamineRecordDto);
                    put("deviceExamineRecordDetailList", deviceExamineRecordDetailList);
                    //获取核查人的签名地址
                    put("checkerUserUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceExamineRecordDto.getCheckerUserId()));
                    //获取审查人的签名地址
                    put("reviewUserUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceExamineRecordDto.getReviewUserId()));
                }});
        String deviceName = StringUtils.isNotEmpty(deviceExamineRecordDto.getDeviceName()) ? deviceExamineRecordDto.getDeviceName() : "";
        String fileName;
        try {
            fileName = URLEncoder.encode(deviceName + "期间核查记录.docx", "UTF-8");
        } catch (Exception e) {
            fileName = "examine-record.docx";
        }
        // 按设备匹配专用模板,匹配不到时套用通用期间核查记录表
        String templatePath = resolveExamineRecordTemplate(planDetails, device);
        exportByDeviceTemplate(templatePath, planDetails, deviceExamineRecordDto, fileName, response);
    }
    /**
     * 按设备名称/型号匹配期间核查记录模板
     */
    private String resolveExamineRecordTemplate(DeviceExaminePlanDetails planDetails, Device device) {
        String deviceName = planDetails == null ? null : planDetails.getDeviceName();
        if (StringUtils.isBlank(deviceName) && device != null) {
            deviceName = device.getDeviceName();
        }
        String deviceModel = planDetails == null ? null : planDetails.getDeviceModel();
        if (StringUtils.isBlank(deviceModel) && device != null) {
            deviceModel = device.getSpecificationModel();
        }
        String name = deviceName == null ? "" : deviceName.trim();
        String model = deviceModel == null ? "" : deviceModel.trim().toUpperCase();
        if (name.contains("表面张力")) {
            return TEMPLATE_SURFACE_TENSION;
        }
        if (name.contains("旋转粘度") || name.contains("六速")) {
            return TEMPLATE_ROTARY_VISCOSITY;
        }
        if (name.contains("品氏")) {
            return TEMPLATE_VISCOSITY_PIN;
        }
        if (name.contains("分光光度")) {
            return TEMPLATE_SPECTROPHOTOMETER;
        }
        // 电子天平先按型号细分,再按档位名称
        if (model.contains("BSA2202S")) {
            return TEMPLATE_BALANCE_PERCENT_BSA2202S;
        }
        if (model.contains("ME204E")) {
            return TEMPLATE_BALANCE_0_1MG_ME204E;
        }
        if (model.contains("AL204")) {
            return TEMPLATE_BALANCE_0_1MG_AL204;
        }
        if (name.contains("万分之")) {
            return TEMPLATE_BALANCE_0_1MG_AL204;
        }
        if (name.contains("千分之")) {
            return TEMPLATE_BALANCE_THOUSANDTH;
        }
        if (name.contains("百分之")) {
            return TEMPLATE_BALANCE_PERCENT;
        }
        return TEMPLATE_GENERIC;
    }
    /**
     * 按设备专用模板导出:只填表头信息,检测结果表格留空
     */
    private void exportByDeviceTemplate(String templatePath, DeviceExaminePlanDetails planDetails,
                                        DeviceExamineRecordDto recordDto, String fileName, HttpServletResponse response) {
        try (InputStream inputStream = this.getClass().getResourceAsStream(templatePath)) {
            if (inputStream == null) {
                throw new ErrorException("核查记录模板不存在:" + templatePath);
            }
            XWPFDocument document = new XWPFDocument(inputStream);
            // 通用表按参考图带记录编号;设备专用模板保持模板原样
            if (TEMPLATE_GENERIC.equals(templatePath)) {
                insertRecordNumberLine(document);
            }
            if (!document.getTables().isEmpty()) {
                fillTemplateTable(document.getTables().get(0), planDetails, recordDto);
            }
            response.setContentType("application/msword");
            String deviceName = StringUtils.isNotEmpty(deviceExamineRecordDto.getDeviceName()) ? deviceExamineRecordDto.getDeviceName() : "";
            String fileName = URLEncoder.encode(
                    deviceName+ "核查记录", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName);
            OutputStream os = response.getOutputStream();
            template.write(os);
            document.write(os);
            os.flush();
            os.close();
            inputStream.close();
            document.close();
        } catch (ErrorException e) {
            throw e;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        }
    }
    /**
     * 在标题上方插入固定的记录编号行
     */
    private void insertRecordNumberLine(XWPFDocument document) {
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        if (paragraphs.isEmpty()) {
            return;
        }
        XmlCursor cursor = paragraphs.get(0).getCTP().newCursor();
        XWPFParagraph paragraph = document.insertNewParagraph(cursor);
        paragraph.setAlignment(ParagraphAlignment.LEFT);
        XWPFRun run = paragraph.createRun();
        run.setFontFamily("宋体");
        run.setFontSize(10.5);
        run.setText(RECORD_NUMBER_TEXT);
    }
    /**
     * 按标签定位并填写表头,仪器现状打勾,补填评价/审核人
     */
    private void fillTemplateTable(XWPFTable table, DeviceExaminePlanDetails planDetails, DeviceExamineRecordDto recordDto) {
        String checkDate = StringUtils.isNotEmpty(recordDto.getUpdateTimeStr()) ? recordDto.getUpdateTimeStr()
                : (planDetails == null ? "" : planDetails.getCheckTime());
        String checkerName = resolveCheckerName(planDetails, recordDto);
        String reviewerName = resolveUserName(recordDto.getReviewUserId(), recordDto.getReviewUser());
        String appearanceStatus = planDetails == null ? null : planDetails.getAppearanceStatus();
        String runningStatus = planDetails == null ? null : planDetails.getRunningStatus();
        for (XWPFTableRow row : table.getRows()) {
            List<XWPFTableCell> cells = row.getTableCells();
            for (int i = 0; i < cells.size(); i++) {
                String label = normalizeCellLabel(cells.get(i).getText());
                if ("外观情况".equals(label)) {
                    markOptionCells(cells, i + 1, appearanceStatus);
                    continue;
                }
                if ("运行是否正常".equals(label)) {
                    markOptionCells(cells, i + 1, runningStatus);
                    continue;
                }
                if (label.isEmpty() || i + 1 >= cells.size()) {
                    continue;
                }
                XWPFTableCell valueCell = cells.get(i + 1);
                switch (label) {
                    case "仪器名称":
                        setCellText(valueCell, planDetails == null ? recordDto.getDeviceName() : planDetails.getDeviceName(), true);
                        break;
                    case "管理编号":
                    case "设备编号":
                        setCellText(valueCell, planDetails == null ? recordDto.getDeviceNumber() : planDetails.getDeviceNumber(), true);
                        break;
                    case "设备型号":
                        setCellText(valueCell, planDetails == null ? "" : planDetails.getDeviceModel(), true);
                        break;
                    case "检查日期":
                        setCellText(valueCell, checkDate == null ? "" : checkDate, true);
                        break;
                    case "核查人":
                        setCellText(valueCell, checkerName == null ? "" : checkerName, true);
                        break;
                    case "环境温湿度":
                        setCellText(valueCell, planDetails == null ? "" : planDetails.getEnvironment(), true);
                        break;
                    case "核查依据":
                        setCellText(valueCell, planDetails == null ? null : planDetails.getCheckBasis(), false);
                        break;
                    case "核查方式":
                        setCellText(valueCell, planDetails == null ? null : planDetails.getCheckMethod(), false);
                        break;
                    default:
                        break;
                }
            }
        }
        fillEvaluationCell(table, reviewerName, checkDate, appearanceStatus, runningStatus);
    }
    /**
     * 仪器现状三选一:选中项显示☑,其余保持□
     */
    private void markOptionCells(List<XWPFTableCell> cells, int startIndex, String selected) {
        for (int i = startIndex; i < cells.size() && i <= startIndex + 2; i++) {
            String option = normalizeCellLabel(cells.get(i).getText());
            if (!"良好".equals(option) && !"一般".equals(option) && !"损坏".equals(option)
                    && !"正常".equals(option) && !"异常".equals(option) && !"停机".equals(option)) {
                continue;
            }
            setCellText(cells.get(i), option + (option.equals(selected) ? "☑" : "□"), true);
        }
    }
    /**
     * 核查结果评价勾选 + 审核人/时间填写
     */
    private void fillEvaluationCell(XWPFTable table, String reviewerName, String checkDate,
                                    String appearanceStatus, String runningStatus) {
        boolean hasStatus = StringUtils.isNotEmpty(appearanceStatus) || StringUtils.isNotEmpty(runningStatus);
        boolean satisfied = hasStatus && !"损坏".equals(appearanceStatus)
                && !"异常".equals(runningStatus) && !"停机".equals(runningStatus);
        for (XWPFTableRow row : table.getRows()) {
            for (XWPFTableCell cell : row.getTableCells()) {
                for (XWPFParagraph paragraph : cell.getParagraphs()) {
                    String text = paragraph.getText();
                    if (text == null) {
                        continue;
                    }
                    if (text.contains("核查结果评价") && text.contains("满足") && hasStatus) {
                        // 模板里两个复选框依次是“满足”“不满足”
                        markParagraphSymbol(paragraph, satisfied ? 0 : 1);
                    }
                    if (text.contains("审核人")) {
                        rebuildReviewerParagraph(paragraph, reviewerName, checkDate);
                    }
                }
            }
        }
    }
    /**
     * 修改段落里第 targetIndex 个 Wingdings 2 复选框为勾选状态(0x52=☑,0xA3=□)
     */
    private void markParagraphSymbol(XWPFParagraph paragraph, int targetIndex) {
        int index = 0;
        for (XWPFRun run : paragraph.getRuns()) {
            Node runNode = run.getCTR().getDomNode();
            for (Node child = runNode.getFirstChild(); child != null; child = child.getNextSibling()) {
                if (child.getNodeType() == Node.ELEMENT_NODE && "sym".equals(child.getLocalName())) {
                    ((Element) child).setAttributeNS(WORD_NS, "w:char", index == targetIndex ? "0052" : "00A3");
                    index++;
                }
            }
        }
    }
    /**
     * 重写“审核人/时间”行,填入审核人和日期
     */
    private void rebuildReviewerParagraph(XWPFParagraph paragraph, String reviewerName, String checkDate) {
        String text = paragraph.getText();
        int labelIndex = text == null ? -1 : text.indexOf("审核人");
        if (labelIndex < 0) {
            return;
        }
        String prefix = text.substring(0, labelIndex);
        String dateLabel = text.contains("日期") ? "日期" : "时间";
        String newText = prefix + "审核人:" + (reviewerName == null ? "" : reviewerName)
                + "                   " + dateLabel + ":" + (checkDate == null ? "" : checkDate);
        List<XWPFRun> runs = paragraph.getRuns();
        if (runs.isEmpty()) {
            XWPFRun run = paragraph.createRun();
            run.setFontFamily("宋体");
            run.setFontSize(12);
            run.setText(newText);
            return;
        }
        runs.get(0).setText(newText, 0);
        for (int i = runs.size() - 1; i >= 1; i--) {
            paragraph.removeRun(i);
        }
    }
    /**
     * 写入单元格文本;overwriteBlank=false 时保留模板里的固定文字
     */
    private void setCellText(XWPFTableCell cell, String text, boolean overwriteBlank) {
        String value = text == null ? "" : text;
        if (!overwriteBlank && value.trim().isEmpty()) {
            return;
        }
        List<XWPFParagraph> paragraphs = cell.getParagraphs();
        XWPFParagraph paragraph = paragraphs.isEmpty() ? cell.addParagraph() : paragraphs.get(0);
        for (int i = paragraphs.size() - 1; i >= 1; i--) {
            cell.removeParagraph(i);
        }
        List<XWPFRun> runs = paragraph.getRuns();
        if (runs.isEmpty()) {
            XWPFRun run = paragraph.createRun();
            run.setFontFamily("宋体");
            run.setFontSize(12);
            run.setText(value);
            return;
        }
        runs.get(0).setText(value, 0);
        for (int i = runs.size() - 1; i >= 1; i--) {
            paragraph.removeRun(i);
        }
    }
    private String normalizeCellLabel(String text) {
        if (text == null) {
            return "";
        }
        return text.replace(" ", "").replace("\u00A0", "").replace("□", "").replace("☑", "")
                .replace("\n", "").replace("\r", "").trim();
    }
    private String resolveCheckerName(DeviceExaminePlanDetails planDetails, DeviceExamineRecordDto recordDto) {
        if (planDetails != null && StringUtils.isNotBlank(planDetails.getCheckChargerUser())) {
            return planDetails.getCheckChargerUser();
        }
        if (StringUtils.isNotBlank(recordDto.getCheckerUser())) {
            return recordDto.getCheckerUser();
        }
        Integer userId = planDetails != null && planDetails.getCheckChargerUserId() != null
                ? planDetails.getCheckChargerUserId() : recordDto.getCheckerUserId();
        return resolveUserName(userId, null);
    }
    private String resolveUserName(Integer userId, String userName) {
        if (StringUtils.isNotBlank(userName)) {
            return userName;
        }
        if (userId == null) {
            return null;
        }
        User user = userMapper.selectById(userId);
        return user == null ? null : user.getName();
    }
}
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceImpl.java
@@ -1,42 +1,38 @@
package com.ruoyi.device.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
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;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.Pictures;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.device.dto.DeviceImpowerDetailsDto;
import com.ruoyi.device.dto.DeviceImpowerDto;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.dto.DeviceMaintenanceDto;
import com.ruoyi.device.mapper.DeviceMaintenanceMapper;
import com.ruoyi.device.mapper.DeviceMapper;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceImpower;
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import com.ruoyi.device.service.DeviceMaintenancePlanService;
import com.ruoyi.device.service.DeviceMaintenanceService;
import com.ruoyi.device.service.DeviceService;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.UserUtils;
import com.ruoyi.system.service.ISysDictDataService;
import com.ruoyi.system.service.ISysDictTypeService;
import lombok.AllArgsConstructor;
import com.ruoyi.system.mapper.UserMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -45,14 +41,14 @@
 * 设备维护保养
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class DeviceMaintenanceImpl extends ServiceImpl<DeviceMaintenanceMapper, DeviceMaintenance> implements DeviceMaintenanceService {
    @Resource
    private DeviceService deviceService;
    @Resource
    private ISysDictTypeService iSysDictTypeService;
    @Value("${file.path}")
    private String imgUrl;
    private DeviceMaintenancePlanService deviceMaintenancePlanService;
    @Resource
    private UserMapper userMapper;
    /**
     * 设备维护分页查询
@@ -67,52 +63,160 @@
    }
    /**
     * 导出设备维护保养
     * 保存保养记录(关联保养计划明细,保存后进入待审核,不滚动设备档案日期)
     */
    @Override
    public boolean saveDeviceMaintenance(DeviceMaintenance deviceMaintenance) {
        if (deviceMaintenance == null) {
            throw new ErrorException("缺少保养记录");
        }
        if (deviceMaintenance.getDeviceId() == null) {
            throw new ErrorException("缺少设备id");
        }
        if (deviceMaintenance.getMaintenanceDate() == null) {
            throw new ErrorException("请选择保养日期");
        }
        if (deviceMaintenance.getMaintenanceUserId() == null) {
            throw new ErrorException("请选择维护人");
        }
        DeviceMaintenance exist = deviceMaintenance.getId() == null ? null : baseMapper.selectById(deviceMaintenance.getId());
        if (exist != null) {
            if (exist.getMaintenancePlanDetailId() == null) {
                throw new ErrorException("历史保养记录不允许修改");
            }
            if (exist.getReviewStatus() != null) {
                throw new ErrorException("已审核的保养记录不允许修改");
            }
            // 记录归属由库里的关联决定,避免改到别的设备或计划
            deviceMaintenance.setDeviceId(exist.getDeviceId());
            deviceMaintenance.setMaintenancePlanDetailId(exist.getMaintenancePlanDetailId());
            deviceMaintenance.setReviewStatus(null);
        } else {
            DeviceMaintenancePlanDetails detail = deviceMaintenancePlanService.resolveMaintenancePlanDetail(
                    deviceMaintenance.getDeviceId(), deviceMaintenance.getMaintenancePlanDetailId(), deviceMaintenance.getMaintenanceDate());
            if (!deviceMaintenance.getDeviceId().equals(detail.getDeviceId())) {
                throw new ErrorException("保养计划明细与设备不匹配");
            }
            DeviceMaintenance other = baseMapper.selectOne(Wrappers.<DeviceMaintenance>lambdaQuery()
                    .eq(DeviceMaintenance::getMaintenancePlanDetailId, detail.getMaintenancePlanDetailId())
                    .last("limit 1"));
            if (other != null) {
                throw new ErrorException("该计划已存在保养记录");
            }
            deviceMaintenance.setMaintenancePlanDetailId(detail.getMaintenancePlanDetailId());
            deviceMaintenance.setReviewStatus(null);
            deviceMaintenance.setReviewUserId(null);
            deviceMaintenance.setReviewUserName(null);
            deviceMaintenance.setReviewRemark(null);
            deviceMaintenance.setReviewTime(null);
        }
        User user = userMapper.selectById(deviceMaintenance.getMaintenanceUserId());
        deviceMaintenance.setMaintenanceUserName(user == null ? null : user.getName());
        return saveOrUpdate(deviceMaintenance);
    }
    /**
     * 审核保养记录;审核通过后按审核通过当天滚动设备档案的最近/下次保养日期(与核查计划一致)
     */
    @Override
    public boolean reviewDeviceMaintenance(DeviceMaintenanceDto deviceMaintenanceDto) {
        DeviceMaintenance record = findMaintenanceRecord(deviceMaintenanceDto);
        if (record == null) {
            throw new ErrorException("该计划暂无保养记录,请先填写保养记录");
        }
        if (record.getMaintenancePlanDetailId() == null) {
            throw new ErrorException("历史保养记录无需审核");
        }
        if (deviceMaintenanceDto.getReviewStatus() == null) {
            throw new ErrorException("请选择审核结果");
        }
        if (record.getReviewStatus() != null && record.getReviewStatus() == 1) {
            throw new ErrorException("该保养记录已审核通过");
        }
        if (record.getMaintenanceUserId() == null && StringUtils.isBlank(record.getMaintenanceUserName())) {
            throw new ErrorException("请先在编辑中填写维护人");
        }
        Integer reviewerId = SecurityUtils.getUserId().intValue();
        User reviewer = userMapper.selectById(reviewerId);
        boolean updated = this.update(Wrappers.<DeviceMaintenance>lambdaUpdate()
                .eq(DeviceMaintenance::getId, record.getId())
                .set(DeviceMaintenance::getReviewStatus, deviceMaintenanceDto.getReviewStatus())
                .set(DeviceMaintenance::getReviewUserId, reviewerId)
                .set(DeviceMaintenance::getReviewUserName, reviewer == null ? null : reviewer.getName())
                .set(DeviceMaintenance::getReviewRemark, deviceMaintenanceDto.getReviewRemark())
                .set(DeviceMaintenance::getReviewTime, LocalDateTime.now()));
        // 审核通过后,按审核通过当天回写档案的最近保养日期并推算下一轮(与核查计划一致)
        if (updated && deviceMaintenanceDto.getReviewStatus() == 1) {
            LocalDate reviewDate = LocalDate.now();
            // 审核完成年份即归类年份:提前完成的明细从原计划年份挪到审核年份
            deviceMaintenancePlanService.reclassifyMaintenanceDetailByReviewDate(record.getMaintenancePlanDetailId(), reviewDate);
            deviceMaintenancePlanService.updateDeviceMaintenanceDate(record.getDeviceId(), reviewDate);
        }
        return updated;
    }
    /**
     * 删除保养记录;审核通过和历史记录不允许删除
     */
    @Override
    public boolean deleteDeviceMaintenance(Integer id) {
        DeviceMaintenance record = id == null ? null : baseMapper.selectById(id);
        if (record == null) {
            return false;
        }
        if (record.getMaintenancePlanDetailId() == null) {
            throw new ErrorException("历史保养记录为留档内容,不允许删除");
        }
        if (record.getReviewStatus() != null && record.getReviewStatus() == 1) {
            throw new ErrorException("审核通过的保养记录为历史记录,不允许删除");
        }
        return removeById(id);
    }
    /**
     * 按记录id或计划明细id定位保养记录
     */
    private DeviceMaintenance findMaintenanceRecord(DeviceMaintenanceDto dto) {
        if (dto == null) {
            return null;
        }
        if (dto.getId() != null) {
            return baseMapper.selectById(dto.getId());
        }
        if (dto.getMaintenancePlanDetailId() != null) {
            return baseMapper.selectOne(Wrappers.<DeviceMaintenance>lambdaQuery()
                    .eq(DeviceMaintenance::getMaintenancePlanDetailId, dto.getMaintenancePlanDetailId())
                    .last("limit 1"));
        }
        return null;
    }
    /**
     * 导出仪器设备维护保养记录(只读台账,数据来自保养计划生成的保养记录)
     * @param deviceId
     * @param response
     */
    @Override
    public void exportDeviceMaintenance(Integer deviceId, HttpServletResponse response) {
        // 查询设备维护保养
        List<DeviceMaintenance> deviceMaintenanceList = baseMapper.selectList(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getDeviceId, deviceId));
        // 获取到第一个设备使用授权
        DeviceMaintenanceDto deviceMaintenanceDto = new DeviceMaintenanceDto();
        List<SysDictData> sysDictDataList = null;
        if (CollectionUtils.isNotEmpty(deviceMaintenanceList)) {
            BeanUtils.copyProperties(deviceMaintenanceList.get(0), deviceMaintenanceDto);
            // 查询设备信息
            Device device = deviceService.getById(deviceId);
            deviceMaintenanceDto.setDeviceName(device.getDeviceName());
            deviceMaintenanceDto.setManagementNumber(device.getManagementNumber());
            //查询维护保养导出字典值
            sysDictDataList = getDeviceMaintenanceDict(device);
        Device device = deviceId == null ? null : deviceService.getById(deviceId);
        if (device == null) {
            throw new ErrorException("设备不存在");
        }
        // 添加字典枚举值
        for (DeviceMaintenance deviceMaintenance : deviceMaintenanceList) {
            StringBuilder maintenanceContent = new StringBuilder(deviceMaintenance.getMaintenanceContent());
            if (CollectionUtils.isNotEmpty(sysDictDataList) && StringUtils.isNotBlank(deviceMaintenance.getMaintenanceContent())) {
                maintenanceContent = new StringBuilder();
                List<String> splitList = StrUtil.split(deviceMaintenance.getMaintenanceContent(), ',');
                // 循环判断是否有一样key
                for (SysDictData sysDictData : sysDictDataList) {
                    if (splitList.contains(sysDictData.getDictLabel())) {
                        maintenanceContent.append("☑").append(sysDictData.getDictLabel()).append(" ");
                    } else {
                        maintenanceContent.append("□").append(sysDictData.getDictLabel()).append(" ");
                    }
                }
            }
            deviceMaintenance.setMaintenanceContent(maintenanceContent.toString());
            // 添加维护人图片
            deviceMaintenance.setMaintenanceUserUrlRender(UserUtils.getFinalUserSignatureOrTransparent(deviceMaintenance.getMaintenanceUserId()));
        // 台账按维护日期正序排列
        List<DeviceMaintenance> records = baseMapper.selectList(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getDeviceId, deviceId)
                .orderByAsc(DeviceMaintenance::getMaintenanceDate)
                .orderByAsc(DeviceMaintenance::getId));
        List<DeviceMaintenanceDto> deviceMaintenances = new ArrayList<>();
        int index = 1;
        for (DeviceMaintenance record : records) {
            DeviceMaintenanceDto dto = new DeviceMaintenanceDto();
            BeanUtils.copyProperties(record, dto);
            dto.setIndex(index++);
            // 维护人列展示签名图片,未配置签名时用透明图片兜底
            dto.setMaintenanceUserUrlRender(UserUtils.getFinalUserSignatureOrTransparent(record.getMaintenanceUserId()));
            deviceMaintenances.add(dto);
        }
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/device-maintenance.docx");
@@ -121,14 +225,17 @@
                .build();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("device", deviceMaintenanceDto);
                    put("deviceMaintenances", deviceMaintenanceList);
                    put("deviceName", device.getDeviceName());
                    put("specificationModel", device.getSpecificationModel());
                    put("managementNumber", device.getManagementNumber());
                    put("cycleNote", buildCycleNote(device.getMaintenanceCycle()));
                    put("deviceMaintenances", deviceMaintenances);
                }});
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "设备保养维护", "UTF-8");
                    "仪器设备维护保养记录", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
@@ -144,54 +251,36 @@
    }
    /**
     * ****查询维护保养导出字典值***
     * @param device
     * 表格底部的维护保养频率说明,按档案里的保养周期(月)生成
     */
    private List<SysDictData> getDeviceMaintenanceDict(Device device) {
        List<SysDictData> sysDictDataList = new ArrayList<>();
        // 查询维护保养导出字典值
        if (StringUtils.isNotBlank(device.getStoragePoint())) {
            String dictType = null;
            switch (device.getStoragePoint()) {
                case "老化室":
                    dictType = "aging_maintenance_content";
                    break;
                case "老化实验室":
                    dictType = "aging_maintenance_content";
                    break;
                case "电性能实验室":
                    dictType = "electrical_maintenance_content";
                    break;
                case "环境实验室":
                    dictType = "environmental_maintenance_content";
                    break;
                case "燃烧实验室":
                    dictType = "burn_maintenance_content";
                    break;
                case "混炼实验室":
                    dictType = "mixing_maintenance_content";
                    break;
                case "检测中心电性能实验室":
                    dictType = "center_electrical_maintenance_content";
                    break;
                case "恒温二":
                    dictType = "temperature2_maintenance_content";
                    break;
                case "恒温一":
                    dictType = "temperature1_maintenance_content";
                    break;
                case "化学实验室":
                    dictType = "chemistry_maintenance_content";
                    break;
                case "制样室":
                    dictType = "sample_maintenance_content";
                    break;
                case "低温实验室":
                    dictType = "hypothermia_maintenance_content";
                    break;
            }
            sysDictDataList = iSysDictTypeService.selectDictDataByType(dictType);
    private String buildCycleNote(String maintenanceCycle) {
        if (StringUtils.isBlank(maintenanceCycle)) {
            return "";
        }
        return sysDictDataList;
        int months;
        try {
            months = Integer.parseInt(maintenanceCycle.trim());
        } catch (NumberFormatException e) {
            return "";
        }
        if (months <= 0) {
            return "";
        }
        if (months == 1) {
            return "维护保养每月 1 次";
        }
        if (months == 3) {
            return "维护保养每季度 1 次";
        }
        if (months == 6) {
            return "维护保养每半年 1 次";
        }
        if (months == 12) {
            return "维护保养每年 1 次";
        }
        if (months % 12 == 0) {
            return "维护保养每 " + months / 12 + " 年 1 次";
        }
        return "维护保养每 " + months + " 个月 1 次";
    }
}
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceMaintenancePlanServiceImpl.java
@@ -17,14 +17,17 @@
import com.ruoyi.common.utils.WxCpUtils;
import com.ruoyi.device.dto.DeviceMaintenancePlanDetailsDto;
import com.ruoyi.device.dto.DeviceMaintenancePlanDto;
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.DeviceMaintenancePlan;
import com.ruoyi.device.pojo.DeviceMaintenancePlanDetails;
import com.ruoyi.device.service.DeviceMaintenancePlanDetailsService;
import com.ruoyi.device.service.DeviceMaintenancePlanService;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.UserUtils;
import com.ruoyi.system.mapper.UserMapper;
@@ -42,9 +45,12 @@
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -71,6 +77,8 @@
    private UserMapper userMapper;
    @Resource
    private DeviceMapper deviceMapper;
    @Resource
    private DeviceMaintenanceMapper deviceMaintenanceMapper;
    /**
     * 分页查询设备保养计划
@@ -328,20 +336,38 @@
    }
    /**
     * 设备档案变更后,把已到点的设备同步到对应年份的设备保养计划明细
     * 保养计划年份列表:删除没有任何明细的空年份计划后,返回有明细的年份(倒序)
     */
    @Override
    public List<String> getMaintenancePlanYears() {
        // 历史数据自愈:先让已审核的明细按审核完成年份归位,再清理空年份计划
        reclassifyReviewedDetails();
        List<DeviceMaintenancePlan> plans = baseMapper.selectList(null);
        for (DeviceMaintenancePlan plan : plans) {
            Long detailCount = deviceMaintenancePlanDetailsMapper.selectCount(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                    .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, plan.getMaintenancePlanId()));
            if (detailCount == null || detailCount == 0) {
                removeById(plan.getMaintenancePlanId());
            }
        }
        return baseMapper.selectPlanYears();
    }
    /**
     * 设备档案变更后,立即把设备登记到下次保养日期所在年份的设备保养计划明细
     */
    @Override
    public void syncDeviceMaintenanceDetail(Device device) {
        LocalDate remind = calcMaintenanceRemindDate(device);
        if (remind == null || remind.isAfter(LocalDate.now())) {
        LocalDate due = calcMaintenanceDueDate(device);
        if (due == null) {
            return;
        }
        DeviceMaintenancePlan plan = getOrCreatePlanEntity(String.valueOf(remind.getYear()));
        DeviceMaintenancePlan plan = getOrCreatePlanEntity(String.valueOf(due.getYear()));
        fillMaintenanceDetailIfAbsent(plan, device);
    }
    /**
     * 保养记录保存后,滚动设备档案的最近/下次保养日期
     * 审核通过后,按审核通过当天滚动设备档案的最近/下次保养日期
     */
    @Override
    public void updateDeviceMaintenanceDate(Integer deviceId, LocalDate maintenanceDate) {
@@ -365,6 +391,75 @@
            }
        }
        deviceMapper.updateById(device);
        // 本轮保养完成后立即登记下一轮,历史明细保持不动
        syncDeviceMaintenanceDetail(device);
    }
    /**
     * 审核通过后,把计划明细归到审核完成年份的计划下;提前完成的明细不再留在原计划年份
     */
    @Override
    public void reclassifyMaintenanceDetailByReviewDate(Integer maintenancePlanDetailId, LocalDate reviewDate) {
        if (maintenancePlanDetailId == null || reviewDate == null) {
            return;
        }
        DeviceMaintenancePlanDetails detail = deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId);
        if (detail == null || detail.getMaintenancePlanId() == null) {
            return;
        }
        DeviceMaintenancePlan sourcePlan = baseMapper.selectById(detail.getMaintenancePlanId());
        if (sourcePlan == null) {
            return;
        }
        String targetYear = String.valueOf(reviewDate.getYear());
        if (StringUtils.isNotBlank(sourcePlan.getPlanYear()) && targetYear.equals(sourcePlan.getPlanYear().trim())) {
            return;
        }
        moveMaintenanceDetailToYear(detail, reviewDate);
    }
    /**
     * 把计划明细挪到审核完成年份的计划下,计划日期按实际完成日期记录
     */
    private void moveMaintenanceDetailToYear(DeviceMaintenancePlanDetails detail, LocalDate reviewDate) {
        DeviceMaintenancePlan targetPlan = getOrCreatePlanEntity(String.valueOf(reviewDate.getYear()));
        DeviceMaintenancePlanDetails update = new DeviceMaintenancePlanDetails();
        update.setMaintenancePlanDetailId(detail.getMaintenancePlanDetailId());
        update.setMaintenancePlanId(targetPlan.getMaintenancePlanId());
        // 计划日期按实际完成日期记录,列表按完成年份排序,不再按原计划日期标红
        update.setMaintenanceDate(reviewDate);
        deviceMaintenancePlanDetailsMapper.updateById(update);
    }
    /**
     * 历史数据自愈:已审核通过但计划年份与审核完成年份不一致的明细,按审核完成年份归位
     */
    private void reclassifyReviewedDetails() {
        List<DeviceMaintenance> records = deviceMaintenanceMapper.selectList(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getReviewStatus, 1)
                .isNotNull(DeviceMaintenance::getMaintenancePlanDetailId)
                .isNotNull(DeviceMaintenance::getReviewTime));
        if (CollectionUtils.isEmpty(records)) {
            return;
        }
        List<Integer> detailIds = records.stream().map(DeviceMaintenance::getMaintenancePlanDetailId)
                .distinct().collect(Collectors.toList());
        Map<Integer, DeviceMaintenancePlanDetails> detailMap = deviceMaintenancePlanDetailsMapper.selectBatchIds(detailIds)
                .stream().collect(Collectors.toMap(DeviceMaintenancePlanDetails::getMaintenancePlanDetailId, item -> item, (a, b) -> a));
        Map<Integer, String> planYears = baseMapper.selectList(null).stream()
                .collect(Collectors.toMap(DeviceMaintenancePlan::getMaintenancePlanId,
                        item -> item.getPlanYear() == null ? "" : item.getPlanYear().trim(), (a, b) -> a));
        for (DeviceMaintenance record : records) {
            DeviceMaintenancePlanDetails detail = detailMap.get(record.getMaintenancePlanDetailId());
            if (detail == null || detail.getMaintenancePlanId() == null) {
                continue;
            }
            String reviewYear = String.valueOf(record.getReviewTime().getYear());
            if (reviewYear.equals(planYears.get(detail.getMaintenancePlanId()))) {
                continue;
            }
            moveMaintenanceDetailToYear(detail, record.getReviewTime().toLocalDate());
        }
    }
    /**
@@ -401,7 +496,7 @@
    }
    /**
     * 扫描所有设备,把已到点(下次保养日期前1个月起,含已过期)的设备补进本年度计划
     * 扫描所有设备,把应保养日期落在该计划年份的设备补进计划(当前年及以往年度包含逾期未保养的设备)
     */
    private void ensureMaintenanceDetails(DeviceMaintenancePlan plan) {
        List<Device> devices = deviceMapper.selectList(null);
@@ -411,9 +506,9 @@
    }
    /**
     * 设备提醒起点:下次保养日期前1个月(没填下次日期时用最近日期+保养周期兜底)
     * 设备本轮应保养日期(没填下次日期时用最近日期+保养周期兜底)
     */
    private LocalDate calcMaintenanceRemindDate(Device device) {
    private LocalDate calcMaintenanceDueDate(Device device) {
        LocalDateTime base = device.getNextMaintenanceDate();
        if (base == null && device.getLastMaintenanceDate() != null && StringUtils.isNotBlank(device.getMaintenanceCycle())) {
            try {
@@ -421,24 +516,42 @@
            } catch (NumberFormatException ignored) {
            }
        }
        return base == null ? null : base.toLocalDate().minusMonths(1);
        return base == null ? null : base.toLocalDate();
    }
    /**
     * 已到点且计划中尚无该设备本轮明细时自动补一条
     * 计划中尚无该设备本轮明细时自动补一条;保养时间取应保养日期
     */
    private void fillMaintenanceDetailIfAbsent(DeviceMaintenancePlan plan, Device device) {
        LocalDate remind = calcMaintenanceRemindDate(device);
        if (remind == null || remind.isAfter(LocalDate.now())) {
        LocalDate due = calcMaintenanceDueDate(device);
        if (due == null) {
            return;
        }
        if (plan.getPlanYear() != null && String.valueOf(remind.getYear()).compareTo(plan.getPlanYear()) > 0) {
        if (StringUtils.isBlank(plan.getPlanYear())) {
            return;
        }
        int planYearValue;
        try {
            planYearValue = Integer.parseInt(plan.getPlanYear().trim());
        } catch (NumberFormatException e) {
            return;
        }
        int dueYear = due.getYear();
        // 未来年份的计划只登记应保养日期就在该年份的设备,避免把往年的旧日期补进未来的计划
        if (planYearValue > LocalDate.now().getYear()) {
            if (dueYear != planYearValue) {
                return;
            }
        } else if (dueYear > planYearValue) {
            return;
        }
        // 同一设备同一应保养月份只保留一条,避免手改过行内日期后刷新重复生成
        LocalDate monthStart = due.withDayOfMonth(1);
        Long exist = deviceMaintenancePlanDetailsMapper.selectCount(Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                .eq(DeviceMaintenancePlanDetails::getMaintenancePlanId, plan.getMaintenancePlanId())
                .eq(DeviceMaintenancePlanDetails::getDeviceId, device.getId())
                .eq(DeviceMaintenancePlanDetails::getMaintenanceDate, remind));
                .ge(DeviceMaintenancePlanDetails::getMaintenanceDate, monthStart)
                .lt(DeviceMaintenancePlanDetails::getMaintenanceDate, monthStart.plusMonths(1)));
        if (exist != null && exist > 0) {
            return;
        }
@@ -446,8 +559,117 @@
        detail.setMaintenancePlanId(plan.getMaintenancePlanId());
        detail.setDeviceId(device.getId());
        detail.setMaintenanceIntervals(device.getMaintenanceCycle());
        detail.setMaintenanceDate(remind);
        detail.setMaintenanceDate(due);
        deviceMaintenancePlanDetailsMapper.insert(detail);
    }
    /**
     * 为保养记录找一条可关联的计划明细;没有可用明细时在当前年度计划新增本轮明细
     */
    @Override
    public DeviceMaintenancePlanDetails resolveMaintenancePlanDetail(Integer deviceId, Integer maintenancePlanDetailId, LocalDate maintenanceDate) {
        if (maintenancePlanDetailId != null) {
            DeviceMaintenancePlanDetails detail = deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId);
            if (detail == null) {
                throw new ErrorException("保养计划明细不存在");
            }
            return detail;
        }
        if (deviceId == null) {
            throw new ErrorException("缺少设备id");
        }
        Device device = deviceMapper.selectById(deviceId);
        if (device == null) {
            throw new ErrorException("保养设备不存在");
        }
        LocalDate planDate = maintenanceDate == null ? LocalDate.now() : maintenanceDate;
        DeviceMaintenancePlan plan = getOrCreatePlanEntity(String.valueOf(planDate.getYear()));
        List<DeviceMaintenancePlanDetails> details = deviceMaintenancePlanDetailsMapper.selectList(
                Wrappers.<DeviceMaintenancePlanDetails>lambdaQuery()
                        .eq(DeviceMaintenancePlanDetails::getDeviceId, deviceId)
                        .orderByAsc(DeviceMaintenancePlanDetails::getMaintenanceDate));
        DeviceMaintenancePlanDetails nearest = null;
        long nearestDistance = Long.MAX_VALUE;
        for (DeviceMaintenancePlanDetails detail : details) {
            // 已有保养记录的明细代表已完成的一轮,后续记录永远不复用、不覆盖
            Long recordCount = deviceMaintenanceMapper.selectCount(Wrappers.<DeviceMaintenance>lambdaQuery()
                    .eq(DeviceMaintenance::getMaintenancePlanDetailId, detail.getMaintenancePlanDetailId()));
            if (recordCount != null && recordCount > 0) {
                continue;
            }
            if (detail.getMaintenancePlanId() != null && detail.getMaintenancePlanId().equals(plan.getMaintenancePlanId())) {
                return detail;
            }
            long distance = monthDistance(detail.getMaintenanceDate(), planDate);
            if (distance < nearestDistance) {
                nearest = detail;
                nearestDistance = distance;
            }
        }
        if (nearest != null) {
            return nearest;
        }
        DeviceMaintenancePlanDetails detail = new DeviceMaintenancePlanDetails();
        detail.setMaintenancePlanId(plan.getMaintenancePlanId());
        detail.setDeviceId(device.getId());
        detail.setMaintenanceIntervals(device.getMaintenanceCycle());
        detail.setMaintenanceDate(planDate);
        deviceMaintenancePlanDetailsMapper.insert(detail);
        return detail;
    }
    /**
     * 修改设备保养计划详情(已审核的明细不允许修改)
     */
    @Override
    public boolean updateMaintenancePlanDetail(DeviceMaintenancePlanDetails detail) {
        if (detail == null || detail.getMaintenancePlanDetailId() == null) {
            throw new ErrorException("缺少设备保养计划明细id");
        }
        DeviceMaintenance record = deviceMaintenanceMapper.selectOne(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getMaintenancePlanDetailId, detail.getMaintenancePlanDetailId())
                .last("limit 1"));
        if (record != null && record.getReviewStatus() != null) {
            throw new ErrorException("该计划已审核,不允许修改");
        }
        return deviceMaintenancePlanDetailsMapper.updateById(detail) > 0;
    }
    /**
     * 删除设备保养计划详情;已出审核结果的记录随历史留档,未审核的记录随明细一起删除
     */
    @Override
    public boolean removeMaintenancePlanDetail(Integer maintenancePlanDetailId) {
        if (maintenancePlanDetailId == null) {
            throw new ErrorException("缺少设备保养计划明细id");
        }
        DeviceMaintenancePlanDetails detail = deviceMaintenancePlanDetailsMapper.selectById(maintenancePlanDetailId);
        if (detail == null) {
            return false;
        }
        List<DeviceMaintenance> records = deviceMaintenanceMapper.selectList(Wrappers.<DeviceMaintenance>lambdaQuery()
                .eq(DeviceMaintenance::getMaintenancePlanDetailId, maintenancePlanDetailId));
        // 审核不通过的记录允许连同明细一起删除;审核通过的记录为历史留档,不允许删除
        boolean hasPassedRecord = records.stream().anyMatch(item -> item.getReviewStatus() != null
                && item.getReviewStatus() == 1);
        if (hasPassedRecord) {
            throw new ErrorException("该计划已存在审核通过的保养记录,为保留历史记录不允许删除");
        }
        if (!records.isEmpty()) {
            List<Integer> recordIds = records.stream().map(DeviceMaintenance::getId).collect(Collectors.toList());
            deviceMaintenanceMapper.deleteBatchIds(recordIds);
        }
        return deviceMaintenancePlanDetailsMapper.deleteById(maintenancePlanDetailId) > 0;
    }
    /**
     * 两个保养日期之间相差的月数
     */
    private long monthDistance(LocalDate from, LocalDate target) {
        if (from == null || target == null) {
            return Long.MAX_VALUE;
        }
        return Math.abs(ChronoUnit.MONTHS.between(YearMonth.from(from), YearMonth.from(target)));
    }
}
cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml
@@ -3,8 +3,8 @@
<mapper namespace="com.ruoyi.device.mapper.DeviceExaminePlanDetailsMapper">
    <!--设备核查计划详情列表-->
    <select id="pageDeviceExaminePlanDetail" resultType="com.ruoyi.device.dto.DeviceExaminePlanDetailsDto">
    <!--设备核查计划详情公共查询:列表与导出共用同一套字段和排序-->
    <sql id="deviceExaminePlanDetailQuery">
        select * from (
        select
            d.plan_details_id,
@@ -36,6 +36,7 @@
        WHEN dc.review_status = 1 THEN 2
        WHEN dc.record_id IS NOT NULL THEN 1
        ELSE 0 END record_status,
        CASE WHEN dc.review_status = 1 THEN date_format(dc.update_time, '%Y-%m-%d') END complete_date,
        CASE
        WHEN dd.review_status = 0 THEN 3
        WHEN dd.review_status = 1 THEN 2
@@ -56,6 +57,16 @@
            ${ew.customSqlSegment}
        </if>
        order by device_number asc, check_time desc
    </sql>
    <!--设备核查计划详情列表-->
    <select id="pageDeviceExaminePlanDetail" resultType="com.ruoyi.device.dto.DeviceExaminePlanDetailsDto">
        <include refid="deviceExaminePlanDetailQuery"/>
    </select>
    <!--设备核查计划详情全量列表(导出用,不分页)-->
    <select id="listDeviceExaminePlanDetail" resultType="com.ruoyi.device.dto.DeviceExaminePlanDetailsDto">
        <include refid="deviceExaminePlanDetailQuery"/>
    </select>
</mapper>
cnas-device/src/main/resources/mapper/DeviceExaminePlanMapper.xml
@@ -31,4 +31,12 @@
        where cqm.plan_id = #{deviceExaminePlanId}
    </select>
    <!-- 有明细的核查计划年份(倒序),没有明细的年份不返回 -->
    <select id="selectPlanYears" resultType="java.lang.String">
        select distinct p.plan_year
        from device_examine_plan p
        inner join device_examine_plan_details d on d.plan_id = p.plan_id
        order by p.plan_year desc
    </select>
</mapper>
cnas-device/src/main/resources/mapper/DeviceMaintenanceMapper.xml
@@ -9,7 +9,11 @@
        select * from (
        select db.*,
        d.device_name,
        d.management_number
        d.management_number,
        case when db.maintenance_plan_detail_id is null then 4
             when db.review_status = 1 then 2
             when db.review_status = 0 then 3
             else 1 end as record_status
        from device_maintenance db
        left join device d on db.device_id = d.id)a
        <if test="ew.customSqlSegment != null and ew.customSqlSegment != ''">
cnas-device/src/main/resources/mapper/DeviceMaintenancePlanDetailsMapper.xml
@@ -17,7 +17,7 @@
        where dmpd.maintenance_plan_id = #{maintenancePlanId}
    </select>
    <!-- 设备保养计划详情列表(含到点标红) -->
    <!-- 设备保养计划详情列表(含到点标红、关联的保养记录与审核状态) -->
    <select id="pageMaintenancePlanDetail" resultType="com.ruoyi.device.dto.DeviceMaintenancePlanDetailsDto">
        select
            dmpd.*,
@@ -26,12 +26,37 @@
            d.specification_model,
            d.management_number,
            d.storage_point,
            d.maintenance_cycle,
            dm.id record_id,
            dm.maintenance_date record_maintenance_date,
            dm.maintenance_user_id,
            dm.maintenance_user_name,
            dm.appearance_good,
            dm.normal_use,
            dm.surface_clean,
            dm.label_intact,
            dm.display_light_normal,
            dm.no_rust,
            dm.calibration_normal,
            dm.parts_replace,
            dm.result_normal,
            dm.remark record_remark,
            dm.review_status,
            dm.review_user_id,
            dm.review_user_name,
            dm.review_remark,
            dm.review_time,
            case when dm.id is null then 0
                 when dm.review_status = 1 then 2
                 when dm.review_status = 0 then 3
                 else 1 end as record_status,
            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
        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
        where dmpd.maintenance_plan_id = #{planId}
        <if test="deviceName != null and deviceName != ''">
            and d.device_name like concat('%', #{deviceName}, '%')
cnas-device/src/main/resources/mapper/DeviceMaintenancePlanMapper.xml
@@ -20,4 +20,12 @@
        where dmp.maintenance_plan_id = #{maintenancePlanId}
    </select>
    <!-- 有明细的保养计划年份(倒序),没有明细的年份不返回 -->
    <select id="selectPlanYears" resultType="java.lang.String">
        select distinct p.plan_year
        from device_maintenance_plan p
        inner join device_maintenance_plan_details d on d.maintenance_plan_id = p.maintenance_plan_id
        order by p.plan_year desc
    </select>
</mapper>
cnas-device/src/main/resources/static/word/device-maintenance.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-0.1mg-al204.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-0.1mg-me204e.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-percent-bsa2202s.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-percent.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-thousandth.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-generic.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-rotary-viscosity.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-spectrophotometer.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-surface-tension.docx
Binary files differ
cnas-device/src/main/resources/static/word/examine/examine-viscosity-pin.docx
Binary files differ
cnas-manage/src/main/java/com/ruoyi/manage/controller/ClientSatisfactionController.java
@@ -52,7 +52,9 @@
    @Operation(summary = "客户满意度调查新增")
    @PostMapping("/addClientSatisfaction")
    public Result addClientSatisfaction(@RequestBody ClientSatisfaction clientSatisfaction){
        return Result.success(clientSatisfactionService.save(clientSatisfaction));
        clientSatisfactionService.save(clientSatisfaction);
        // 返回保存后的记录(含自增id),前端据此上传新增时暂存的照片
        return Result.success(clientSatisfaction);
    }
    /**
cnas-require/src/main/java/com/ruoyi/require/pojo/ProcurementSuppliesList.java
@@ -57,9 +57,12 @@
    @Schema(description = "参考价格")
    private BigDecimal referencePrice;
    @Schema(description = "存放位置")
    @Schema(description = "所属目录节点id")
    private Integer contentId;
    @Schema(description = "存放位置")
    private String storageLocation;
    @Schema(description = "负责人")
    private Integer personInCharge;
cnas-require/src/main/resources/mapper/ProcurementSuppliesListMapper.xml
@@ -13,6 +13,7 @@
        <result column="unit" property="unit" />
        <result column="reference_price" property="referencePrice" />
        <result column="content_id" property="contentId" />
        <result column="storage_location" property="storageLocation" />
        <result column="person_in_charge" property="personInCharge" />
        <result column="upper_limit" property="upperLimit" />
        <result column="lower_limit" property="lowerLimit" />