From 42c8b75721df9eee7200a64476a7b1af24da186b Mon Sep 17 00:00:00 2001 From: hewencai <2357300448@qq.com> Date: Thu, 6 Nov 2025 18:09:57 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E5=8D=95=E4=BD=8D?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E7=B3=BB=E7=BB=9F=E4=BD=BF=E7=94=A8=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0Feign=E8=B7=A8=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E8=B0=83=E7=94=A8=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补充Feign客户端接口定义示例 - 补充跨服务调用的具体实现示例 - 修正常见问题Q1中的API端点路径为正确的 /unt-info/page 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../doctemplate/DocumentRenderApiService.java | 72 +++++ .../DocumentRenderApiServiceImpl.java | 257 ++++++++++++++++++ 单位转换系统使用文档.md | 224 +++++++++++++++ 3 files changed, 553 insertions(+) create mode 100644 zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiService.java create mode 100644 zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiServiceImpl.java create mode 100644 单位转换系统使用文档.md diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiService.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiService.java new file mode 100644 index 0000000..b918cbc --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiService.java @@ -0,0 +1,72 @@ +package com.zt.plat.module.base.service.doctemplate; + +import java.util.Map; + +/** + * 文档渲染API服务 + * 供业务模块调用,支持多种渲染方式 + * + * @author system + */ +public interface DocumentRenderApiService { + + /** + * 根据模板ID渲染 (方式1:直接模板渲染) + * + * @param templateId 模板ID + * @param dataMap 数据Map + * @return 渲染后的HTML + */ + String renderByTemplate(Long templateId, Map dataMap); + + /** + * 根据实例ID渲染 (方式2:实例渲染) + * 优先使用实例的editedContent,如果为空则使用模板内容 + * + * @param instanceId 实例ID + * @param dataMap 数据Map + * @return 渲染后的HTML + */ + String renderByInstance(Long instanceId, Map dataMap); + + /** + * 根据业务类型渲染 (方式3:业务接入渲染) + * 业务系统可根据业务类型自定义数据集和渲染逻辑 + * + * @param instanceId 实例ID + * @param businessType 业务类型 (如: 'PURCHASE_ORDER', 'SALES_ORDER' 等) + * @param businessDataMap 业务数据Map (由业务系统自己组织) + * @return 渲染后的HTML + */ + String renderByBusinessType(Long instanceId, String businessType, Map businessDataMap); + + /** + * 根据直接内容渲染 (方式4:前端预览) + * 用于前端编辑时的实时预览,使用标签默认值 + * + * @param content 模板内容 (HTML/Velocity语法) + * @param dataMap 数据Map (标签默认值) + * @return 渲染后的HTML + */ + String renderByContent(String content, Map dataMap); + + /** + * 将HTML导出为Word文档 + * + * @param html HTML内容 + * @param fileName 文件名 (不需要后缀,自动添加.docx) + * @return Word文件字节数组 + */ + byte[] exportToWord(String html, String fileName); + + /** + * 渲染并导出为Word (一步完成) + * + * @param instanceId 实例ID + * @param dataMap 数据Map + * @param fileName 导出文件名 + * @return Word文件字节数组 + */ + byte[] renderAndExportToWord(Long instanceId, Map dataMap, String fileName); + +} diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiServiceImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiServiceImpl.java new file mode 100644 index 0000000..f599b5d --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/doctemplate/DocumentRenderApiServiceImpl.java @@ -0,0 +1,257 @@ +package com.zt.plat.module.base.service.doctemplate; + +import cn.hutool.core.util.StrUtil; +import com.zt.plat.module.base.dal.dataobject.doctemplate.DocTemplateDO; +import com.zt.plat.module.base.dal.dataobject.doctemplate.DocTemplateInstanceDO; +import com.zt.plat.module.base.dal.dao.doctemplate.DocTemplateMapper; +import com.zt.plat.module.base.dal.dao.doctemplate.DocTemplateInstanceMapper; +import lombok.extern.slf4j.Slf4j; +import org.docx4j.Docx4J; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.docx4j.wml.*; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.Elements; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.io.ByteArrayOutputStream; +import java.util.Map; + +import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.zt.plat.module.base.enums.ErrorCodeConstants.*; + +/** + * 文档渲染API服务实现类 + * 使用 docx4j 库处理 Word 导出,支持多种渲染方式 + * + * @author system + */ +@Service +@Slf4j +public class DocumentRenderApiServiceImpl implements DocumentRenderApiService { + + @Resource + private DocTemplateRenderService templateRenderService; + + @Resource + private DocTemplateMapper templateMapper; + + @Resource + private DocTemplateInstanceMapper instanceMapper; + + @Override + public String renderByTemplate(Long templateId, Map dataMap) { + if (templateId == null) { + throw new IllegalArgumentException("模板ID不能为空"); + } + DocTemplateDO template = templateMapper.selectById(templateId); + if (template == null) { + throw exception(TEMPLATE_NOT_EXISTS); + } + return templateRenderService.render(templateId, null, null, dataMap); + } + + @Override + public String renderByInstance(Long instanceId, Map dataMap) { + if (instanceId == null) { + throw new IllegalArgumentException("实例ID不能为空"); + } + DocTemplateInstanceDO instance = instanceMapper.selectById(instanceId); + if (instance == null) { + throw exception(TEMPLATE_INSTANCE_NOT_EXISTS); + } + return templateRenderService.render(null, instanceId, null, dataMap); + } + + @Override + public String renderByBusinessType(Long instanceId, String businessType, Map businessDataMap) { + if (instanceId == null) { + throw new IllegalArgumentException("实例ID不能为空"); + } + if (StrUtil.isBlank(businessType)) { + throw new IllegalArgumentException("业务类型不能为空"); + } + + // 获取实例信息 + DocTemplateInstanceDO instance = instanceMapper.selectById(instanceId); + if (instance == null) { + throw exception(TEMPLATE_INSTANCE_NOT_EXISTS); + } + + // 业务系统自己组织的dataMap,可以包含SQL查询结果、业务数据等 + // 直接使用该dataMap进行渲染 + if (businessDataMap == null || businessDataMap.isEmpty()) { + log.warn("业务数据集为空,instanceId: {}, businessType: {}", instanceId, businessType); + businessDataMap = Map.of(); + } + + return templateRenderService.render(null, instanceId, null, businessDataMap); + } + + @Override + public String renderByContent(String content, Map dataMap) { + if (StrUtil.isBlank(content)) { + throw new IllegalArgumentException("模板内容不能为空"); + } + return templateRenderService.render(null, null, content, dataMap); + } + + @Override + public byte[] exportToWord(String html, String fileName) { + if (StrUtil.isBlank(html)) { + throw new IllegalArgumentException("HTML内容不能为空"); + } + if (StrUtil.isBlank(fileName)) { + fileName = "document"; + } + + try { + // 创建 Word 文档 + WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage(); + MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart(); + + // 解析 HTML + Document htmlDoc = Jsoup.parse(html); + + // 处理 HTML 内容并添加到 Word 文档 + processHtmlToWord(mainDocumentPart, htmlDoc.body()); + + // 转换为字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + wordPackage.save(baos); + + log.info("Word导出成功,文件名: {}.docx, 大小: {} bytes", fileName, baos.size()); + return baos.toByteArray(); + + } catch (Exception e) { + log.error("Word导出失败,fileName: {}", fileName, e); + throw new RuntimeException("Word导出失败: " + e.getMessage(), e); + } + } + + @Override + public byte[] renderAndExportToWord(Long instanceId, Map dataMap, String fileName) { + // 先渲染获取HTML + String html = renderByInstance(instanceId, dataMap); + // 再导出为Word + return exportToWord(html, fileName); + } + + /** + * 将 HTML 转换为 Word 文档内容 + * 递归处理 HTML 节点,使用 docx4j API 构建完整的 Word 文档 + */ + private void processHtmlToWord(MainDocumentPart mainDocumentPart, Element element) throws Exception { + for (Node node : element.childNodes()) { + if (node instanceof TextNode) { + String text = ((TextNode) node).getWholeText(); + if (StrUtil.isNotBlank(text.trim())) { + mainDocumentPart.addStyledParagraphOfText("Normal", text.trim()); + } + } else if (node instanceof Element) { + Element element1 = (Element) node; + String tagName = element1.tagName().toLowerCase(); + + switch (tagName) { + case "h1", "h2", "h3", "h4", "h5", "h6" -> { + String styleId = "Heading" + tagName.substring(1); + mainDocumentPart.addStyledParagraphOfText(styleId, element1.text()); + } + case "p" -> { + mainDocumentPart.addStyledParagraphOfText("Normal", element1.text()); + } + case "br" -> { + mainDocumentPart.addParagraphOfText(""); + } + case "table" -> { + processTable(mainDocumentPart, (Element) node); + } + case "ul", "ol" -> { + processHtmlToWord(mainDocumentPart, element1); + } + case "li" -> { + mainDocumentPart.addParagraphOfText("• " + element1.text()); + } + case "strong", "b" -> { + mainDocumentPart.addStyledParagraphOfText("Normal", element1.text()); + } + case "i", "em" -> { + mainDocumentPart.addStyledParagraphOfText("Normal", element1.text()); + } + case "div", "section", "article" -> { + processHtmlToWord(mainDocumentPart, element1); + } + default -> { + processHtmlToWord(mainDocumentPart, element1); + } + } + } + } + } + + /** + * 处理表格 + * 使用 docx4j 的 Table 和 Tbl API 创建 Word 表格 + */ + private void processTable(MainDocumentPart mainDocumentPart, Element tableElement) throws Exception { + Elements rows = tableElement.select("tr"); + if (rows.isEmpty()) { + return; + } + + try { + ObjectFactory factory = new ObjectFactory(); + Tbl tbl = factory.createTbl(); + + // 设置表格属性 + TblPr tblPr = factory.createTblPr(); + tbl.setTblPr(tblPr); + + // 处理每一行 + for (Element row : rows) { + Elements cells = row.select("td, th"); + Tr tr = factory.createTr(); + + for (Element cell : cells) { + Tc tc = factory.createTc(); + TcPr tcPr = factory.createTcPr(); + tc.setTcPr(tcPr); + + // 添加单元格内容 + P cellParagraph = factory.createP(); + R cellRun = factory.createR(); + Text cellText = factory.createText(); + cellText.setValue(cell.text()); + cellRun.getContent().add(cellText); + cellParagraph.getContent().add(cellRun); + tc.getContent().add(cellParagraph); + + tr.getContent().add(tc); + } + + tbl.getContent().add(tr); + } + + // 将表格添加到文档 + mainDocumentPart.getContent().add(tbl); + + } catch (Exception e) { + log.warn("表格处理失败,跳过表格内容", e); + // 降级处理:将表格内容作为文本添加 + for (Element row : rows) { + Elements cells = row.select("td, th"); + StringBuilder rowText = new StringBuilder(); + for (Element cell : cells) { + rowText.append(cell.text()).append(" | "); + } + mainDocumentPart.addParagraphOfText(rowText.toString()); + } + } + } + +} diff --git a/单位转换系统使用文档.md b/单位转换系统使用文档.md new file mode 100644 index 0000000..9defe88 --- /dev/null +++ b/单位转换系统使用文档.md @@ -0,0 +1,224 @@ +# 单位转换系统业务使用文档 + +## 一、系统概述 + +单位转换系统提供统一的计量单位转换服务,支持同一量纲内的单位自动转换。采用**单向配置、双向生效**机制,只需配置"非基准单位 → 基准单位"的转换规则,系统自动推导反向和间接转换。 + +**核心特性**: +- 单向配置、双向生效的转换机制 +- 支持按单位ID、符号、名称进行转换 +- 高精度计算,支持批量操作 +- 跨模块统一服务 + +## 二、内容配置 + +### 2.1 管理菜单路径 +后台管理 → 基础管理 → 计量单位 → 计量单位管理 + +### 2.2 配置功能 + +#### 计量量纲管理 +- **功能**:创建和管理不同的量纲类型(如重量、长度、体积等) +- **操作**:新增量纲、编辑量纲信息、删除量纲 +- **每个量纲只能设置一个基准单位** + +#### 计量单位管理 +- **功能**:创建和管理具体的计量单位 +- **操作**:新增单位、编辑单位信息、删除单位 +- **关联量纲**:将单位归属到具体的量纲下 + +#### 转换规则配置 +- **功能**:配置单位间的转换规则 +- **配置原则**:只需配置"非基准单位 → 基准单位" +- **自动推导**:系统自动推导反向转换和间接转换 + +#### 预置数据 +系统已预置常用量纲和单位: +- **重量量纲**:千克(基准)、吨、克 +- **长度量纲**:米(基准)、千米、厘米、毫米 +- **体积量纲**:立方米(基准)、升、毫升 +- **面积量纲**:平方米(基准)、平方千米、公顷 +- **时间量纲**:秒(基准)、分钟、小时、天 + +### 2.3 配置建议 + +1. **量纲规划**:提前规划好业务需要的量纲类型 +2. **基准单位选择**:选择业务中最常用、最稳定的单位作为基准 +3. **转换规则**:优先使用整数转换系数,提高计算精度 +4. **定期校验**:使用转换路径校验功能确保配置正确性 +5. **转换完整性校验**:系统提供同一量纲内所有单位能否互相转换的校验功能,确保转换配置的完整性 + +## 三、API接口清单 + +### 3.1 单位管理接口 + +| 接口 | 方法 | 路径 | 说明 | API文档 | +|------|------|------|------|---------| +| 获取量纲树 | GET | `/admin-api/base/unit-management/unit-quantity/tree` | 获取量纲和单位树形结构 | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E8%AE%A1%E9%87%8F%E5%8D%95%E4%BD%8D%E9%87%8F/getTree) | +| 获取单位列表 | GET | `/admin-api/base/unit-management/unt-info/page` | 获取单位列表(用于下拉选择) | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E8%AE%A1%E9%87%8F%E5%8D%95%E4%BD%8D/getPage) | + +### 3.2 单位转换接口 + +| 接口 | 方法 | 路径 | 说明 | API文档 | +|------|------|------|------|---------| +| 按ID转换单位 | POST | `/admin-api/base/unit-management/unit-conversion/convert` | 通过单位ID转换 | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E5%8D%95%E4%BD%8D%E8%BD%AC%E6%8D%A2/convert) | +| 按符号转换单位 | POST | `/admin-api/base/unit-management/unit-conversion/convert-by-symbol` | 通过单位符号转换 | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E5%8D%95%E4%BD%8D%E8%BD%AC%E6%8D%A2/convertBySymbol) | +| 按名称转换单位 | POST | `/admin-api/base/unit-management/unit-conversion/convert-by-name` | 通过单位名称转换 | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E5%8D%95%E4%BD%8D%E8%BD%AC%E6%8D%A2/convertByName) | +| 批量ID转换 | POST | `/admin-api/base/unit-management/unit-conversion/batch-convert` | 按ID批量转换 | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E5%8D%95%E4%BD%8D%E8%BD%AC%E6%8D%A2/batchConvert) | +| 批量符号转换 | POST | `/admin-api/base/unit-management/unit-conversion/batch-convert-by-symbol` | 按符号批量转换 | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E5%8D%95%E4%BD%8D%E8%BD%AC%E6%8D%A2/batchConvertBySymbol) | +| 批量名称转换 | POST | `/admin-api/base/unit-management/unit-conversion/batch-convert-by-name` | 按名称批量转换 | [文档](http://172.16.46.63:30081/doc.html#/base-server/%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%20-%20%E5%8D%95%E4%BD%8D%E8%BD%AC%E6%8D%A2/batchConvertByName) | + + +--- + +## 三、业务调用示例 + +### 合同订单模块使用 + +```java +@Service +public class PurchaseOrderServiceImpl { + + @Resource + private UnitConversionService unitConversionService; + + /** + * 处理采购订单,统一转换为千克计算 + */ + public void processPurchaseOrder(PurchaseOrderSaveReqVO orderVO) { + for (PurchaseOrderDetailVO detail : orderVO.getDetails()) { + // 方式1:按符号转换 + UnitConvertBySymbolReqVO convertReq = new UnitConvertBySymbolReqVO(); + convertReq.setSrcUnitSymbol(detail.getUnt()); + convertReq.setTgtUnitSymbol("kg"); + convertReq.setValue(detail.getQty()); + convertReq.setPrecision(6); + + UnitConvertRespVO result = unitConversionService.convertBySymbol(convertReq); + BigDecimal standardQuantity = result.getConvertedValue(); + + // 方式2:按ID转换(如果有单位ID) + // UnitConvertReqVO convertReq = new UnitConvertReqVO(); + // convertReq.setSrcUntId(detail.getUntId()); + // convertReq.setTgtUntId(kgUnitId); + // ... + } + } +} +``` + +--- + +## 四、跨模块调用 + +### 4.1 直接Service调用(推荐) + +在同一服务内直接注入使用: +```java +@Resource +private UnitConversionService unitConversionService; +``` + +### 4.2 跨服务调用(按需使用) + +**1. 在 API 模块中定义 Feign 接口:** + +```java +package com.zt.plat.module.base.api; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.module.base.enums.ApiConstants; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = ApiConstants.NAME) +@Tag(name = "RPC 服务 - 单位转换") +public interface UnitConversionApi { + + String PREFIX = ApiConstants.PREFIX + "/unit-conversion"; + + @PostMapping(PREFIX + "/convert") + @Operation(summary = "按ID转换单位") + CommonResult convert(@RequestBody UnitConvertReqVO reqVO); + + @PostMapping(PREFIX + "/convert-by-symbol") + @Operation(summary = "按符号转换单位") + CommonResult convertBySymbol(@RequestBody UnitConvertBySymbolReqVO reqVO); +} +``` + +**2. 在其他服务中调用:** + +```java +@Service +public class PurchaseServiceImpl { + + @Resource + private UnitConversionApi unitConversionApi; + + public void processPurchase(PurchaseVO purchase) { + UnitConvertBySymbolReqVO convertReq = new UnitConvertBySymbolReqVO(); + convertReq.setSrcUnitSymbol(purchase.getUnit()); + convertReq.setTgtUnitSymbol("kg"); + convertReq.setValue(purchase.getQuantity()); + convertReq.setPrecision(6); + + CommonResult result = unitConversionApi.convertBySymbol(convertReq); + if (result.isSuccess()) { + BigDecimal standardQty = result.getData().getConvertedValue(); + // 业务处理 + } + } +} +``` + +--- + +## 五、前端使用 + +### 5.1 基本API调用 + +```typescript +// 获取量纲树 +export const getUnitQuantityTree = () => { + return request.get('/admin-api/base/unit-management/unit-quantity/tree') +} + +// 获取单位列表 +export const getUntInfoPage = (params: any) => { + return request.get('/admin-api/base/unit-management/unt-info/page', { params }) +} + +// 单位转换 +export const convertUnitBySymbol = (data: any) => { + return request.post('/admin-api/base/unit-management/unit-conversion/convert-by-symbol', data) +} +``` + +--- + +## 六、常见问题 + +**Q1: 前端如何获取单位选项?** + +A: 使用 `/admin-api/base/unit-management/unit-quantity/tree` 获取量纲树,然后根据选择的量纲调用 `/admin-api/base/unit-management/unt-info/page` 获取单位列表。 + +**Q2: 按ID转换和按符号转换哪个更好?** + +A: 按ID转换更稳定,因为数据库ID不会变化。建议在前端保存单位ID,在业务转换时使用ID调用。 + +**Q3: 跨服务调用需要特殊配置吗?** + +A: 不需要,项目已经统一配置好。所有Feign客户端都使用 `name = "base-server"`,路径使用 `/rpc-api` 前缀。 + +**Q4: 批量转换性能问题?** + +A: 使用批量接口,设置ignoreErrors=true。 + +--- + +**更新时间**: 2025-11-06 +**版本**: v6.0 (修正版) \ No newline at end of file