表格调整部分代码
This commit is contained in:
@@ -126,12 +126,19 @@
|
||||
</dependency>
|
||||
|
||||
|
||||
<!--波形工具模块-->
|
||||
<dependency>
|
||||
<groupId>com.njcn.gather</groupId>
|
||||
<artifactId>wave-comtrade</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!--报告工具模块-->
|
||||
<dependency>
|
||||
<groupId>com.njcn.gather</groupId>
|
||||
<artifactId>report-generator</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Object> uploadReportToCloud(@RequestBody(required = false) List<String> deviceIds) {
|
||||
String methodDescribe = getMethodDescribe("uploadReportToCloud");
|
||||
LogUtil.njcnDebug(log, "{},设备ID列表为:{}", methodDescribe, deviceIds);
|
||||
|
||||
@@ -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<PqReportMapper, PqReport> 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<PqReportVO> list(ReportParam.QueryParam queryParam) {
|
||||
@@ -309,22 +284,23 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> i
|
||||
// 读取模板文件
|
||||
ClassPathResource resource = new ClassPathResource("/model/" + reportName.getCode() + ReportConstant.DOCX);
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
// 加载Word文档
|
||||
XWPFDocument baseModelDocument = new XWPFDocument(inputStream);
|
||||
// 处理基础模版中的信息
|
||||
Map<String, String> baseModelDataMap = dealBaseModelData(pqDevVO, devType, "${", "}");
|
||||
// 替换模板中的信息,避免信息丢失,段落和表格均参与替换
|
||||
WordUtil.replacePlaceholders(baseModelDocument, baseModelDataMap);
|
||||
Map<String, String> baseModelDataMap = dealBaseModelData(pqDevVO, devType);
|
||||
InputStream wordFinishInputStream = wordReportService.replacePlaceholders(inputStream, baseModelDataMap);
|
||||
List<InputStream> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<String, String> baseModelDataMap = dealBaseModelData(pqDevVO, devType);
|
||||
InputStream wordFinishInputStream = wordReportService.replacePlaceholders(baseInputStream, baseModelDataMap);
|
||||
WordprocessingMLPackage baseModelDocument = WordprocessingMLPackage.load(wordFinishInputStream);
|
||||
MainDocumentPart baseDocumentPart = baseModelDocument.getMainDocumentPart();
|
||||
Map<String, String> 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<PqReportMapper, PqReport> i
|
||||
Collections.sort(bookmarkEnums);
|
||||
// 定义个结果,以便存在结果信息的书签
|
||||
Map<String/*指标名称*/, List<Boolean/*以回路的顺序填充结果*/>> resultMap = new HashMap<>();
|
||||
List<Object> todoInsertList = new ArrayList<>();
|
||||
BookmarkUtil.BookmarkInfo bookmarkInfo = null;
|
||||
List<Object> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<Map.Entry<String, List<PqScriptDtlDataVO>>> iterator = scriptMap.entrySet().iterator();
|
||||
@@ -1314,9 +1295,9 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> i
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean documented(List<String> ids) {
|
||||
if (CollUtil.isNotEmpty(ids)) {
|
||||
List<PqDevVO> pqDevVOList = pqDevMapper.listByDevIds(ids);
|
||||
@@ -1401,66 +1382,66 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> i
|
||||
* 处理基础模版中的信息,非数据页报告
|
||||
* 此处为什么要抽出拼接的前缀&后缀,是因为Docx4j工具包替换时会默认增加${},故在使用docx4j时前后缀必须为空
|
||||
*/
|
||||
private Map<String, String> dealBaseModelData(PqDevVO pqDevVO, DevType devType, String prefix, String suffix) {
|
||||
private Map<String, String> dealBaseModelData(PqDevVO pqDevVO, DevType devType) {
|
||||
// 首先获取非数据页中需要的信息
|
||||
Map<String, String> 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<PqReportMapper, PqReport> 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<InputStream> 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<String, String> dataModelMap = new HashMap<>();
|
||||
dataModelMap.put("${CreateId}", pqDevVO.getCreateId());
|
||||
dataModelMap.put("${total}", pqDevVO.getDevChns().toString());
|
||||
dataModelMap.put("${count}", i + "");
|
||||
|
||||
Map<String, String> dataModelMap = new HashMap<>(16);
|
||||
// 读取模板文件中的占位符
|
||||
List<String> allMarkList = getAllKeys(dataModelDocumentTemp);
|
||||
Set<String> allMarkList = WordDocumentUtil.extractPlaceholders(resource.getInputStream(), false, Arrays.asList("CreateId", "total", "count"));
|
||||
Map<String, Set<String>> 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<SimAndDigNonHarmonicResult> simAndDigNonHarmonicResultList = adNonHarmonicService.listSimAndDigBaseResult(devReportParam.getScriptId(), devReportParam.getPlanCode(), devReportParam.getDevId() + "_" + i);
|
||||
List<SimAndDigHarmonicResult> adHarmonicResultList = adHarmonicService.listAllResultData(devReportParam.getScriptId(), devReportParam.getPlanCode(), devReportParam.getDevId() + "_" + i);
|
||||
@@ -1518,21 +1486,21 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> 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<SimAndDigNonHarmonicResult> allNonHarmonicResultList, Map<String, String> dataModelMap, Set<String> keys, String index) {
|
||||
keys.forEach(key -> {
|
||||
@@ -1540,9 +1508,9 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> i
|
||||
/**
|
||||
* 填充数据(T相)
|
||||
*
|
||||
* @param allNonHarmonicResultList
|
||||
* @param dataModelMap
|
||||
* @param keys
|
||||
* @param index
|
||||
* @param allNonHarmonicResultList 结果数据
|
||||
* @param dataModelMap 替换数据
|
||||
* @param keys key
|
||||
* @param index index
|
||||
*/
|
||||
private void fillMapValueT(List<SimAndDigNonHarmonicResult> allNonHarmonicResultList, Map<String, String> dataModelMap, Set<String> keys, String index) {
|
||||
keys.forEach(key -> {
|
||||
@@ -1562,7 +1530,7 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> i
|
||||
/**
|
||||
* 填充数据(谐波类)
|
||||
*
|
||||
* @param allHarmonicResultList
|
||||
* @param dataModelMap
|
||||
* @param keys
|
||||
* @param index
|
||||
* @param allHarmonicResultList 结果数据
|
||||
* @param dataModelMap 替换数据
|
||||
* @param keys key
|
||||
* @param index index
|
||||
*/
|
||||
private void fillMapValueHarm(List<SimAndDigHarmonicResult> allHarmonicResultList, Map<String, String> dataModelMap, Set<String> keys, String index) {
|
||||
keys.forEach(key -> {
|
||||
@@ -1609,9 +1577,9 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> 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<PqReportMapper, PqReport> 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<String> getAllKeys(XWPFDocument dataModelDocumentTemp) {
|
||||
List<String> allMarkList = new ArrayList<>();
|
||||
List<XWPFTable> tables = dataModelDocumentTemp.getTables();
|
||||
|
||||
for (XWPFTable table : tables) {
|
||||
List<XWPFTableRow> rows = table.getRows();
|
||||
for (XWPFTableRow row : rows) {
|
||||
List<XWPFTableCell> cells = row.getTableCells();
|
||||
for (XWPFTableCell cell : cells) {
|
||||
List<XWPFParagraph> paragraphs = cell.getParagraphs();
|
||||
for (XWPFParagraph paragraph : paragraphs) {
|
||||
List<XWPFRun> 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<PqReportMapper, PqReport> i
|
||||
@Override
|
||||
public void uploadReportToCloud(List<String> deviceIds) {
|
||||
log.info("开始批量上传检测报告到云端,设备ID列表:{}", deviceIds);
|
||||
|
||||
|
||||
List<PqDevSub> 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<String> devIds = deviceSubs.stream().map(PqDevSub::getDevId).collect(Collectors.toList());
|
||||
|
||||
List<PqDev> 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<PqReportMapper, PqReport> 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()) {
|
||||
|
||||
@@ -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<BookmarkInfo> findAllBookmarks(ContentAccessor contentAccessor) {
|
||||
List<BookmarkInfo> 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<Object> parentContent = info.parentContainer.getContent();
|
||||
int idx = parentContent.indexOf(info.parentParagraph);
|
||||
parentContent.add(idx + 1, paragraph);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在书签后插入表格
|
||||
*/
|
||||
public static void insertTableAfter(BookmarkInfo info, Tbl table) {
|
||||
List<Object> parentContent = info.parentContainer.getContent();
|
||||
int idx = parentContent.indexOf(info.parentParagraph);
|
||||
parentContent.add(idx + 1, table);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在书签后插入元素,可能是段落、表格、图片、书签等
|
||||
*/
|
||||
public static void insertElement(BookmarkInfo info, List<Object> elements) {
|
||||
List<Object> 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<Object> paragraphs = body.getContent();
|
||||
|
||||
// 用于标记是否在空白页中
|
||||
boolean inBlankPage = false;
|
||||
// 用于存储要删除的段落
|
||||
List<P> 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<Object> 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<BookmarkInfo> bookmarks) {
|
||||
BookmarkUtil.BookmarkInfo bookmarkInfo = null;
|
||||
for (BookmarkUtil.BookmarkInfo info : bookmarks) {
|
||||
String name = info.bookmark.getName();
|
||||
if (key.equalsIgnoreCase(name)) {
|
||||
bookmarkInfo = info;
|
||||
}
|
||||
}
|
||||
return bookmarkInfo;
|
||||
}
|
||||
}
|
||||
@@ -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<Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> placeholders) {
|
||||
for (XWPFTable table : document.getTables()) {
|
||||
for (XWPFTableRow row : table.getRows()) {
|
||||
for (XWPFTableCell cell : row.getTableCells()) {
|
||||
for (XWPFParagraph paragraph : cell.getParagraphs()) {
|
||||
List<XWPFRun> runs = paragraph.getRuns();
|
||||
if (runs != null) {
|
||||
for (XWPFRun run : runs) {
|
||||
String text = run.getText(0);
|
||||
if (text != null) {
|
||||
for (Map.Entry<String, String> 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<String, String> placeholders) {
|
||||
for (XWPFParagraph paragraph : document.getParagraphs()) {
|
||||
List<XWPFRun> runs = paragraph.getRuns();
|
||||
if (runs != null) {
|
||||
for (XWPFRun run : runs) {
|
||||
String text = run.getText(0);
|
||||
if (text != null) {
|
||||
for (Map.Entry<String, String> 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<String, String> placeholders) {
|
||||
replacePlaceholdersInParagraphs(document,placeholders);
|
||||
replacePlaceholdersInTables(document,placeholders);
|
||||
}
|
||||
|
||||
|
||||
public static List<XWPFParagraph> findHeadingLevel5Paragraphs(XWPFDocument document) {
|
||||
List<XWPFParagraph> 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<IBodyElement> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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<ProcessResult> batchProcessTemplates(List<TemplateRequest> 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项目的需求,也为未来的数据生成工具、文件处理工具等提供了良好的架构基础。
|
||||
@@ -13,7 +13,7 @@
|
||||
<artifactId>report-generator</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>报告生成工具</name>
|
||||
<description>通用的文档模板处理和报告生成工具,支持占位符替换、书签插入、动态表格等功能</description>
|
||||
<description>基于docx4j的通用Word文档处理工具,支持占位符替换、文档合并、动态表格、图片处理等完整功能</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -38,32 +38,8 @@
|
||||
<version>2.3.12</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI for Word document processing - 与detection模块版本保持一致 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>4.1.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>4.1.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml-schemas</artifactId>
|
||||
<version>4.1.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-scratchpad</artifactId>
|
||||
<version>4.1.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- docx4j for advanced Word processing - 与detection模块版本保持一致 -->
|
||||
<!-- docx4j - 统一的Word文档处理解决方案,支持占位符替换、文档合并、表格操作、图片处理等全功能 -->
|
||||
<dependency>
|
||||
<groupId>jakarta.xml.bind</groupId>
|
||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||
|
||||
@@ -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<String, Object> 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<Map<String, Object>> processAdvancedTemplate(@RequestBody AdvancedProcessRequest request) {
|
||||
log.info("接收到高级模板处理请求");
|
||||
|
||||
try {
|
||||
// 构建模板请求
|
||||
TemplateRequest templateRequest = buildTemplateRequest(request);
|
||||
|
||||
// 处理模板
|
||||
ProcessResult result = reportGeneratorService.processTemplate(templateRequest);
|
||||
|
||||
// 构建响应
|
||||
Map<String, Object> 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<String, Object> 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<Map<String, Object>> processTemplateAsync(@RequestBody AdvancedProcessRequest request) {
|
||||
log.info("接收到异步模板处理请求");
|
||||
|
||||
try {
|
||||
// 构建模板请求
|
||||
TemplateRequest templateRequest = buildTemplateRequest(request);
|
||||
|
||||
// 启动异步处理
|
||||
String requestId = reportGeneratorService.processTemplateAsync(templateRequest);
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<Map<String, Object>> getAsyncResult(@PathVariable String requestId) {
|
||||
ProcessResult result = reportGeneratorService.getAsyncResult(requestId);
|
||||
|
||||
Map<String, Object> 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<Map<String, Object>> batchProcessTemplates(@RequestBody List<AdvancedProcessRequest> requests) {
|
||||
log.info("接收到批量模板处理请求,数量: {}", requests.size());
|
||||
|
||||
try {
|
||||
// 构建模板请求列表
|
||||
List<TemplateRequest> templateRequests = new java.util.ArrayList<>();
|
||||
for (AdvancedProcessRequest req : requests) {
|
||||
templateRequests.add(buildTemplateRequest(req));
|
||||
}
|
||||
|
||||
// 批量处理
|
||||
List<ProcessResult> results = reportGeneratorService.batchProcessTemplates(templateRequests);
|
||||
|
||||
// 统计结果
|
||||
long successCount = results.stream().mapToLong(r -> r.isSuccess() ? 1 : 0).sum();
|
||||
long failureCount = results.size() - successCount;
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "批量处理完成");
|
||||
response.put("totalCount", results.size());
|
||||
response.put("successCount", successCount);
|
||||
response.put("failureCount", failureCount);
|
||||
List<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> validateTemplate(@RequestBody AdvancedProcessRequest request) {
|
||||
try {
|
||||
TemplateRequest templateRequest = buildTemplateRequest(request);
|
||||
ProcessResult result = reportGeneratorService.validateTemplate(templateRequest);
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> buildResultSummary(ProcessResult result) {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> getData() { return data; }
|
||||
public void setData(Map<String, Object> 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; }
|
||||
}
|
||||
}
|
||||
@@ -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("不支持的模板类型或处理请求");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> data, ProcessOptions options) {
|
||||
// 将数据转换为字符串映射
|
||||
Map<String, String> placeholders = convertDataToStringMap(data, options);
|
||||
|
||||
if (placeholders.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 执行占位符替换
|
||||
WordDocumentUtil.replacePlaceholders(document, placeholders);
|
||||
|
||||
return placeholders.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据对象转换为字符串映射
|
||||
*/
|
||||
private Map<String, String> convertDataToStringMap(Map<String, Object> data, ProcessOptions options) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
ProcessOptions.PlaceholderPattern pattern = options.getPlaceholderPattern();
|
||||
|
||||
for (Map.Entry<String, Object> 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<String, Object> 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"};
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> warnings;
|
||||
|
||||
/**
|
||||
* 额外的元数据
|
||||
*/
|
||||
private Map<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> data;
|
||||
|
||||
/**
|
||||
* 处理选项(可选)
|
||||
* 如果为空则使用默认配置
|
||||
*/
|
||||
private ProcessOptions options;
|
||||
|
||||
/**
|
||||
* 输出目标配置(必填)
|
||||
*/
|
||||
@NotNull(message = "输出目标不能为空")
|
||||
private OutputTarget outputTarget;
|
||||
|
||||
/**
|
||||
* 请求标识(可选)
|
||||
* 用于追踪和日志记录
|
||||
*/
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 用户标识(可选)
|
||||
* 用于权限控制和审计
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 额外的上下文数据(可选)
|
||||
*/
|
||||
private Map<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.njcn.gather.report.pojo.constant;
|
||||
package com.njcn.gather.tools.report.model.constant;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.njcn.gather.tools.report.model;
|
||||
package com.njcn.gather.tools.report.model.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.njcn.gather.tools.report.model;
|
||||
package com.njcn.gather.tools.report.model.enums;
|
||||
|
||||
/**
|
||||
* 模板文档类型枚举
|
||||
@@ -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<String, String> placeholderMap) throws Exception;
|
||||
|
||||
}
|
||||
@@ -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<ProcessResult> batchProcessTemplates(java.util.List<TemplateRequest> requests);
|
||||
|
||||
/**
|
||||
* 验证模板是否有效
|
||||
*
|
||||
* @param request 模板处理请求
|
||||
* @return 验证结果
|
||||
*/
|
||||
ProcessResult validateTemplate(TemplateRequest request);
|
||||
}
|
||||
@@ -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<DocumentProcessor> documentProcessors;
|
||||
|
||||
// 异步处理结果缓存
|
||||
private final Map<String, ProcessResult> 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<ProcessResult> batchProcessTemplates(List<TemplateRequest> requests) {
|
||||
log.info("开始批量处理模板,数量: {}", requests.size());
|
||||
|
||||
List<ProcessResult> 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<String> expiredKeys = new ArrayList<>();
|
||||
for (Map.Entry<String, ProcessResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<BookmarkInfo> findAllBookmarks(ContentAccessor contentAccessor) {
|
||||
List<BookmarkInfo> 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<Object> parentContent = info.parentContainer.getContent();
|
||||
int idx = parentContent.indexOf(info.parentParagraph);
|
||||
parentContent.add(idx + 1, paragraph);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在书签后插入表格
|
||||
*/
|
||||
public static void insertTableAfter(BookmarkInfo info, Tbl table) {
|
||||
List<Object> parentContent = info.parentContainer.getContent();
|
||||
int idx = parentContent.indexOf(info.parentParagraph);
|
||||
parentContent.add(idx + 1, table);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在书签后插入元素,可能是段落、表格、图片、书签等
|
||||
*/
|
||||
public static void insertElement(BookmarkInfo info, List<Object> elements) {
|
||||
List<Object> 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<Object> 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<BookmarkInfo> bookmarks) {
|
||||
BookmarkInfo bookmarkInfo = null;
|
||||
for (BookmarkInfo info : bookmarks) {
|
||||
String name = info.bookmark.getName();
|
||||
if (key.equalsIgnoreCase(name)) {
|
||||
bookmarkInfo = info;
|
||||
}
|
||||
}
|
||||
return bookmarkInfo;
|
||||
}
|
||||
}
|
||||
@@ -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<HeadingContent> extractHeadingContents(List<Object> allContent, String headingLevel) {
|
||||
List<HeadingContent> 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<Object> content = row.getContent();
|
||||
if (content.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 取最后一个单元格,判断是否包含中文
|
||||
Object cellObject = content.get(content.size() - 1);
|
||||
if (cellObject instanceof JAXBElement) {
|
||||
@SuppressWarnings("unchecked")
|
||||
JAXBElement<Tc> cellElement = (JAXBElement<Tc>) 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<Object> 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<Object> 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<String> getTableKeys(Tr row) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
List<Object> content = row.getContent();
|
||||
|
||||
for (Object cellObject : content) {
|
||||
if (cellObject instanceof JAXBElement) {
|
||||
@SuppressWarnings("unchecked")
|
||||
JAXBElement<Tc> cellElement = (JAXBElement<Tc>) 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<String, String> valueMap,
|
||||
List<String> 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<String> cellValues,
|
||||
String ascFontStyle, String eastFontStyle, Integer size,
|
||||
boolean boldFlag, List<Integer> 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<Tbl> deepCopyTbl(JAXBElement<Tbl> 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<Object> 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<Object> getSubContent() {
|
||||
return subContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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及其子内容
|
||||
* 提取文档中所有Heading 5标题及其子内容
|
||||
*
|
||||
* @param allContent 文档内所有内容
|
||||
* 该方法按文档顺序遍历内容,识别Heading 5级别的标题,并收集每个标题下的所有子内容,
|
||||
* 直到遇到下一个Heading 5标题为止。
|
||||
*
|
||||
* 处理逻辑:
|
||||
* 1. 遇到Heading 5时,保存前一个标题组并开始新的收集
|
||||
* 2. 在标题组内时,收集所有段落和表格等子内容(包括1-4级标题)
|
||||
* 3. 只有遇到下一个Heading 5标题时才结束当前标题组的收集
|
||||
* 4. 文档末尾时,保存最后一个标题组
|
||||
*
|
||||
* @param allContent 文档内所有内容对象的列表(包含段落、表格等)
|
||||
* @return Heading 5标题及其子内容的列表,按文档中出现的顺序排列
|
||||
*/
|
||||
public static List<HeadingContent> extractHeading5Contents(List<Object> allContent) {
|
||||
// 参数验证
|
||||
if (allContent == null || allContent.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<HeadingContent> 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<Object> subContent = new ArrayList<>();
|
||||
|
||||
public void setHeadingText(String text) {
|
||||
this.headingText = text;
|
||||
}
|
||||
|
||||
public String getHeadingText() {
|
||||
return headingText;
|
||||
}
|
||||
private final List<Object> subContent = new ArrayList<>();
|
||||
|
||||
public void addSubContent(Object obj) {
|
||||
subContent.add(obj);
|
||||
}
|
||||
|
||||
public List<Object> getSubContent() {
|
||||
return subContent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定书签在文档段落中的位置索引
|
||||
*
|
||||
* @param documentPart 主文档部分
|
||||
* @param bookmarkName 书签名称
|
||||
* @return 段落索引,找不到返回 -1
|
||||
*/
|
||||
public static int getParagraphPosition(MainDocumentPart documentPart, String bookmarkName) {
|
||||
List<Object> 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<Object> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格样式
|
||||
*/
|
||||
@@ -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<InputStream> 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<WordprocessingMLPackage> 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<InputStream> 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<InputStream> 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<WordprocessingMLPackage> 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<Object> sourceContent = source.getContent();
|
||||
|
||||
if (CollectionUtils.isEmpty(sourceContent)) {
|
||||
log.debug("源文档内容为空,跳过合并");
|
||||
return;
|
||||
}
|
||||
|
||||
// 深度复制源文档内容到目标文档
|
||||
List<Object> 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<Integer> validateDocuments(List<InputStream> inputStreams) {
|
||||
List<Integer> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, String> placeholderMap) {
|
||||
if (mainDocumentPart == null || placeholderMap == null || placeholderMap.isEmpty()) {
|
||||
log.warn("文档部分或占位符映射为空,跳过替换操作");
|
||||
return;
|
||||
}
|
||||
// 预处理占位符映射
|
||||
Map<String, String> 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<String, String> 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<String, String> preprocessPlaceholderMap(Map<String, String> originalMap) {
|
||||
if (originalMap == null || originalMap.isEmpty()) {
|
||||
return new HashMap<>(16);
|
||||
}
|
||||
|
||||
Map<String, String> processedMap = new HashMap<>(16);
|
||||
|
||||
for (Map.Entry<String, String> 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<String, String> 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<String, String> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 报告生成异常工具类
|
||||
@@ -11,90 +11,6 @@ import com.njcn.gather.tools.report.model.ReportResponseEnum;
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -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配合使用,提供完整的模板处理解决方案
|
||||
* <p>
|
||||
* 技术栈:纯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<String, String> placeholders) {
|
||||
if (document == null || placeholders == null || placeholders.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
replacePlaceholdersInParagraphs(document, placeholders);
|
||||
replacePlaceholdersInTables(document, placeholders);
|
||||
}
|
||||
private static final Pattern PLACEHOLDER_PATTERN_DOLLAR = Pattern.compile("\\$\\{([^}]+)}");
|
||||
|
||||
/**
|
||||
* 替换段落中的占位符
|
||||
* 从Word文档输入流中提取所有${placeholder}格式的占位符
|
||||
*
|
||||
* @param document 文档对象
|
||||
* @param placeholders 占位符键值对
|
||||
* @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<String, String> placeholders) {
|
||||
for (XWPFParagraph paragraph : document.getParagraphs()) {
|
||||
replacePlaceholdersInParagraph(paragraph, placeholders);
|
||||
public static Set<String> extractPlaceholders(InputStream templateInputStream, boolean keepFormat, List<String> excludePlaceholders) {
|
||||
if (templateInputStream == null) {
|
||||
throw ReportExceptionUtil.create(ReportResponseEnum.VALIDATION_ERROR);
|
||||
}
|
||||
}
|
||||
Set<String> placeholders = new HashSet<>();
|
||||
try {
|
||||
WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(templateInputStream);
|
||||
MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
|
||||
|
||||
/**
|
||||
* 替换单个段落中的占位符
|
||||
*
|
||||
* @param paragraph 段落对象
|
||||
* @param placeholders 占位符键值对
|
||||
*/
|
||||
public static void replacePlaceholdersInParagraph(XWPFParagraph paragraph, Map<String, String> placeholders) {
|
||||
List<XWPFRun> runs = paragraph.getRuns();
|
||||
if (runs != null) {
|
||||
for (XWPFRun run : runs) {
|
||||
String text = run.getText(0);
|
||||
if (text != null) {
|
||||
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||
text = text.replace(entry.getKey(), entry.getValue());
|
||||
// 使用TraversalUtil遍历所有Text节点
|
||||
CallbackImpl textCallback = new CallbackImpl() {
|
||||
@Override
|
||||
public List<Object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换表格中的占位符
|
||||
*
|
||||
* @param document 文档对象
|
||||
* @param placeholders 占位符键值对
|
||||
*/
|
||||
public static void replacePlaceholdersInTables(XWPFDocument document, Map<String, String> placeholders) {
|
||||
for (XWPFTable table : document.getTables()) {
|
||||
replacePlaceholdersInTable(table, placeholders);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换单个表格中的占位符
|
||||
*
|
||||
* @param table 表格对象
|
||||
* @param placeholders 占位符键值对
|
||||
*/
|
||||
public static void replacePlaceholdersInTable(XWPFTable table, Map<String, String> placeholders) {
|
||||
for (XWPFTableRow row : table.getRows()) {
|
||||
for (XWPFTableCell cell : row.getTableCells()) {
|
||||
for (XWPFParagraph paragraph : cell.getParagraphs()) {
|
||||
replacePlaceholdersInParagraph(paragraph, placeholders);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将源文档的内容追加到目标文档中
|
||||
*
|
||||
* @param target 目标文档
|
||||
* @param source 源文档
|
||||
*/
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制段落内容
|
||||
*
|
||||
* @param source 源段落
|
||||
* @param target 目标段落
|
||||
*/
|
||||
private static void copyParagraphContent(XWPFParagraph source, XWPFParagraph target) {
|
||||
try {
|
||||
// 简单的内容复制,保持样式
|
||||
target.getCTP().set(source.getCTP());
|
||||
};
|
||||
// 遍历整个文档
|
||||
TraversalUtil.visit(mainDocumentPart, textCallback);
|
||||
} catch (Exception e) {
|
||||
// 如果复制失败,采用文本复制方式
|
||||
target.createRun().setText(source.getText());
|
||||
log.error("提取占位符时发生异常", e);
|
||||
throw ReportExceptionUtil.create(ReportResponseEnum.TEMPLATE_PROCESS_ERROR);
|
||||
}
|
||||
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制表格内容
|
||||
* 从Word文档输入流中提取所有${placeholder}格式的占位符(返回纯变量名)
|
||||
*
|
||||
* @param source 源表格
|
||||
* @param target 目标表格
|
||||
* @param templateInputStream Word模板文档输入流
|
||||
* @return 包含所有占位符变量名的Set集合(去重)
|
||||
*/
|
||||
private static void copyTableContent(XWPFTable source, XWPFTable target) {
|
||||
public static Set<String> extractPlaceholders(InputStream templateInputStream, List<String> excludePlaceholders) {
|
||||
return extractPlaceholders(templateInputStream, false, excludePlaceholders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Word文档输入流中提取所有占位符(返回完整${...}格式)
|
||||
*
|
||||
* @param templateInputStream Word模板文档输入流
|
||||
* @return 包含所有完整${...}格式占位符的Set集合
|
||||
*/
|
||||
public static Set<String> extractPlaceholdersWithFormat(InputStream templateInputStream, List<String> excludePlaceholders) {
|
||||
return extractPlaceholders(templateInputStream, true, excludePlaceholders);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证Word文档中是否包含指定的占位符
|
||||
*
|
||||
* @param templateInputStream Word模板文档输入流
|
||||
* @param placeholder 要验证的占位符(纯变量名)
|
||||
* @return true 如果文档包含该占位符,false 否则
|
||||
*/
|
||||
public static boolean containsPlaceholder(InputStream templateInputStream, String placeholder) {
|
||||
if (templateInputStream == null || !StringUtils.hasText(placeholder)) {
|
||||
throw ReportExceptionUtil.create(ReportResponseEnum.VALIDATION_ERROR);
|
||||
}
|
||||
try {
|
||||
// 简单的内容复制,保持样式
|
||||
target.getCTTbl().set(source.getCTTbl());
|
||||
} catch (Exception e) {
|
||||
// 如果复制失败,采用逐行复制方式
|
||||
copyTableRowByRow(source, target);
|
||||
}
|
||||
}
|
||||
WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(templateInputStream);
|
||||
MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
|
||||
|
||||
/**
|
||||
* 逐行复制表格内容
|
||||
*
|
||||
* @param source 源表格
|
||||
* @param target 目标表格
|
||||
*/
|
||||
private static void copyTableRowByRow(XWPFTable source, XWPFTable target) {
|
||||
// 先清空目标表格的默认行
|
||||
if (target.getRows().size() > 0) {
|
||||
target.removeRow(0);
|
||||
}
|
||||
// 使用TraversalUtil遍历所有Text节点查找占位符
|
||||
final boolean[] found = {false};
|
||||
final String targetPlaceholder = "${" + placeholder + "}";
|
||||
|
||||
// 复制每一行
|
||||
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");
|
||||
CallbackImpl textCallback = new CallbackImpl() {
|
||||
@Override
|
||||
public List<Object> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 遍历整个文档
|
||||
TraversalUtil.visit(mainDocumentPart, textCallback);
|
||||
|
||||
/**
|
||||
* 在文档中插入分页符
|
||||
*
|
||||
* @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<XWPFParagraph> findParagraphsByStyle(XWPFDocument document, String styleId) {
|
||||
List<XWPFParagraph> 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<IBodyElement> 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;
|
||||
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<String> 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<IBodyElement> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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("模板类型测试通过");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user