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 <= date_add(curdate(), interval 1 month) and (d.last_maintenance_date is null or d.last_maintenance_date < 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.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-0.1mg-al204.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-0.1mg-me204e.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-percent-bsa2202s.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-percent.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-balance-thousandth.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-generic.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-rotary-viscosity.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-spectrophotometer.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-surface-tension.docxBinary files differ
cnas-device/src/main/resources/static/word/examine/examine-viscosity-pin.docxBinary 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" />