package com.ruoyi.device.service.impl; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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.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.SecurityUtils; import com.ruoyi.common.utils.WxCpUtils; import com.ruoyi.device.dto.DeviceExamineRecordDto; import com.ruoyi.device.mapper.DeviceExaminePlanDetailsMapper; import com.ruoyi.device.mapper.DeviceExamineRecordMapper; import com.ruoyi.device.mapper.DeviceMapper; import com.ruoyi.device.pojo.Device; import com.ruoyi.device.pojo.DeviceExaminePlanDetails; import com.ruoyi.device.pojo.DeviceExamineRecord; import com.ruoyi.device.pojo.DeviceExamineRecordDetail; import com.ruoyi.device.service.DeviceExaminePlanService; import com.ruoyi.device.service.DeviceExamineRecordDetailService; import com.ruoyi.device.service.DeviceExamineRecordService; import com.ruoyi.framework.exception.ErrorException; 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; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import java.io.InputStream; import java.io.OutputStream; import java.net.URLEncoder; import java.time.LocalDateTime; import java.util.List; /** *

* 设备核查记录表 服务实现类 *

* * @author 江苏鵷雏网络科技有限公司 * @since 2024-12-16 07:14:28 */ @Service @Transactional(rollbackFor = Exception.class) public class DeviceExamineRecordServiceImpl extends ServiceImpl 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; @Resource private DeviceExaminePlanService deviceExaminePlanService; @Resource private DeviceExaminePlanDetailsMapper deviceExaminePlanDetailsMapper; @Resource private DeviceMapper deviceMapper; @Resource private UserMapper userMapper; @Resource private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Resource private InformationNotificationService informationNotificationService; /** * 查询核查记录 * * @return */ @Override public DeviceExamineRecordDto getExamineRecord(Integer planDetailsId) { DeviceExamineRecordDto deviceExamineRecord = baseMapper.getExamineRecord(planDetailsId); // 判断是否为空 if (deviceExamineRecord == null) { deviceExamineRecord = new DeviceExamineRecordDto(); // 查询设备详情 DeviceExaminePlanDetails deviceExaminePlanDetails = deviceExaminePlanDetailsMapper.selectById(planDetailsId); Device device = deviceMapper.selectById(deviceExaminePlanDetails.getDeviceId()); if (device != null) { deviceExamineRecord.setDeviceName(device.getDeviceName()); deviceExamineRecord.setDeviceNumber(device.getManagementNumber()); deviceExamineRecord.setPlanDetailsId(deviceExaminePlanDetails.getPlanDetailsId()); } } else { // 查询详情 List list = deviceExamineRecordDetailService.list(Wrappers.lambdaQuery() .eq(DeviceExamineRecordDetail::getRecordId, deviceExamineRecord.getRecordId())); deviceExamineRecord.setRecordDetailList(list); } return deviceExamineRecord; } /** * 新增核查记录 * * @return */ @Override public boolean addExamineRecord(DeviceExamineRecordDto deviceExamineRecordDto) { if (deviceExamineRecordDto.getPlanDetailsId() == null) { throw new ErrorException("缺少计划详细信息id"); } if (deviceExamineRecordDto.getRecordId() == null) { Integer userId = SecurityUtils.getUserId().intValue(); User user = userMapper.selectById(SecurityUtils.getUserId()); deviceExamineRecordDto.setCheckerUserId(userId); deviceExamineRecordDto.setCheckerUser(user.getName()); // 查询审查人id if (deviceExamineRecordDto.getReviewUserId() != null) { User reviewUser = userMapper.selectById(deviceExamineRecordDto.getReviewUserId()); deviceExamineRecordDto.setReviewUser(reviewUser.getName()); // 消息发送 InformationNotification info = new InformationNotification(); // 发送人 info.setCreateUser(user.getName()); info.setMessageType("6"); info.setTheme("CNAS设备核查计划待批准"); info.setContent("设备编号为: " + deviceExamineRecordDto.getDeviceNumber() + " 设备核查计划待审批"); info.setSenderId(userId); // 接收人 info.setConsigneeId(deviceExamineRecordDto.getReviewUserId()); info.setJumpPath(MenuJumpPathConstants.DEVICE); informationNotificationService.addInformationNotification(info); // 发送企业微信通知 threadPoolTaskExecutor.execute(() -> { // 查询发送人 User people = userMapper.selectById(deviceExamineRecordDto.getReviewUserId()); String message = ""; message += "CNAS设备核查计划待批准"; message += "\n请去资源要求-设备-设备核查计划审批"; message += "\n" + "设备编号为: " + deviceExamineRecordDto.getDeviceNumber() + "设备核查计划待审批"; //发送企业微信消息通知 try { WxCpUtils.inform(people.getAccount(), message, null); } catch (Exception e) { throw new RuntimeException(e); } }); } } this.saveOrUpdate(deviceExamineRecordDto); // 删除全部详情 deviceExamineRecordDetailService.remove(Wrappers.lambdaQuery() .eq(DeviceExamineRecordDetail::getRecordId, deviceExamineRecordDto.getRecordId())); //添加详情 if (CollectionUtils.isNotEmpty(deviceExamineRecordDto.getRecordDetailList())) { for (DeviceExamineRecordDetail deviceExamineRecordDetail : deviceExamineRecordDto.getRecordDetailList()) { deviceExamineRecordDetail.setRecordId(deviceExamineRecordDto.getRecordId()); } deviceExamineRecordDetailService.saveBatch(deviceExamineRecordDto.getRecordDetailList()); } return true; } /** * 复核核查记录 * * @return */ @Override public boolean reviewExamineRecord(DeviceExamineRecordDto dto) { if (dto.getPlanDetailsId() == null) { throw new ErrorException("缺少计划详细信息id"); } DeviceExaminePlanDetails planDetails = deviceExaminePlanDetailsMapper.selectById(dto.getPlanDetailsId()); if (planDetails == null) { throw new ErrorException("核查计划明细不存在"); } if (planDetails.getCheckChargerUserId() == null && StringUtils.isBlank(planDetails.getCheckChargerUser())) { throw new ErrorException("请先在编辑中填写核查负责人"); } Integer reviewStatus = dto.getReviewStatus(); Integer reviewerId = SecurityUtils.getUserId().intValue(); User reviewer = userMapper.selectById(reviewerId); LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate() .eq(DeviceExamineRecord::getPlanDetailsId, dto.getPlanDetailsId()) .set(DeviceExamineRecord::getReviewStatus, reviewStatus) .set(DeviceExamineRecord::getReviewRemark, dto.getReviewRemark()) // 审核动作时间即核查完成时间,列表「核查时间」按它显示 .set(DeviceExamineRecord::getUpdateTime, LocalDateTime.now()); // 为0清除审核人 if (reviewStatus != null && reviewStatus == 0) { wrapper.set(DeviceExamineRecord::getReviewUserId, null) .set(DeviceExamineRecord::getReviewUser, null); } else { wrapper.set(DeviceExamineRecord::getReviewUserId, reviewerId) .set(DeviceExamineRecord::getReviewUser, reviewer == null ? null : reviewer.getName()); } boolean updated = this.update(wrapper); // 计划行尚未录入核查记录时,补一条审核结果记录,保证列表能显示通过/不通过状态 if (!updated) { DeviceExamineRecord record = new DeviceExamineRecord(); record.setPlanDetailsId(dto.getPlanDetailsId()); record.setReviewStatus(reviewStatus); record.setReviewRemark(dto.getReviewRemark()); record.setCheckerUserId(planDetails.getCheckChargerUserId()); record.setCheckerUser(planDetails.getCheckChargerUser()); if (reviewStatus != null && reviewStatus == 1) { record.setReviewUserId(reviewerId); record.setReviewUser(reviewer == null ? null : reviewer.getName()); } this.save(record); } // 复核通过后,设备档案的最近/下次核查日期按审核当天滚动;计划行的检查日期是计划日期,审核不改它 if (reviewStatus != null && reviewStatus == 1) { deviceExaminePlanService.updateDeviceExamineDate(dto.getPlanDetailsId()); } return true; } /** * 导出复核核查记录 * * @param planDetailsId 复核核查记录id * @return */ @Override public void exportReviewExamineRecordDetail(Integer planDetailsId, HttpServletResponse response) { // 查询复核核查记录 DeviceExamineRecordDto deviceExamineRecordDto = baseMapper.selectReviewExamineRecordDto(planDetailsId); if (deviceExamineRecordDto == null) { throw new ErrorException("该计划明细暂无核查记录,请先录入后再导出"); } DeviceExaminePlanDetails planDetails = deviceExaminePlanDetailsMapper.selectById(planDetailsId); Device device = planDetails != null && planDetails.getDeviceId() != null ? deviceMapper.selectById(planDetails.getDeviceId()) : null; 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"); response.setHeader("Content-disposition", "attachment;filename=" + fileName); OutputStream os = response.getOutputStream(); document.write(os); os.flush(); os.close(); document.close(); } catch (ErrorException e) { throw e; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("导出失败"); } } /** * 在标题上方插入固定的记录编号行 */ private void insertRecordNumberLine(XWPFDocument document) { List 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 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 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 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 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 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(); } }