package com.ruoyi.device.service.impl; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; 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.ruoyi.common.constant.MenuJumpPathConstants; import com.ruoyi.common.core.domain.entity.InformationNotification; import com.ruoyi.common.core.domain.entity.User; import com.ruoyi.common.utils.QueryWrappers; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.WxCpUtils; 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.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 org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.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; /** *

* 设备核查计划主表 服务实现类 *

* * @author 江苏鵷雏网络科技有限公司 * @since 2024-12-16 07:14:04 */ @Service @Transactional(rollbackFor = Exception.class) public class DeviceExaminePlanServiceImpl extends ServiceImpl implements DeviceExaminePlanService { @Resource private DeviceExaminePlanDetailsMapper deviceExaminePlanDetailsMapper; @Resource private DeviceExaminePlanDetailsService deviceExaminePlanDetailsService; @Resource private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Resource private InformationNotificationService informationNotificationService; @Resource 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; /** * 新增设备核查计划 * @param examinePlanDto * @return */ @Override public boolean addDeviceExaminePlan(DeviceExaminePlanDto examinePlanDto) { // 当前登录用户 Integer userId = SecurityUtils.getUserId().intValue(); // 文件名称 examinePlanDto.setWriteUserId(userId); examinePlanDto.setWriteTime(LocalDateTime.now()); baseMapper.insert(examinePlanDto); // 添加详情 if (CollectionUtils.isNotEmpty(examinePlanDto.getExaminePlanDetailsList())) { for (DeviceExaminePlanDetails deviceExaminePlanDetails : examinePlanDto.getExaminePlanDetailsList()) { deviceExaminePlanDetails.setPlanId(examinePlanDto.getPlanId()); } deviceExaminePlanDetailsService.saveBatch(examinePlanDto.getExaminePlanDetailsList()); } return true; } /** * 导入设备核查计划 * @param file * @return */ @Override public boolean importDeviceExaminePlan(MultipartFile file) { // 当前登录用户 Integer userId = SecurityUtils.getUserId().intValue(); // 文件名称 String fileName = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf(".")); DeviceExaminePlan examinePlan = new DeviceExaminePlan(); examinePlan.setPlanName(fileName); examinePlan.setWriteUserId(userId); examinePlan.setWriteTime(LocalDateTime.now()); baseMapper.insert(examinePlan); List examinePlanDetails = new ArrayList<>(); // 导入附件内容 try { // excel解析 EasyExcel.read(file.getInputStream(), DeviceExaminePlanUpload.class, new AnalysisEventListener() { @Override public void invoke(DeviceExaminePlanUpload detailsUpload, AnalysisContext analysisContext) { // 判断是否为空 if (StringUtils.isNotBlank(detailsUpload.getDeviceName()) && StringUtils.isNotBlank(detailsUpload.getDeviceNumber())) { // 对象复制 DeviceExaminePlanDetails planDetails = new DeviceExaminePlanDetails(); BeanUtils.copyProperties(detailsUpload, planDetails); planDetails.setPlanId(examinePlan.getPlanId()); examinePlanDetails.add(planDetails); } } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }).sheet().doRead(); for (DeviceExaminePlanDetails examinePlanDetail : examinePlanDetails) { Device device = deviceMapper.selectOne(Wrappers.lambdaQuery() .eq(Device::getManagementNumber, examinePlanDetail.getDeviceNumber().trim())); if (device == null) { throw new ErrorException("设备编号" + examinePlanDetail.getDeviceNumber() + "未查询到设备, 请重新导入"); } User user = userMapper.selectOne(Wrappers.lambdaQuery() .eq(User::getName, examinePlanDetail.getCheckChargerUser())); if (user == null) { throw new ErrorException("设备编号" + examinePlanDetail.getDeviceNumber() + "未查询到核查人"); } examinePlanDetail.setDeviceId(device.getId()); examinePlanDetail.setDeviceModel(device.getSpecificationModel()); examinePlanDetail.setCheckChargerUserId(user.getId()); } deviceExaminePlanDetailsService.saveBatch(examinePlanDetails); } catch (IOException e) { throw new RuntimeException(e); } return true; } /** * 提交批准 * @param deviceExaminePlan * @return */ @Override public boolean submitRatifyDeviceExaminePlan(DeviceExaminePlan deviceExaminePlan) { baseMapper.update(null, Wrappers.lambdaUpdate() .eq(DeviceExaminePlan::getPlanId, deviceExaminePlan.getPlanId()) .set(DeviceExaminePlan::getRatifyUserId, deviceExaminePlan.getRatifyUserId()) .set(DeviceExaminePlan::getRatifyRemark, null) .set(DeviceExaminePlan::getRatifyStatus, null) .set(DeviceExaminePlan::getRatifyTime, null) ); DeviceExaminePlan examinePlan = baseMapper.selectById(deviceExaminePlan.getPlanId()); // 发送消息 Integer userId = SecurityUtils.getUserId().intValue(); User user = userMapper.selectById(userId); // 消息发送 InformationNotification info = new InformationNotification(); // 发送人 info.setCreateUser(user.getName()); info.setMessageType("6"); info.setTheme("CNAS设备核查计划批准通知"); info.setContent(examinePlan.getPlanName() + "的设备核查计划待批准"); info.setSenderId(userId); // 接收人 info.setConsigneeId(examinePlan.getRatifyUserId()); info.setJumpPath(MenuJumpPathConstants.DEVICE); informationNotificationService.addInformationNotification(info); // 发送企业微信通知 threadPoolTaskExecutor.execute(() -> { // 查询接收人 User personnel = userMapper.selectById(examinePlan.getRatifyUserId()); String message = ""; message += "CNAS设备核查计划批准通知"; message += "\n请去资源管理-设备一层目录-设备核查计划填写"; message += "\n" + examinePlan.getPlanName() + "的校准核查划待批准"; //发送企业微信消息通知 try { WxCpUtils.inform(personnel.getAccount(), message, null); } catch (Exception e) { throw new RuntimeException(e); } }); return false; } /** * 设备核查计划批准 * @param deviceExaminePlan * @return */ @Override public boolean ratifyDeviceExaminePlan(DeviceExaminePlan deviceExaminePlan) { // 当前登录用户 baseMapper.update(null, Wrappers.lambdaUpdate() .eq(DeviceExaminePlan::getPlanId, deviceExaminePlan.getPlanId()) .set(DeviceExaminePlan::getRatifyRemark, deviceExaminePlan.getRatifyRemark()) .set(DeviceExaminePlan::getRatifyStatus, deviceExaminePlan.getRatifyStatus()) .set(DeviceExaminePlan::getRatifyTime, LocalDateTime.now()) ); return true; } /** * 设备核查计划列表 * @param page * @param deviceExaminePlan * @return */ @Override public IPage pageDeviceExaminePlan(Page page, DeviceExaminePlan deviceExaminePlan) { return baseMapper.deviceExaminePlanDetailsMapper(page, QueryWrappers.queryWrappers(deviceExaminePlan)); } /** * 设备核查计划详情列表 * @param page * @param deviceExaminePlanDetails * @return */ @Override public IPage pageDeviceExaminePlanDetail(Page page, DeviceExaminePlanDetails deviceExaminePlanDetails) { if (deviceExaminePlanDetails.getPlanId() == null) { return new Page(); } return deviceExaminePlanDetailsMapper.pageDeviceExaminePlanDetail(page, QueryWrappers.queryWrappers(deviceExaminePlanDetails)); } /** * 导出设备核查计划(Excel):有筛选条件时导出筛选结果,没有筛选时导出该年份全部 * @param deviceExaminePlanId 设备核查计划id * @param deviceName 设备名称筛选 * @param deviceNumber 设备编号筛选 * @param response 响应体 */ @Override 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 deviceExaminePlanDetailsDtoList = deviceExaminePlanDetailsMapper.listDeviceExaminePlanDetail(QueryWrappers.queryWrappers(query)); String planName = StringUtils.isNotBlank(deviceExaminePlan.getPlanName()) ? deviceExaminePlan.getPlanName() : "设备核查计划"; String[] columns = {"序号", "设备编号", "设备名称", "设备型号", "核查时间", "核查指标", "核查方法", "结果如何判定", "核查负责人", "审核状态", "备注"}; List 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()) }); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("UTF-8"); try { response.setHeader("Content-disposition", "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 dataRows) throws IOException { Workbook workbook = new XSSFWorkbook(); Map 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 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 public boolean updateDeviceExaminePlan(DeviceExaminePlanDto examinePlanDto) { // 当前登录用户 Integer userId = SecurityUtils.getUserId().intValue(); // 文件名称 examinePlanDto.setWriteUserId(userId); examinePlanDto.setWriteTime(LocalDateTime.now()); baseMapper.updateById(examinePlanDto); deviceExaminePlanDetailsService.remove(Wrappers.lambdaQuery() .eq(DeviceExaminePlanDetails::getPlanId, examinePlanDto.getPlanId())); // 添加详情 if (CollectionUtils.isNotEmpty(examinePlanDto.getExaminePlanDetailsList())) { for (DeviceExaminePlanDetails deviceExaminePlanDetails : examinePlanDto.getExaminePlanDetailsList()) { deviceExaminePlanDetails.setPlanId(examinePlanDto.getPlanId()); } deviceExaminePlanDetailsService.saveBatch(examinePlanDto.getExaminePlanDetailsList()); } return true; } @Override public DeviceExaminePlanDto getDeviceExaminePlan(Integer planId) { DeviceExaminePlan examinePlan = baseMapper.selectById(planId); DeviceExaminePlanDto deviceExaminePlanDto = new DeviceExaminePlanDto(); BeanUtils.copyProperties(examinePlan, deviceExaminePlanDto); // 查询详情 List list = deviceExaminePlanDetailsService.list(Wrappers.lambdaQuery() .eq(DeviceExaminePlanDetails::getPlanId, planId) .orderByAsc(DeviceExaminePlanDetails::getCheckTime)); deviceExaminePlanDto.setExaminePlanDetailsList(list); 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 getExaminePlanYears() { List plans = baseMapper.selectList(null); for (DeviceExaminePlan plan : plans) { Long detailCount = deviceExaminePlanDetailsMapper.selectCount(Wrappers.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.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 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.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.lambdaQuery() .eq(DeviceExaminePlan::getPlanYear, planYear) .orderByDesc(DeviceExaminePlan::getPlanId) .last("limit 1")); } List details = deviceExaminePlanDetailsMapper.selectList( Wrappers.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.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 records = deviceExamineRecordMapper.selectList(Wrappers.lambdaQuery() .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId)); List contrasts = deviceExamineRecordContrastMapper.selectList(Wrappers.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 recordIds = records.stream().map(DeviceExamineRecord::getRecordId).collect(Collectors.toList()); deviceExamineRecordDetailMapper.delete(Wrappers.lambdaQuery() .in(DeviceExamineRecordDetail::getRecordId, recordIds)); deviceExamineRecordMapper.deleteBatchIds(recordIds); } if (!contrasts.isEmpty()) { List contrastIds = contrasts.stream().map(DeviceExamineRecordContrast::getRecordContrastId).collect(Collectors.toList()); deviceExamineRecordContrastDetailsMapper.delete(Wrappers.lambdaQuery() .in(DeviceExamineRecordContrastDetails::getRecordContrastId, contrastIds)); deviceExamineRecordContrastMapper.deleteBatchIds(contrastIds); } deviceExamineAttachmentMapper.delete(Wrappers.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.lambdaQuery() .eq(DeviceExaminePlanDetails::getDeviceId, removedDetail.getDeviceId())); boolean isCurrentRound = sameExamineMonth(removedDetail.getCheckTime(), device.getNextExamineDate()); // 删除的不是档案当前登记的那一轮,且设备还有其他计划时,不动档案 if (!isCurrentRound && remainCount != null && remainCount > 0) { return; } LocalDate lastDone = null; List details = deviceExaminePlanDetailsMapper.selectList( Wrappers.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.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.lambdaUpdate() .eq(Device::getId, removedDetail.getDeviceId()) .set(Device::getLastExamineDate, last) .set(Device::getNextExamineDate, next)); } /** * 明细下是否存在审核通过的核查记录或比对记录 */ private boolean hasPassedExamineRecord(Integer planDetailsId) { Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.lambdaQuery() .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId) .eq(DeviceExamineRecord::getReviewStatus, 1)); if (recordCount != null && recordCount > 0) { return true; } Long contrastCount = deviceExamineRecordContrastMapper.selectCount(Wrappers.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 selectAttachments(Integer planDetailsId) { if (planDetailsId == null) { return new ArrayList<>(); } return deviceExamineAttachmentMapper.selectList(Wrappers.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; } }