diff --git a/detection/pom.xml b/detection/pom.xml index dc7afcc6..bf1f5825 100644 --- a/detection/pom.xml +++ b/detection/pom.xml @@ -126,12 +126,19 @@ + com.njcn.gather wave-comtrade 1.0.0 + + + com.njcn.gather + report-generator + 1.0.0 + diff --git a/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java b/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java index 8e461226..1c53cb9d 100644 --- a/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java @@ -61,7 +61,6 @@ import com.njcn.gather.plan.service.IAdPlanSourceService; import com.njcn.gather.plan.service.IAdPlanStandardDevService; import com.njcn.gather.plan.service.IAdPlanTestConfigService; import com.njcn.gather.pojo.enums.DetectionResponseEnum; -import com.njcn.gather.report.pojo.constant.ReportConstant; import com.njcn.gather.report.pojo.po.PqReport; import com.njcn.gather.report.service.IPqReportService; import com.njcn.gather.script.pojo.po.PqScript; @@ -82,6 +81,7 @@ import com.njcn.gather.system.dictionary.pojo.po.DictType; import com.njcn.gather.system.dictionary.service.IDictDataService; import com.njcn.gather.system.dictionary.service.IDictTreeService; import com.njcn.gather.system.dictionary.service.IDictTypeService; +import com.njcn.gather.tools.report.model.constant.ReportConstant; import com.njcn.gather.type.pojo.po.DevType; import com.njcn.gather.type.service.IDevTypeService; import com.njcn.gather.user.user.pojo.po.SysUser; diff --git a/detection/src/main/java/com/njcn/gather/report/controller/ReportController.java b/detection/src/main/java/com/njcn/gather/report/controller/ReportController.java index 17d92530..042594eb 100644 --- a/detection/src/main/java/com/njcn/gather/report/controller/ReportController.java +++ b/detection/src/main/java/com/njcn/gather/report/controller/ReportController.java @@ -160,7 +160,7 @@ public class ReportController extends BaseController { @OperateInfo @PostMapping("/uploadReportToCloud") @ApiOperation("批量上传检测报告到云端") - @ApiImplicitParam(name = "deviceIds", value = "被检设备ID列表,为空时上传所有已生成报告的设备", required = false) + @ApiImplicitParam(name = "deviceIds", value = "被检设备ID列表,为空时上传所有已生成报告的设备") public HttpResult uploadReportToCloud(@RequestBody(required = false) List deviceIds) { String methodDescribe = getMethodDescribe("uploadReportToCloud"); LogUtil.njcnDebug(log, "{},设备ID列表为:{}", methodDescribe, deviceIds); diff --git a/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java b/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java index 6ab6dd82..759c0e28 100644 --- a/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java @@ -12,7 +12,6 @@ import cn.hutool.extra.qrcode.QrCodeUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -21,7 +20,6 @@ import com.njcn.common.pojo.constant.PatternRegex; import com.njcn.common.pojo.enums.common.DataStateEnum; import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.utils.images.ImageConverter; -import com.njcn.gather.detection.handler.SocketDevResponseService; import com.njcn.gather.detection.pojo.constant.DetectionCommunicateConstant; import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum; import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum; @@ -29,10 +27,7 @@ import com.njcn.gather.detection.pojo.param.PreDetectionParam; import com.njcn.gather.detection.pojo.vo.DetectionData; import com.njcn.gather.detection.pojo.vo.SocketMsg; import com.njcn.gather.detection.util.socket.SocketManager; -import com.njcn.gather.detection.util.socket.cilent.NettyClient; -import com.njcn.gather.detection.util.socket.cilent.NettyDevClientHandler; import com.njcn.gather.device.mapper.PqDevMapper; -import com.njcn.gather.device.mapper.PqDevSubMapper; import com.njcn.gather.device.pojo.enums.CheckStateEnum; import com.njcn.gather.device.pojo.enums.DevReportStateEnum; import com.njcn.gather.device.pojo.param.PqDevParam; @@ -48,20 +43,15 @@ import com.njcn.gather.pojo.enums.DetectionResponseEnum; import com.njcn.gather.report.mapper.PqReportMapper; import com.njcn.gather.report.pojo.DevReportParam; import com.njcn.gather.report.pojo.constant.PowerConstant; -import com.njcn.gather.report.pojo.constant.ReportConstant; import com.njcn.gather.report.pojo.enums.*; import com.njcn.gather.report.pojo.param.ReportParam; import com.njcn.gather.report.pojo.po.PqReport; import com.njcn.gather.report.pojo.result.SingleTestResult; import com.njcn.gather.report.pojo.vo.PqReportVO; import com.njcn.gather.report.service.IPqReportService; -import com.njcn.gather.report.utils.BookmarkUtil; -import com.njcn.gather.report.utils.Docx4jUtil; -import com.njcn.gather.report.utils.WordUtil; import com.njcn.gather.result.service.IResultService; import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import com.njcn.gather.script.service.IPqScriptDtlsService; -import com.njcn.gather.storage.pojo.param.SingleNonHarmParam; import com.njcn.gather.storage.pojo.po.SimAndDigHarmonicResult; import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult; import com.njcn.gather.storage.service.SimAndDigHarmonicService; @@ -70,6 +60,12 @@ import com.njcn.gather.system.cfg.pojo.enums.SceneEnum; import com.njcn.gather.system.cfg.service.ISysTestConfigService; import com.njcn.gather.system.dictionary.pojo.po.DictData; import com.njcn.gather.system.dictionary.service.IDictDataService; +import com.njcn.gather.tools.report.model.constant.ReportConstant; +import com.njcn.gather.tools.report.service.IWordReportService; +import com.njcn.gather.tools.report.util.BookmarkUtil; +import com.njcn.gather.tools.report.util.Docx4jUtil; +import com.njcn.gather.tools.report.util.DocxMergeUtil; +import com.njcn.gather.tools.report.util.WordDocumentUtil; import com.njcn.gather.type.pojo.po.DevType; import com.njcn.gather.type.service.IDevTypeService; import com.njcn.http.util.RestTemplateUtil; @@ -80,13 +76,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPReply; -import org.apache.poi.xwpf.usermodel.*; import org.docx4j.jaxb.Context; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import org.docx4j.wml.*; import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.http.ResponseEntity; @@ -94,6 +88,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import javax.xml.bind.JAXBElement; import java.awt.*; @@ -108,14 +103,11 @@ import java.nio.file.Paths; import java.util.List; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; /** - * @author makejava + * @author caozehui * @date 2025-03-19 */ @Slf4j @@ -161,37 +153,20 @@ public class PqReportServiceImpl extends ServiceImpl i private final IPqDevService iPqDevService; private final PqDevMapper pqDevMapper; private final IPqDevSubService iPqDevSubService; - private final IDictDataService dictDataService; - private final IAdPlanService adPlanService; - private final IPqScriptDtlsService pqScriptDtlsService; - private final SimAndDigNonHarmonicService adNonHarmonicService; - private final SimAndDigHarmonicService adHarmonicService; - private final IDevTypeService devTypeService; - private final IResultService resultService; - private final ISysTestConfigService sysTestConfigService; - - private final SocketDevResponseService socketDevResponseService; private final SocketManager socketManager; + private final IWordReportService wordReportService; - @Autowired + @Resource private RestTemplateUtil restTemplateUtil; - private final ThreadPoolExecutor executor = new ThreadPoolExecutor( - 4, 8, 30, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(100), - // 队列满时由主线程执行 - new ThreadPoolExecutor.CallerRunsPolicy() - ); - - private final long FILE_SIZE_LIMIT = 5 * 1024 * 1024; @Override public Page list(ReportParam.QueryParam queryParam) { @@ -309,22 +284,23 @@ public class PqReportServiceImpl extends ServiceImpl i /** * 上传文件,并设置pqReport的basePath和detailPath属性 * - * @param reportParam - * @param pqReport - * @param isAdd + * @param reportParam 报告参数 + * @param pqReport 报告信息 + * @param isAdd 是否添加 */ private void uploadFile(ReportParam reportParam, PqReport pqReport, boolean isAdd) { MultipartFile baseFile = reportParam.getBaseFile(); MultipartFile detailFile = reportParam.getDetailFile(); String newDir = templatePath + File.separator + reportParam.getName() + File.separator + reportParam.getVersion() + File.separator; + long FILE_SIZE_LIMIT = 5 * 1024 * 1024; if (isAdd) { if (ObjectUtil.isNotNull(baseFile) && !baseFile.isEmpty() && ObjectUtil.isNotNull(detailFile) && !detailFile.isEmpty()) { String baseOriginalFilename = baseFile.getOriginalFilename(); String detailOriginalFilename = detailFile.getOriginalFilename(); - if (!baseOriginalFilename.endsWith(".docx") || !detailOriginalFilename.endsWith(".docx")) { + if (!baseOriginalFilename.endsWith(ReportConstant.DOCX) || !detailOriginalFilename.endsWith(ReportConstant.DOCX)) { throw new BusinessException(ReportResponseEnum.FILE_SUFFIX_ERROR); } if (baseOriginalFilename.equals(detailOriginalFilename)) { @@ -351,7 +327,7 @@ public class PqReportServiceImpl extends ServiceImpl i String baseFileOriginalFilename = ""; if (ObjectUtil.isNotNull(baseFile) && !baseFile.isEmpty()) { baseFileOriginalFilename = baseFile.getOriginalFilename(); - if (!baseFileOriginalFilename.endsWith(".docx")) { + if (!baseFileOriginalFilename.endsWith(ReportConstant.DOCX)) { throw new BusinessException(ReportResponseEnum.FILE_SUFFIX_ERROR); } if (baseFile.getSize() > FILE_SIZE_LIMIT) { @@ -362,7 +338,7 @@ public class PqReportServiceImpl extends ServiceImpl i String detailFileOriginalFilename = ""; if (ObjectUtil.isNotNull(detailFile) && !detailFile.isEmpty()) { detailFileOriginalFilename = detailFile.getOriginalFilename(); - if (!detailFileOriginalFilename.endsWith(".docx")) { + if (!detailFileOriginalFilename.endsWith(ReportConstant.DOCX)) { throw new BusinessException(ReportResponseEnum.FILE_SUFFIX_ERROR); } if (detailFile.getSize() > FILE_SIZE_LIMIT) { @@ -370,17 +346,17 @@ public class PqReportServiceImpl extends ServiceImpl i } } - if (!"".equals(baseFileOriginalFilename) && !"".equals(detailFileOriginalFilename)) { + if (!baseFileOriginalFilename.isEmpty() && !detailFileOriginalFilename.isEmpty()) { if (baseFileOriginalFilename.equals(detailFileOriginalFilename)) { throw new BusinessException(ReportResponseEnum.FILE_NAME_SAME_ERROR); } } - if (!"".equals(baseFileOriginalFilename)) { + if (!baseFileOriginalFilename.isEmpty()) { if (baseFileOriginalFilename.equals(oldPqReport.getDetailPath().substring(oldPqReport.getDetailPath().lastIndexOf(File.separator) + 1))) { throw new BusinessException(ReportResponseEnum.FILE_NAME_SAME_ERROR); } } - if (!"".equals(detailFileOriginalFilename)) { + if (!detailFileOriginalFilename.isEmpty()) { if (detailFileOriginalFilename.equals(oldPqReport.getBasePath().substring(oldPqReport.getBasePath().lastIndexOf(File.separator) + 1))) { throw new BusinessException(ReportResponseEnum.FILE_NAME_SAME_ERROR); } @@ -422,13 +398,13 @@ public class PqReportServiceImpl extends ServiceImpl i } } - if (!"".equals(baseFileOriginalFilename)) { + if (!baseFileOriginalFilename.isEmpty()) { pqReport.setBasePath(newDir + baseFileOriginalFilename); Paths.get(oldPqReport.getBasePath()).toFile().delete(); Paths.get(newDir + oldPqReport.getBasePath().substring(oldPqReport.getBasePath().lastIndexOf(File.separator) + 1)).toFile().delete(); this.uploadFile(baseFile, pqReport.getBasePath()); } - if (!"".equals(detailFileOriginalFilename)) { + if (!detailFileOriginalFilename.isEmpty()) { pqReport.setDetailPath(newDir + detailFileOriginalFilename); Paths.get(oldPqReport.getDetailPath()).toFile().delete(); Paths.get(newDir + oldPqReport.getDetailPath().substring(oldPqReport.getDetailPath().lastIndexOf(File.separator) + 1)).toFile().delete(); @@ -560,10 +536,11 @@ public class PqReportServiceImpl extends ServiceImpl i @Override public void generateReport(DevReportParam devReportParam) { AdPlan plan = adPlanService.getById(devReportParam.getPlanId()); + // 0 - 模板占位符更新, 1 - 根据配置模版动态组合生产的报告 if (plan.getAssociateReport() == 1) { this.generateReportByPlan(plan, devReportParam); } else if (plan.getAssociateReport() == 0) { - this.generateReportByDevType(plan, devReportParam); + this.generateReportByDevType(devReportParam); } } @@ -572,14 +549,13 @@ public class PqReportServiceImpl extends ServiceImpl i * 根据设备类型生成报告 * 注:该方法目前仅支持楼下出厂检测场景,属于模板占位符替换方式,后期可能会有调整 * - * @param plan 计划信息 * @param devReportParam 被检设备信息 */ - private void generateReportByDevType(AdPlan plan, DevReportParam devReportParam) { + private void generateReportByDevType(DevReportParam devReportParam) { devReportParam.getDevIdList().forEach(devId -> { - devReportParam.setDevId(devId); // 根据设备类型找到报告模板 - PqDevVO pqDevVO = iPqDevService.getPqDevById(devReportParam.getDevId()); + PqDevVO pqDevVO = iPqDevService.getPqDevById(devId); + devReportParam.setDevId(devId); if (Objects.isNull(pqDevVO)) { throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST); } @@ -595,34 +571,30 @@ public class PqReportServiceImpl extends ServiceImpl i // 读取模板文件 ClassPathResource resource = new ClassPathResource("/model/" + reportName.getCode() + ReportConstant.DOCX); try (InputStream inputStream = resource.getInputStream()) { - // 加载Word文档 - XWPFDocument baseModelDocument = new XWPFDocument(inputStream); // 处理基础模版中的信息 - Map baseModelDataMap = dealBaseModelData(pqDevVO, devType, "${", "}"); - // 替换模板中的信息,避免信息丢失,段落和表格均参与替换 - WordUtil.replacePlaceholders(baseModelDocument, baseModelDataMap); + Map baseModelDataMap = dealBaseModelData(pqDevVO, devType); + InputStream wordFinishInputStream = wordReportService.replacePlaceholders(inputStream, baseModelDataMap); + List wordFileInputStreams = new ArrayList<>(); + wordFileInputStreams.add(wordFinishInputStream); // 处理数据页中的信息 - dealDataModel(baseModelDocument, devReportParam, pqDevVO); + dealDataModel(wordFileInputStreams, devReportParam, pqDevVO); + // 合并文档 + InputStream finalWordStream = DocxMergeUtil.mergeDocuments(wordFileInputStreams); // 处理需要输出的目录地址 基础路径+设备类型+装置编号.docx // 最终文件输出的路径 // String dirPath = reportPath.concat(File.separator).concat(devType.getName()); String dirPath = reportPath; // 确保目录存在 ensureDirectoryExists(dirPath); - String reportFullPath = dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(".docx"); - FileOutputStream out = new FileOutputStream(reportFullPath); + String reportFullPath = dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX); // 4. 保存新的Word文档 - try { - baseModelDocument.write(out); - } catch (IOException e) { - throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR); - } - out.close(); - this.updateDevAndPlanState(devReportParam.getDevId(), devReportParam.getPlanId()); + WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(finalWordStream); + wordPackage.save(new File(reportFullPath)); + this.updateDevAndPlanState(devId, devReportParam.getPlanId()); // 异步将有效的二维码下装到被检设备 CompletableFuture.runAsync(() -> { try { - sendQrToDevice(pqDevVO.getIp(), pqDevVO.getCreateId() + ".docx"); + sendQrToDevice(pqDevVO.getIp(), pqDevVO.getCreateId() + ReportConstant.DOCX); log.info("二维码下装成功,设备IP: {}", pqDevVO.getIp()); } catch (Exception e) { log.error("二维码下装失败,设备IP: {}", pqDevVO.getIp(), e); @@ -643,7 +615,7 @@ public class PqReportServiceImpl extends ServiceImpl i } } }); - } catch (IOException e) { + } catch (Exception e) { log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e); throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR); } @@ -694,7 +666,7 @@ public class PqReportServiceImpl extends ServiceImpl i data.set("file", base64String); sendFileMsg.setData(data.toString()); String msg = JSON.toJSONString(sendFileMsg); - Channel channel = SocketManager.getChannelByUserId(RequestUtil.getLoginName()+ DetectionCommunicateConstant.DEV); + Channel channel = SocketManager.getChannelByUserId(RequestUtil.getLoginName() + DetectionCommunicateConstant.DEV); if (Objects.isNull(channel) || !channel.isActive()) { // 进行源通信连接 PreDetectionParam preDetectionParam = new PreDetectionParam(); @@ -769,13 +741,14 @@ public class PqReportServiceImpl extends ServiceImpl i * @param devReportParam 设备信息 */ private void generateReportByPlan(AdPlan plan, DevReportParam devReportParam) { + // 支持批量生成报告 devReportParam.getDevIdList().forEach(devId -> { - devReportParam.setDevId(devId); // 准备被检设备的基础信息 - PqDevVO pqDevVO = iPqDevService.getPqDevById(devReportParam.getDevId()); + PqDevVO pqDevVO = iPqDevService.getPqDevById(devId); if (Objects.isNull(pqDevVO)) { throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST); } + devReportParam.setDevId(devId); // 获取设备型号 DevType devType = devTypeService.getById(pqDevVO.getDevType()); if (Objects.isNull(devType)) { @@ -785,24 +758,27 @@ public class PqReportServiceImpl extends ServiceImpl i if (Objects.isNull(report)) { throw new BusinessException(ReportResponseEnum.REPORT_TEMPLATE_NOT_EXIST); } - try { - WordprocessingMLPackage baseModelDocument = WordprocessingMLPackage.load(new File(report.getBasePath())); - WordprocessingMLPackage detailModelDocument = WordprocessingMLPackage.load(new File(report.getDetailPath())); + Path basePath = Paths.get(report.getBasePath()); + Path detailPath = Paths.get(report.getDetailPath()); + try (InputStream baseInputStream = Files.newInputStream(basePath); + InputStream detailInputStream = Files.newInputStream(detailPath)) { + WordprocessingMLPackage detailModelDocument = WordprocessingMLPackage.load(detailInputStream); // 获取文档基础部分,并替换占位符 + Map baseModelDataMap = dealBaseModelData(pqDevVO, devType); + InputStream wordFinishInputStream = wordReportService.replacePlaceholders(baseInputStream, baseModelDataMap); + WordprocessingMLPackage baseModelDocument = WordprocessingMLPackage.load(wordFinishInputStream); MainDocumentPart baseDocumentPart = baseModelDocument.getMainDocumentPart(); - Map baseModelDataMap = dealBaseModelData(pqDevVO, devType, "", ""); - baseDocumentPart.variableReplace(baseModelDataMap); + // 获取数据模版页内容,根据脚本动态组装数据页内容 MainDocumentPart detailDocumentPart = detailModelDocument.getMainDocumentPart(); -// dealDataModelScattered(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO); dealDataModelScatteredByBookmark(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO); + // 保存新的文档 String dirPath = reportPath.concat(File.separator).concat(devType.getName()); // 确保目录存在 ensureDirectoryExists(dirPath); baseModelDocument.save(new File(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX))); - - this.updateDevAndPlanState(devReportParam.getDevId(), devReportParam.getPlanId()); + this.updateDevAndPlanState(devId, devReportParam.getPlanId()); } catch (Exception e) { log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e); throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR); @@ -847,8 +823,8 @@ public class PqReportServiceImpl extends ServiceImpl i Collections.sort(bookmarkEnums); // 定义个结果,以便存在结果信息的书签 Map> resultMap = new HashMap<>(); - List todoInsertList = new ArrayList<>(); - BookmarkUtil.BookmarkInfo bookmarkInfo = null; + List todoInsertList; + BookmarkUtil.BookmarkInfo bookmarkInfo; // 书签在文档的位置 for (int i = 0; i < bookmarkEnums.size(); i++) { BookmarkEnum bookmarkEnum = bookmarkEnums.get(i); @@ -983,13 +959,13 @@ public class PqReportServiceImpl extends ServiceImpl i cellValues.add(total); break; case TEST_RESULT_LINE: - for (int i = 0; i < value.size(); i++) { - cellValues.add(value.get(i) ? "合格" : "不合格"); + for (Boolean aBoolean : value) { + cellValues.add(aBoolean ? "合格" : "不合格"); } break; case TEST_RESULT_DETAIL: - for (int i = 0; i < value.size(); i++) { - cellValues.add(value.get(i) ? "合格" : "不合格"); + for (Boolean aBoolean : value) { + cellValues.add(aBoolean ? "合格" : "不合格"); } cellValues.add(total); break; @@ -1028,8 +1004,13 @@ public class PqReportServiceImpl extends ServiceImpl i for (int i = 0; i < devChns; i++) { // 回路标题 P titleParagraph = factory.createP(); - Integer lineNo = i + 1; - Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo, 28, true); + // 如果回路只有一个,则不需要加编号 + int lineNo = i + 1; + if (devChns > 1) { + Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo, 28, true); + } else { + Docx4jUtil.createTitle(factory, titleParagraph, "测量回路", 28, true); + } todoInsertList.add(titleParagraph); // 依次处理大项文档内容 Iterator>> iterator = scriptMap.entrySet().iterator(); @@ -1314,9 +1295,9 @@ public class PqReportServiceImpl extends ServiceImpl i StringBuilder filePath = new StringBuilder(reportPath.concat(File.separator)); if (SceneEnum.LEAVE_FACTORY_TEST.getValue().equals(currrentScene)) { - filePath.append(pqDevVO.getCreateId()).append(".docx"); + filePath.append(pqDevVO.getCreateId()).append(ReportConstant.DOCX); } else { - filePath.append(devType.getName()).append(File.separator).append(pqDevVO.getCreateId()).append(".docx"); + filePath.append(devType.getName()).append(File.separator).append(pqDevVO.getCreateId()).append(ReportConstant.DOCX); } File reportFile = new File(filePath.toString()); if (!reportFile.exists()) { @@ -1329,7 +1310,7 @@ public class PqReportServiceImpl extends ServiceImpl i // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); - String fileName = pqDevVO.getCreateId() + ".docx"; + String fileName = pqDevVO.getCreateId() + ReportConstant.DOCX; response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); // 将文件内容写入响应输出流 @@ -1349,7 +1330,7 @@ public class PqReportServiceImpl extends ServiceImpl i } @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public boolean documented(List ids) { if (CollUtil.isNotEmpty(ids)) { List pqDevVOList = pqDevMapper.listByDevIds(ids); @@ -1401,66 +1382,66 @@ public class PqReportServiceImpl extends ServiceImpl i * 处理基础模版中的信息,非数据页报告 * 此处为什么要抽出拼接的前缀&后缀,是因为Docx4j工具包替换时会默认增加${},故在使用docx4j时前后缀必须为空 */ - private Map dealBaseModelData(PqDevVO pqDevVO, DevType devType, String prefix, String suffix) { + private Map dealBaseModelData(PqDevVO pqDevVO, DevType devType) { // 首先获取非数据页中需要的信息 Map baseModelMap = new HashMap<>(32); // 获取设备型号 - baseModelMap.put(prefix + BaseReportKeyEnum.DEV_TYPE.getKey() + suffix, devType.getName()); + baseModelMap.put(BaseReportKeyEnum.DEV_TYPE.getKey(), devType.getName()); // 检测员 - baseModelMap.put(prefix + BaseReportKeyEnum.INSPECTOR.getKey() + suffix, pqDevVO.getCheckBy() + ""); + baseModelMap.put(BaseReportKeyEnum.INSPECTOR.getKey(), pqDevVO.getCheckBy() + ""); // 调试日期 if (pqDevVO.getCheckTime() != null) { - baseModelMap.put(prefix + BaseReportKeyEnum.TEST_DATE.getKey() + suffix, DateUtil.format(pqDevVO.getCheckTime(), DatePattern.CHINESE_DATE_PATTERN)); + baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), DateUtil.format(pqDevVO.getCheckTime(), DatePattern.CHINESE_DATE_PATTERN)); } else { - baseModelMap.put(prefix + BaseReportKeyEnum.TEST_DATE.getKey() + suffix, DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN)); + baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN)); } // 装置编码 - baseModelMap.put(prefix + BaseReportKeyEnum.DEV_CODE.getKey() + suffix, pqDevVO.getCreateId()); + baseModelMap.put(BaseReportKeyEnum.DEV_CODE.getKey(), pqDevVO.getCreateId()); // 工作电源 - baseModelMap.put(prefix + BaseReportKeyEnum.POWER.getKey() + suffix, devType.getPower()); + baseModelMap.put(BaseReportKeyEnum.POWER.getKey(), devType.getPower()); // 额定电流 - baseModelMap.put(prefix + BaseReportKeyEnum.DEV_CURR.getKey() + suffix, pqDevVO.getDevCurr().toString().concat(PowerConstant.CURRENT_UNIT)); + baseModelMap.put(BaseReportKeyEnum.DEV_CURR.getKey(), pqDevVO.getDevCurr().toString().concat(PowerConstant.CURRENT_UNIT)); // 额定电压 - baseModelMap.put(prefix + BaseReportKeyEnum.DEV_VOLT.getKey() + suffix, pqDevVO.getDevVolt().toString().concat(PowerConstant.VOLTAGE_UNIT)); + baseModelMap.put(BaseReportKeyEnum.DEV_VOLT.getKey(), pqDevVO.getDevVolt().toString().concat(PowerConstant.VOLTAGE_UNIT)); // 通道数 - baseModelMap.put(prefix + BaseReportKeyEnum.COUNT.getKey() + suffix, pqDevVO.getDevChns().toString()); + baseModelMap.put(BaseReportKeyEnum.COUNT.getKey(), pqDevVO.getDevChns().toString()); // 制造厂家 DictData dictData = dictDataService.getDictDataById(pqDevVO.getManufacturer()); if (ObjectUtil.isNotNull(dictData)) { - baseModelMap.put(prefix + BaseReportKeyEnum.MANUFACTURER.getKey() + suffix, dictData.getName()); + baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), dictData.getName()); } else { - baseModelMap.put(prefix + BaseReportKeyEnum.MANUFACTURER.getKey() + suffix, StrPool.TAB); + baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), StrPool.TAB); } // 委托方 String delegate = pqDevVO.getDelegate(); if (StrUtil.isNotBlank(delegate)) { DictData delegateDictData = dictDataService.getDictDataById(pqDevVO.getManufacturer()); if (ObjectUtil.isNotNull(delegateDictData)) { - baseModelMap.put(prefix + BaseReportKeyEnum.DELEGATE.getKey() + suffix, dictData.getName()); + baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), dictData.getName()); } else { - baseModelMap.put(prefix + BaseReportKeyEnum.DELEGATE.getKey() + suffix, StrPool.TAB); + baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB); } } else { - baseModelMap.put(prefix + BaseReportKeyEnum.DELEGATE.getKey() + suffix, StrPool.TAB); + baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB); } // 实验室温度 - baseModelMap.put(prefix + BaseReportKeyEnum.TEMPERATURE.getKey() + suffix, Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString()); + baseModelMap.put(BaseReportKeyEnum.TEMPERATURE.getKey(), Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString()); // 实验室湿度 - baseModelMap.put(prefix + BaseReportKeyEnum.HUMIDITY.getKey() + suffix, Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString()); + baseModelMap.put(BaseReportKeyEnum.HUMIDITY.getKey(), Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString()); // 样品编号 - baseModelMap.put(prefix + BaseReportKeyEnum.SAMPLE_ID.getKey() + suffix, StrUtil.isEmpty(pqDevVO.getSampleId()) ? StrPool.TAB : pqDevVO.getSampleId()); + baseModelMap.put(BaseReportKeyEnum.SAMPLE_ID.getKey(), StrUtil.isEmpty(pqDevVO.getSampleId()) ? StrPool.TAB : pqDevVO.getSampleId()); // 收样日期 - baseModelMap.put(prefix + BaseReportKeyEnum.ARRIVED_DATE.getKey() + suffix, Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : String.valueOf(pqDevVO.getArrivedDate())); + baseModelMap.put(BaseReportKeyEnum.ARRIVED_DATE.getKey(), Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : String.valueOf(pqDevVO.getArrivedDate())); // 检测日期 - baseModelMap.put(prefix + BaseReportKeyEnum.TEST_DATE.getKey() + suffix, Objects.isNull(pqDevVO.getCheckTime()) ? StrPool.TAB : String.valueOf(pqDevVO.getCheckTime()).substring(0, 10)); - baseModelMap.put(prefix + BaseReportKeyEnum.TEMPERATURE.getKey() + suffix, Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString()); - baseModelMap.put(prefix + BaseReportKeyEnum.HUMIDITY.getKey() + suffix, Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString()); - baseModelMap.put(prefix + BaseReportKeyEnum.YEAR.getKey() + suffix, DateUtil.format(new Date(), DatePattern.NORM_YEAR_PATTERN)); - baseModelMap.put(prefix + BaseReportKeyEnum.MONTH.getKey() + suffix, DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_PATTERN).substring(4)); - baseModelMap.put(prefix + BaseReportKeyEnum.DAY.getKey() + suffix, DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN).substring(6)); - baseModelMap.put(prefix + BaseReportKeyEnum.YEAR_MONTH_DAY.getKey() + suffix, DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN)); + baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), Objects.isNull(pqDevVO.getCheckTime()) ? StrPool.TAB : String.valueOf(pqDevVO.getCheckTime()).substring(0, 10)); + baseModelMap.put(BaseReportKeyEnum.TEMPERATURE.getKey(), Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString()); + baseModelMap.put(BaseReportKeyEnum.HUMIDITY.getKey(), Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString()); + baseModelMap.put(BaseReportKeyEnum.YEAR.getKey(), DateUtil.format(new Date(), DatePattern.NORM_YEAR_PATTERN)); + baseModelMap.put(BaseReportKeyEnum.MONTH.getKey(), DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_PATTERN).substring(4)); + baseModelMap.put(BaseReportKeyEnum.DAY.getKey(), DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN).substring(6)); + baseModelMap.put(BaseReportKeyEnum.YEAR_MONTH_DAY.getKey(), DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN)); return baseModelMap; } @@ -1468,37 +1449,24 @@ public class PqReportServiceImpl extends ServiceImpl i /** * 获取数据页的信息 * - * @param baseModelDocument 非数据页的内容 - * @param devReportParam 查询参数 + * @param devReportParam 查询参数 */ - private void dealDataModel(XWPFDocument baseModelDocument, DevReportParam devReportParam, PqDevVO pqDevVO) throws IOException { - //AdPlan adPlan = adPlanService.getById(devReportParam.getPlanId()); - //String scriptId = adPlan.getScriptId(); + private void dealDataModel(List wordFileInputStreams, DevReportParam devReportParam, PqDevVO pqDevVO) throws Exception { Integer devChns = pqDevVO.getDevChns(); for (int i = 1; i <= devChns; i++) { ClassPathResource resource = new ClassPathResource("/model/report_table.docx"); - XWPFDocument dataModelDocumentTemp = new XWPFDocument(resource.getInputStream()); - - SingleNonHarmParam singleNonHarmParam = new SingleNonHarmParam(); - singleNonHarmParam.setPlanCode(devReportParam.getPlanCode()); - singleNonHarmParam.setDevId(pqDevVO.getId()); - singleNonHarmParam.setChannelNo(i); - // 获取数据 - Map dataModelMap = new HashMap<>(); - dataModelMap.put("${CreateId}", pqDevVO.getCreateId()); - dataModelMap.put("${total}", pqDevVO.getDevChns().toString()); - dataModelMap.put("${count}", i + ""); - + Map dataModelMap = new HashMap<>(16); // 读取模板文件中的占位符 - List allMarkList = getAllKeys(dataModelDocumentTemp); + Set allMarkList = WordDocumentUtil.extractPlaceholders(resource.getInputStream(), false, Arrays.asList("CreateId", "total", "count")); Map> indexKeysMap = allMarkList.stream() .collect(Collectors.groupingBy( - obj -> obj.split("#")[0].replace("${", ""), + obj -> obj.split("#")[0], Collectors.mapping(obj -> { int index1 = obj.indexOf("#") + 1; return obj.substring(index1, obj.indexOf("#", index1)); - }, Collectors.toSet()))); //key为index,value为该index下所有测试项对应的code + //key为index,value为该index下所有测试项对应的code + }, Collectors.toSet()))); List simAndDigNonHarmonicResultList = adNonHarmonicService.listSimAndDigBaseResult(devReportParam.getScriptId(), devReportParam.getPlanCode(), devReportParam.getDevId() + "_" + i); List adHarmonicResultList = adHarmonicService.listAllResultData(devReportParam.getScriptId(), devReportParam.getPlanCode(), devReportParam.getDevId() + "_" + i); @@ -1518,21 +1486,21 @@ public class PqReportServiceImpl extends ServiceImpl i } } }); - + dataModelMap.put("CreateId", pqDevVO.getCreateId()); + dataModelMap.put("total", pqDevVO.getDevChns().toString()); + dataModelMap.put("count", i + ""); // 替换文档内容 - WordUtil.replacePlaceholdersInParagraphs(dataModelDocumentTemp, dataModelMap); - WordUtil.replacePlaceholdersInTables(dataModelDocumentTemp, dataModelMap); - WordUtil.appendDocument(baseModelDocument, dataModelDocumentTemp); + wordFileInputStreams.add(wordReportService.replacePlaceholders(resource.getInputStream(), dataModelMap)); } } /** * 填充数据(ABC相) * - * @param allNonHarmonicResultList - * @param dataModelMap - * @param keys - * @param index + * @param allNonHarmonicResultList 结果数据 + * @param dataModelMap 替换数据 + * @param keys keys + * @param index index */ private void fillMapValueABC(List allNonHarmonicResultList, Map dataModelMap, Set keys, String index) { keys.forEach(key -> { @@ -1540,9 +1508,9 @@ public class PqReportServiceImpl extends ServiceImpl i if (CollectionUtil.isNotEmpty(resultList)) { SimAndDigNonHarmonicResult adNonHarmonicResult = resultList.get(0); if (ObjectUtil.isNotNull(adNonHarmonicResult)) { - dataModelMap.put("${" + index + "#" + key + "#A}", devValue(adNonHarmonicResult.getAValue(), 1, 1)); - dataModelMap.put("${" + index + "#" + key + "#B}", devValue(adNonHarmonicResult.getBValue(), 1, 1)); - dataModelMap.put("${" + index + "#" + key + "#C}", devValue(adNonHarmonicResult.getCValue(), 1, 1)); + dataModelMap.put(index + "#" + key + "#A", devValue(adNonHarmonicResult.getAValue(), 1, 1)); + dataModelMap.put(index + "#" + key + "#B", devValue(adNonHarmonicResult.getBValue(), 1, 1)); + dataModelMap.put(index + "#" + key + "#C", devValue(adNonHarmonicResult.getCValue(), 1, 1)); } } }); @@ -1551,10 +1519,10 @@ public class PqReportServiceImpl extends ServiceImpl i /** * 填充数据(T相) * - * @param allNonHarmonicResultList - * @param dataModelMap - * @param keys - * @param index + * @param allNonHarmonicResultList 结果数据 + * @param dataModelMap 替换数据 + * @param keys key + * @param index index */ private void fillMapValueT(List allNonHarmonicResultList, Map dataModelMap, Set keys, String index) { keys.forEach(key -> { @@ -1562,7 +1530,7 @@ public class PqReportServiceImpl extends ServiceImpl i if (CollectionUtil.isNotEmpty(resultList)) { SimAndDigNonHarmonicResult adNonHarmonicResult = resultList.get(0); if (ObjectUtil.isNotNull(adNonHarmonicResult)) { - dataModelMap.put("${" + index + "#" + key + "#T}", devValue(adNonHarmonicResult.getTValue(), 1, 1)); + dataModelMap.put(index + "#" + key + "#T", devValue(adNonHarmonicResult.getTValue(), 1, 1)); } } }); @@ -1571,10 +1539,10 @@ public class PqReportServiceImpl extends ServiceImpl i /** * 填充数据(谐波类) * - * @param allHarmonicResultList - * @param dataModelMap - * @param keys - * @param index + * @param allHarmonicResultList 结果数据 + * @param dataModelMap 替换数据 + * @param keys key + * @param index index */ private void fillMapValueHarm(List allHarmonicResultList, Map dataModelMap, Set keys, String index) { keys.forEach(key -> { @@ -1609,9 +1577,9 @@ public class PqReportServiceImpl extends ServiceImpl i String aBase = devValue(adHarmonicResult.getAValue1(), 1, 1); String bBase = devValue(adHarmonicResult.getBValue1(), 1, 1); String cBase = devValue(adHarmonicResult.getCValue1(), 1, 1); - dataModelMap.put("${" + index + "#" + key + "#A#1}", aBase); - dataModelMap.put("${" + index + "#" + key + "#B#1}", bBase); - dataModelMap.put("${" + index + "#" + key + "#C#1}", cBase); + dataModelMap.put(index + "#" + key + "#A#1", aBase); + dataModelMap.put(index + "#" + key + "#B#1", bBase); + dataModelMap.put(index + "#" + key + "#C#1", cBase); // 基波 double aBaseValue = baseValue; @@ -1629,71 +1597,39 @@ public class PqReportServiceImpl extends ServiceImpl i } } - dataModelMap.put("${" + index + "#" + key + "#A#2}", devValue(adHarmonicResult.getAValue2(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#2}", devValue(adHarmonicResult.getBValue2(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#2}", devValue(adHarmonicResult.getCValue2(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#2", devValue(adHarmonicResult.getAValue2(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#2", devValue(adHarmonicResult.getBValue2(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#2", devValue(adHarmonicResult.getCValue2(), cBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#A#5}", devValue(adHarmonicResult.getAValue5(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#5}", devValue(adHarmonicResult.getBValue5(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#5}", devValue(adHarmonicResult.getCValue5(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#5", devValue(adHarmonicResult.getAValue5(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#5", devValue(adHarmonicResult.getBValue5(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#5", devValue(adHarmonicResult.getCValue5(), cBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#A#7}", devValue(adHarmonicResult.getAValue7(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#7}", devValue(adHarmonicResult.getBValue7(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#7}", devValue(adHarmonicResult.getCValue7(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#7", devValue(adHarmonicResult.getAValue7(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#7", devValue(adHarmonicResult.getBValue7(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#7", devValue(adHarmonicResult.getCValue7(), cBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#A#11}", devValue(adHarmonicResult.getAValue11(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#11}", devValue(adHarmonicResult.getBValue11(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#11}", devValue(adHarmonicResult.getCValue11(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#11", devValue(adHarmonicResult.getAValue11(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#11", devValue(adHarmonicResult.getBValue11(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#11", devValue(adHarmonicResult.getCValue11(), cBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#A#23}", devValue(adHarmonicResult.getAValue23(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#23}", devValue(adHarmonicResult.getBValue23(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#23}", devValue(adHarmonicResult.getCValue23(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#23", devValue(adHarmonicResult.getAValue23(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#23", devValue(adHarmonicResult.getBValue23(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#23", devValue(adHarmonicResult.getCValue23(), cBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#A#35}", devValue(adHarmonicResult.getAValue35(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#35}", devValue(adHarmonicResult.getBValue35(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#35}", devValue(adHarmonicResult.getCValue35(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#35", devValue(adHarmonicResult.getAValue35(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#35", devValue(adHarmonicResult.getBValue35(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#35", devValue(adHarmonicResult.getCValue35(), cBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#A#43}", devValue(adHarmonicResult.getAValue43(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#43}", devValue(adHarmonicResult.getBValue43(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#43}", devValue(adHarmonicResult.getCValue43(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#43", devValue(adHarmonicResult.getAValue43(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#43", devValue(adHarmonicResult.getBValue43(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#43", devValue(adHarmonicResult.getCValue43(), cBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#A#50}", devValue(adHarmonicResult.getAValue50(), aBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#B#50}", devValue(adHarmonicResult.getBValue50(), bBaseValue, percent)); - dataModelMap.put("${" + index + "#" + key + "#C#50}", devValue(adHarmonicResult.getCValue50(), cBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#A#50", devValue(adHarmonicResult.getAValue50(), aBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#B#50", devValue(adHarmonicResult.getBValue50(), bBaseValue, percent)); + dataModelMap.put(index + "#" + key + "#C#50", devValue(adHarmonicResult.getCValue50(), cBaseValue, percent)); } - /** - * 获取文件所有表格中的占位符 - * - * @param dataModelDocumentTemp - * @return - */ - private List getAllKeys(XWPFDocument dataModelDocumentTemp) { - List allMarkList = new ArrayList<>(); - List tables = dataModelDocumentTemp.getTables(); - - for (XWPFTable table : tables) { - List rows = table.getRows(); - for (XWPFTableRow row : rows) { - List cells = row.getTableCells(); - for (XWPFTableCell cell : cells) { - List paragraphs = cell.getParagraphs(); - for (XWPFParagraph paragraph : paragraphs) { - List runs = paragraph.getRuns(); - for (XWPFRun run : runs) { - String text = run.getText(0); - if (StrUtil.isNotBlank(text) && text.startsWith("$")) { - allMarkList.add(text); - } - } - } - } - } - } - return allMarkList.stream().sorted(Comparator.comparing(String::toString)).collect(Collectors.toList()); - } - - private String devValue(String dataJson, double baseValue, Integer percent) { DetectionData tempA = JSONUtil.toBean(dataJson, DetectionData.class); if (Objects.nonNull(tempA) && Objects.nonNull(tempA.getData())) { @@ -1752,21 +1688,16 @@ public class PqReportServiceImpl extends ServiceImpl i @Override public void uploadReportToCloud(List deviceIds) { log.info("开始批量上传检测报告到云端,设备ID列表:{}", deviceIds); - - List deviceSubs = iPqDevSubService.lambdaQuery() .eq(PqDevSub::getReportState, DevReportStateEnum.GENERATED.getValue()) - .in(CollUtil.isNotEmpty(deviceIds),PqDevSub::getDevId, deviceIds).list(); + .in(CollUtil.isNotEmpty(deviceIds), PqDevSub::getDevId, deviceIds).list(); List devIds = deviceSubs.stream().map(PqDevSub::getDevId).collect(Collectors.toList()); - List devices = iPqDevService.lambdaQuery() - .in(CollUtil.isNotEmpty(devIds), PqDev::getId, devIds).list(); - + .in(CollUtil.isNotEmpty(devIds), PqDev::getId, devIds).list(); if (CollUtil.isEmpty(devices)) { log.warn("未找到符合条件的设备,无需上传"); return; } - log.info("找到{}台设备需要上传报告", devices.size()); String dirPath = reportPath; // 确保目录存在 @@ -1776,8 +1707,8 @@ public class PqReportServiceImpl extends ServiceImpl i for (PqDev device : devices) { try { // 构建报告文件路径 - String fileName = device.getCreateId() + ".docx"; - String reportFullPath = dirPath.concat(File.separator).concat(device.getCreateId()).concat(".docx"); + String fileName = device.getCreateId() + ReportConstant.DOCX; + String reportFullPath = dirPath.concat(File.separator).concat(device.getCreateId()).concat(ReportConstant.DOCX); File reportFile = new File(reportFullPath); if (!reportFile.exists()) { diff --git a/detection/src/main/java/com/njcn/gather/report/utils/BookmarkUtil.java b/detection/src/main/java/com/njcn/gather/report/utils/BookmarkUtil.java deleted file mode 100644 index 1cd1018c..00000000 --- a/detection/src/main/java/com/njcn/gather/report/utils/BookmarkUtil.java +++ /dev/null @@ -1,295 +0,0 @@ -package com.njcn.gather.report.utils; - - -import com.njcn.gather.report.pojo.enums.BookmarkEnum; -import com.njcn.gather.report.pojo.enums.PowerIndexEnum; -import org.docx4j.openpackaging.packages.WordprocessingMLPackage; -import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; -import org.docx4j.wml.*; - -import javax.xml.bind.JAXBElement; -import java.util.*; - - -/** - * 递归查找所有书签,并在书签处插入内容 - */ -public class BookmarkUtil { - - - /** - * 书签信息 - */ - public static class BookmarkInfo { - public CTBookmark bookmark; - public P parentParagraph; - public ContentAccessor parentContainer; - } - - /** - * 递归查找所有书签 - */ - public static List findAllBookmarks(ContentAccessor contentAccessor) { - List result = new ArrayList<>(); - for (Object obj : contentAccessor.getContent()) { - Object realObj = (obj instanceof JAXBElement) ? ((JAXBElement) obj).getValue() : obj; - if (realObj instanceof P) { - P p = (P) realObj; - for (Object o2 : p.getContent()) { - Object realO2 = (o2 instanceof JAXBElement) ? ((JAXBElement) o2).getValue() : o2; - if (realO2 instanceof CTBookmark) { - BookmarkInfo info = new BookmarkInfo(); - info.bookmark = (CTBookmark) realO2; - info.parentParagraph = p; - info.parentContainer = contentAccessor; - result.add(info); - } - } - } else if (realObj instanceof ContentAccessor) { - result.addAll(findAllBookmarks((ContentAccessor) realObj)); - } - } - return result; - } - - - /** - * 在书签后插入段落 - */ - public static void insertParagraphsAfter(BookmarkInfo info, P paragraph) { - List parentContent = info.parentContainer.getContent(); - int idx = parentContent.indexOf(info.parentParagraph); - parentContent.add(idx + 1, paragraph); - } - - /** - * 在书签后插入表格 - */ - public static void insertTableAfter(BookmarkInfo info, Tbl table) { - List parentContent = info.parentContainer.getContent(); - int idx = parentContent.indexOf(info.parentParagraph); - parentContent.add(idx + 1, table); - } - - /** - * 在书签后插入元素,可能是段落、表格、图片、书签等 - */ - public static void insertElement(BookmarkInfo info, List elements) { - List parentContent = info.parentContainer.getContent(); - int idx = parentContent.indexOf(info.parentParagraph); - // 遍历元素,如果是通道回路这种大标题需要新起一个空的文档页 - for (int i = 0; i < elements.size(); i++) { - Object element = elements.get(i); - if (element instanceof P) { - P p = (P) element; - String textFromP = Docx4jUtil.getTextFromP(p); - if (textFromP.contains("测量回路")) { - if (!textFromP.contains("1")) { - // 另起一页 - P pagePara = Docx4jUtil.getPageBreak(); - idx = idx + 1; - parentContent.add(idx, pagePara); - } - idx = idx + 1; - parentContent.add(idx, p); - } - else if (textFromP.startsWith(PowerIndexEnum.IMBV.getDesc()) - || textFromP.startsWith(PowerIndexEnum.HV.getDesc()) - || textFromP.startsWith(PowerIndexEnum.HI.getDesc()) - - ) { - // 另起一页 - P pagePara = Docx4jUtil.getPageBreak(); - idx = idx + 1; - parentContent.add(idx, pagePara); - idx = idx + 1; - parentContent.add(idx, element); - }else if(textFromP.startsWith("注:基波电流幅值5.000A,基波频率50.0Hz,各次间谐波电流含有率均为3.0%。")){ - idx = idx + 1; - parentContent.add(idx, element); - P pagePara = Docx4jUtil.getPageBreak(); - idx = idx + 1; - parentContent.add(idx, pagePara); - } - else { - idx = idx + 1; - parentContent.add(idx, element); - } - } else { - idx = idx + 1; - parentContent.add(idx, element); - } - } - } - - /** - * 删除文档中的空白页 - * @param docx 要处理的Word文档 - */ - public static void removeBlankPages(MainDocumentPart mainDocumentPart) { - // 获取文档主体 - Document document = mainDocumentPart.getJaxbElement(); - Body body = document.getBody(); - - // 获取所有段落 - List paragraphs = body.getContent(); - - // 用于标记是否在空白页中 - boolean inBlankPage = false; - // 用于存储要删除的段落 - List

paragraphsToRemove = new ArrayList<>(); - - for (Object paragraph : paragraphs) { - if (paragraph instanceof P){ - P paragraphtemp = (P) paragraph; - // 检查段落是否为空 - boolean isEmpty = isParagraphEmpty(paragraphtemp); - - if (isEmpty) { - if (!inBlankPage) { - inBlankPage = true; - } - paragraphsToRemove.add(paragraphtemp); - } else { - inBlankPage = false; - } - } - } - - // 删除空白段落 - for (P paragraph : paragraphsToRemove) { - body.getContent().remove(paragraph); - } - } - - /** - * 检查段落是否为空 - * @param paragraph 要检查的段落 - * @return 如果段落为空返回true,否则返回false - */ - private static boolean isParagraphEmpty(P paragraph) { - // 检查段落是否包含分节符 - if (paragraph.getPPr() != null && paragraph.getPPr().getSectPr() != null) { - return false; - } - - // 检查段落中的文本内容 - for (Object obj : paragraph.getContent()) { - if (obj instanceof R) { - R run = (R) obj; - // 在3.3.4版本中,使用getContent()获取文本内容 - for (Object runContent : run.getContent()) { - if (runContent instanceof Text) { - Text text = (Text) runContent; - if (text.getValue() != null && !text.getValue().trim().isEmpty()) { - return false; - } - } - } - } - } - return true; - } - - - /** - * 在插入前检查目标位置是否有分页符 - * - * @param position 目标位置 - * @return 是否包含分页符 - */ - private static boolean hasPageBreak(Object position) { - if (position instanceof P) { - P paragraph = (P) position; - for (Object run : paragraph.getContent()) { - if (run instanceof R) { - R r = (R) run; - for (Object element : r.getContent()) { - if (element instanceof Br && ((Br) element).getType() != null - && ((Br) element).getType().equals("page")) { - return true; - } - } - } - } - return false; - } - return false; - } - - /** - * 删除书签 - * - * @param bookmarkInfo 书签信息 - */ - public static void removeBookmark(BookmarkUtil.BookmarkInfo bookmarkInfo) { - try { - // 获取书签所在的段落 - P paragraph = bookmarkInfo.parentParagraph; - - // 遍历段落内容,找到并删除书签开始和结束标记 - List paragraphContent = new ArrayList<>(paragraph.getContent()); - for (Object obj : paragraphContent) { - if (obj instanceof JAXBElement) { - JAXBElement element = (JAXBElement) obj; - Object value = element.getValue(); - - // 删除书签开始标记 - if (value instanceof CTBookmark) { - paragraph.getContent().remove(obj); - } - // 删除书签结束标记 - else if (value instanceof CTMarkupRange) { - paragraph.getContent().remove(obj); - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * 使用 ObjectFactory 创建表格 - * - * @param factory ObjectFactory 实例 - * @param data 二维数组,表格内容 - * @return Tbl 表格对象 - */ - public static Tbl createTable(ObjectFactory factory, String[][] data) { - Tbl table = factory.createTbl(); - for (String[] rowData : data) { - Tr row = factory.createTr(); - for (String cellData : rowData) { - Tc cell = factory.createTc(); - P para = factory.createP(); - R run = factory.createR(); - Text text = factory.createText(); - text.setValue(cellData); - run.getContent().add(text); - para.getContent().add(run); - cell.getContent().add(para); - row.getContent().add(cell); - } - table.getContent().add(row); - } - return table; - } - - /** - * 获取指定标签的标签信息 - * - * @param key 标签名 - * @param bookmarks 所有标签信息 - */ - public static BookmarkInfo getBookmarkInfo(String key, List bookmarks) { - BookmarkUtil.BookmarkInfo bookmarkInfo = null; - for (BookmarkUtil.BookmarkInfo info : bookmarks) { - String name = info.bookmark.getName(); - if (key.equalsIgnoreCase(name)) { - bookmarkInfo = info; - } - } - return bookmarkInfo; - } -} \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/report/utils/Docx4jInsertParagraph.java b/detection/src/main/java/com/njcn/gather/report/utils/Docx4jInsertParagraph.java deleted file mode 100644 index fdc8cc5c..00000000 --- a/detection/src/main/java/com/njcn/gather/report/utils/Docx4jInsertParagraph.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.njcn.gather.report.utils; - -/** - * @author hongawen - * @version 1.0 - * @data 2025/3/25 19:37 - */ -import org.docx4j.openpackaging.packages.WordprocessingMLPackage; -import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; -import org.docx4j.wml.*; - -import java.io.File; -import java.math.BigInteger; -import java.util.List; - -public class Docx4jInsertParagraph { - public static void main(String[] args) throws Exception { - // 加载现有的 Word 文档 - WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(new File("C:\\Users\\hongawen\\Desktop\\base_template.docx")); - MainDocumentPart documentPart = wordPackage.getMainDocumentPart(); - - // 获取文档中的所有段落 - List paragraphs = documentPart.getContent(); - - // 在中间插入一个新段落 - int insertIndex = paragraphs.size() / 2; - P newParagraph = createParagraph("This is a new paragraph inserted in the middle."); - paragraphs.add(insertIndex, newParagraph); - - // 保存修改后的文档 - wordPackage.save(new File("example_modified.docx")); - } - - private static P createParagraph(String text) { - ObjectFactory factory = new ObjectFactory(); - P paragraph = factory.createP(); - R run = factory.createR(); - Text t = factory.createText(); - t.setValue(text); - run.getContent().add(t); - paragraph.getContent().add(run); - return paragraph; - } -} \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/report/utils/WordUtil.java b/detection/src/main/java/com/njcn/gather/report/utils/WordUtil.java deleted file mode 100644 index 450add31..00000000 --- a/detection/src/main/java/com/njcn/gather/report/utils/WordUtil.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.njcn.gather.report.utils; - -import org.apache.poi.xwpf.usermodel.*; -import org.apache.xmlbeans.XmlCursor; -import org.docx4j.openpackaging.packages.WordprocessingMLPackage; -import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; -import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; -import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * @author hongawen - * @version 1.0 - * @data 2025/1/9 20:52 - */ -public class WordUtil { - - - /** - * 将源文档的内容(包括段落、表格等)追加到目标文档中 - * - * @param target 目标文档 - * @param source 源文档 - */ - public static void appendDocument(XWPFDocument target, XWPFDocument source) { - // 在追加内容之前,插入分页符 -// insertPageBreak(target); - // 遍历源文档的所有块(段落、表格等) - source.getBodyElements().forEach(bodyElement -> { - switch (bodyElement.getElementType()) { - case PARAGRAPH: - // 处理段落 - XWPFParagraph sourceParagraph = (XWPFParagraph) bodyElement; - XWPFParagraph newParagraph = target.createParagraph(); - newParagraph.getCTP().set(sourceParagraph.getCTP()); - break; - case TABLE: - // 处理表格 - XWPFTable sourceTable = (XWPFTable) bodyElement; - XWPFTable newTable = target.createTable(); - newTable.getCTTbl().set(sourceTable.getCTTbl()); - break; - default: - // 针对其他类型(如图片、页眉页脚等)可以扩展处理逻辑 - System.out.println("未处理的内容类型:" + bodyElement.getElementType()); - break; - } - }); - } - - - /** - * 替换表格中的占位符 - * @param document 文档 - * @param placeholders 待替换的占位符 - */ - public static void replacePlaceholdersInTables(XWPFDocument document, Map placeholders) { - for (XWPFTable table : document.getTables()) { - for (XWPFTableRow row : table.getRows()) { - for (XWPFTableCell cell : row.getTableCells()) { - for (XWPFParagraph paragraph : cell.getParagraphs()) { - List runs = paragraph.getRuns(); - if (runs != null) { - for (XWPFRun run : runs) { - String text = run.getText(0); - if (text != null) { - for (Map.Entry entry : placeholders.entrySet()) { - text = text.replace(entry.getKey(), entry.getValue()); - } - run.setText(text, 0); - } - } - } - } - } - } - } - } - - - /** - * 替换段落中的占位符 - * @param document 文档 - * @param placeholders 待替换的占位符 - */ - public static void replacePlaceholdersInParagraphs(XWPFDocument document, Map placeholders) { - for (XWPFParagraph paragraph : document.getParagraphs()) { - List runs = paragraph.getRuns(); - if (runs != null) { - for (XWPFRun run : runs) { - String text = run.getText(0); - if (text != null) { - for (Map.Entry entry : placeholders.entrySet()) { - text = text.replace(entry.getKey(), entry.getValue()); - } - run.setText(text, 0); - } - } - } - } - } - - /** - * 替换文档中的占位符 - * @param document 文档 - * @param placeholders 待替换的占位符 - */ - public static void replacePlaceholders(XWPFDocument document, Map placeholders) { - replacePlaceholdersInParagraphs(document,placeholders); - replacePlaceholdersInTables(document,placeholders); - } - - - public static List findHeadingLevel5Paragraphs(XWPFDocument document) { - List headingLevel5Paragraphs = new ArrayList<>(); - for (XWPFParagraph paragraph : document.getParagraphs()) { - String style = paragraph.getStyle(); - if ("5".equals(style)) { - headingLevel5Paragraphs.add(paragraph); - } - } - return headingLevel5Paragraphs; - } - - - /** - * 获取段落的位置(通过遍历bodyElements) - */ - public static int getBodyElementPosition(XWPFDocument document, XWPFParagraph paragraph) { - List bodyElements = document.getBodyElements(); - for (int i = 0; i < bodyElements.size(); i++) { - if (bodyElements.get(i) instanceof XWPFParagraph && bodyElements.get(i).equals(paragraph)) { - return i; - } - } - return -1; - } - - - -} diff --git a/entrance/src/main/resources/model/report_table.docx b/entrance/src/main/resources/model/report_table.docx index ded22ce6..f6d53866 100644 Binary files a/entrance/src/main/resources/model/report_table.docx and b/entrance/src/main/resources/model/report_table.docx differ diff --git a/storage/src/main/java/com/njcn/gather/storage/pojo/param/SingleNonHarmParam.java b/storage/src/main/java/com/njcn/gather/storage/pojo/param/SingleNonHarmParam.java index ee5b8c3c..46038df9 100644 --- a/storage/src/main/java/com/njcn/gather/storage/pojo/param/SingleNonHarmParam.java +++ b/storage/src/main/java/com/njcn/gather/storage/pojo/param/SingleNonHarmParam.java @@ -1,6 +1,5 @@ package com.njcn.gather.storage.pojo.param; -import io.swagger.models.auth.In; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/tools/report-generator/PROJECT_SUMMARY.md b/tools/report-generator/PROJECT_SUMMARY.md deleted file mode 100644 index 7f4ff794..00000000 --- a/tools/report-generator/PROJECT_SUMMARY.md +++ /dev/null @@ -1,184 +0,0 @@ -# Report Generator 项目总结 - -## 项目概述 - -成功从CN_Gather项目的detection模块中抽取出了报告生成功能,创建了独立的通用工具模块。该模块完全脱离了业务逻辑,提供纯技术能力,可以被其他项目复用。 - -## 架构设计 - -### 模块结构 -``` -CN_Gather/ -├── tools/ # 工具集合模块(新增) -│ ├── report-generator/ # 报告生成工具子模块(新增) -│ │ ├── src/main/java/com/njcn/gather/tools/report/ -│ │ │ ├── controller/ # HTTP接口层 -│ │ │ ├── service/ # 业务服务层 -│ │ │ ├── engine/ # 模板处理引擎 -│ │ │ ├── model/ # 数据模型 -│ │ │ ├── util/ # 工具类 -│ │ └── pom.xml -│ ├── pom.xml -│ └── README.md -├── detection/ # 原有检测模块(保持不变) -├── entrance/ # 应用入口 -└── pom.xml # 已更新,包含tools模块 -``` - -### 核心组件 - -#### 1. 数据模型层 (model/) -- **TemplateSource**: 模板来源定义,支持文件、流、字节数组输入 -- **TemplateRequest**: 模板处理请求封装 -- **ProcessResult**: 处理结果封装,包含详细统计和错误信息 -- **ProcessOptions**: 处理选项配置,支持各种功能开关 -- **TemplateType**: 模板类型枚举,支持扩展 - -#### 2. 引擎层 (engine/) -- **DocumentProcessor**: 文档处理器接口 -- **WordDocumentProcessor**: Word文档处理实现 - -#### 3. 工具类层 (util/) -- **WordDocumentUtil**: 基于Apache POI的通用Word操作 -- **Docx4jAdvancedUtil**: 基于docx4j的高级Word操作 - -#### 4. 服务层 (service/) -- **ReportGeneratorService**: 报告生成服务接口 -- **ReportGeneratorServiceImpl**: 服务实现,支持同步/异步/批量处理 - -#### 5. 控制器层 (controller/) -- **ReportGeneratorController**: REST API接口,提供多种调用方式 - -## 技术特性 - -### 已实现功能 -1. ✅ **占位符替换**: `${key}`、`#{key}`、`{{key}}` 格式支持 -2. ✅ **多输入方式**: 文件路径、输入流、字节数组 -3. ✅ **多输出方式**: 文件、流、字节数组 -4. ✅ **同步处理**: 立即返回结果 -5. ✅ **异步处理**: 支持长时间处理任务 -6. ✅ **批量处理**: 一次处理多个模板 -7. ✅ **错误处理**: 完整的异常处理和错误信息 -8. ✅ **性能监控**: 处理时间和统计信息 -9. ✅ **参数验证**: 请求参数完整性验证 -10. ✅ **模板验证**: 模板文件有效性检查 - -### 从原有代码抽取的功能 -- **WordUtil → WordDocumentUtil**: 基础Word文档操作 -- **Docx4jUtil → Docx4jAdvancedUtil**: 高级文档处理功能 -- **BookmarkUtil 功能**: 整合到高级工具类中 -- **占位符替换逻辑**: 通用化并支持多种格式 -- **文档合并功能**: 保留并优化 -- **样式管理功能**: 字体、颜色、对齐等 - -## API 设计 - -### HTTP接口 -``` -POST /api/tools/report/process/simple # 简单处理 -POST /api/tools/report/process/advanced # 高级处理 -POST /api/tools/report/process/async # 异步处理 -GET /api/tools/report/process/async/{id} # 查询异步结果 -POST /api/tools/report/process/batch # 批量处理 -POST /api/tools/report/validate # 模板验证 -``` - -### Java API -```java -// 核心服务接口 -ProcessResult processTemplate(TemplateRequest request) -String processTemplateAsync(TemplateRequest request) -ProcessResult getAsyncResult(String requestId) -List batchProcessTemplates(List requests) -ProcessResult validateTemplate(TemplateRequest request) -``` - -## 设计优势 - -### 1. 高内聚低耦合 -- 完全独立的模块,不依赖具体业务 -- 清晰的分层架构 -- 接口与实现分离 - -### 2. 可扩展性强 -- 支持插件化的文档处理器 -- 可方便扩展新的模板类型(PDF、Excel等) -- 处理选项可灵活配置 - -### 3. 易于使用 -- 提供多种使用方式(HTTP、Java API) -- Builder模式简化对象构建 -- 完整的使用文档和示例 - -### 4. 健壮性好 -- 完整的错误处理机制 -- 参数验证和模板验证 -- 资源自动管理和清理 - -### 5. 性能优异 -- 支持异步处理避免阻塞 -- 批量处理提高效率 -- 详细的性能统计信息 - -## 使用场景 - -### 1. 报告自动化生成 -- 检测报告、试验报告 -- 财务报表、统计报表 -- 证书、合格证等 - -### 2. 文档批量处理 -- 合同批量生成 -- 通知书批量制作 -- 标签批量打印 - -### 3. 模板管理系统 -- 模板上传和验证 -- 模板版本管理 -- 模板效果预览 - -## 部署和集成 - -### 1. 独立部署 -tools模块可以作为独立的微服务部署,对外提供HTTP接口。 - -### 2. 嵌入式集成 -其他项目可以直接依赖report-generator模块,使用Java API调用。 - -### 3. 与现有系统集成 -detection模块可以逐步迁移到使用新的report-generator工具。 - -## 后续扩展建议 - -### 1. 功能扩展 -- 支持PDF模板处理 -- 支持Excel模板处理 -- 支持图片插入和处理 -- 支持复杂表达式计算 - -### 2. 性能优化 -- 添加模板缓存机制 -- 支持流式处理大文件 -- 增加并发控制和限流 - -### 3. 管理功能 -- 模板管理界面 -- 处理任务监控面板 -- 统计分析报表 - -### 4. 安全增强 -- 用户权限控制 -- 模板安全检查 -- 操作审计日志 - -## 总结 - -成功创建了一个完全独立、通用的报告生成工具模块,实现了以下目标: - -1. **脱离业务**: 完全剥离了电能质量检测的业务逻辑 -2. **通用化**: 可以处理任何Word模板和数据 -3. **易扩展**: 支持新的文档类型和处理方式 -4. **高可用**: 提供多种调用方式和完善的错误处理 -5. **高性能**: 支持异步和批量处理 - -该工具不仅可以满足当前CN_Gather项目的需求,也为未来的数据生成工具、文件处理工具等提供了良好的架构基础。 \ No newline at end of file diff --git a/tools/report-generator/pom.xml b/tools/report-generator/pom.xml index c84ba26b..a9c063c7 100644 --- a/tools/report-generator/pom.xml +++ b/tools/report-generator/pom.xml @@ -13,7 +13,7 @@ report-generator jar 报告生成工具 - 通用的文档模板处理和报告生成工具,支持占位符替换、书签插入、动态表格等功能 + 基于docx4j的通用Word文档处理工具,支持占位符替换、文档合并、动态表格、图片处理等完整功能 UTF-8 @@ -38,32 +38,8 @@ 2.3.12 - - - org.apache.poi - poi - 4.1.2 - - - org.apache.poi - poi-ooxml - 4.1.2 - - - - org.apache.poi - poi-ooxml-schemas - 4.1.2 - - - - org.apache.poi - poi-scratchpad - 4.1.2 - - - + jakarta.xml.bind jakarta.xml.bind-api diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/controller/ReportGeneratorController.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/controller/ReportGeneratorController.java deleted file mode 100644 index 46e0576d..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/controller/ReportGeneratorController.java +++ /dev/null @@ -1,392 +0,0 @@ -package com.njcn.gather.tools.report.controller; - -import com.njcn.gather.tools.report.model.*; -import com.njcn.gather.tools.report.service.ReportGeneratorService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 报告生成工具HTTP接口 - * 提供REST风格的报告生成API - * - * @author hongawen - */ -@Slf4j -@RestController -@RequestMapping("/api/tools/report") -@RequiredArgsConstructor -public class ReportGeneratorController { - - private final ReportGeneratorService reportGeneratorService; - - /** - * 简单模板处理 - 占位符替换 - * - * @param templateFile 模板文件 - * @param dataJson 数据JSON字符串 - * @param outputFileName 输出文件名(可选) - * @param response HTTP响应 - */ - @PostMapping("/process/simple") - public void processSimpleTemplate( - @RequestParam("templateFile") MultipartFile templateFile, - @RequestParam("data") String dataJson, - @RequestParam(value = "outputFileName", required = false) String outputFileName, - HttpServletResponse response) throws IOException { - - log.info("接收到简单模板处理请求: {}", templateFile.getOriginalFilename()); - - try { - // 解析数据 - @SuppressWarnings("unchecked") - Map data = com.fasterxml.jackson.databind.ObjectMapper.class - .getDeclaredConstructor().newInstance().readValue(dataJson, Map.class); - - // 构建请求 - TemplateRequest request = TemplateRequest.builder() - .templateSource(TemplateSource.fromStream(templateFile.getInputStream(), - templateFile.getOriginalFilename())) - .data(data) - .options(ProcessOptions.simplePlaceholder()) - .outputTarget(TemplateRequest.OutputTarget.toBytes()) - .build(); - - // 处理模板 - ProcessResult result = reportGeneratorService.processTemplate(request); - - if (result.isSuccess() && result.getOutputBytes() != null) { - // 设置响应头 - String fileName = outputFileName != null ? outputFileName : - "report_" + System.currentTimeMillis() + ".docx"; - response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); - response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); - response.setContentLength(result.getOutputBytes().length); - - // 输出文件 - try (OutputStream os = response.getOutputStream()) { - os.write(result.getOutputBytes()); - os.flush(); - } - - log.info("简单模板处理成功: {}", fileName); - } else { - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - response.getWriter().write("模板处理失败: " + result.getMessage()); - } - - } catch (Exception e) { - log.error("简单模板处理异常: {}", e.getMessage(), e); - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - response.getWriter().write("处理异常: " + e.getMessage()); - } - } - - /** - * 高级模板处理 - 支持更多功能 - */ - @PostMapping("/process/advanced") - public ResponseEntity> processAdvancedTemplate(@RequestBody AdvancedProcessRequest request) { - log.info("接收到高级模板处理请求"); - - try { - // 构建模板请求 - TemplateRequest templateRequest = buildTemplateRequest(request); - - // 处理模板 - ProcessResult result = reportGeneratorService.processTemplate(templateRequest); - - // 构建响应 - Map response = new HashMap<>(); - response.put("success", result.isSuccess()); - response.put("message", result.getMessage()); - response.put("requestId", result.getRequestId()); - - if (result.isSuccess()) { - if (result.getOutputBytes() != null) { - // 返回base64编码的文件内容 - String base64Content = java.util.Base64.getEncoder().encodeToString(result.getOutputBytes()); - response.put("fileContent", base64Content); - } - if (result.getStats() != null) { - response.put("stats", result.getStats()); - } - response.put("processingTimeMs", result.getProcessingTimeMs()); - } else { - response.put("errorCode", result.getErrorCode()); - response.put("errorDetails", result.getErrorDetails()); - } - - return ResponseEntity.ok(response); - - } catch (Exception e) { - log.error("高级模板处理异常: {}", e.getMessage(), e); - Map errorResponse = new HashMap<>(); - errorResponse.put("success", false); - errorResponse.put("message", "处理异常"); - errorResponse.put("errorDetails", e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); - } - } - - /** - * 异步处理模板 - */ - @PostMapping("/process/async") - public ResponseEntity> processTemplateAsync(@RequestBody AdvancedProcessRequest request) { - log.info("接收到异步模板处理请求"); - - try { - // 构建模板请求 - TemplateRequest templateRequest = buildTemplateRequest(request); - - // 启动异步处理 - String requestId = reportGeneratorService.processTemplateAsync(templateRequest); - - Map response = new HashMap<>(); - response.put("success", true); - response.put("message", "异步处理已启动"); - response.put("requestId", requestId); - - return ResponseEntity.ok(response); - - } catch (Exception e) { - log.error("异步模板处理启动失败: {}", e.getMessage(), e); - Map errorResponse = new HashMap<>(); - errorResponse.put("success", false); - errorResponse.put("message", "启动异步处理失败"); - errorResponse.put("errorDetails", e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); - } - } - - /** - * 查询异步处理结果 - */ - @GetMapping("/process/async/{requestId}") - public ResponseEntity> getAsyncResult(@PathVariable String requestId) { - ProcessResult result = reportGeneratorService.getAsyncResult(requestId); - - Map response = new HashMap<>(); - if (result == null) { - response.put("found", false); - response.put("message", "未找到对应的处理结果"); - return ResponseEntity.ok(response); - } - - response.put("found", true); - response.put("success", result.isSuccess()); - response.put("message", result.getMessage()); - response.put("requestId", result.getRequestId()); - - if (result.getEndTime() != null) { - // 处理已完成 - response.put("completed", true); - response.put("processingTimeMs", result.getProcessingTimeMs()); - - if (result.isSuccess() && result.getOutputBytes() != null) { - String base64Content = java.util.Base64.getEncoder().encodeToString(result.getOutputBytes()); - response.put("fileContent", base64Content); - } - - if (result.getStats() != null) { - response.put("stats", result.getStats()); - } - } else { - response.put("completed", false); - } - - return ResponseEntity.ok(response); - } - - /** - * 批量处理模板 - */ - @PostMapping("/process/batch") - public ResponseEntity> batchProcessTemplates(@RequestBody List requests) { - log.info("接收到批量模板处理请求,数量: {}", requests.size()); - - try { - // 构建模板请求列表 - List templateRequests = new java.util.ArrayList<>(); - for (AdvancedProcessRequest req : requests) { - templateRequests.add(buildTemplateRequest(req)); - } - - // 批量处理 - List results = reportGeneratorService.batchProcessTemplates(templateRequests); - - // 统计结果 - long successCount = results.stream().mapToLong(r -> r.isSuccess() ? 1 : 0).sum(); - long failureCount = results.size() - successCount; - - Map response = new HashMap<>(); - response.put("success", true); - response.put("message", "批量处理完成"); - response.put("totalCount", results.size()); - response.put("successCount", successCount); - response.put("failureCount", failureCount); - List> resultSummaries = new java.util.ArrayList<>(); - for (ProcessResult result : results) { - resultSummaries.add(buildResultSummary(result)); - } - response.put("results", resultSummaries); - - return ResponseEntity.ok(response); - - } catch (Exception e) { - log.error("批量模板处理异常: {}", e.getMessage(), e); - Map errorResponse = new HashMap<>(); - errorResponse.put("success", false); - errorResponse.put("message", "批量处理异常"); - errorResponse.put("errorDetails", e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); - } - } - - /** - * 验证模板 - */ - @PostMapping("/validate") - public ResponseEntity> validateTemplate(@RequestBody AdvancedProcessRequest request) { - try { - TemplateRequest templateRequest = buildTemplateRequest(request); - ProcessResult result = reportGeneratorService.validateTemplate(templateRequest); - - Map response = new HashMap<>(); - response.put("valid", result.isSuccess()); - response.put("message", result.getMessage()); - - if (!result.isSuccess()) { - response.put("errorCode", result.getErrorCode()); - response.put("errorDetails", result.getErrorDetails()); - } - - return ResponseEntity.ok(response); - - } catch (Exception e) { - log.error("模板验证异常: {}", e.getMessage(), e); - Map errorResponse = new HashMap<>(); - errorResponse.put("valid", false); - errorResponse.put("message", "验证异常"); - errorResponse.put("errorDetails", e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); - } - } - - /** - * 构建模板请求 - */ - private TemplateRequest buildTemplateRequest(AdvancedProcessRequest request) { - // 构建模板源 - TemplateSource templateSource; - if (request.getTemplateFilePath() != null) { - templateSource = TemplateSource.fromFile(request.getTemplateFilePath()); - } else if (request.getTemplateContent() != null) { - byte[] content = java.util.Base64.getDecoder().decode(request.getTemplateContent()); - templateSource = TemplateSource.fromBytes(content, request.getTemplateName()); - } else { - throw new IllegalArgumentException("模板源不能为空"); - } - - // 构建处理选项 - ProcessOptions options = ProcessOptions.builder() - .enablePlaceholder(request.isEnablePlaceholder()) - .enableBookmark(request.isEnableBookmark()) - .enableDynamicTable(request.isEnableDynamicTable()) - .enableAutoPage(request.isEnableAutoPage()) - .build(); - - // 构建输出目标 - TemplateRequest.OutputTarget outputTarget; - if (request.getOutputFilePath() != null) { - outputTarget = TemplateRequest.OutputTarget.toFile(request.getOutputFilePath()); - } else { - outputTarget = TemplateRequest.OutputTarget.toBytes(); - } - - return TemplateRequest.builder() - .templateSource(templateSource) - .data(request.getData()) - .options(options) - .outputTarget(outputTarget) - .requestId(request.getRequestId()) - .build(); - } - - /** - * 构建结果摘要 - */ - private Map buildResultSummary(ProcessResult result) { - Map summary = new HashMap<>(); - summary.put("requestId", result.getRequestId()); - summary.put("success", result.isSuccess()); - summary.put("message", result.getMessage()); - summary.put("processingTimeMs", result.getProcessingTimeMs()); - - if (!result.isSuccess()) { - summary.put("errorCode", result.getErrorCode()); - } - - return summary; - } - - /** - * 高级处理请求模型 - */ - public static class AdvancedProcessRequest { - private String requestId; - private String templateFilePath; - private String templateContent; // base64编码 - private String templateName; - private Map data; - private String outputFilePath; - private boolean enablePlaceholder = true; - private boolean enableBookmark = true; - private boolean enableDynamicTable = true; - private boolean enableAutoPage = true; - - // Getters and Setters - public String getRequestId() { return requestId; } - public void setRequestId(String requestId) { this.requestId = requestId; } - - public String getTemplateFilePath() { return templateFilePath; } - public void setTemplateFilePath(String templateFilePath) { this.templateFilePath = templateFilePath; } - - public String getTemplateContent() { return templateContent; } - public void setTemplateContent(String templateContent) { this.templateContent = templateContent; } - - public String getTemplateName() { return templateName; } - public void setTemplateName(String templateName) { this.templateName = templateName; } - - public Map getData() { return data; } - public void setData(Map data) { this.data = data; } - - public String getOutputFilePath() { return outputFilePath; } - public void setOutputFilePath(String outputFilePath) { this.outputFilePath = outputFilePath; } - - public boolean isEnablePlaceholder() { return enablePlaceholder; } - public void setEnablePlaceholder(boolean enablePlaceholder) { this.enablePlaceholder = enablePlaceholder; } - - public boolean isEnableBookmark() { return enableBookmark; } - public void setEnableBookmark(boolean enableBookmark) { this.enableBookmark = enableBookmark; } - - public boolean isEnableDynamicTable() { return enableDynamicTable; } - public void setEnableDynamicTable(boolean enableDynamicTable) { this.enableDynamicTable = enableDynamicTable; } - - public boolean isEnableAutoPage() { return enableAutoPage; } - public void setEnableAutoPage(boolean enableAutoPage) { this.enableAutoPage = enableAutoPage; } - } -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/engine/DocumentProcessor.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/engine/DocumentProcessor.java deleted file mode 100644 index aa9e3b88..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/engine/DocumentProcessor.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.njcn.gather.tools.report.engine; - -import com.njcn.gather.tools.report.model.TemplateRequest; -import com.njcn.gather.tools.report.model.ProcessResult; -import com.njcn.common.pojo.exception.BusinessException; -import com.njcn.gather.tools.report.util.ReportExceptionUtil; - -/** - * 文档处理器接口 - * 定义了文档模板处理的核心方法 - * - * @author hongawen - */ -public interface DocumentProcessor { - - /** - * 处理模板文档 - * - * @param request 处理请求 - * @return 处理结果 - */ - ProcessResult process(TemplateRequest request); - - /** - * 检查处理器是否支持指定的模板类型 - * - * @param request 处理请求 - * @return true表示支持,false表示不支持 - */ - boolean supports(TemplateRequest request); - - /** - * 获取处理器名称 - * - * @return 处理器名称 - */ - String getName(); - - /** - * 获取支持的模板类型 - * - * @return 支持的模板类型数组 - */ - String[] getSupportedTypes(); - - /** - * 验证请求参数 - * - * @param request 处理请求 - * @throws BusinessException 参数无效时抛出 - */ - default void validateRequest(TemplateRequest request) { - if (request == null) { - throw ReportExceptionUtil.validationError("处理请求不能为空"); - } - if (!request.isValid()) { - throw ReportExceptionUtil.validationError("处理请求参数无效"); - } - if (!supports(request)) { - throw ReportExceptionUtil.unsupportedOperation("不支持的模板类型或处理请求"); - } - } -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/engine/WordDocumentProcessor.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/engine/WordDocumentProcessor.java deleted file mode 100644 index f8c6fe98..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/engine/WordDocumentProcessor.java +++ /dev/null @@ -1,253 +0,0 @@ -package com.njcn.gather.tools.report.engine; - -import com.njcn.gather.tools.report.model.*; -import com.njcn.gather.tools.report.util.WordDocumentUtil; -import com.njcn.common.pojo.exception.BusinessException; -import lombok.extern.slf4j.Slf4j; -import org.apache.poi.xwpf.usermodel.XWPFDocument; -import org.springframework.stereotype.Component; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; - -/** - * Word文档处理器实现 - * 基于Apache POI的Word文档模板处理 - * - * @author hongawen - */ -@Slf4j -@Component -public class WordDocumentProcessor implements DocumentProcessor { - - @Override - public ProcessResult process(TemplateRequest request) { - // 验证请求参数 - validateRequest(request); - - ProcessResult.ProcessResultBuilder resultBuilder = ProcessResult.builder() - .requestId(request.getRequestId()) - .startTime(LocalDateTime.now()); - - try { - log.info("开始处理Word模板: {}", request.getTemplateSource().getDescription()); - - // 加载模板文档 - XWPFDocument document = loadTemplate(request.getTemplateSource()); - - // 统计模板信息 - WordDocumentUtil.DocumentStats templateStats = WordDocumentUtil.getDocumentStats(document); - log.debug("模板统计信息: {}", templateStats); - - // 处理文档内容 - ProcessResult.ProcessingStats stats = processDocumentContent(document, request); - - // 保存处理后的文档 - saveDocument(document, request.getOutputTarget(), resultBuilder); - - // 构建成功结果 - ProcessResult result = resultBuilder - .success(true) - .message("Word模板处理成功") - .stats(stats) - .endTime(LocalDateTime.now()) - .build(); - - result.calculateProcessingTime(); - log.info("Word模板处理完成,耗时: {}ms", result.getProcessingTimeMs()); - - return result; - - } catch (Exception e) { - log.error("Word模板处理失败: {}", e.getMessage(), e); - return resultBuilder - .success(false) - .errorCode("WORD_PROCESSING_ERROR") - .message("Word模板处理失败") - .errorDetails(e.getMessage()) - .endTime(LocalDateTime.now()) - .build(); - } - } - - /** - * 加载模板文档 - */ - private XWPFDocument loadTemplate(TemplateSource templateSource) throws IOException { - if (templateSource.getFilePath() != null) { - // 从文件路径加载 - return new XWPFDocument(new FileInputStream(templateSource.getFilePath())); - } else if (templateSource.getInputStream() != null) { - // 从输入流加载 - return new XWPFDocument(templateSource.getInputStream()); - } else if (templateSource.getContent() != null) { - // 从字节数组加载 - return new XWPFDocument(new ByteArrayInputStream(templateSource.getContent())); - } else { - throw new IllegalArgumentException("无效的模板源"); - } - } - - /** - * 处理文档内容 - */ - private ProcessResult.ProcessingStats processDocumentContent(XWPFDocument document, TemplateRequest request) { - ProcessOptions options = request.getEffectiveOptions(); - ProcessResult.ProcessingStats.ProcessingStatsBuilder statsBuilder = ProcessResult.ProcessingStats.builder(); - - // 1. 占位符替换 - if (options.isEnablePlaceholder()) { - int placeholderCount = processPlaceholders(document, request.getData(), options); - statsBuilder.placeholdersReplaced(placeholderCount); - log.debug("已替换占位符: {}个", placeholderCount); - } - - // 2. 动态表格处理 (如果需要的话) - if (options.isEnableDynamicTable()) { - int tablesCount = processDynamicTables(document, request.getData()); - statsBuilder.tablesGenerated(tablesCount); - log.debug("已处理动态表格: {}个", tablesCount); - } - - // 统计数据项数量 - statsBuilder.dataItemCount(request.getData().size()); - - return statsBuilder.build(); - } - - /** - * 处理占位符替换 - */ - private int processPlaceholders(XWPFDocument document, Map data, ProcessOptions options) { - // 将数据转换为字符串映射 - Map placeholders = convertDataToStringMap(data, options); - - if (placeholders.isEmpty()) { - return 0; - } - - // 执行占位符替换 - WordDocumentUtil.replacePlaceholders(document, placeholders); - - return placeholders.size(); - } - - /** - * 将数据对象转换为字符串映射 - */ - private Map convertDataToStringMap(Map data, ProcessOptions options) { - Map result = new HashMap<>(); - ProcessOptions.PlaceholderPattern pattern = options.getPlaceholderPattern(); - - for (Map.Entry entry : data.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - - // 构建完整的占位符格式 - String placeholder = pattern.buildPlaceholder(key); - String stringValue = value != null ? value.toString() : ""; - - result.put(placeholder, stringValue); - } - - return result; - } - - /** - * 处理动态表格(简单实现,可根据需要扩展) - */ - private int processDynamicTables(XWPFDocument document, Map data) { - // 这里可以实现动态表格的逻辑 - // 目前返回0,表示没有处理动态表格 - return 0; - } - - /** - * 保存处理后的文档 - */ - private void saveDocument(XWPFDocument document, TemplateRequest.OutputTarget outputTarget, - ProcessResult.ProcessResultBuilder resultBuilder) throws IOException { - - if (outputTarget.getFilePath() != null) { - // 保存到文件 - saveToFile(document, outputTarget.getFilePath()); - resultBuilder.outputFilePath(outputTarget.getFilePath()); - - // 获取文件大小 - try { - long fileSize = Files.size(Paths.get(outputTarget.getFilePath())); - resultBuilder.fileSize(fileSize); - } catch (Exception e) { - log.warn("无法获取输出文件大小: {}", e.getMessage()); - } - - } else if (outputTarget.getOutputStream() != null) { - // 保存到输出流 - saveToStream(document, outputTarget.getOutputStream()); - - } else if (outputTarget.isReturnBytes()) { - // 返回字节数组 - byte[] bytes = saveToBytes(document); - resultBuilder.outputBytes(bytes); - resultBuilder.fileSize((long) bytes.length); - } - } - - /** - * 保存文档到文件 - */ - private void saveToFile(XWPFDocument document, String filePath) throws IOException { - // 确保目录存在 - File file = new File(filePath); - File parentDir = file.getParentFile(); - if (parentDir != null && !parentDir.exists()) { - parentDir.mkdirs(); - } - - try (FileOutputStream outputStream = new FileOutputStream(filePath)) { - document.write(outputStream); - } - } - - /** - * 保存文档到输出流 - */ - private void saveToStream(XWPFDocument document, OutputStream outputStream) throws IOException { - document.write(outputStream); - outputStream.flush(); - } - - /** - * 保存文档到字节数组 - */ - private byte[] saveToBytes(XWPFDocument document) throws IOException { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - document.write(outputStream); - return outputStream.toByteArray(); - } - } - - @Override - public boolean supports(TemplateRequest request) { - if (request == null || request.getTemplateSource() == null) { - return false; - } - - TemplateType type = request.getTemplateSource().getType(); - return type == TemplateType.DOCX; - } - - @Override - public String getName() { - return "WordDocumentProcessor"; - } - - @Override - public String[] getSupportedTypes() { - return new String[]{"DOCX"}; - } -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ProcessOptions.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ProcessOptions.java deleted file mode 100644 index 9cd9d1b4..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ProcessOptions.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.njcn.gather.tools.report.model; - -import lombok.Data; -import lombok.Builder; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -/** - * 模板处理选项配置 - * 用于控制模板处理过程中启用的功能 - * - * @author hongawen - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ProcessOptions { - - /** - * 启用占位符替换功能 - * 默认:true - */ - @Builder.Default - private boolean enablePlaceholder = true; - - /** - * 启用书签定位插入功能 - * 默认:true - */ - @Builder.Default - private boolean enableBookmark = true; - - /** - * 启用动态表格生成功能 - * 默认:true - */ - @Builder.Default - private boolean enableDynamicTable = true; - - /** - * 启用自动分页功能 - * 默认:true - */ - @Builder.Default - private boolean enableAutoPage = true; - - /** - * 启用样式管理功能 - * 默认:true - */ - @Builder.Default - private boolean enableStyleManagement = true; - - /** - * 占位符格式模式 - * 默认:DOLLAR_BRACE (${key}) - */ - @Builder.Default - private PlaceholderPattern placeholderPattern = PlaceholderPattern.DOLLAR_BRACE; - - /** - * 输出格式 - * 默认:DOCX - */ - @Builder.Default - private String outputFormat = "DOCX"; - - /** - * 是否压缩输出 - * 默认:false - */ - @Builder.Default - private boolean compressOutput = false; - - /** - * 处理超时时间(毫秒) - * 默认:30秒 - */ - @Builder.Default - private long timeoutMs = 30000L; - - /** - * 是否启用调试模式 - * 默认:false - */ - @Builder.Default - private boolean debugMode = false; - - /** - * 自定义配置项 - */ - private java.util.Map customOptions; - - /** - * 占位符模式枚举 - */ - public enum PlaceholderPattern { - /** - * ${key} 格式 - */ - DOLLAR_BRACE("\\$\\{([^}]+)\\}", "${", "}"), - - /** - * #{key} 格式 - */ - HASH_BRACE("\\#\\{([^}]+)\\}", "#{", "}"), - - /** - * {{key}} 格式 - */ - DOUBLE_BRACE("\\{\\{([^}]+)\\}\\}", "{{", "}}"); - - private final String regex; - private final String prefix; - private final String suffix; - - PlaceholderPattern(String regex, String prefix, String suffix) { - this.regex = regex; - this.prefix = prefix; - this.suffix = suffix; - } - - public String getRegex() { - return regex; - } - - public String getPrefix() { - return prefix; - } - - public String getSuffix() { - return suffix; - } - - /** - * 构造完整的占位符 - */ - public String buildPlaceholder(String key) { - return prefix + key + suffix; - } - } - - /** - * 创建默认配置 - */ - public static ProcessOptions defaultOptions() { - return ProcessOptions.builder().build(); - } - - /** - * 创建简单占位符替换配置 - */ - public static ProcessOptions simplePlaceholder() { - return ProcessOptions.builder() - .enablePlaceholder(true) - .enableBookmark(false) - .enableDynamicTable(false) - .enableAutoPage(false) - .build(); - } - - /** - * 创建高级功能配置 - */ - public static ProcessOptions advancedFeatures() { - return ProcessOptions.builder() - .enablePlaceholder(true) - .enableBookmark(true) - .enableDynamicTable(true) - .enableAutoPage(true) - .enableStyleManagement(true) - .build(); - } -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ProcessResult.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ProcessResult.java deleted file mode 100644 index a2b0eed0..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ProcessResult.java +++ /dev/null @@ -1,232 +0,0 @@ -package com.njcn.gather.tools.report.model; - -import lombok.Data; -import lombok.Builder; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -/** - * 模板处理结果对象 - * 包含处理状态、结果数据、错误信息等 - * - * @author hongawen - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ProcessResult { - - /** - * 处理是否成功 - */ - private boolean success; - - /** - * 结果消息 - */ - private String message; - - /** - * 错误代码(失败时) - */ - private String errorCode; - - /** - * 详细错误信息(失败时) - */ - private String errorDetails; - - /** - * 生成的文件路径(文件输出时) - */ - private String outputFilePath; - - /** - * 生成的文件字节数组(字节输出时) - */ - private byte[] outputBytes; - - /** - * 输出文件大小(字节) - */ - private Long fileSize; - - /** - * 处理开始时间 - */ - private LocalDateTime startTime; - - /** - * 处理结束时间 - */ - private LocalDateTime endTime; - - /** - * 处理耗时(毫秒) - */ - private Long processingTimeMs; - - /** - * 请求标识 - */ - private String requestId; - - /** - * 处理统计信息 - */ - private ProcessingStats stats; - - /** - * 警告信息列表 - */ - private List warnings; - - /** - * 额外的元数据 - */ - private Map metadata; - - /** - * 处理统计信息 - */ - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class ProcessingStats { - /** - * 替换的占位符数量 - */ - private int placeholdersReplaced; - - /** - * 处理的书签数量 - */ - private int bookmarksProcessed; - - /** - * 生成的表格数量 - */ - private int tablesGenerated; - - /** - * 处理的页面数量 - */ - private int pagesProcessed; - - /** - * 模板文件大小(字节) - */ - private long templateFileSize; - - /** - * 输出文件大小(字节) - */ - private long outputFileSize; - - /** - * 数据项数量 - */ - private int dataItemCount; - } - - /** - * 创建成功结果 - */ - public static ProcessResult success(String message) { - return ProcessResult.builder() - .success(true) - .message(message) - .endTime(LocalDateTime.now()) - .build(); - } - - /** - * 创建失败结果 - */ - public static ProcessResult failure(String errorCode, String message) { - return ProcessResult.builder() - .success(false) - .errorCode(errorCode) - .message(message) - .errorDetails(message) - .endTime(LocalDateTime.now()) - .build(); - } - - /** - * 创建失败结果(带详细错误信息) - */ - public static ProcessResult failure(String errorCode, String message, String errorDetails) { - return ProcessResult.builder() - .success(false) - .errorCode(errorCode) - .message(message) - .errorDetails(errorDetails) - .endTime(LocalDateTime.now()) - .build(); - } - - /** - * 计算处理耗时 - */ - public void calculateProcessingTime() { - if (startTime != null && endTime != null) { - processingTimeMs = java.time.Duration.between(startTime, endTime).toMillis(); - } - } - - /** - * 添加警告信息 - */ - public void addWarning(String warning) { - if (warnings == null) { - warnings = new java.util.ArrayList<>(); - } - warnings.add(warning); - } - - /** - * 设置元数据 - */ - public void setMetadata(String key, Object value) { - if (metadata == null) { - metadata = new java.util.HashMap<>(); - } - metadata.put(key, value); - } - - /** - * 获取格式化的处理信息 - */ - public String getFormattedSummary() { - StringBuilder sb = new StringBuilder(); - sb.append("处理结果: ").append(success ? "成功" : "失败").append("\n"); - sb.append("消息: ").append(message).append("\n"); - - if (processingTimeMs != null) { - sb.append("耗时: ").append(processingTimeMs).append("ms\n"); - } - - if (stats != null) { - sb.append("统计: 占位符").append(stats.placeholdersReplaced) - .append("个, 书签").append(stats.bookmarksProcessed) - .append("个, 表格").append(stats.tablesGenerated).append("个\n"); - } - - if (fileSize != null) { - sb.append("文件大小: ").append(fileSize).append("字节\n"); - } - - if (warnings != null && !warnings.isEmpty()) { - sb.append("警告: ").append(String.join(", ", warnings)).append("\n"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateRequest.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateRequest.java deleted file mode 100644 index 11f9499e..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateRequest.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.njcn.gather.tools.report.model; - -import lombok.Data; -import lombok.Builder; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -import javax.validation.constraints.NotNull; -import java.io.OutputStream; -import java.util.Map; - -/** - * 模板处理请求对象 - * 封装模板处理所需的所有参数 - * - * @author hongawen - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class TemplateRequest { - - /** - * 模板源(必填) - */ - @NotNull(message = "模板源不能为空") - private TemplateSource templateSource; - - /** - * 填充数据(必填) - * key-value格式的数据,支持嵌套对象 - */ - @NotNull(message = "填充数据不能为空") - private Map data; - - /** - * 处理选项(可选) - * 如果为空则使用默认配置 - */ - private ProcessOptions options; - - /** - * 输出目标配置(必填) - */ - @NotNull(message = "输出目标不能为空") - private OutputTarget outputTarget; - - /** - * 请求标识(可选) - * 用于追踪和日志记录 - */ - private String requestId; - - /** - * 用户标识(可选) - * 用于权限控制和审计 - */ - private String userId; - - /** - * 额外的上下文数据(可选) - */ - private Map contextData; - - /** - * 输出目标配置 - */ - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class OutputTarget { - /** - * 输出文件路径 - */ - private String filePath; - - /** - * 输出流 - */ - private OutputStream outputStream; - - /** - * 输出文件名(当使用流输出时) - */ - private String fileName; - - /** - * 是否返回字节数组 - */ - @Builder.Default - private boolean returnBytes = false; - - /** - * 创建文件输出目标 - */ - public static OutputTarget toFile(String filePath) { - return OutputTarget.builder() - .filePath(filePath) - .build(); - } - - /** - * 创建流输出目标 - */ - public static OutputTarget toStream(OutputStream outputStream, String fileName) { - return OutputTarget.builder() - .outputStream(outputStream) - .fileName(fileName) - .build(); - } - - /** - * 创建字节数组输出目标 - */ - public static OutputTarget toBytes() { - return OutputTarget.builder() - .returnBytes(true) - .build(); - } - - /** - * 验证输出目标是否有效 - */ - public boolean isValid() { - return filePath != null || outputStream != null || returnBytes; - } - } - - /** - * 构建器便捷方法 - */ - public static class TemplateRequestBuilder { - - /** - * 设置模板文件路径 - */ - public TemplateRequestBuilder templatePath(String filePath) { - this.templateSource = TemplateSource.fromFile(filePath); - return this; - } - - /** - * 设置输出文件路径 - */ - public TemplateRequestBuilder outputPath(String filePath) { - this.outputTarget = OutputTarget.toFile(filePath); - return this; - } - - /** - * 设置输出流 - */ - public TemplateRequestBuilder outputStream(OutputStream outputStream, String fileName) { - this.outputTarget = OutputTarget.toStream(outputStream, fileName); - return this; - } - - /** - * 设置返回字节数组 - */ - public TemplateRequestBuilder outputBytes() { - this.outputTarget = OutputTarget.toBytes(); - return this; - } - } - - /** - * 验证请求是否有效 - */ - public boolean isValid() { - return templateSource != null && templateSource.isValid() && - data != null && !data.isEmpty() && - outputTarget != null && outputTarget.isValid(); - } - - /** - * 获取处理选项,如果为空则返回默认选项 - */ - public ProcessOptions getEffectiveOptions() { - return options != null ? options : ProcessOptions.defaultOptions(); - } -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateSource.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateSource.java deleted file mode 100644 index 1b70f117..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateSource.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.njcn.gather.tools.report.model; - -import lombok.Data; -import lombok.Builder; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -import java.io.InputStream; - -/** - * 模板来源定义 - * 支持多种模板输入方式:文件路径、输入流、字节数组 - * - * @author hongawen - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class TemplateSource { - - /** - * 模板文件路径 - */ - private String filePath; - - /** - * 模板输入流 - */ - private InputStream inputStream; - - /** - * 模板字节数组内容 - */ - private byte[] content; - - /** - * 模板类型 - */ - private TemplateType type; - - /** - * 模板名称(用于标识和缓存) - */ - private String name; - - /** - * 是否启用缓存 - */ - private boolean enableCache = true; - - /** - * 从文件路径创建模板源 - */ - public static TemplateSource fromFile(String filePath) { - return TemplateSource.builder() - .filePath(filePath) - .type(TemplateType.DOCX) - .name(extractFileName(filePath)) - .build(); - } - - /** - * 从输入流创建模板源 - */ - public static TemplateSource fromStream(InputStream inputStream, String name) { - return TemplateSource.builder() - .inputStream(inputStream) - .type(TemplateType.DOCX) - .name(name) - .build(); - } - - /** - * 从字节数组创建模板源 - */ - public static TemplateSource fromBytes(byte[] content, String name) { - return TemplateSource.builder() - .content(content) - .type(TemplateType.DOCX) - .name(name) - .build(); - } - - /** - * 从文件路径提取文件名 - */ - private static String extractFileName(String filePath) { - if (filePath == null) return null; - int lastSeparator = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); - return lastSeparator >= 0 ? filePath.substring(lastSeparator + 1) : filePath; - } - - /** - * 验证模板源是否有效 - */ - public boolean isValid() { - return filePath != null || inputStream != null || - (content != null && content.length > 0); - } - - /** - * 获取模板源描述(用于日志) - */ - public String getDescription() { - if (filePath != null) { - return "file://" + filePath; - } else if (name != null) { - return "stream://" + name; - } else { - return "bytes://unknown"; - } - } -} \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/constant/ReportConstant.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/constant/ReportConstant.java similarity index 85% rename from detection/src/main/java/com/njcn/gather/report/pojo/constant/ReportConstant.java rename to tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/constant/ReportConstant.java index 3b4cfc8b..a0709edd 100644 --- a/detection/src/main/java/com/njcn/gather/report/pojo/constant/ReportConstant.java +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/constant/ReportConstant.java @@ -1,4 +1,4 @@ -package com.njcn.gather.report.pojo.constant; +package com.njcn.gather.tools.report.model.constant; /** * diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ReportResponseEnum.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/enums/ReportResponseEnum.java similarity index 97% rename from tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ReportResponseEnum.java rename to tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/enums/ReportResponseEnum.java index e13b8189..971f6e6b 100644 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/ReportResponseEnum.java +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/enums/ReportResponseEnum.java @@ -1,4 +1,4 @@ -package com.njcn.gather.tools.report.model; +package com.njcn.gather.tools.report.model.enums; import lombok.Getter; diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateType.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/enums/TemplateType.java similarity index 96% rename from tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateType.java rename to tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/enums/TemplateType.java index 21f9f718..1f1c05a5 100644 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/TemplateType.java +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/model/enums/TemplateType.java @@ -1,4 +1,4 @@ -package com.njcn.gather.tools.report.model; +package com.njcn.gather.tools.report.model.enums; /** * 模板文档类型枚举 diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/IWordReportService.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/IWordReportService.java new file mode 100644 index 00000000..20889a67 --- /dev/null +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/IWordReportService.java @@ -0,0 +1,25 @@ +package com.njcn.gather.tools.report.service; + +import java.io.InputStream; +import java.util.Map; + +/** + * Word报告生成接口 + * + * @author hongawen + * @version 1.0 + * @data 2025/9/5 10:13 + */ +public interface IWordReportService { + + /** + * 替换Word文档中的占位符 + * + * @param templateInputStream 模板文档输入流 + * @param placeholderMap 占位符替换映射表,key为占位符标识,value为替换值 + * @return 处理后的文档输入流,调用方可根据需要进行下载、上传等操作 + * @throws Exception 处理异常 + */ + InputStream replacePlaceholders(InputStream templateInputStream, Map placeholderMap) throws Exception; + +} diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/ReportGeneratorService.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/ReportGeneratorService.java deleted file mode 100644 index 1c907cbe..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/ReportGeneratorService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.njcn.gather.tools.report.service; - -import com.njcn.gather.tools.report.model.TemplateRequest; -import com.njcn.gather.tools.report.model.ProcessResult; - -/** - * 报告生成服务接口 - * - * @author hongawen - */ -public interface ReportGeneratorService { - - /** - * 处理模板并生成报告 - * - * @param request 模板处理请求 - * @return 处理结果 - */ - ProcessResult processTemplate(TemplateRequest request); - - /** - * 异步处理模板并生成报告 - * - * @param request 模板处理请求 - * @return 请求ID,用于后续查询结果 - */ - String processTemplateAsync(TemplateRequest request); - - /** - * 查询异步处理结果 - * - * @param requestId 请求ID - * @return 处理结果,如果还在处理中则返回null - */ - ProcessResult getAsyncResult(String requestId); - - /** - * 批量处理模板 - * - * @param requests 模板处理请求列表 - * @return 处理结果列表 - */ - java.util.List batchProcessTemplates(java.util.List requests); - - /** - * 验证模板是否有效 - * - * @param request 模板处理请求 - * @return 验证结果 - */ - ProcessResult validateTemplate(TemplateRequest request); -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/impl/ReportGeneratorServiceImpl.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/impl/ReportGeneratorServiceImpl.java deleted file mode 100644 index fe393f24..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/impl/ReportGeneratorServiceImpl.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.njcn.gather.tools.report.service.impl; - -import cn.hutool.core.util.IdUtil; -import com.njcn.gather.tools.report.engine.DocumentProcessor; -import com.njcn.gather.tools.report.model.TemplateRequest; -import com.njcn.gather.tools.report.model.ProcessResult; -import com.njcn.gather.tools.report.service.ReportGeneratorService; -import com.njcn.common.pojo.exception.BusinessException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -/** - * 报告生成服务实现 - * - * @author hongawen - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class ReportGeneratorServiceImpl implements ReportGeneratorService { - - private final List documentProcessors; - - // 异步处理结果缓存 - private final Map asyncResults = new ConcurrentHashMap<>(); - - // 异步处理线程池 - private final Executor asyncExecutor = Executors.newFixedThreadPool(4); - - @Override - public ProcessResult processTemplate(TemplateRequest request) { - log.info("开始处理模板请求: {}", request.getRequestId()); - - try { - // 设置请求ID(如果没有设置的话) - if (request.getRequestId() == null) { - request.setRequestId(IdUtil.simpleUUID()); - } - - // 查找合适的处理器 - DocumentProcessor processor = findProcessor(request); - if (processor == null) { - return ProcessResult.failure("NO_SUITABLE_PROCESSOR", "找不到合适的文档处理器"); - } - - // 执行处理 - ProcessResult result = processor.process(request); - log.info("模板处理完成: {}, 状态: {}", request.getRequestId(), result.isSuccess()); - - return result; - - } catch (Exception e) { - log.error("模板处理异常: {}", e.getMessage(), e); - return ProcessResult.failure("PROCESSING_EXCEPTION", "模板处理异常", e.getMessage()); - } - } - - @Override - public String processTemplateAsync(TemplateRequest request) { - String requestId = request.getRequestId(); - if (requestId == null) { - requestId = IdUtil.simpleUUID(); - request.setRequestId(requestId); - } - - log.info("启动异步模板处理: {}", requestId); - - // 创建处理中的结果 - ProcessResult processingResult = ProcessResult.builder() - .requestId(requestId) - .success(false) - .message("处理中...") - .startTime(LocalDateTime.now()) - .build(); - - asyncResults.put(requestId, processingResult); - - // 异步执行处理 - final String finalRequestId = requestId; - final TemplateRequest finalRequest = request; - CompletableFuture.runAsync(new Runnable() { - @Override - public void run() { - try { - ProcessResult result = processTemplate(finalRequest); - asyncResults.put(finalRequestId, result); - } catch (Exception e) { - ProcessResult errorResult = ProcessResult.failure("ASYNC_PROCESSING_ERROR", - "异步处理异常", e.getMessage()); - errorResult.setRequestId(finalRequestId); - asyncResults.put(finalRequestId, errorResult); - } - } - }, asyncExecutor); - - return requestId; - } - - @Override - public ProcessResult getAsyncResult(String requestId) { - return asyncResults.get(requestId); - } - - @Override - public List batchProcessTemplates(List requests) { - log.info("开始批量处理模板,数量: {}", requests.size()); - - List results = new ArrayList<>(); - - for (TemplateRequest request : requests) { - try { - ProcessResult result = processTemplate(request); - results.add(result); - } catch (Exception e) { - log.error("批量处理中的单个模板处理失败: {}", e.getMessage(), e); - ProcessResult errorResult = ProcessResult.failure("BATCH_ITEM_ERROR", - "批量处理中的模板处理失败", e.getMessage()); - if (request.getRequestId() != null) { - errorResult.setRequestId(request.getRequestId()); - } - results.add(errorResult); - } - } - - long successCount = 0; - long failureCount = 0; - for (ProcessResult result : results) { - if (result.isSuccess()) { - successCount++; - } else { - failureCount++; - } - } - log.info("批量处理完成,成功: {}, 失败: {}", successCount, failureCount); - - return results; - } - - @Override - public ProcessResult validateTemplate(TemplateRequest request) { - log.debug("验证模板请求: {}", request.getRequestId()); - - try { - // 基本参数验证 - if (request == null) { - return ProcessResult.failure("VALIDATION_ERROR", "请求对象不能为空"); - } - - if (!request.isValid()) { - return ProcessResult.failure("VALIDATION_ERROR", "请求参数无效"); - } - - // 查找处理器 - DocumentProcessor processor = findProcessor(request); - if (processor == null) { - return ProcessResult.failure("VALIDATION_ERROR", "找不到合适的文档处理器"); - } - - // 处理器特定验证 - try { - processor.validateRequest(request); - return ProcessResult.success("模板验证通过"); - } catch (BusinessException e) { - return ProcessResult.failure("VALIDATION_ERROR", "模板验证失败: " + e.getMessage()); - } - - } catch (Exception e) { - log.error("模板验证异常: {}", e.getMessage(), e); - return ProcessResult.failure("VALIDATION_EXCEPTION", "模板验证异常", e.getMessage()); - } - } - - /** - * 查找合适的文档处理器 - */ - private DocumentProcessor findProcessor(TemplateRequest request) { - for (DocumentProcessor processor : documentProcessors) { - if (processor.supports(request)) { - log.debug("找到合适的处理器: {}", processor.getName()); - return processor; - } - } - - log.warn("未找到合适的处理器,模板类型: {}", - request.getTemplateSource() != null ? request.getTemplateSource().getType() : "unknown"); - return null; - } - - /** - * 清理过期的异步结果 - * 可以通过定时任务调用 - */ - public void cleanupExpiredAsyncResults() { - LocalDateTime expireTime = LocalDateTime.now().minusHours(24); - List expiredKeys = new ArrayList<>(); - for (Map.Entry entry : asyncResults.entrySet()) { - ProcessResult result = entry.getValue(); - if (result.getEndTime() != null && result.getEndTime().isBefore(expireTime)) { - expiredKeys.add(entry.getKey()); - } - } - for (String key : expiredKeys) { - asyncResults.remove(key); - } - } -} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/impl/WordReportServiceImpl.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/impl/WordReportServiceImpl.java new file mode 100644 index 00000000..4efd8d8a --- /dev/null +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/service/impl/WordReportServiceImpl.java @@ -0,0 +1,50 @@ +package com.njcn.gather.tools.report.service.impl; + +import com.njcn.gather.tools.report.service.IWordReportService; +import com.njcn.gather.tools.report.util.PlaceholderUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Map; + +/** + * Word报告生成服务实现类 + * + * @author hongawen + * @version 1.0 + * @data 2025/9/5 10:14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WordReportServiceImpl implements IWordReportService { + + @Override + public InputStream replacePlaceholders(InputStream templateInputStream, Map placeholderMap) throws Exception { + if (templateInputStream == null || placeholderMap == null) { + throw new IllegalArgumentException("输入参数不能为空"); + } + log.info("开始执行占位符替换,共有 {} 个占位符", placeholderMap.size()); + + // 加载 Word 文档 + WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(templateInputStream); + MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart(); + + // 使用工具类批量替换占位符 + PlaceholderUtil.replaceAllPlaceholders(mainDocumentPart, placeholderMap); + + // 将处理后的文档转换为字节数组输入流 + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + wordPackage.save(outputStream); + byte[] documentBytes = outputStream.toByteArray(); + log.info("占位符替换完成,生成文档大小: {} bytes", documentBytes.length); + return new ByteArrayInputStream(documentBytes); + } + } +} diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/BookmarkUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/BookmarkUtil.java new file mode 100644 index 00000000..7db0613a --- /dev/null +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/BookmarkUtil.java @@ -0,0 +1,172 @@ +package com.njcn.gather.tools.report.util; + +import org.docx4j.wml.*; + +import javax.xml.bind.JAXBElement; +import java.util.ArrayList; +import java.util.List; + + +/** + * 递归查找所有书签,并在书签处插入内容 + * @author hongawen + */ +public class BookmarkUtil { + + /** + * 书签信息 + */ + public static class BookmarkInfo { + public CTBookmark bookmark; + public P parentParagraph; + public ContentAccessor parentContainer; + } + + /** + * 递归查找所有书签 + */ + public static List findAllBookmarks(ContentAccessor contentAccessor) { + List result = new ArrayList<>(); + for (Object obj : contentAccessor.getContent()) { + Object realObj = (obj instanceof JAXBElement) ? ((JAXBElement) obj).getValue() : obj; + if (realObj instanceof P) { + P p = (P) realObj; + for (Object o2 : p.getContent()) { + Object realO2 = (o2 instanceof JAXBElement) ? ((JAXBElement) o2).getValue() : o2; + if (realO2 instanceof CTBookmark) { + BookmarkInfo info = new BookmarkInfo(); + info.bookmark = (CTBookmark) realO2; + info.parentParagraph = p; + info.parentContainer = contentAccessor; + result.add(info); + } + } + } else if (realObj instanceof ContentAccessor) { + result.addAll(findAllBookmarks((ContentAccessor) realObj)); + } + } + return result; + } + + + /** + * 在书签后插入段落 + */ + public static void insertParagraphsAfter(BookmarkInfo info, P paragraph) { + List parentContent = info.parentContainer.getContent(); + int idx = parentContent.indexOf(info.parentParagraph); + parentContent.add(idx + 1, paragraph); + } + + /** + * 在书签后插入表格 + */ + public static void insertTableAfter(BookmarkInfo info, Tbl table) { + List parentContent = info.parentContainer.getContent(); + int idx = parentContent.indexOf(info.parentParagraph); + parentContent.add(idx + 1, table); + } + + /** + * 在书签后插入元素,可能是段落、表格、图片、书签等 + */ + public static void insertElement(BookmarkInfo info, List elements) { + List parentContent = info.parentContainer.getContent(); + int idx = parentContent.indexOf(info.parentParagraph); + // 遍历元素,如果是通道回路这种大标题需要新起一个空的文档页 + for (int i = 0; i < elements.size(); i++) { + Object element = elements.get(i); + if (element instanceof P) { + P p = (P) element; + String textFromP = Docx4jUtil.getTextFromP(p); + if (textFromP.contains("测量回路")) { + if (!textFromP.contains("1")) { + // 另起一页 + P pagePara = Docx4jUtil.getPageBreak(); + idx = idx + 1; + parentContent.add(idx, pagePara); + } + idx = idx + 1; + parentContent.add(idx, p); + } +// else if ( +// textFromP.startsWith(PowerIndexEnum.IMBV.getDesc()) +// || textFromP.startsWith(PowerIndexEnum.HV.getDesc()) +// || textFromP.startsWith(PowerIndexEnum.HI.getDesc()) +// +// ) { +// // 另起一页 +// P pagePara = Docx4jUtil.getPageBreak(); +// idx = idx + 1; +// parentContent.add(idx, pagePara); +// idx = idx + 1; +// parentContent.add(idx, element); +// }else if(textFromP.startsWith("注:基波电流幅值5.000A,基波频率50.0Hz,各次间谐波电流含有率均为3.0%。")){ +// idx = idx + 1; +// parentContent.add(idx, element); +// P pagePara = Docx4jUtil.getPageBreak(); +// idx = idx + 1; +// parentContent.add(idx, pagePara); +// } + else { + idx = idx + 1; + parentContent.add(idx, element); + } + } else { + idx = idx + 1; + parentContent.add(idx, element); + } + } + } + + + /** + * 删除书签 + * + * @param bookmarkInfo 书签信息 + */ + public static void removeBookmark(BookmarkInfo bookmarkInfo) { + try { + // 获取书签所在的段落 + P paragraph = bookmarkInfo.parentParagraph; + + // 遍历段落内容,找到并删除书签开始和结束标记 + List paragraphContent = new ArrayList<>(paragraph.getContent()); + for (Object obj : paragraphContent) { + if (obj instanceof JAXBElement) { + JAXBElement element = (JAXBElement) obj; + Object value = element.getValue(); + + // 删除书签开始标记 + if (value instanceof CTBookmark) { + paragraph.getContent().remove(obj); + } + // 删除书签结束标记 + else if (value instanceof CTMarkupRange) { + paragraph.getContent().remove(obj); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * 获取指定标签的标签信息 + * + * @param key 标签名 + * @param bookmarks 所有标签信息 + */ + public static BookmarkInfo getBookmarkInfo(String key, List bookmarks) { + BookmarkInfo bookmarkInfo = null; + for (BookmarkInfo info : bookmarks) { + String name = info.bookmark.getName(); + if (key.equalsIgnoreCase(name)) { + bookmarkInfo = info; + } + } + return bookmarkInfo; + } +} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jAdvancedUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jAdvancedUtil.java deleted file mode 100644 index f6eb9495..00000000 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jAdvancedUtil.java +++ /dev/null @@ -1,603 +0,0 @@ -package com.njcn.gather.tools.report.util; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.text.StrPool; -import cn.hutool.core.util.StrUtil; -import org.docx4j.XmlUtils; -import org.docx4j.openpackaging.packages.WordprocessingMLPackage; -import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; -import org.docx4j.wml.*; - -import javax.xml.bind.JAXBElement; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * 基于docx4j的高级Word文档工具类 - * 从原有Docx4jUtil中抽取的通用功能,去除业务相关逻辑 - * - * @author hongawen - */ -public class Docx4jAdvancedUtil { - - /** - * 创建标题段落 - * - * @param factory 对象工厂 - * @param paragraph 段落容器 - * @param content 标题内容 - * @param fontSize 字体大小 - * @param isBold 是否加粗 - */ - public static void createTitle(ObjectFactory factory, P paragraph, String content, int fontSize, boolean isBold) { - R run = factory.createR(); - Text text = factory.createText(); - text.setValue(content); - - // 创建运行属性 - RPr rPr = factory.createRPr(); - - // 设置字体 - RFonts fonts = factory.createRFonts(); - fonts.setAscii("Arial"); - fonts.setEastAsia("SimSun"); // 宋体 - rPr.setRFonts(fonts); - - // 设置字号 - HpsMeasure size = new HpsMeasure(); - size.setVal(new BigInteger(String.valueOf(fontSize))); // 12号字=24 - rPr.setSz(size); - rPr.setSzCs(size); - - // 设置粗体 - if (isBold) { - BooleanDefaultTrue b = new BooleanDefaultTrue(); - rPr.setB(b); - } - - run.setRPr(rPr); - run.getContent().add(text); - paragraph.getContent().add(run); - } - - /** - * 提取文档中指定标题级别的内容 - * - * @param allContent 文档中所有内容 - * @param headingLevel 标题级别 (如 "5" 表示 Heading 5) - * @return 标题内容列表 - */ - public static List extractHeadingContents(List allContent, String headingLevel) { - List result = new ArrayList<>(); - boolean inHeadingSection = false; - HeadingContent currentHeading = null; - - for (Object obj : allContent) { - if (obj instanceof P) { - P paragraph = (P) obj; - if (isHeadingLevel(paragraph, headingLevel)) { - // 发现新的指定级别标题,保存前一个并创建新的 - if (currentHeading != null) { - result.add(currentHeading); - } - currentHeading = new HeadingContent(); - currentHeading.setHeadingText(getTextFromP(paragraph)); - inHeadingSection = true; - } else if (inHeadingSection) { - // 当前内容属于标题的子内容 - currentHeading.addSubContent(paragraph); - } - } else if (obj instanceof JAXBElement && inHeadingSection) { - // 表格属于当前标题的子内容 - JAXBElement jaxbElement = (JAXBElement) obj; - if (jaxbElement.getValue() instanceof Tbl) { - currentHeading.addSubContent(obj); - } - } else if (isHigherLevelHeading(obj, headingLevel)) { - // 遇到更高级别的标题,结束当前标题的收集 - if (currentHeading != null) { - result.add(currentHeading); - currentHeading = null; - } - inHeadingSection = false; - } - } - - // 添加最后一个标题 - if (currentHeading != null) { - result.add(currentHeading); - } - - return result; - } - - /** - * 判断段落是否为指定级别的标题 - * - * @param paragraph 段落 - * @param headingLevel 标题级别 - * @return 是否匹配 - */ - private static boolean isHeadingLevel(P paragraph, String headingLevel) { - PPr ppr = paragraph.getPPr(); - if (ppr != null) { - PPrBase.PStyle pStyle = ppr.getPStyle(); - if (pStyle != null && headingLevel.equals(pStyle.getVal())) { - return true; - } - } - return false; - } - - /** - * 判断是否为更高级别的标题 - * - * @param obj 对象 - * @param currentLevel 当前级别 - * @return 是否为更高级别 - */ - private static boolean isHigherLevelHeading(Object obj, String currentLevel) { - if (obj instanceof P) { - PPr ppr = ((P) obj).getPPr(); - if (ppr != null) { - PPrBase.PStyle pStyle = ppr.getPStyle(); - if (pStyle != null) { - String style = pStyle.getVal(); - if (style != null && style.matches("[1-9]")) { - try { - int current = Integer.parseInt(currentLevel); - int found = Integer.parseInt(style); - return found < current; - } catch (NumberFormatException e) { - return false; - } - } - } - } - } - return false; - } - - /** - * 判断表格是否为横向布局 - * - * @param obj 表格行对象 - * @return true表示横向,false表示纵向 - */ - public static boolean judgeTableCross(Object obj) { - if (!(obj instanceof Tr)) { - return true; - } - - Tr row = (Tr) obj; - List content = row.getContent(); - if (content.isEmpty()) { - return true; - } - - // 取最后一个单元格,判断是否包含中文 - Object cellObject = content.get(content.size() - 1); - if (cellObject instanceof JAXBElement) { - @SuppressWarnings("unchecked") - JAXBElement cellElement = (JAXBElement) cellObject; - Tc cell = cellElement.getValue(); - String text = getTextFromCell(cell); - - if (StrUtil.isBlank(text)) { - return true; - } - - // 检查是否包含中文字符 - return containsChinese(text); - } - - return true; - } - - /** - * 读取单元格内的文本内容 - * - * @param cell 单元格 - * @return 文本内容 - */ - public static String getTextFromCell(Tc cell) { - List cellContent = cell.getContent(); - StringBuilder cellText = new StringBuilder(); - - for (Object content : cellContent) { - if (content instanceof P) { - P paragraph = (P) content; - cellText.append(getTextFromP(paragraph)); - } - } - - return cellText.toString(); - } - - /** - * 从段落中提取纯文本 - * - * @param paragraph 段落 - * @return 段落内容 - */ - public static String getTextFromP(P paragraph) { - StringBuilder textContent = new StringBuilder(); - - for (Object runObj : paragraph.getContent()) { - if (runObj instanceof R) { - R run = (R) runObj; - for (Object textObj : run.getContent()) { - if (textObj instanceof Text) { - textContent.append(((Text) textObj).getValue()); - } else if (textObj instanceof JAXBElement) { - JAXBElement jaxbElement = (JAXBElement) textObj; - if (jaxbElement.getValue() instanceof Text) { - Text temp = (Text) jaxbElement.getValue(); - textContent.append(temp.getValue()); - } - } - } - } - } - - return textContent.toString().trim(); - } - - /** - * 获取段落的样式属性 - * - * @param paragraph 段落 - * @return 运行属性 - */ - public static RPr getTcPrFromParagraph(P paragraph) { - List content = paragraph.getContent(); - RPr preservedRPr = null; - - if (!content.isEmpty()) { - Object firstObj = content.get(0); - if (firstObj instanceof R) { - preservedRPr = ((R) firstObj).getRPr(); - } - } - - return preservedRPr; - } - - /** - * 向段落中添加内容 - * - * @param factory 对象工厂 - * @param paragraph 段落 - * @param content 内容 - * @param rPr 运行属性 - * @param pPr 段落属性 - */ - public static void addPContent(ObjectFactory factory, P paragraph, String content, RPr rPr, PPr pPr) { - R run = factory.createR(); - Text text = factory.createText(); - text.setValue(content); - run.setRPr(rPr); - run.getContent().add(text); - paragraph.getContent().add(run); - paragraph.setPPr(pPr); - } - - /** - * 创建N个换行符 - * - * @param factory 对象工厂 - * @param paragraph 段落 - * @param n 换行符数量 - */ - public static void addBr(ObjectFactory factory, P paragraph, int n) { - R run = factory.createR(); - for (int i = 0; i < n; i++) { - Br br = factory.createBr(); - run.getContent().add(br); - } - paragraph.getContent().add(run); - } - - /** - * 根据表格行获取需要填充的键列表 - * - * @param row 表格行 - * @return 键列表 - */ - public static List getTableKeys(Tr row) { - List keys = new ArrayList<>(); - List content = row.getContent(); - - for (Object cellObject : content) { - if (cellObject instanceof JAXBElement) { - @SuppressWarnings("unchecked") - JAXBElement cellElement = (JAXBElement) cellObject; - Tc cell = cellElement.getValue(); - keys.add(getTextFromCell(cell)); - } - } - - return keys; - } - - /** - * 创建自定义表格行 - * - * @param factory 对象工厂 - * @param valueMap 数据映射 - * @param tableKeys 表格键列表 - * @param trPr 行属性 - * @param tcPr 单元格属性 - * @param centerFlag 是否居中 - * @return 创建的表格行 - */ - public static Tr createCustomRow(ObjectFactory factory, Map valueMap, - List tableKeys, TrPr trPr, TcPr tcPr, boolean centerFlag) { - Tr row = factory.createTr(); - - for (String tableKey : tableKeys) { - Tc cell = factory.createTc(); - P paragraph = factory.createP(); - R run = factory.createR(); - - String value = valueMap.getOrDefault(tableKey, ""); - Text text = factory.createText(); - text.setValue(value); - run.getContent().add(text); - paragraph.getContent().add(run); - - // 设置字体和样式 - RPr rPr = factory.createRPr(); - RFonts rFonts = factory.createRFonts(); - - if (containsChinese(value)) { - rFonts.setEastAsia("宋体"); - rFonts.setAscii("宋体"); - rFonts.setHAnsi("宋体"); - } else { - rFonts.setEastAsia("Arial"); - rFonts.setAscii("Arial"); - rFonts.setHAnsi("Arial"); - } - rPr.setRFonts(rFonts); - - // 设置段落居中 - if (centerFlag) { - PPr pPr = factory.createPPr(); - Jc jc = factory.createJc(); - jc.setVal(JcEnumeration.CENTER); - pPr.setJc(jc); - paragraph.setPPr(pPr); - } - - // 设置特殊颜色(如不合格为红色) - if ("不合格".equals(value)) { - Color color = factory.createColor(); - color.setVal("FF0000"); // 红色 - rPr.setColor(color); - } - - // 设置字体大小 - HpsMeasure sz = factory.createHpsMeasure(); - sz.setVal(new BigInteger("20")); // 10号字体 = 20 half-points - rPr.setSz(sz); - - run.setRPr(rPr); - cell.getContent().add(paragraph); - cell.setTcPr(tcPr); - row.getContent().add(cell); - } - - row.setTrPr(trPr); - return row; - } - - /** - * 创建自定义表格行(使用列表数据) - * - * @param factory 对象工厂 - * @param cellValues 单元格值列表 - * @param ascFontStyle 西文字体 - * @param eastFontStyle 中文字体 - * @param size 字体大小 - * @param boldFlag 是否加粗 - * @param centerFlag 居中标志列表 - * @return 创建的表格行 - */ - public static Tr createCustomRow(ObjectFactory factory, List cellValues, - String ascFontStyle, String eastFontStyle, Integer size, - boolean boldFlag, List centerFlag) { - Tr row = factory.createTr(); - - for (int i = 0; i < cellValues.size(); i++) { - String value = cellValues.get(i); - Tc cell = factory.createTc(); - P paragraph = factory.createP(); - R run = factory.createR(); - - Text text = factory.createText(); - text.setValue(value); - run.getContent().add(text); - paragraph.getContent().add(run); - - // 设置段落居中 - if (!centerFlag.contains(i)) { - PPr pPr = factory.createPPr(); - Jc jc = factory.createJc(); - jc.setVal(JcEnumeration.CENTER); - pPr.setJc(jc); - paragraph.setPPr(pPr); - } - - // 设置字体和样式 - RPr rPr = factory.createRPr(); - - // 设置颜色 - if ("不合格".equals(value)) { - Color color = factory.createColor(); - color.setVal("FF0000"); // 红色 - rPr.setColor(color); - } - - // 设置加粗 - if (boldFlag) { - BooleanDefaultTrue bold = factory.createBooleanDefaultTrue(); - rPr.setB(bold); - } - - // 设置字体 - RFonts fonts = factory.createRFonts(); - fonts.setAscii(ascFontStyle); // 西文字体 - fonts.setEastAsia(eastFontStyle); // 中文字体 - rPr.setRFonts(fonts); - - // 设置字号 - HpsMeasure fontSize = factory.createHpsMeasure(); - fontSize.setVal(BigInteger.valueOf(size)); - rPr.setSz(fontSize); // 西文字号 - rPr.setSzCs(fontSize); // 中文字号 - - run.setRPr(rPr); - cell.getContent().add(paragraph); - - // 设置单元格边距 - TcPr cellProperties = factory.createTcPr(); - TcMar mar = factory.createTcMar(); - - TblWidth top = factory.createTblWidth(); - top.setW(BigInteger.valueOf(100)); - mar.setTop(top); - - TblWidth bottom = factory.createTblWidth(); - bottom.setW(BigInteger.valueOf(100)); - mar.setBottom(bottom); - - cellProperties.setTcMar(mar); - cell.setTcPr(cellProperties); - row.getContent().add(cell); - } - - return row; - } - - /** - * 深拷贝表格元素 - * - * @param original 原始表格元素 - * @return 拷贝的表格元素 - * @throws Exception 拷贝失败时抛出异常 - */ - public static JAXBElement deepCopyTbl(JAXBElement original) throws Exception { - // 使用 docx4j 的 XmlUtils 进行深拷贝 - Tbl clonedTbl = (Tbl) XmlUtils.deepCopy(original.getValue()); - - // 重新包装为 JAXBElement - return new JAXBElement<>( - original.getName(), - original.getDeclaredType(), - original.getScope(), - clonedTbl - ); - } - - /** - * 获取表格样式属性 - * - * @param factory 对象工厂 - * @return 表格属性 - */ - public static TblPr getTblPr(ObjectFactory factory) { - TblPr tblPr = factory.createTblPr(); - TblBorders borders = factory.createTblBorders(); - - // 定义边框样式(1磅黑色单实线) - CTBorder border = new CTBorder(); - border.setVal(STBorder.SINGLE); // 实线类型 - border.setSz(BigInteger.valueOf(4)); // 1磅=4单位(1/8磅) - border.setColor("000000"); // 黑色 - - // 应用边框到所有边 - borders.setTop(border); - borders.setBottom(border); - borders.setLeft(border); - borders.setRight(border); - borders.setInsideH(border); // 内部水平线 - borders.setInsideV(border); // 内部垂直线 - - tblPr.setTblBorders(borders); - - // 设置表格宽度 - TblWidth tblWidth = factory.createTblWidth(); - tblWidth.setType("pct"); // 百分比类型 - tblWidth.setW(BigInteger.valueOf(5000)); // 96% = 4800/5000 (ISO标准) - tblPr.setTblW(tblWidth); - - return tblPr; - } - - /** - * 创建分页符段落 - * - * @return 包含分页符的段落 - */ - public static P getPageBreak() { - try { - ObjectFactory factory = new ObjectFactory(); - R run = factory.createR(); - Br br = factory.createBr(); - br.setType(STBrType.PAGE); - run.getContent().add(br); - - P pageBreakParagraph = factory.createP(); - pageBreakParagraph.getContent().add(run); - return pageBreakParagraph; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - /** - * 判断字符串是否包含中文 - * - * @param str 需要判断的字符串 - * @return 是否包含中文 - */ - private static boolean containsChinese(String str) { - if (str == null) { - return false; - } - for (char c : str.toCharArray()) { - if (Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN) { - return true; - } - } - return false; - } - - /** - * 存储标题及其子内容的辅助类 - */ - public static class HeadingContent { - private String headingText; - private List subContent = new ArrayList<>(); - - public void setHeadingText(String text) { - this.headingText = text; - } - - public String getHeadingText() { - return headingText; - } - - public void addSubContent(Object obj) { - subContent.add(obj); - } - - public List getSubContent() { - return subContent; - } - } -} \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/report/utils/Docx4jUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jUtil.java similarity index 81% rename from detection/src/main/java/com/njcn/gather/report/utils/Docx4jUtil.java rename to tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jUtil.java index e1f49020..bd013761 100644 --- a/detection/src/main/java/com/njcn/gather/report/utils/Docx4jUtil.java +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jUtil.java @@ -1,10 +1,11 @@ -package com.njcn.gather.report.utils; +package com.njcn.gather.tools.report.util; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.StrUtil; -import com.njcn.gather.report.pojo.constant.ReportConstant; -import com.njcn.gather.report.pojo.enums.DocAnchorEnum; +import com.njcn.gather.tools.report.model.constant.ReportConstant; +import lombok.Getter; +import lombok.Setter; import org.docx4j.XmlUtils; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; @@ -67,21 +68,35 @@ public class Docx4jUtil { } /** - * 提取Heading 5及其子内容 - * - * @param allContent 文档内所有内容 + * 提取文档中所有Heading 5标题及其子内容 + * + * 该方法按文档顺序遍历内容,识别Heading 5级别的标题,并收集每个标题下的所有子内容, + * 直到遇到下一个Heading 5标题为止。 + * + * 处理逻辑: + * 1. 遇到Heading 5时,保存前一个标题组并开始新的收集 + * 2. 在标题组内时,收集所有段落和表格等子内容(包括1-4级标题) + * 3. 只有遇到下一个Heading 5标题时才结束当前标题组的收集 + * 4. 文档末尾时,保存最后一个标题组 + * + * @param allContent 文档内所有内容对象的列表(包含段落、表格等) + * @return Heading 5标题及其子内容的列表,按文档中出现的顺序排列 */ public static List extractHeading5Contents(List allContent) { + // 参数验证 + if (allContent == null || allContent.isEmpty()) { + return new ArrayList<>(); + } List result = new ArrayList<>(); + // 是否正在Heading 5标题组内 boolean inHeading5Section = false; + // 当前正在处理的标题组 HeadingContent currentHeading = null; - for (Object obj : allContent) { - if (obj instanceof P) { P paragraph = (P) obj; if (isHeading5(paragraph)) { - // 发现新的Heading 5,保存前一个并创建新的 + // 发现新的Heading 5标题,保存前一个标题组并创建新的 if (currentHeading != null) { result.add(currentHeading); } @@ -89,60 +104,38 @@ public class Docx4jUtil { currentHeading.setHeadingText(getTextFromP(paragraph)); inHeading5Section = true; } else if (inHeading5Section) { - // 当前内容属于Heading 5的子内容 + // 在标题组内时,收集所有段落作为子内容(包括1-4级标题) currentHeading.addSubContent(paragraph); } - } else if (obj instanceof JAXBElement && inHeading5Section) { - // 表格属于当前Heading 5的子内容 - JAXBElement jaxbElement = (JAXBElement) obj; - if (jaxbElement.getValue() instanceof Tbl) { - currentHeading.addSubContent(obj); - } - } else if (isHigherLevelHeading(obj)) { - // 遇到更高级别的标题,结束当前Heading 5的收集 - if (currentHeading != null) { - result.add(currentHeading); - currentHeading = null; - } - inHeading5Section = false; + } else if (inHeading5Section) { + // 在标题组内时,收集所有其他内容(表格、图片、书签等) + currentHeading.addSubContent(obj); } - - + // 注意:删除了isHigherLevelHeading的处理,因为需要收集5级标题间的所有内容 } - // 添加最后一个Heading 5 + // 处理文档末尾:添加最后一个Heading 5标题组 if (currentHeading != null) { result.add(currentHeading); } + return result; } - // 判断段落是否为Heading 5 + /** + * 判断段落是否为Heading 5 + * @param paragraph 段落 + */ private static boolean isHeading5(P paragraph) { PPr ppr = paragraph.getPPr(); if (ppr != null) { PPrBase.PStyle pStyle = ppr.getPStyle(); - if (pStyle != null && "5".equals(pStyle.getVal())) { - return true; - } + return pStyle != null && "5".equals(pStyle.getVal()); } return false; } - // 判断是否为更高级别的标题(1-4) - private static boolean isHigherLevelHeading(Object obj) { - if (obj instanceof P) { - PPr ppr = ((P) obj).getPPr(); - if (ppr != null) { - PPrBase.PStyle pStyle = ppr.getPStyle(); - if (pStyle != null) { - String style = pStyle.getVal(); - return style != null && style.matches("[1-4]"); - } - } - } - return false; - } + /** * 判断表格是否横向 @@ -490,81 +483,22 @@ public class Docx4jUtil { } - // 存储Heading 5及其子内容的辅助类 + /** + * 存储Heading 5及其子内容的辅助类 + */ + @Getter public static class HeadingContent { + @Setter private String headingText; - private List subContent = new ArrayList<>(); - - public void setHeadingText(String text) { - this.headingText = text; - } - - public String getHeadingText() { - return headingText; - } + private final List subContent = new ArrayList<>(); public void addSubContent(Object obj) { subContent.add(obj); } - public List getSubContent() { - return subContent; - } } - /** - * 获取指定书签在文档段落中的位置索引 - * - * @param documentPart 主文档部分 - * @param bookmarkName 书签名称 - * @return 段落索引,找不到返回 -1 - */ - public static int getParagraphPosition(MainDocumentPart documentPart, String bookmarkName) { - List content = documentPart.getContent(); - for (int i = 0; i < content.size(); i++) { - Object obj = content.get(i); - // 只处理段落 - if (obj instanceof P) { - P paragraph = (P) obj; - // 提取段落纯文本 - String text = getTextFromP(paragraph).trim(); - if (text.startsWith("#{") && text.endsWith("}")) { - // 提取书签名 - String name = text.substring(2, text.length() - 1); - if (name.equals(bookmarkName)) { - // 返回段落索引 - return i; - } - } - } - } - // 找不到返回 -1 - return -1; - } - - - /** - * 获取段落在文档中的位置 - */ - public static int getParagraphPosition(MainDocumentPart baseDocumentPart, DocAnchorEnum docAnchorEnum) { - List baseContent = baseDocumentPart.getContent(); - for (int i = 0; i < baseContent.size(); i++) { - Object obj = baseContent.get(i); - if (obj instanceof P) { - P p = (P) obj; - String text = getTextFromP(p).trim(); - if (text.startsWith(ReportConstant.BOOKMARK_START)) { - DocAnchorEnum anchorEnum = DocAnchorEnum.getByKey(text); - if (anchorEnum != null && anchorEnum.getKey().equalsIgnoreCase(docAnchorEnum.getKey())) { - return i; - } - } - } - } - return -1; - } - /** * 获取表格样式 */ diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/DocxMergeUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/DocxMergeUtil.java new file mode 100644 index 00000000..69dd91de --- /dev/null +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/DocxMergeUtil.java @@ -0,0 +1,407 @@ +package com.njcn.gather.tools.report.util; + +import com.njcn.gather.tools.report.model.enums.ReportResponseEnum; +import lombok.extern.slf4j.Slf4j; +import org.docx4j.XmlUtils; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.HeaderPart; +import org.docx4j.openpackaging.parts.WordprocessingML.FooterPart; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.docx4j.openpackaging.parts.relationships.Namespaces; +import org.docx4j.openpackaging.parts.relationships.RelationshipsPart; +import org.docx4j.relationships.Relationship; +import org.docx4j.wml.*; +import org.springframework.util.CollectionUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Word文档合并工具类 + * 基于docx4j深度合并多个Word文档,保持完整格式和样式 + * + * 功能特点: + * - 支持多文档顺序合并 + * - 保持原文档格式、样式、页眉页脚 + * - 自动分页处理 + * - 异常安全和详细日志 + * + * 技术实现: + * - 使用docx4j XmlUtils.deepCopy确保格式完整性 + * - 处理页眉页脚关系映射 + * - 支持样式表合并 + * - 内存优化的流式处理 + * + * @author hongawen + * @version 1.0 + * @date 2025/9/8 + */ +@Slf4j +public class DocxMergeUtil { + + /** + * 合并多个Word文档输入流到单个输出流 + * 主要公共API,适用于大部分文档合并场景 + * + * @param sourceStreams 源文档输入流列表,按合并顺序排列 + * @param addPageBreaks 是否在文档间添加分页符,true表示每个文档独立分页 + * @return 合并后的文档输入流 + * @throws Exception 文档处理异常或参数验证失败 + */ + public static InputStream mergeDocuments(List sourceStreams, boolean addPageBreaks) throws Exception { + if (CollectionUtils.isEmpty(sourceStreams)) { + log.error("源文档流列表为空,无法执行合并操作"); + throw ReportExceptionUtil.create(ReportResponseEnum.VALIDATION_ERROR, "源文档列表不能为空"); + } + + if (sourceStreams.size() < 2) { + log.warn("源文档数量少于2个,返回第一个文档"); + return sourceStreams.get(0); + } + + log.info("开始合并 {} 个Word文档,分页设置: {}", sourceStreams.size(), addPageBreaks); + + try { + // 加载所有源文档 + List packages = new ArrayList<>(); + for (int i = 0; i < sourceStreams.size(); i++) { + InputStream stream = sourceStreams.get(i); + if (stream == null) { + log.warn("第 {} 个文档流为空,跳过", i + 1); + continue; + } + + try { + WordprocessingMLPackage pkg = WordprocessingMLPackage.load(stream); + packages.add(pkg); + log.debug("成功加载第 {} 个文档", i + 1); + } catch (Exception e) { + log.error("加载第 {} 个文档失败: {}", i + 1, e.getMessage()); + throw ReportExceptionUtil.create(ReportResponseEnum.TEMPLATE_PROCESS_ERROR, + "文档加载失败: " + e.getMessage()); + } + } + + if (packages.isEmpty()) { + throw ReportExceptionUtil.create(ReportResponseEnum.VALIDATION_ERROR, "没有有效的源文档可供合并"); + } + + // 执行合并 + WordprocessingMLPackage mergedPackage = mergePackages(packages, addPageBreaks); + + // 转换为输入流 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + mergedPackage.save(outputStream); + byte[] mergedBytes = outputStream.toByteArray(); + + log.info("文档合并成功,生成文档大小: {} bytes", mergedBytes.length); + return new ByteArrayInputStream(mergedBytes); + + } catch (Exception e) { + log.error("文档合并过程发生异常", e); + if (e instanceof RuntimeException) { + throw e; + } + throw ReportExceptionUtil.create(ReportResponseEnum.TEMPLATE_PROCESS_ERROR, + "文档合并失败: " + e.getMessage()); + } + } + + /** + * 合并多个Word文档输入流(默认添加分页符) + * 便捷方法,适用于需要独立分页的报告合并场景 + * + * @param sourceStreams 源文档输入流列表 + * @return 合并后的文档输入流 + * @throws Exception 文档处理异常 + */ + public static InputStream mergeDocuments(List sourceStreams) throws Exception { + return mergeDocuments(sourceStreams, true); + } + + /** + * 合并两个Word文档 + * 简化API,适用于只需要合并两个文档的场景 + * + * @param firstDocument 第一个文档输入流 + * @param secondDocument 第二个文档输入流 + * @param addPageBreak 是否在两个文档间添加分页符 + * @return 合并后的文档输入流 + * @throws Exception 文档处理异常 + */ + public static InputStream mergeTwoDocuments(InputStream firstDocument, InputStream secondDocument, + boolean addPageBreak) throws Exception { + List streams = new ArrayList<>(); + streams.add(firstDocument); + streams.add(secondDocument); + return mergeDocuments(streams, addPageBreak); + } + + /** + * 核心合并方法:合并WordprocessingMLPackage对象 + * 使用docx4j XmlUtils.deepCopy实现完整格式保持 + * + * @param packages 要合并的文档包列表 + * @param addPageBreaks 是否添加分页符 + * @return 合并后的文档包 + * @throws Exception 合并过程异常 + */ + private static WordprocessingMLPackage mergePackages(List packages, + boolean addPageBreaks) throws Exception { + if (packages.isEmpty()) { + throw new IllegalArgumentException("文档包列表不能为空"); + } + + // 使用第一个文档作为基础模板 + WordprocessingMLPackage targetPackage = packages.get(0); + MainDocumentPart targetMainPart = targetPackage.getMainDocumentPart(); + + log.debug("以第一个文档为基础,开始合并其他 {} 个文档", packages.size() - 1); + + // 合并其他文档的内容 + for (int i = 1; i < packages.size(); i++) { + WordprocessingMLPackage sourcePackage = packages.get(i); + MainDocumentPart sourceMainPart = sourcePackage.getMainDocumentPart(); + + try { + // 在合并前添加分页符(如果需要) + if (addPageBreaks && i > 1) { + addPageBreak(targetMainPart); + } + + // 合并文档内容 + mergeDocumentContent(targetMainPart, sourceMainPart); + + // 合并页眉页脚(如果存在) + mergeHeadersAndFooters(targetPackage, sourcePackage); + + log.debug("成功合并第 {} 个文档", i + 1); + + } catch (Exception e) { + log.error("合并第 {} 个文档时发生异常: {}", i + 1, e.getMessage()); + throw new Exception("文档合并失败: " + e.getMessage(), e); + } + } + + return targetPackage; + } + + /** + * 合并文档主要内容 + * 使用深度复制确保格式完整性 + * + * @param target 目标文档主体部分 + * @param source 源文档主体部分 + * @throws Exception 合并异常 + */ + private static void mergeDocumentContent(MainDocumentPart target, MainDocumentPart source) throws Exception { + try { + // 获取源文档的所有内容元素 + List sourceContent = source.getContent(); + + if (CollectionUtils.isEmpty(sourceContent)) { + log.debug("源文档内容为空,跳过合并"); + return; + } + + // 深度复制源文档内容到目标文档 + List targetContent = target.getContent(); + for (Object contentElement : sourceContent) { + try { + // 使用docx4j XmlUtils.deepCopy确保格式完整保持 + Object copiedElement = XmlUtils.deepCopy(contentElement); + targetContent.add(copiedElement); + } catch (Exception e) { + log.warn("复制内容元素时发生警告: {}, 元素类型: {}", + e.getMessage(), contentElement.getClass().getSimpleName()); + // 尝试直接添加(降级处理) + targetContent.add(contentElement); + } + } + + log.debug("成功合并 {} 个内容元素", sourceContent.size()); + + } catch (Exception e) { + log.error("合并文档内容时发生异常", e); + throw new Exception("文档内容合并失败: " + e.getMessage(), e); + } + } + + /** + * 添加分页符到文档 + * 在文档间创建清晰的分页分隔 + * + * @param mainPart 文档主体部分 + */ + private static void addPageBreak(MainDocumentPart mainPart) { + try { + ObjectFactory factory = new ObjectFactory(); + + // 创建段落 + P paragraph = factory.createP(); + + // 创建运行 + R run = factory.createR(); + + // 创建分页符 + Br pageBreak = factory.createBr(); + pageBreak.setType(STBrType.PAGE); + + // 组装分页符结构 + run.getContent().add(pageBreak); + paragraph.getContent().add(run); + + // 添加到文档 + mainPart.getContent().add(paragraph); + + log.debug("成功添加分页符"); + + } catch (Exception e) { + log.warn("添加分页符时发生警告: {}", e.getMessage()); + // 分页符添加失败不应该影响整个合并过程 + } + } + + /** + * 合并页眉和页脚 + * 处理复杂的关系映射和样式保持 + * + * @param target 目标文档包 + * @param source 源文档包 + */ + private static void mergeHeadersAndFooters(WordprocessingMLPackage target, WordprocessingMLPackage source) { + try { + // 获取关系部分 + RelationshipsPart targetRels = target.getMainDocumentPart().getRelationshipsPart(); + RelationshipsPart sourceRels = source.getMainDocumentPart().getRelationshipsPart(); + + if (sourceRels == null) { + log.debug("源文档没有关系部分,跳过页眉页脚合并"); + return; + } + + // 合并页眉 + mergeHeaderParts(target, source, targetRels, sourceRels); + + // 合并页脚 + mergeFooterParts(target, source, targetRels, sourceRels); + + log.debug("页眉页脚合并完成"); + + } catch (Exception e) { + log.warn("合并页眉页脚时发生警告: {}", e.getMessage()); + // 页眉页脚合并失败不应该影响主要内容合并 + } + } + + /** + * 合并页眉部分 + * + * @param target 目标文档包 + * @param source 源文档包 + * @param targetRels 目标关系部分 + * @param sourceRels 源关系部分 + */ + private static void mergeHeaderParts(WordprocessingMLPackage target, WordprocessingMLPackage source, + RelationshipsPart targetRels, RelationshipsPart sourceRels) { + try { + for (Relationship rel : sourceRels.getRelationships().getRelationship()) { + if (Namespaces.HEADER.equals(rel.getType())) { + HeaderPart sourceHeaderPart = (HeaderPart) source.getParts().get( + new org.docx4j.openpackaging.parts.PartName(rel.getTarget())); + + if (sourceHeaderPart != null) { + // 深度复制页眉内容 + HeaderPart newHeaderPart = new HeaderPart(); + newHeaderPart.setJaxbElement(XmlUtils.deepCopy(sourceHeaderPart.getJaxbElement())); + + // 添加到目标文档 + String newRelId = target.getMainDocumentPart().addTargetPart(newHeaderPart).getId(); + log.debug("成功合并页眉,关系ID: {}", newRelId); + } + } + } + } catch (Exception e) { + log.warn("合并页眉时发生异常: {}", e.getMessage()); + } + } + + /** + * 合并页脚部分 + * + * @param target 目标文档包 + * @param source 源文档包 + * @param targetRels 目标关系部分 + * @param sourceRels 源关系部分 + */ + private static void mergeFooterParts(WordprocessingMLPackage target, WordprocessingMLPackage source, + RelationshipsPart targetRels, RelationshipsPart sourceRels) { + try { + for (Relationship rel : sourceRels.getRelationships().getRelationship()) { + if (Namespaces.FOOTER.equals(rel.getType())) { + FooterPart sourceFooterPart = (FooterPart) source.getParts().get( + new org.docx4j.openpackaging.parts.PartName(rel.getTarget())); + + if (sourceFooterPart != null) { + // 深度复制页脚内容 + FooterPart newFooterPart = new FooterPart(); + newFooterPart.setJaxbElement(XmlUtils.deepCopy(sourceFooterPart.getJaxbElement())); + + // 添加到目标文档 + String newRelId = target.getMainDocumentPart().addTargetPart(newFooterPart).getId(); + log.debug("成功合并页脚,关系ID: {}", newRelId); + } + } + } + } catch (Exception e) { + log.warn("合并页脚时发生异常: {}", e.getMessage()); + } + } + + /** + * 验证文档输入流是否有效 + * 用于合并前的预检查 + * + * @param inputStream 文档输入流 + * @return true如果文档有效,false否则 + */ + public static boolean validateDocument(InputStream inputStream) { + if (inputStream == null) { + return false; + } + + try { + WordprocessingMLPackage.load(inputStream); + return true; + } catch (Exception e) { + log.debug("文档验证失败: {}", e.getMessage()); + return false; + } + } + + /** + * 批量验证多个文档输入流 + * + * @param inputStreams 文档输入流列表 + * @return 验证结果,包含无效文档的索引列表 + */ + public static List validateDocuments(List inputStreams) { + List invalidIndexes = new ArrayList<>(); + + if (CollectionUtils.isEmpty(inputStreams)) { + return invalidIndexes; + } + + for (int i = 0; i < inputStreams.size(); i++) { + if (!validateDocument(inputStreams.get(i))) { + invalidIndexes.add(i); + } + } + + return invalidIndexes; + } + +} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/PlaceholderUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/PlaceholderUtil.java new file mode 100644 index 00000000..78437427 --- /dev/null +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/PlaceholderUtil.java @@ -0,0 +1,262 @@ +package com.njcn.gather.tools.report.util; + +import lombok.extern.slf4j.Slf4j; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 占位符处理工具类 + * + * @author hongawen + * @version 1.0 + * {@code @data} 2025/9/5 10:30 + */ +@Slf4j +public class PlaceholderUtil { + + + /** + * 批量替换占位符 + * 优先尝试一次性批量替换,失败时降级为逐个替换 + * + * @param mainDocumentPart 主文档部分 + * @param placeholderMap 占位符映射表(原始格式,会自动预处理) + */ + public static void replaceAllPlaceholders(MainDocumentPart mainDocumentPart, Map placeholderMap) { + if (mainDocumentPart == null || placeholderMap == null || placeholderMap.isEmpty()) { + log.warn("文档部分或占位符映射为空,跳过替换操作"); + return; + } + // 预处理占位符映射 + Map processedMap = preprocessPlaceholderMap(placeholderMap); + log.info("开始批量替换占位符,共 {} 个", processedMap.size()); + // 优先尝试批量替换(性能更好) + try { + mainDocumentPart.variableReplace(processedMap); + // 验证批量替换是否真正成功(检查是否还有占位符残留) + int remainingPlaceholders = 0; + for (String placeholder : processedMap.keySet()) { + String checkFormat = "${" + placeholder + "}"; + if (containsPlaceholder(mainDocumentPart, checkFormat)) { + remainingPlaceholders++; + } + } + if (remainingPlaceholders == 0) { + log.info("批量占位符替换成功,所有 {} 个占位符都已替换", processedMap.size()); + return; + } else { + log.warn("批量替换后仍有 {} 个占位符未被替换,降级为逐个处理", remainingPlaceholders); + } + } catch (Exception e) { + log.warn("批量替换发生异常,降级为逐个替换: {}", e.getMessage()); + } + + // 降级策略:逐个替换(容错性更好) + int successCount = 0; + int failCount = 0; + for (Map.Entry entry : processedMap.entrySet()) { + String placeholder = entry.getKey(); + String replacement = entry.getValue(); + try { + replaceSinglePlaceholder(mainDocumentPart, placeholder, replacement); + successCount++; + } catch (Exception e) { + log.error("占位符 [{}] 替换失败: {}", placeholder, e.getMessage()); + failCount++; + } + } + + log.info("逐个替换完成,成功: {} 个,失败: {} 个", successCount, failCount); + } + + + /** + * 预处理占位符映射,确保格式符合 docx4j 预期 + * + * @param originalMap 原始占位符映射 + * @return 处理后的占位符映射 + */ + public static Map preprocessPlaceholderMap(Map originalMap) { + if (originalMap == null || originalMap.isEmpty()) { + return new HashMap<>(16); + } + + Map processedMap = new HashMap<>(16); + + for (Map.Entry entry : originalMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue() != null ? entry.getValue() : ""; + + // 确保占位符格式符合 docx4j 预期 + String formattedKey = formatPlaceholder(key); + processedMap.put(formattedKey, value); + } + + return processedMap; + } + + /** + * 格式化占位符,确保符合 docx4j 预期格式 + * + * docx4j 的 variableReplace 方法期望的 Map key 格式是纯变量名,不包含 ${} 或 {{}} 等括号 + * 例如:Word 文档中写 ${companyName},但 Map 的 key 应该是 "companyName" + * + * @param placeholder 原始占位符 + * @return 格式化后的占位符(纯变量名) + */ + public static String formatPlaceholder(String placeholder) { + if (!StringUtils.hasText(placeholder)) { + return placeholder; + } + + // 如果占位符包含 ${} 格式,去掉外层括号只保留变量名 + if (placeholder.startsWith("${") && placeholder.endsWith("}")) { + return placeholder.substring(2, placeholder.length() - 1); + } + + // 如果占位符包含 {{}} 格式,去掉外层括号只保留变量名 + if (placeholder.startsWith("{{") && placeholder.endsWith("}}")) { + return placeholder.substring(2, placeholder.length() - 2); + } + + // 直接返回原始占位符名称(已经是纯变量名) + return placeholder; + } + + /** + * 检查文档中是否还包含指定的占位符 + * 用于验证替换是否成功(docx4j 替换失败时不抛异常,需要手动检查) + * + * @param mainDocumentPart 主文档部分 + * @param format 要检查的占位符格式 + * @return true 如果文档中仍包含该占位符,false 表示已被替换 + */ + private static boolean containsPlaceholder(MainDocumentPart mainDocumentPart, String format) { + try { + String documentText = mainDocumentPart.getContent().toString(); + return documentText.contains(format); + } catch (Exception e) { + log.debug("检查占位符时发生异常: {}", e.getMessage()); + // 出现异常时假设不包含 + return false; + } + } + + /** + * 在文档中进行多格式文本替换 + * 这是 docx4j variableReplace 失败时的备用方案 + * 特别处理 docx4j 的静默失败问题(替换失败不抛异常,占位符仍存在) + * + * @param mainDocumentPart 主文档部分 + * @param placeholder 占位符变量名 + * @param replacement 替换文本 + * @throws Exception 处理异常 + */ + public static void replaceTextInDocument(MainDocumentPart mainDocumentPart, String placeholder, String replacement) throws Exception { + if (mainDocumentPart == null || !StringUtils.hasText(placeholder)) { + throw new IllegalArgumentException("文档部分和占位符不能为空"); + } + + String safeReplacement = replacement != null ? replacement : ""; + + try { + // 尝试多种格式的文本替换,按常用程度排序 + String[] searchFormats = { + // ${placeholder} - docx4j 标准格式 + "${" + placeholder + "}", + // {{placeholder}} - 常见的模板格式 + "{{" + placeholder + "}}", + // placeholder - 纯变量名 + placeholder + }; + + boolean replacementSuccess = false; + for (String format : searchFormats) { + // 先检查文档中是否包含该格式的占位符 + if (!containsPlaceholder(mainDocumentPart, format)) { + log.debug("文档中不包含格式 [{}] 的占位符,跳过", format); + continue; + } + + try { + // 使用 docx4j 6.1.0 进行替换 + java.util.HashMap mappings = new java.util.HashMap<>(); + mappings.put(format, safeReplacement); + mainDocumentPart.variableReplace(mappings); + + // 验证替换是否真正成功(docx4j 静默失败的关键处理) + if (!containsPlaceholder(mainDocumentPart, format)) { + log.debug("成功替换格式 [{}] 的占位符", format); + replacementSuccess = true; + break; + } else { + log.debug("格式 [{}] 替换后仍存在,可能是 Word 文档中占位符被分割", format); + } + } catch (Exception e) { + log.debug("格式 [{}] 替换时发生异常: {}", format, e.getMessage()); + } + } + + if (!replacementSuccess) { + log.warn("所有格式尝试后占位符 [{}] 仍未被替换,可能原因:", placeholder); + log.warn("1. Word 文档中不存在该占位符"); + log.warn("2. 占位符在 Word 中被格式化分割(如 ${com}panyName)"); + log.warn("3. 占位符格式与预期不匹配"); + } + + } catch (Exception e) { + log.warn("文本替换过程发生异常: {} -> {}", placeholder, safeReplacement, e); + throw new Exception("文本替换失败: " + e.getMessage(), e); + } + } + + /** + * 执行单个占位符的变量替换 + * 优先使用 docx4j 的 variableReplace,失败时降级为多格式文本替换 + * 包含 docx4j 静默失败的验证逻辑 + * + * @param mainDocumentPart 主文档部分 + * @param placeholder 占位符变量名(已格式化,纯变量名) + * @param replacement 替换值 + */ + public static void replaceSinglePlaceholder(MainDocumentPart mainDocumentPart, String placeholder, String replacement) { + if (mainDocumentPart == null || !StringUtils.hasText(placeholder)) { + log.warn("文档部分或占位符为空,跳过替换"); + return; + } + String safeReplacement = replacement != null ? replacement : ""; + // 检查标准格式 + String checkFormat = "${" + placeholder + "}"; + // 先检查文档中是否包含该占位符 + if (!containsPlaceholder(mainDocumentPart, checkFormat)) { + log.debug("文档中不包含占位符 [{}],跳过替换", placeholder); + return; + } + try { + // 优先使用 docx4j 的变量替换功能 + HashMap singleReplacement = new HashMap<>(16); + singleReplacement.put(placeholder, safeReplacement); + mainDocumentPart.variableReplace(singleReplacement); + // 验证替换是否真正成功(docx4j 静默失败检测) + if (!containsPlaceholder(mainDocumentPart, checkFormat)) { + log.debug("成功替换占位符: {} -> {}", placeholder, safeReplacement); + return; + } else { + log.debug("占位符 [{}] 替换后仍存在,可能被格式化分割,尝试多格式替换", placeholder); + } + } catch (Exception e) { + log.debug("占位符变量替换发生异常: {}", placeholder, e); + } + // 降级为多格式文本替换 + try { + replaceTextInDocument(mainDocumentPart, placeholder, safeReplacement); + } catch (Exception textReplaceException) { + log.error("占位符 [{}] 的所有替换方式都失败了: {}", placeholder, textReplaceException.getMessage()); + } + } + + +} \ No newline at end of file diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/ReportExceptionUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/ReportExceptionUtil.java index 6ad767c9..72777817 100644 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/ReportExceptionUtil.java +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/ReportExceptionUtil.java @@ -1,7 +1,7 @@ package com.njcn.gather.tools.report.util; import com.njcn.common.pojo.exception.BusinessException; -import com.njcn.gather.tools.report.model.ReportResponseEnum; +import com.njcn.gather.tools.report.model.enums.ReportResponseEnum; /** * 报告生成异常工具类 @@ -10,91 +10,7 @@ import com.njcn.gather.tools.report.model.ReportResponseEnum; * @author hongawen */ public class ReportExceptionUtil { - - /** - * 模板加载异常 - */ - public static BusinessException templateLoadError(String message) { - return new BusinessException(ReportResponseEnum.TEMPLATE_LOAD_ERROR, message); - } - - /** - * 模板处理异常 - */ - public static BusinessException templateProcessError(String message) { - return new BusinessException(ReportResponseEnum.TEMPLATE_PROCESS_ERROR, message); - } - - /** - * 文件保存异常 - */ - public static BusinessException fileSaveError(String message) { - return new BusinessException(ReportResponseEnum.FILE_SAVE_ERROR, message); - } - - /** - * 参数验证异常 - */ - public static BusinessException validationError(String message) { - return new BusinessException(ReportResponseEnum.VALIDATION_ERROR, message); - } - - /** - * 不支持的操作异常 - */ - public static BusinessException unsupportedOperation(String message) { - return new BusinessException(ReportResponseEnum.UNSUPPORTED_OPERATION, message); - } - - /** - * 通用报告生成异常 - */ - public static BusinessException reportGenerationError(String message) { - return new BusinessException(ReportResponseEnum.REPORT_GENERATION_ERROR, message); - } - - /** - * 异步处理异常 - */ - public static BusinessException asyncProcessingError(String message) { - return new BusinessException(ReportResponseEnum.ASYNC_PROCESSING_ERROR, message); - } - - /** - * 批量处理异常 - */ - public static BusinessException batchProcessingError(String message) { - return new BusinessException(ReportResponseEnum.BATCH_PROCESSING_ERROR, message); - } - - /** - * 模板文件不存在异常 - */ - public static BusinessException templateNotFound(String message) { - return new BusinessException(ReportResponseEnum.TEMPLATE_NOT_FOUND, message); - } - - /** - * 数据为空异常 - */ - public static BusinessException dataEmpty(String message) { - return new BusinessException(ReportResponseEnum.DATA_EMPTY, message); - } - - /** - * 找不到合适的处理器异常 - */ - public static BusinessException noSuitableProcessor(String message) { - return new BusinessException(ReportResponseEnum.NO_SUITABLE_PROCESSOR, message); - } - - /** - * 处理超时异常 - */ - public static BusinessException processingTimeout(String message) { - return new BusinessException(ReportResponseEnum.PROCESSING_TIMEOUT, message); - } - + /** * 根据异常类型和消息创建BusinessException */ diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/WordDocumentUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/WordDocumentUtil.java index 7c1f3a7a..e0992341 100644 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/WordDocumentUtil.java +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/WordDocumentUtil.java @@ -1,332 +1,180 @@ package com.njcn.gather.tools.report.util; -import org.apache.poi.xwpf.usermodel.*; -import org.apache.xmlbeans.XmlCursor; -import java.util.ArrayList; +import com.njcn.gather.tools.report.model.enums.ReportResponseEnum; +import lombok.extern.slf4j.Slf4j; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.docx4j.wml.*; +import org.docx4j.TraversalUtil; +import org.docx4j.TraversalUtil.CallbackImpl; +import org.springframework.util.StringUtils; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** - * Word文档工具类 - * 基于Apache POI的通用Word文档操作工具 - * 从原有WordUtil中抽取的通用功能 - * + * Word文档分析工具类 + * 基于docx4j的Word文档分析工具,专门用于占位符提取和验证 + * 与PlaceholderUtil配合使用,提供完整的模板处理解决方案 + *

+ * 技术栈:纯docx4j解决方案,专注于${placeholder}格式处理 + * * @author hongawen + * @version 2.0 + * @data 2025/9/5 11:00 */ +@Slf4j public class WordDocumentUtil { /** - * 替换文档中的占位符 - * - * @param document 文档对象 - * @param placeholders 占位符键值对 + * 支持的占位符格式正则表达式 + * ${placeholder} */ - public static void replacePlaceholders(XWPFDocument document, Map placeholders) { - if (document == null || placeholders == null || placeholders.isEmpty()) { - return; - } - - replacePlaceholdersInParagraphs(document, placeholders); - replacePlaceholdersInTables(document, placeholders); - } - + private static final Pattern PLACEHOLDER_PATTERN_DOLLAR = Pattern.compile("\\$\\{([^}]+)}"); + /** - * 替换段落中的占位符 - * - * @param document 文档对象 - * @param placeholders 占位符键值对 + * 从Word文档输入流中提取所有${placeholder}格式的占位符 + * + * @param templateInputStream Word模板文档输入流 + * @param keepFormat 是否保持${...}完整格式,true返回${companyName},false返回companyName + * @return 包含所有占位符的Set集合(去重) + * @throws com.njcn.common.pojo.exception.BusinessException 模板处理失败或参数验证失败 */ - public static void replacePlaceholdersInParagraphs(XWPFDocument document, Map placeholders) { - for (XWPFParagraph paragraph : document.getParagraphs()) { - replacePlaceholdersInParagraph(paragraph, placeholders); + public static Set extractPlaceholders(InputStream templateInputStream, boolean keepFormat, List excludePlaceholders) { + if (templateInputStream == null) { + throw ReportExceptionUtil.create(ReportResponseEnum.VALIDATION_ERROR); } - } - - /** - * 替换单个段落中的占位符 - * - * @param paragraph 段落对象 - * @param placeholders 占位符键值对 - */ - public static void replacePlaceholdersInParagraph(XWPFParagraph paragraph, Map placeholders) { - List runs = paragraph.getRuns(); - if (runs != null) { - for (XWPFRun run : runs) { - String text = run.getText(0); - if (text != null) { - for (Map.Entry entry : placeholders.entrySet()) { - text = text.replace(entry.getKey(), entry.getValue()); + Set placeholders = new HashSet<>(); + try { + WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(templateInputStream); + MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart(); + + // 使用TraversalUtil遍历所有Text节点 + CallbackImpl textCallback = new CallbackImpl() { + @Override + public List apply(Object content) { + if (content instanceof Text) { + Text textNode = (Text) content; + String text = textNode.getValue(); + if (StringUtils.hasText(text)) { + // 在每个文本节点中搜索占位符 + Matcher matcher = PLACEHOLDER_PATTERN_DOLLAR.matcher(text); + while (matcher.find()) { + String result; + if (keepFormat) { + result = matcher.group(0); + } else { + result = matcher.group(1); + } + // 添加到结果集合中,排除指定占位符 + if (StringUtils.hasText(result) && !excludePlaceholders.contains(matcher.group(1))) { + placeholders.add(result.trim()); + } + } + } } - run.setText(text, 0); + return null; } - } + }; + // 遍历整个文档 + TraversalUtil.visit(mainDocumentPart, textCallback); + } catch (Exception e) { + log.error("提取占位符时发生异常", e); + throw ReportExceptionUtil.create(ReportResponseEnum.TEMPLATE_PROCESS_ERROR); } + + return placeholders; } /** - * 替换表格中的占位符 - * - * @param document 文档对象 - * @param placeholders 占位符键值对 + * 从Word文档输入流中提取所有${placeholder}格式的占位符(返回纯变量名) + * + * @param templateInputStream Word模板文档输入流 + * @return 包含所有占位符变量名的Set集合(去重) */ - public static void replacePlaceholdersInTables(XWPFDocument document, Map placeholders) { - for (XWPFTable table : document.getTables()) { - replacePlaceholdersInTable(table, placeholders); - } - } - - /** - * 替换单个表格中的占位符 - * - * @param table 表格对象 - * @param placeholders 占位符键值对 - */ - public static void replacePlaceholdersInTable(XWPFTable table, Map placeholders) { - for (XWPFTableRow row : table.getRows()) { - for (XWPFTableCell cell : row.getTableCells()) { - for (XWPFParagraph paragraph : cell.getParagraphs()) { - replacePlaceholdersInParagraph(paragraph, placeholders); - } - } - } + public static Set extractPlaceholders(InputStream templateInputStream, List excludePlaceholders) { + return extractPlaceholders(templateInputStream, false, excludePlaceholders); } /** - * 将源文档的内容追加到目标文档中 - * - * @param target 目标文档 - * @param source 源文档 + * 从Word文档输入流中提取所有占位符(返回完整${...}格式) + * + * @param templateInputStream Word模板文档输入流 + * @return 包含所有完整${...}格式占位符的Set集合 */ - public static void appendDocument(XWPFDocument target, XWPFDocument source) { - if (target == null || source == null) { - return; - } - - // 在追加内容之前,插入分页符 - insertPageBreak(target); - - // 遍历源文档的所有块(段落、表格等) - source.getBodyElements().forEach(bodyElement -> { - switch (bodyElement.getElementType()) { - case PARAGRAPH: - // 处理段落 - XWPFParagraph sourceParagraph = (XWPFParagraph) bodyElement; - XWPFParagraph newParagraph = target.createParagraph(); - copyParagraphContent(sourceParagraph, newParagraph); - break; - case TABLE: - // 处理表格 - XWPFTable sourceTable = (XWPFTable) bodyElement; - XWPFTable newTable = target.createTable(); - copyTableContent(sourceTable, newTable); - break; - default: - // 其他类型的内容处理 - break; - } - }); + public static Set extractPlaceholdersWithFormat(InputStream templateInputStream, List excludePlaceholders) { + return extractPlaceholders(templateInputStream, true, excludePlaceholders); } - + + /** - * 复制段落内容 - * - * @param source 源段落 - * @param target 目标段落 + * 验证Word文档中是否包含指定的占位符 + * + * @param templateInputStream Word模板文档输入流 + * @param placeholder 要验证的占位符(纯变量名) + * @return true 如果文档包含该占位符,false 否则 */ - private static void copyParagraphContent(XWPFParagraph source, XWPFParagraph target) { + public static boolean containsPlaceholder(InputStream templateInputStream, String placeholder) { + if (templateInputStream == null || !StringUtils.hasText(placeholder)) { + throw ReportExceptionUtil.create(ReportResponseEnum.VALIDATION_ERROR); + } try { - // 简单的内容复制,保持样式 - target.getCTP().set(source.getCTP()); - } catch (Exception e) { - // 如果复制失败,采用文本复制方式 - target.createRun().setText(source.getText()); - } - } - - /** - * 复制表格内容 - * - * @param source 源表格 - * @param target 目标表格 - */ - private static void copyTableContent(XWPFTable source, XWPFTable target) { - try { - // 简单的内容复制,保持样式 - target.getCTTbl().set(source.getCTTbl()); - } catch (Exception e) { - // 如果复制失败,采用逐行复制方式 - copyTableRowByRow(source, target); - } - } - - /** - * 逐行复制表格内容 - * - * @param source 源表格 - * @param target 目标表格 - */ - private static void copyTableRowByRow(XWPFTable source, XWPFTable target) { - // 先清空目标表格的默认行 - if (target.getRows().size() > 0) { - target.removeRow(0); - } - - // 复制每一行 - for (XWPFTableRow sourceRow : source.getRows()) { - XWPFTableRow targetRow = target.createRow(); - - // 确保目标行有足够的单元格 - int cellsNeeded = sourceRow.getTableCells().size(); - while (targetRow.getTableCells().size() < cellsNeeded) { - targetRow.createCell(); - } - - // 复制每个单元格的内容 - for (int i = 0; i < cellsNeeded; i++) { - XWPFTableCell sourceCell = sourceRow.getTableCells().get(i); - XWPFTableCell targetCell = targetRow.getTableCells().get(i); - - // 复制单元格文本内容 - StringBuilder cellText = new StringBuilder(); - for (XWPFParagraph para : sourceCell.getParagraphs()) { - if (cellText.length() > 0) { - cellText.append("\n"); + WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(templateInputStream); + MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart(); + + // 使用TraversalUtil遍历所有Text节点查找占位符 + final boolean[] found = {false}; + final String targetPlaceholder = "${" + placeholder + "}"; + + CallbackImpl textCallback = new CallbackImpl() { + @Override + public List apply(Object content) { + if (content instanceof Text && !found[0]) { + Text textNode = (Text) content; + String text = textNode.getValue(); + if (StringUtils.hasText(text)) { + if (text.contains(targetPlaceholder) || text.contains(placeholder)) { + found[0] = true; + } + } } - cellText.append(para.getText()); + return null; } - - if (cellText.length() > 0) { - targetCell.getParagraphs().get(0).createRun().setText(cellText.toString()); - } - } - } - } - - /** - * 在文档中插入分页符 - * - * @param document 文档对象 - */ - public static void insertPageBreak(XWPFDocument document) { - XWPFParagraph paragraph = document.createParagraph(); - XWPFRun run = paragraph.createRun(); - run.addBreak(BreakType.PAGE); - } - - /** - * 查找文档中指定样式的段落 - * - * @param document 文档对象 - * @param styleId 样式ID - * @return 匹配的段落列表 - */ - public static List findParagraphsByStyle(XWPFDocument document, String styleId) { - List result = new ArrayList<>(); - for (XWPFParagraph paragraph : document.getParagraphs()) { - String style = paragraph.getStyle(); - if (styleId.equals(style)) { - result.add(paragraph); - } - } - return result; - } - - /** - * 获取段落在文档中的位置索引 - * - * @param document 文档对象 - * @param paragraph 段落对象 - * @return 位置索引,找不到返回-1 - */ - public static int getParagraphPosition(XWPFDocument document, XWPFParagraph paragraph) { - List bodyElements = document.getBodyElements(); - for (int i = 0; i < bodyElements.size(); i++) { - if (bodyElements.get(i) instanceof XWPFParagraph && - bodyElements.get(i).equals(paragraph)) { - return i; - } - } - return -1; - } - - /** - * 在指定位置插入段落 - * - * @param document 文档对象 - * @param position 插入位置 - * @param text 段落文本 - * @return 创建的段落对象 - */ - public static XWPFParagraph insertParagraphAt(XWPFDocument document, int position, String text) { - try { - XWPFParagraph paragraph = document.insertNewParagraph(getCursorAtPosition(document, position)); - if (text != null && !text.isEmpty()) { - paragraph.createRun().setText(text); - } - return paragraph; + }; + + // 遍历整个文档 + TraversalUtil.visit(mainDocumentPart, textCallback); + + return found[0]; } catch (Exception e) { - // 如果插入失败,则在末尾添加 - XWPFParagraph paragraph = document.createParagraph(); - if (text != null && !text.isEmpty()) { - paragraph.createRun().setText(text); + log.error("验证占位符时发生异常: {}", placeholder, e); + throw ReportExceptionUtil.create(ReportResponseEnum.TEMPLATE_PROCESS_ERROR); + } + } + + + /** + * 测试方法 + */ + public static void main(String[] args) { + String templatePath = "F:\\gitea\\fusionForce\\CN_Gather\\entrance\\src\\main\\resources\\model\\report_table.docx"; + + try (java.io.FileInputStream templateStream = new java.io.FileInputStream(templatePath)) { + Set placeholders = extractPlaceholders(templateStream, Arrays.asList("CreateId", "total", "count")); + + System.out.println("模板文件: " + templatePath); + System.out.println("发现 " + placeholders.size() + " 个占位符:"); + for (String placeholder : placeholders) { + System.out.println("${" + placeholder + "}"); } - return paragraph; - } - } - - /** - * 获取指定位置的游标 - * - * @param document 文档对象 - * @param position 位置 - * @return XML游标 - */ - private static XmlCursor getCursorAtPosition(XWPFDocument document, int position) { - List bodyElements = document.getBodyElements(); - if (position >= 0 && position < bodyElements.size()) { - IBodyElement element = bodyElements.get(position); - if (element instanceof XWPFParagraph) { - return ((XWPFParagraph) element).getCTP().newCursor(); - } - } - // 默认返回文档末尾的游标 - return document.getDocument().getBody().newCursor(); - } - - /** - * 统计文档信息 - * - * @param document 文档对象 - * @return 文档统计信息 - */ - public static DocumentStats getDocumentStats(XWPFDocument document) { - DocumentStats stats = new DocumentStats(); - - stats.paragraphCount = document.getParagraphs().size(); - stats.tableCount = document.getTables().size(); - - // 统计文本长度 - int textLength = 0; - for (XWPFParagraph paragraph : document.getParagraphs()) { - textLength += paragraph.getText().length(); - } - stats.textLength = textLength; - - return stats; - } - - /** - * 文档统计信息 - */ - public static class DocumentStats { - public int paragraphCount; - public int tableCount; - public int textLength; - - @Override - public String toString() { - return String.format("段落: %d, 表格: %d, 文本长度: %d", - paragraphCount, tableCount, textLength); + + } catch (Exception e) { + System.err.println("错误: " + e.getMessage()); } } } \ No newline at end of file diff --git a/tools/report-generator/src/test/java/com/njcn/gather/tools/report/ReportGeneratorTest.java b/tools/report-generator/src/test/java/com/njcn/gather/tools/report/ReportGeneratorTest.java deleted file mode 100644 index cdf4c475..00000000 --- a/tools/report-generator/src/test/java/com/njcn/gather/tools/report/ReportGeneratorTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.njcn.gather.tools.report; - -import com.njcn.gather.tools.report.model.*; -import com.njcn.gather.tools.report.engine.WordDocumentProcessor; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -import java.util.HashMap; -import java.util.Map; - -/** - * 报告生成工具测试类 - * - * @author hongawen - */ -@Slf4j -public class ReportGeneratorTest { - - @Test - public void testWordDocumentProcessor() { - // 创建测试数据 - Map data = new HashMap<>(); - data.put("name", "张三"); - data.put("date", "2024-01-01"); - data.put("amount", "1000.00"); - data.put("company", "灿能公司"); - - // 创建处理选项 - ProcessOptions options = ProcessOptions.simplePlaceholder(); - - // 这里需要真实的模板文件进行测试 - // 由于测试环境可能没有模板文件,这个测试主要验证代码结构 - log.info("报告生成工具基础结构测试通过"); - - // 测试模型创建 - TemplateSource templateSource = TemplateSource.builder() - .filePath("test-template.docx") - .type(TemplateType.DOCX) - .name("测试模板") - .build(); - - TemplateRequest.OutputTarget outputTarget = TemplateRequest.OutputTarget.toBytes(); - - TemplateRequest request = TemplateRequest.builder() - .templateSource(templateSource) - .data(data) - .options(options) - .outputTarget(outputTarget) - .requestId("test-001") - .build(); - - // 验证请求有效性 - assert request.isValid() : "请求应该是有效的"; - assert request.getEffectiveOptions() != null : "应该有有效的处理选项"; - - log.info("模型验证测试通过"); - } - - @Test - public void testProcessOptions() { - // 测试默认选项 - ProcessOptions defaultOptions = ProcessOptions.defaultOptions(); - assert defaultOptions.isEnablePlaceholder() : "默认应启用占位符替换"; - - // 测试简单占位符选项 - ProcessOptions simpleOptions = ProcessOptions.simplePlaceholder(); - assert simpleOptions.isEnablePlaceholder() : "简单选项应启用占位符替换"; - assert !simpleOptions.isEnableBookmark() : "简单选项应禁用书签处理"; - - // 测试高级功能选项 - ProcessOptions advancedOptions = ProcessOptions.advancedFeatures(); - assert advancedOptions.isEnablePlaceholder() : "高级选项应启用占位符替换"; - assert advancedOptions.isEnableBookmark() : "高级选项应启用书签处理"; - assert advancedOptions.isEnableDynamicTable() : "高级选项应启用动态表格"; - - log.info("处理选项测试通过"); - } - - @Test - public void testPlaceholderPattern() { - ProcessOptions.PlaceholderPattern pattern = ProcessOptions.PlaceholderPattern.DOLLAR_BRACE; - - String placeholder = pattern.buildPlaceholder("name"); - assert "${name}".equals(placeholder) : "占位符格式应该正确"; - - log.info("占位符模式测试通过"); - } - - @Test - public void testTemplateType() { - TemplateType type = TemplateType.fromExtension("test.docx"); - assert type == TemplateType.DOCX : "应该识别为DOCX类型"; - - type = TemplateType.fromExtension("test.pdf"); - assert type == TemplateType.PDF : "应该识别为PDF类型"; - - type = TemplateType.fromExtension("unknown.xyz"); - assert type == TemplateType.DOCX : "未知类型应该默认为DOCX"; - - log.info("模板类型测试通过"); - } -} \ No newline at end of file