| | |
| | | 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; |
| | |
| | | import com.ruoyi.device.dto.DeviceExaminePlanDetailsDto; |
| | | import com.ruoyi.device.dto.DeviceExaminePlanDto; |
| | | import com.ruoyi.device.excel.upload.DeviceExaminePlanUpload; |
| | | import com.ruoyi.device.mapper.DeviceExamineAttachmentMapper; |
| | | import com.ruoyi.device.mapper.DeviceExaminePlanDetailsMapper; |
| | | import com.ruoyi.device.mapper.DeviceExaminePlanMapper; |
| | | import com.ruoyi.device.mapper.DeviceExamineRecordContrastDetailsMapper; |
| | | import com.ruoyi.device.mapper.DeviceExamineRecordContrastMapper; |
| | | import com.ruoyi.device.mapper.DeviceExamineRecordDetailMapper; |
| | | import com.ruoyi.device.mapper.DeviceExamineRecordMapper; |
| | | import com.ruoyi.device.mapper.DeviceMapper; |
| | | import com.ruoyi.device.pojo.Device; |
| | | import com.ruoyi.device.pojo.DeviceExamineAttachment; |
| | | import com.ruoyi.device.pojo.DeviceExaminePlan; |
| | | import com.ruoyi.device.pojo.DeviceExaminePlanDetails; |
| | | import com.ruoyi.device.pojo.DeviceExamineRecord; |
| | | import com.ruoyi.device.pojo.DeviceExamineRecordContrast; |
| | | import com.ruoyi.device.pojo.DeviceExamineRecordContrastDetails; |
| | | import com.ruoyi.device.pojo.DeviceExamineRecordDetail; |
| | | 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; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | |
| | | import jakarta.annotation.Resource; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.net.URLEncoder; |
| | | 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.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * <p> |
| | |
| | | private DeviceMapper deviceMapper; |
| | | @Resource |
| | | private UserMapper userMapper; |
| | | @Resource |
| | | private DeviceExamineRecordMapper deviceExamineRecordMapper; |
| | | @Resource |
| | | private DeviceExamineRecordContrastMapper deviceExamineRecordContrastMapper; |
| | | @Resource |
| | | private DeviceExamineRecordDetailMapper deviceExamineRecordDetailMapper; |
| | | @Resource |
| | | private DeviceExamineRecordContrastDetailsMapper deviceExamineRecordContrastDetailsMapper; |
| | | @Resource |
| | | private DeviceExamineAttachmentMapper deviceExamineAttachmentMapper; |
| | | |
| | | @Value("${file.path}") |
| | | private String filePath; |
| | | |
| | | @Value("${wordUrl}") |
| | | private String wordUrl; |
| | | |
| | | /** |
| | | * 新增设备核查计划 |
| | |
| | | throw new ErrorException("设备编号" + examinePlanDetail.getDeviceNumber() + "未查询到核查人"); |
| | | } |
| | | examinePlanDetail.setDeviceId(device.getId()); |
| | | examinePlanDetail.setDeviceModel(device.getSpecificationModel()); |
| | | examinePlanDetail.setCheckChargerUserId(user.getId()); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 导出设备核查计划 |
| | | * 导出设备核查计划(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 |
| | |
| | | |
| | | return deviceExaminePlanDto; |
| | | } |
| | | |
| | | /** |
| | | * 按年份查询设备核查计划(不存在则自动创建),并自动补全已到点的设备明细 |
| | | */ |
| | | @Override |
| | | public DeviceExaminePlanDto getOrCreatePlanByYear(String planYear) { |
| | | if (StringUtils.isBlank(planYear)) { |
| | | planYear = String.valueOf(LocalDate.now().getYear()); |
| | | } |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(planYear); |
| | | ensureExamineDetails(plan); |
| | | DeviceExaminePlanDto planDto = new DeviceExaminePlanDto(); |
| | | BeanUtils.copyProperties(plan, planDto); |
| | | DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | planDto.setWriteTimeStr(plan.getWriteTime() == null ? null : plan.getWriteTime().format(dateFormatter)); |
| | | planDto.setRatifyTimeStr(plan.getRatifyTime() == null ? null : plan.getRatifyTime().format(dateFormatter)); |
| | | if (plan.getWriteUserId() != null) { |
| | | User writeUser = userMapper.selectById(plan.getWriteUserId()); |
| | | if (writeUser != null) { |
| | | planDto.setWriteName(writeUser.getName()); |
| | | } |
| | | } |
| | | if (plan.getRatifyUserId() != null) { |
| | | User ratifyUser = userMapper.selectById(plan.getRatifyUserId()); |
| | | if (ratifyUser != null) { |
| | | planDto.setRatifyName(ratifyUser.getName()); |
| | | } |
| | | } |
| | | 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(); |
| | | } |
| | | |
| | | /** |
| | | * 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细 |
| | | */ |
| | | @Override |
| | | public void syncDeviceExamineDetail(Device device) { |
| | | LocalDate due = calcExamineDueDate(device); |
| | | if (due == null) { |
| | | return; |
| | | } |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(String.valueOf(due.getYear())); |
| | | fillExamineDetailIfAbsent(plan, device); |
| | | } |
| | | |
| | | /** |
| | | * 复核通过后,把设备档案的最近/下次核查日期滚动到本轮 |
| | | */ |
| | | @Override |
| | | public void updateDeviceExamineDate(Integer planDetailsId) { |
| | | DeviceExaminePlanDetails detail = deviceExaminePlanDetailsMapper.selectById(planDetailsId); |
| | | if (detail == null || detail.getDeviceId() == null) { |
| | | return; |
| | | } |
| | | Device device = deviceMapper.selectById(detail.getDeviceId()); |
| | | if (device == null) { |
| | | return; |
| | | } |
| | | // 审核通过当天即视为实际核查完成时间 |
| | | LocalDateTime checkDateTime = LocalDate.now().atStartOfDay(); |
| | | // 只允许向前滚动,避免重复复核旧记录时把日期改回去 |
| | | if (device.getLastExamineDate() != null && !checkDateTime.isAfter(device.getLastExamineDate())) { |
| | | return; |
| | | } |
| | | device.setLastExamineDate(checkDateTime); |
| | | if (StringUtils.isNotBlank(device.getExamineCycle())) { |
| | | try { |
| | | device.setNextExamineDate(checkDateTime.plusMonths(Long.parseLong(device.getExamineCycle().trim()))); |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | deviceMapper.updateById(device); |
| | | // 本轮核查完成后立即登记下一轮,历史明细和记录保持不动 |
| | | syncDeviceExamineDetail(device); |
| | | } |
| | | |
| | | /** |
| | | * 查询年份对应的计划,不存在则创建 |
| | | */ |
| | | private DeviceExaminePlan getOrCreatePlanEntity(String planYear) { |
| | | DeviceExaminePlan plan = baseMapper.selectOne(Wrappers.<DeviceExaminePlan>lambdaQuery() |
| | | .eq(DeviceExaminePlan::getPlanYear, planYear) |
| | | .orderByDesc(DeviceExaminePlan::getPlanId) |
| | | .last("limit 1")); |
| | | if (plan == null) { |
| | | plan = new DeviceExaminePlan(); |
| | | plan.setPlanName(planYear + "年度设备核查计划"); |
| | | plan.setPlanYear(planYear); |
| | | plan.setWriteUserId(SecurityUtils.getUserId().intValue()); |
| | | plan.setWriteTime(LocalDateTime.now()); |
| | | baseMapper.insert(plan); |
| | | } |
| | | return plan; |
| | | } |
| | | |
| | | /** |
| | | * 扫描所有设备,把本年度及往年逾期未核查的设备补进计划 |
| | | */ |
| | | private void ensureExamineDetails(DeviceExaminePlan plan) { |
| | | List<Device> devices = deviceMapper.selectList(null); |
| | | for (Device device : devices) { |
| | | fillExamineDetailIfAbsent(plan, device); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 设备本轮应核查日期(没填下次日期时用最近日期+核查周期兜底) |
| | | */ |
| | | private LocalDate calcExamineDueDate(Device device) { |
| | | LocalDateTime base = device.getNextExamineDate(); |
| | | if (base == null && device.getLastExamineDate() != null && StringUtils.isNotBlank(device.getExamineCycle())) { |
| | | try { |
| | | base = device.getLastExamineDate().plusMonths(Long.parseLong(device.getExamineCycle().trim())); |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | return base == null ? null : base.toLocalDate(); |
| | | } |
| | | |
| | | /** |
| | | * 计划中尚无该设备本轮明细时自动补一条;核查时间取档案下次核查日期(精确到日) |
| | | */ |
| | | private void fillExamineDetailIfAbsent(DeviceExaminePlan plan, Device device) { |
| | | LocalDate due = calcExamineDueDate(device); |
| | | if (due == null) { |
| | | return; |
| | | } |
| | | 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; |
| | | } |
| | | 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()) |
| | | .likeRight(DeviceExaminePlanDetails::getCheckTime, checkTime)); |
| | | if (exist != null && exist > 0) { |
| | | return; |
| | | } |
| | | DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails(); |
| | | detail.setPlanId(plan.getPlanId()); |
| | | detail.setDeviceId(device.getId()); |
| | | detail.setDeviceName(device.getDeviceName() == null ? "" : device.getDeviceName()); |
| | | detail.setDeviceModel(device.getSpecificationModel()); |
| | | detail.setDeviceNumber(device.getManagementNumber()); |
| | | detail.setCheckTime(checkTime); |
| | | deviceExaminePlanDetailsMapper.insert(detail); |
| | | } |
| | | |
| | | /** |
| | | * 为指定设备找一条可录入核查记录的计划明细;没有可用明细时在当前年度计划新增本轮明细 |
| | | */ |
| | | @Override |
| | | public DeviceExaminePlanDetails resolveExaminePlanDetail(Integer deviceId, String checkTime) { |
| | | return findExaminePlanDetail(deviceId, checkTime, true); |
| | | } |
| | | |
| | | /** |
| | | * 查找指定设备可复用的计划明细;只查询,不新增计划或明细 |
| | | */ |
| | | @Override |
| | | public DeviceExaminePlanDetails findExaminePlanDetail(Integer deviceId, String checkTime) { |
| | | return findExaminePlanDetail(deviceId, checkTime, false); |
| | | } |
| | | |
| | | private DeviceExaminePlanDetails findExaminePlanDetail(Integer deviceId, String checkTime, boolean createIfAbsent) { |
| | | if (deviceId == null) { |
| | | throw new ErrorException("请选择核查设备"); |
| | | } |
| | | Device device = deviceMapper.selectById(deviceId); |
| | | if (device == null) { |
| | | throw new ErrorException("核查设备不存在"); |
| | | } |
| | | String checkMonth = normalizeCheckMonth(checkTime); |
| | | String planYear = checkMonth.substring(0, 4); |
| | | DeviceExaminePlan plan; |
| | | if (createIfAbsent) { |
| | | plan = getOrCreatePlanEntity(planYear); |
| | | } else { |
| | | plan = baseMapper.selectOne(Wrappers.<DeviceExaminePlan>lambdaQuery() |
| | | .eq(DeviceExaminePlan::getPlanYear, planYear) |
| | | .orderByDesc(DeviceExaminePlan::getPlanId) |
| | | .last("limit 1")); |
| | | } |
| | | List<DeviceExaminePlanDetails> details = deviceExaminePlanDetailsMapper.selectList( |
| | | Wrappers.<DeviceExaminePlanDetails>lambdaQuery() |
| | | .eq(DeviceExaminePlanDetails::getDeviceId, deviceId) |
| | | .orderByAsc(DeviceExaminePlanDetails::getCheckTime)); |
| | | DeviceExaminePlanDetails nearest = null; |
| | | long nearestDistance = Long.MAX_VALUE; |
| | | for (DeviceExaminePlanDetails detail : details) { |
| | | // 已有核查记录的明细代表已完成的一轮,后续核查永远不复用、不覆盖 |
| | | Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery() |
| | | .eq(DeviceExamineRecord::getPlanDetailsId, detail.getPlanDetailsId())); |
| | | if (recordCount != null && recordCount > 0) { |
| | | continue; |
| | | } |
| | | if (plan != null && detail.getPlanId() != null && detail.getPlanId().equals(plan.getPlanId())) { |
| | | return detail; |
| | | } |
| | | long distance = monthDistance(detail.getCheckTime(), checkMonth); |
| | | if (distance < nearestDistance) { |
| | | nearest = detail; |
| | | nearestDistance = distance; |
| | | } |
| | | } |
| | | // 提前核查时优先复用系统登记、尚未核查的这一轮(可能在别的年份计划里) |
| | | if (nearest != null) { |
| | | return nearest; |
| | | } |
| | | if (!createIfAbsent) { |
| | | return null; |
| | | } |
| | | DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails(); |
| | | detail.setPlanId(plan.getPlanId()); |
| | | detail.setDeviceId(device.getId()); |
| | | detail.setDeviceName(device.getDeviceName() == null ? "" : device.getDeviceName()); |
| | | detail.setDeviceModel(device.getSpecificationModel()); |
| | | detail.setDeviceNumber(device.getManagementNumber()); |
| | | String detailCheckTime = normalizeCheckDate(checkTime); |
| | | detail.setCheckTime(StringUtils.isBlank(detailCheckTime) ? checkMonth : detailCheckTime); |
| | | deviceExaminePlanDetailsMapper.insert(detail); |
| | | return detail; |
| | | } |
| | | |
| | | /** |
| | | * 规范核查月份为 yyyy-MM,非法或为空时取当前月份 |
| | | */ |
| | | private String normalizeCheckMonth(String checkTime) { |
| | | if (StringUtils.isNotBlank(checkTime)) { |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | if (normalized.length() >= 7) { |
| | | try { |
| | | return YearMonth.parse(normalized.substring(0, 7)).toString(); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | } |
| | | return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); |
| | | } |
| | | |
| | | /** |
| | | * 规范核查日期:带具体日期的保留到日,只填月份的规范为 yyyy-MM |
| | | */ |
| | | private String normalizeCheckDate(String checkTime) { |
| | | if (StringUtils.isBlank(checkTime)) { |
| | | return null; |
| | | } |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | if (normalized.length() >= 10) { |
| | | try { |
| | | return LocalDate.parse(normalized.substring(0, 10)).toString(); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | return normalizeCheckMonth(normalized); |
| | | } |
| | | |
| | | /** |
| | | * 两个月份之间相差的月数 |
| | | */ |
| | | private long monthDistance(String fromCheckTime, String targetMonth) { |
| | | try { |
| | | YearMonth from = YearMonth.parse(normalizeCheckMonth(fromCheckTime)); |
| | | YearMonth target = YearMonth.parse(targetMonth); |
| | | return Math.abs(ChronoUnit.MONTHS.between(from, target)); |
| | | } catch (Exception e) { |
| | | return Long.MAX_VALUE; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 修改计划明细;核查月份跨年时,把明细归到核查月份所属年份的计划下 |
| | | */ |
| | | @Override |
| | | public boolean updateExaminePlanDetail(DeviceExaminePlanDetails detail) { |
| | | if (detail == null || detail.getPlanDetailsId() == null) { |
| | | throw new ErrorException("缺少设备核查计划明细id"); |
| | | } |
| | | if (StringUtils.isNotBlank(detail.getCheckTime())) { |
| | | String checkTime = normalizeCheckDate(detail.getCheckTime()); |
| | | detail.setCheckTime(checkTime); |
| | | DeviceExaminePlan plan = getOrCreatePlanEntity(checkTime.substring(0, 4)); |
| | | detail.setPlanId(plan.getPlanId()); |
| | | } |
| | | return deviceExaminePlanDetailsMapper.updateById(detail) > 0; |
| | | } |
| | | |
| | | /** |
| | | * 删除计划明细;已审核完成的核查/比对记录随历史留档不允许删除,未审核的记录随明细一起删除 |
| | | */ |
| | | @Override |
| | | public boolean removeExaminePlanDetail(Integer planDetailsId) { |
| | | if (planDetailsId == null) { |
| | | throw new ErrorException("缺少设备核查计划明细id"); |
| | | } |
| | | DeviceExaminePlanDetails detail = deviceExaminePlanDetailsMapper.selectById(planDetailsId); |
| | | if (detail == null) { |
| | | return false; |
| | | } |
| | | List<DeviceExamineRecord> records = deviceExamineRecordMapper.selectList(Wrappers.<DeviceExamineRecord>lambdaQuery() |
| | | .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId)); |
| | | List<DeviceExamineRecordContrast> contrasts = deviceExamineRecordContrastMapper.selectList(Wrappers.<DeviceExamineRecordContrast>lambdaQuery() |
| | | .eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId)); |
| | | // 审核不通过的记录允许连同明细一起删除;审核通过的记录为历史留档,不允许删除 |
| | | 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()); |
| | | deviceExamineRecordDetailMapper.delete(Wrappers.<DeviceExamineRecordDetail>lambdaQuery() |
| | | .in(DeviceExamineRecordDetail::getRecordId, recordIds)); |
| | | deviceExamineRecordMapper.deleteBatchIds(recordIds); |
| | | } |
| | | if (!contrasts.isEmpty()) { |
| | | List<Integer> contrastIds = contrasts.stream().map(DeviceExamineRecordContrast::getRecordContrastId).collect(Collectors.toList()); |
| | | deviceExamineRecordContrastDetailsMapper.delete(Wrappers.<DeviceExamineRecordContrastDetails>lambdaQuery() |
| | | .in(DeviceExamineRecordContrastDetails::getRecordContrastId, contrastIds)); |
| | | deviceExamineRecordContrastMapper.deleteBatchIds(contrastIds); |
| | | } |
| | | deviceExamineAttachmentMapper.delete(Wrappers.<DeviceExamineAttachment>lambdaQuery() |
| | | .eq(DeviceExamineAttachment::getPlanDetailsId, planDetailsId)); |
| | | boolean removed = deviceExaminePlanDetailsService.removeById(planDetailsId); |
| | | if (removed && detail.getDeviceId() != null) { |
| | | rollbackDeviceExamineDate(detail); |
| | | } |
| | | return removed; |
| | | } |
| | | |
| | | /** |
| | | * 删除未审核的计划后,把设备档案恢复到最近一条审核通过的记录; |
| | | * 没有已完成记录时,才清空档案的核查日期和周期 |
| | | */ |
| | | private void rollbackDeviceExamineDate(DeviceExaminePlanDetails removedDetail) { |
| | | Device device = deviceMapper.selectById(removedDetail.getDeviceId()); |
| | | if (device == null) { |
| | | return; |
| | | } |
| | | Long remainCount = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery() |
| | | .eq(DeviceExaminePlanDetails::getDeviceId, removedDetail.getDeviceId())); |
| | | boolean isCurrentRound = sameExamineMonth(removedDetail.getCheckTime(), device.getNextExamineDate()); |
| | | // 删除的不是档案当前登记的那一轮,且设备还有其他计划时,不动档案 |
| | | if (!isCurrentRound && remainCount != null && remainCount > 0) { |
| | | return; |
| | | } |
| | | LocalDate lastDone = null; |
| | | List<DeviceExaminePlanDetails> details = deviceExaminePlanDetailsMapper.selectList( |
| | | Wrappers.<DeviceExaminePlanDetails>lambdaQuery() |
| | | .eq(DeviceExaminePlanDetails::getDeviceId, removedDetail.getDeviceId())); |
| | | for (DeviceExaminePlanDetails item : details) { |
| | | if (!hasPassedExamineRecord(item.getPlanDetailsId())) { |
| | | continue; |
| | | } |
| | | LocalDate date = parseCheckMonth(item.getCheckTime()); |
| | | if (date != null && (lastDone == null || date.isAfter(lastDone))) { |
| | | lastDone = date; |
| | | } |
| | | } |
| | | if (lastDone == null) { |
| | | deviceMapper.update(null, Wrappers.<Device>lambdaUpdate() |
| | | .eq(Device::getId, removedDetail.getDeviceId()) |
| | | .set(Device::getLastExamineDate, null) |
| | | .set(Device::getNextExamineDate, null) |
| | | .set(Device::getExamineCycle, null)); |
| | | return; |
| | | } |
| | | LocalDateTime last = lastDone.atStartOfDay(); |
| | | LocalDateTime next = null; |
| | | if (StringUtils.isNotBlank(device.getExamineCycle())) { |
| | | try { |
| | | next = last.plusMonths(Long.parseLong(device.getExamineCycle().trim())); |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | deviceMapper.update(null, Wrappers.<Device>lambdaUpdate() |
| | | .eq(Device::getId, removedDetail.getDeviceId()) |
| | | .set(Device::getLastExamineDate, last) |
| | | .set(Device::getNextExamineDate, next)); |
| | | } |
| | | |
| | | /** |
| | | * 明细下是否存在审核通过的核查记录或比对记录 |
| | | */ |
| | | private boolean hasPassedExamineRecord(Integer planDetailsId) { |
| | | Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery() |
| | | .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId) |
| | | .eq(DeviceExamineRecord::getReviewStatus, 1)); |
| | | if (recordCount != null && recordCount > 0) { |
| | | return true; |
| | | } |
| | | Long contrastCount = deviceExamineRecordContrastMapper.selectCount(Wrappers.<DeviceExamineRecordContrast>lambdaQuery() |
| | | .eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId) |
| | | .eq(DeviceExamineRecordContrast::getReviewStatus, 1)); |
| | | return contrastCount != null && contrastCount > 0; |
| | | } |
| | | |
| | | /** |
| | | * 计划明细的核查月份是否就是档案下次核查日期所在月份 |
| | | */ |
| | | private boolean sameExamineMonth(String checkTime, LocalDateTime nextExamineDate) { |
| | | if (StringUtils.isBlank(checkTime) || nextExamineDate == null) { |
| | | return false; |
| | | } |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | return normalized.length() >= 7 |
| | | && normalized.substring(0, 7).equals(nextExamineDate.format(DateTimeFormatter.ofPattern("yyyy-MM"))); |
| | | } |
| | | |
| | | /** |
| | | * 解析明细核查月份为当月首日,格式不合法时返回 null |
| | | */ |
| | | private LocalDate parseCheckMonth(String checkTime) { |
| | | if (StringUtils.isNotBlank(checkTime)) { |
| | | String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); |
| | | if (normalized.length() >= 7) { |
| | | try { |
| | | return YearMonth.parse(normalized.substring(0, 7)).atDay(1); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public List<DeviceExamineAttachment> selectAttachments(Integer planDetailsId) { |
| | | if (planDetailsId == null) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return deviceExamineAttachmentMapper.selectList(Wrappers.<DeviceExamineAttachment>lambdaQuery() |
| | | .eq(DeviceExamineAttachment::getPlanDetailsId, planDetailsId) |
| | | .orderByAsc(DeviceExamineAttachment::getSort, DeviceExamineAttachment::getId)); |
| | | } |
| | | |
| | | @Override |
| | | public DeviceExamineAttachment uploadAttachment(Integer planDetailsId, MultipartFile file) { |
| | | if (planDetailsId == null) { |
| | | throw new ErrorException("缺少核查计划明细id"); |
| | | } |
| | | if (deviceExaminePlanDetailsMapper.selectById(planDetailsId) == null) { |
| | | throw new ErrorException("核查计划明细不存在"); |
| | | } |
| | | if (file == null || file.isEmpty()) { |
| | | throw new ErrorException("附件不能为空"); |
| | | } |
| | | // 系统全局上传上限为200MB,这里提前给出友好提示 |
| | | if (file.getSize() > 200L * 1024 * 1024) { |
| | | throw new ErrorException("附件不能超过200MB"); |
| | | } |
| | | String originalName = file.getOriginalFilename(); |
| | | if (originalName == null || originalName.trim().isEmpty()) { |
| | | originalName = "未命名文件"; |
| | | } |
| | | // 个别浏览器可能带上路径前缀,只保留文件名本身 |
| | | originalName = originalName.replaceAll(".*[/\\\\]", ""); |
| | | try { |
| | | // 图片放图片目录(/img 静态路径),其余任意格式放 word 目录(/word 静态路径) |
| | | boolean image = originalName.matches("(?i).+\\.(jpg|jpeg|png|gif)$"); |
| | | File directory = new File(image ? filePath : wordUrl); |
| | | if (!directory.exists()) { |
| | | directory.mkdirs(); |
| | | } |
| | | String fileUrl = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")) + "-" + originalName; |
| | | file.transferTo(new File(directory, fileUrl)); |
| | | DeviceExamineAttachment attachment = new DeviceExamineAttachment(); |
| | | attachment.setPlanDetailsId(planDetailsId); |
| | | attachment.setFileName(originalName); |
| | | attachment.setFileUrl(fileUrl); |
| | | attachment.setSort(selectAttachments(planDetailsId).size()); |
| | | deviceExamineAttachmentMapper.insert(attachment); |
| | | return attachment; |
| | | } catch (IOException e) { |
| | | throw new ErrorException("附件上传失败"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public boolean deleteAttachment(Long id) { |
| | | return deviceExamineAttachmentMapper.deleteById(id) > 0; |
| | | } |
| | | } |