Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
潘荣晟
2025-11-11 14:05:14 +08:00
65 changed files with 3647 additions and 141 deletions

View File

@@ -126,7 +126,20 @@
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!-- docx4j - Word文档处理 -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-core</artifactId>
<version>11.4.11</version>
</dependency>
<!-- docx4j JAXB 运行时支持 (Java 17 兼容) -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
<version>11.4.11</version>
</dependency>
<!-- 监控相关 -->

View File

@@ -35,4 +35,6 @@ public class ElementPageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@Schema(description = "排序")
private Integer sort;
}

View File

@@ -44,4 +44,6 @@ public class ElementRespVO {
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@Schema(description = "排序")
private Integer sort;
}

View File

@@ -34,4 +34,7 @@ public class ElementSaveReqVO {
@NotEmpty(message = "品位单位不能为空")
private String gradeUnit;
@Schema(description = "排序")
private Integer sort;
}

View File

@@ -7,15 +7,19 @@ import com.zt.plat.module.base.controller.admin.doctemplate.vo.DocTemplateInstan
import com.zt.plat.module.base.controller.admin.doctemplate.vo.DocTemplateInstanceSaveReqVO;
import com.zt.plat.module.base.service.doctemplate.DocTemplateInstanceService;
import com.zt.plat.module.base.service.doctemplate.DocTemplateRenderService;
import com.zt.plat.module.base.service.doctemplate.DocumentRenderApiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
@@ -38,6 +42,9 @@ public class DocTemplateInstanceController {
@Resource
private DocTemplateRenderService renderService;
@Resource
private DocumentRenderApiService documentRenderApiService;
@PostMapping("/create")
@Operation(summary = "创建模板实例")
@PreAuthorize("@ss.hasPermission('base:template-instance:create')")
@@ -104,4 +111,35 @@ public class DocTemplateInstanceController {
return success(true);
}
@PostMapping("/render-and-export-word")
@Operation(summary = "渲染实例并导出为Word")
@PreAuthorize("@ss.hasPermission('base:template-instance:query')")
public ResponseEntity<byte[]> renderAndExportToWord(
@Parameter(name = "instanceId", description = "实例ID", required = true) @RequestParam("instanceId") Long instanceId,
@Parameter(name = "fileName", description = "文件名") @RequestParam(value = "fileName", required = false) String fileName,
@Parameter(name = "dataMap", description = "数据Map") @RequestBody(required = false) Map<String, Object> dataMap) {
byte[] fileContent = documentRenderApiService.renderAndExportToWord(instanceId, dataMap, fileName);
String actualFileName = (fileName != null ? fileName : "document") + ".docx";
String encodedFileName = URLEncoder.encode(actualFileName, StandardCharsets.UTF_8).replace("+", "%20");
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"document.docx\"; filename*=UTF-8''" + encodedFileName)
.header("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
.body(fileContent);
}
@PostMapping("/export-html-to-word")
@Operation(summary = "导出HTML为Word文档")
@PreAuthorize("@ss.hasPermission('base:template-instance:query')")
public ResponseEntity<byte[]> exportHtmlToWord(
@Parameter(name = "html", description = "HTML内容", required = true) @RequestBody String html,
@Parameter(name = "fileName", description = "文件名") @RequestParam(value = "fileName", required = false) String fileName) {
byte[] fileContent = documentRenderApiService.exportToWord(html, fileName);
String actualFileName = (fileName != null ? fileName : "document") + ".docx";
String encodedFileName = URLEncoder.encode(actualFileName, StandardCharsets.UTF_8).replace("+", "%20");
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"document.docx\"; filename*=UTF-8''" + encodedFileName)
.header("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
.body(fileContent);
}
}

View File

@@ -0,0 +1,223 @@
package com.zt.plat.module.base.controller.admin.doctemplate.render;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.base.controller.admin.doctemplate.render.vo.*;
import com.zt.plat.module.base.service.doctemplate.render.DocTemplateAdvancedRenderService;
import com.zt.plat.module.base.service.doctemplate.render.DocTemplateDataSourceType;
import com.zt.plat.module.base.service.doctemplate.render.TemplateRenderRequest;
import com.zt.plat.module.base.service.doctemplate.render.export.ExportType;
import com.zt.plat.module.base.service.doctemplate.render.export.WordExportResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.zt.plat.module.base.enums.ErrorCodeConstants.*;
/**
* 文档模板渲染控制器
*/
@RestController
@RequestMapping("/base/doc-template-instance")
@Tag(name = "管理后台 - 文档模板渲染")
@Slf4j
public class DocTemplateRenderController {
@Resource
private DocTemplateAdvancedRenderService advancedRenderService;
@PostMapping("/preview/default")
@Operation(summary = "默认值预览")
@PreAuthorize("@ss.hasPermission('base:doctemplate:render')")
public CommonResult<String> previewWithDefaultValues(
@RequestParam("templateInstanceId") Long templateInstanceId) {
log.info("默认值预览: {}", templateInstanceId);
try {
TemplateRenderRequest request = new TemplateRenderRequest();
request.setTemplateInstanceId(templateInstanceId);
request.setExportType(ExportType.HTML_PREVIEW);
Object result = advancedRenderService.renderAndExport(request);
return CommonResult.success(result.toString());
} catch (Exception e) {
log.error("预览失败", e);
return CommonResult.error(500, "预览失败: " + e.getMessage());
}
}
@PostMapping("/preview/business")
@Operation(summary = "业务数据预览")
@PreAuthorize("@ss.hasPermission('base:doctemplate:render')")
public CommonResult<String> previewWithBusinessData(@RequestBody BusinessPreviewRequest request) {
log.info("业务预览: {}", request.getTemplateInstanceId());
try {
Map<String, DocTemplateDataSourceType> fieldDataSources = new HashMap<>();
if (request.getFieldMappings() != null) {
for (String key : request.getFieldMappings().keySet()) {
fieldDataSources.put(key, DocTemplateDataSourceType.BUSINESS);
}
}
TemplateRenderRequest renderRequest = new TemplateRenderRequest();
renderRequest.setTemplateInstanceId(request.getTemplateInstanceId());
renderRequest.setBusinessType(request.getBusinessType());
renderRequest.setDataSourceContext(request.getBusinessId());
renderRequest.setFieldDataSources(fieldDataSources);
renderRequest.setExportType(ExportType.HTML_PREVIEW);
Object result = advancedRenderService.renderAndExport(renderRequest);
return CommonResult.success(result.toString());
} catch (Exception e) {
log.error("业务预览失败", e);
return CommonResult.error(500, "预览失败: " + e.getMessage());
}
}
@PostMapping("/preview/custom")
@Operation(summary = "自定义数据预览")
@PreAuthorize("@ss.hasPermission('base:doctemplate:render')")
public CommonResult<String> previewWithCustomData(@RequestBody CustomPreviewRequest request) {
log.info("自定义预览: {}", request.getTemplateInstanceId());
try {
Map<String, DocTemplateDataSourceType> fieldDataSources = new HashMap<>();
if (request.getCustomData() != null) {
for (String key : request.getCustomData().keySet()) {
fieldDataSources.put(key, DocTemplateDataSourceType.REQUEST);
}
}
TemplateRenderRequest renderRequest = new TemplateRenderRequest();
renderRequest.setTemplateInstanceId(request.getTemplateInstanceId());
renderRequest.setFieldDataSources(fieldDataSources);
renderRequest.setDataSourceContext(request.getCustomData());
renderRequest.setExportType(ExportType.HTML_PREVIEW);
Object result = advancedRenderService.renderAndExport(renderRequest);
return CommonResult.success(result.toString());
} catch (Exception e) {
log.error("自定义预览失败", e);
return CommonResult.error(500, "预览失败: " + e.getMessage());
}
}
@PostMapping("/export/default")
@Operation(summary = "默认值导出Word")
@PreAuthorize("@ss.hasPermission('base:doctemplate:render')")
public ResponseEntity<byte[]> exportWithDefaultValues(
@RequestParam("templateInstanceId") Long templateInstanceId) {
log.info("默认值导出: {}", templateInstanceId);
try {
TemplateRenderRequest request = new TemplateRenderRequest();
request.setTemplateInstanceId(templateInstanceId);
request.setExportType(ExportType.WORD);
Object result = advancedRenderService.renderAndExport(request);
if (result instanceof WordExportResult) {
WordExportResult wordResult = (WordExportResult) result;
String encodedFileName = URLEncoder.encode(wordResult.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"document.docx\"; filename*=UTF-8''" + encodedFileName)
.header("Content-Type", wordResult.getContentType())
.body(wordResult.getFileContent());
}
throw exception(TEMPLATE_EXPORT_TYPE_ERROR);
} catch (Exception e) {
log.error("默认值导出失败", e);
throw exception(TEMPLATE_EXPORT_FAILED);
}
}
@PostMapping("/export/business")
@Operation(summary = "业务数据导出Word")
@PreAuthorize("@ss.hasPermission('base:doctemplate:render')")
public ResponseEntity<byte[]> exportWithBusinessData(@RequestBody BusinessExportRequest request) {
log.info("业务导出: {}", request.getTemplateInstanceId());
try {
Map<String, DocTemplateDataSourceType> fieldDataSources = new HashMap<>();
if (request.getFieldMappings() != null) {
for (String key : request.getFieldMappings().keySet()) {
fieldDataSources.put(key, DocTemplateDataSourceType.BUSINESS);
}
}
TemplateRenderRequest renderRequest = new TemplateRenderRequest();
renderRequest.setTemplateInstanceId(request.getTemplateInstanceId());
renderRequest.setBusinessType(request.getBusinessType());
renderRequest.setDataSourceContext(request.getBusinessId());
renderRequest.setFieldDataSources(fieldDataSources);
renderRequest.setExportType(ExportType.WORD);
Object result = advancedRenderService.renderAndExport(renderRequest);
if (result instanceof WordExportResult) {
WordExportResult wordResult = (WordExportResult) result;
String encodedFileName = URLEncoder.encode(wordResult.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"document.docx\"; filename*=UTF-8''" + encodedFileName)
.header("Content-Type", wordResult.getContentType())
.body(wordResult.getFileContent());
}
throw exception(TEMPLATE_EXPORT_TYPE_ERROR);
} catch (Exception e) {
log.error("业务数据导出失败", e);
throw exception(TEMPLATE_EXPORT_FAILED);
}
}
@PostMapping("/export/custom")
@Operation(summary = "自定义数据导出Word")
@PreAuthorize("@ss.hasPermission('base:doctemplate:render')")
public void exportWithCustomData(
@RequestBody CustomExportRequest request,
jakarta.servlet.http.HttpServletResponse response) throws Exception {
log.info("自定义导出: {}", request.getTemplateInstanceId());
try {
Map<String, DocTemplateDataSourceType> fieldDataSources = new HashMap<>();
if (request.getCustomData() != null) {
for (String key : request.getCustomData().keySet()) {
fieldDataSources.put(key, DocTemplateDataSourceType.REQUEST);
}
}
TemplateRenderRequest renderRequest = new TemplateRenderRequest();
renderRequest.setTemplateInstanceId(request.getTemplateInstanceId());
renderRequest.setFieldDataSources(fieldDataSources);
renderRequest.setDataSourceContext(request.getCustomData());
renderRequest.setExportType(ExportType.WORD);
Object result = advancedRenderService.renderAndExport(renderRequest);
if (result instanceof WordExportResult) {
WordExportResult wordResult = (WordExportResult) result;
// 先设置响应头
response.addHeader("Content-Disposition", "attachment;filename=" +
com.zt.plat.framework.common.util.http.HttpUtils.encodeUtf8(wordResult.getFileName()));
response.setContentType(wordResult.getContentType());
// 再写入数据到输出流
response.getOutputStream().write(wordResult.getFileContent());
response.getOutputStream().flush();
return;
}
throw exception(TEMPLATE_EXPORT_TYPE_ERROR);
} catch (Exception e) {
log.error("自定义数据导出失败", e);
throw exception(TEMPLATE_EXPORT_FAILED);
}
}
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.base.controller.admin.doctemplate.render.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BusinessExportRequest {
private Long templateInstanceId;
private String businessType;
private Long businessId;
private Map<String, String> fieldMappings;
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.base.controller.admin.doctemplate.render.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BusinessPreviewRequest {
private Long templateInstanceId;
private String businessType;
private Long businessId;
private Map<String, String> fieldMappings;
}

View File

@@ -0,0 +1,17 @@
package com.zt.plat.module.base.controller.admin.doctemplate.render.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomExportRequest {
private Long templateInstanceId;
private Map<String, Object> customData;
}

View File

@@ -0,0 +1,17 @@
package com.zt.plat.module.base.controller.admin.doctemplate.render.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomPreviewRequest {
private Long templateInstanceId;
private Map<String, Object> customData;
}

View File

@@ -34,9 +34,21 @@ public class DocTemplateCategoryRespVO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "创建人", example = "admin")
@Schema(description = "创建人ID", example = "1")
private String creator;
@Schema(description = "创建人名称", example = "管理员")
private String creatorName;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "更新人ID", example = "1")
private String updater;
@Schema(description = "更新人名称", example = "管理员")
private String updaterName;
@Schema(description = "子分类列表")
private List<DocTemplateCategoryRespVO> children;

View File

@@ -42,6 +42,24 @@ public class DocTemplateInstanceRespVO {
@Schema(description = "状态", example = "draft")
private String status;
@Schema(description = "SQL配置JSON格式", example = "{}")
private String sqlConfig;
@Schema(description = "数据源标识", example = "default")
private String dataSource;
@Schema(description = "分类ID", example = "1")
private Long categoryId;
@Schema(description = "图标", example = "icon-file")
private String icon;
@Schema(description = "描述", example = "采购合同模板实例")
private String description;
@Schema(description = "版本号", example = "v1.0")
private String version;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;

View File

@@ -33,8 +33,7 @@ public class DocTemplateInstanceSaveReqVO {
@Schema(description = "业务关联标签", example = "PC-2025-001")
private String businessLabel;
@Schema(description = "用户编辑后的内容", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "内容不能为空")
@Schema(description = "用户编辑后的内容(创建时可为空,将自动从模板复制)")
private String editedContent;
@Schema(description = "渲染后的最终内容")
@@ -46,4 +45,22 @@ public class DocTemplateInstanceSaveReqVO {
@Schema(description = "状态", example = "draft")
private String status;
@Schema(description = "SQL配置JSON格式", example = "{}")
private String sqlConfig;
@Schema(description = "数据源标识", example = "default")
private String dataSource;
@Schema(description = "分类ID", example = "1")
private Long categoryId;
@Schema(description = "图标", example = "icon-file")
private String icon;
@Schema(description = "描述", example = "采购合同模板实例")
private String description;
@Schema(description = "版本号", example = "v1.0")
private String version;
}

View File

@@ -23,11 +23,8 @@ public class DocTemplatePageReqVO extends PageParam {
@Schema(description = "模板编码", example = "PO_CONTRACT_001")
private String tmplCode;
@Schema(description = "所属", example = "1")
private Long bigCategoryId;
@Schema(description = "所属小类", example = "11")
private Long smallCategoryId;
@Schema(description = "所属", example = "1")
private Long categoryId;
@Schema(description = "状态1=启用0=停用2=草稿)", example = "1")
private String enabled;

View File

@@ -21,11 +21,11 @@ public class DocTemplateRespVO {
@Schema(description = "模板图标", example = "📄")
private String icon;
@Schema(description = "所属", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long bigCategoryId;
@Schema(description = "所属", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long categoryId;
@Schema(description = "所属小类", example = "11")
private Long smallCategoryId;
@Schema(description = "分类名称", example = "合同信息/采购合同")
private String categoryName;
@Schema(description = "版本号", example = "v1.2")
private String version;

View File

@@ -24,12 +24,9 @@ public class DocTemplateSaveReqVO {
@Schema(description = "模板图标", example = "📄")
private String icon;
@Schema(description = "所属", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "所属类不能为空")
private Long bigCategoryId;
@Schema(description = "所属小类", example = "11")
private Long smallCategoryId;
@Schema(description = "所属", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "所属类不能为空")
private Long categoryId;
@Schema(description = "版本号", example = "v1.2")
private String version;

View File

@@ -19,8 +19,7 @@ public interface DocTemplateMapper extends BaseMapperX<DocTemplateDO> {
return selectPage(reqVO, new LambdaQueryWrapperX<DocTemplateDO>()
.likeIfPresent(DocTemplateDO::getTmplName, reqVO.getTmplName())
.likeIfPresent(DocTemplateDO::getTmplCode, reqVO.getTmplCode())
.eqIfPresent(DocTemplateDO::getBigCategoryId, reqVO.getBigCategoryId())
.eqIfPresent(DocTemplateDO::getSmallCategoryId, reqVO.getSmallCategoryId())
.eqIfPresent(DocTemplateDO::getCategoryId, reqVO.getCategoryId())
.eqIfPresent(DocTemplateDO::getEnabled, reqVO.getEnabled())
.betweenIfPresent(DocTemplateDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(DocTemplateDO::getId));

View File

@@ -94,4 +94,7 @@ public class ElementDO extends BusinessBaseDO {
@TableField("UPDATER_NAME")
private String updaterName;
@TableField("SORT")
private Integer sort;
}

View File

@@ -43,16 +43,10 @@ public class DocTemplateDO extends BusinessBaseDO {
private String icon;
/**
* 所属大类
* 所属分类ID支持任意级别分类
*/
@TableField("big_category_id")
private Long bigCategoryId;
/**
* 所属小类
*/
@TableField("small_category_id")
private Long smallCategoryId;
@TableField("category_id")
private Long categoryId;
/**
* 版本号

View File

@@ -84,4 +84,40 @@ public class DocTemplateInstanceDO extends BusinessBaseDO {
@TableField("status")
private String status;
/**
* SQL配置
*/
@TableField("sql_config")
private String sqlConfig;
/**
* 数据源标识
*/
@TableField("data_source")
private String dataSource;
/**
* 分类ID
*/
@TableField("category_id")
private Long categoryId;
/**
* 图标
*/
@TableField("icon")
private String icon;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 版本号
*/
@TableField("version")
private String version;
}

View File

@@ -26,13 +26,14 @@ public interface ElementMapper extends BaseMapperX<ElementDO> {
.likeIfPresent(ElementDO::getCoding, reqVO.getCoding())
.eqIfPresent(ElementDO::getGradeUnit, reqVO.getGradeUnit())
.betweenIfPresent(ElementDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(ElementDO::getId));
.orderByDesc(ElementDO::getSort));
}
String selectMaxCode();
default List<ElementDO> getElementNoPage() {
return selectList(new LambdaQueryWrapperX<ElementDO>()
.orderByDesc(ElementDO::getId));
.eq(ElementDO::getIsEnable, 1)
.orderByDesc(ElementDO::getSort));
}
}

View File

@@ -49,6 +49,37 @@ public class DocTemplateInstanceServiceImpl implements DocTemplateInstanceServic
// 插入实例
DocTemplateInstanceDO templateInstance = DocTemplateInstanceConvert.INSTANCE.convert(createReqVO);
// 深拷贝模板内容到editedContent实例完全独立于模板
if (StrUtil.isBlank(templateInstance.getEditedContent()) && StrUtil.isNotBlank(template.getContent())) {
templateInstance.setEditedContent(template.getContent());
}
// 从模板复制快照字段(实例创建时获取模板的快照,后续修改模板不影响实例)
if (templateInstance.getSqlConfig() == null) {
templateInstance.setSqlConfig(template.getSqlConfig());
}
if (templateInstance.getDataSource() == null) {
templateInstance.setDataSource(template.getDataSource());
}
if (templateInstance.getCategoryId() == null) {
templateInstance.setCategoryId(template.getCategoryId());
}
if (templateInstance.getIcon() == null) {
templateInstance.setIcon(template.getIcon());
}
if (templateInstance.getDescription() == null) {
templateInstance.setDescription(template.getDescription());
}
if (templateInstance.getVersion() == null) {
templateInstance.setVersion(template.getVersion());
}
// 设置默认状态为草稿
if (templateInstance.getStatus() == null) {
templateInstance.setStatus("draft");
}
docTemplateInstanceMapper.insert(templateInstance);
// 更新模板使用次数

View File

@@ -30,6 +30,9 @@ public class DocTemplateServiceImpl implements DocTemplateService {
@Resource
private DocTemplateMapper templateMapper;
@Resource
private DocTemplateCategoryService templateCategoryService;
@Override
public Long createTemplate(DocTemplateSaveReqVO createReqVO) {
// 校验模板编码唯一性
@@ -37,12 +40,13 @@ public class DocTemplateServiceImpl implements DocTemplateService {
// 插入
DocTemplateDO template = DocTemplateConvert.INSTANCE.convert(createReqVO);
// 设置默认值
if (template.getUseCount() == null) {
template.setUseCount(0);
}
if (template.getEnabled() == null) {
template.setEnabled("2"); // 默认为草稿状态
template.setEnabled("0"); // 默认为禁用状态 (0=禁用, 1=启用)
}
templateMapper.insert(template);
// 返回
@@ -97,19 +101,43 @@ public class DocTemplateServiceImpl implements DocTemplateService {
@Override
public DocTemplateRespVO getTemplate(Long id) {
DocTemplateDO template = templateMapper.selectById(id);
return DocTemplateConvert.INSTANCE.convert(template);
}
@Override
public PageResult<DocTemplateRespVO> getTemplatePage(DocTemplatePageReqVO pageReqVO) {
PageResult<DocTemplateDO> pageResult = templateMapper.selectPage(pageReqVO);
return DocTemplateConvert.INSTANCE.convertPage(pageResult);
DocTemplateRespVO respVO = DocTemplateConvert.INSTANCE.convert(template);
// 填充分类名称
fillCategoryName(respVO);
return respVO;
}
@Override
public List<DocTemplateRespVO> getTemplateList() {
List<DocTemplateDO> list = templateMapper.selectList();
return DocTemplateConvert.INSTANCE.convertList(list);
List<DocTemplateRespVO> result = DocTemplateConvert.INSTANCE.convertList(list);
// 填充分类名称
result.forEach(this::fillCategoryName);
return result;
}
@Override
public PageResult<DocTemplateRespVO> getTemplatePage(DocTemplatePageReqVO reqVO) {
PageResult<DocTemplateDO> pageResult = templateMapper.selectPage(reqVO);
PageResult<DocTemplateRespVO> result = DocTemplateConvert.INSTANCE.convertPage(pageResult);
// 填充分类名称
result.getList().forEach(this::fillCategoryName);
return result;
}
/**
* 填充分类名称(包含父级路径)
*
* @param respVO 模板响应VO
*/
private void fillCategoryName(DocTemplateRespVO respVO) {
if (respVO == null || respVO.getCategoryId() == null) {
return;
}
// 调用分类Service获取完整路径
String categoryPath = templateCategoryService.getCategoryFullPath(respVO.getCategoryId());
respVO.setCategoryName(categoryPath);
}
}

View File

@@ -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<String, Object> dataMap);
/**
* 根据实例ID渲染 (方式2实例渲染)
* 优先使用实例的editedContent如果为空则使用模板内容
*
* @param instanceId 实例ID
* @param dataMap 数据Map
* @return 渲染后的HTML
*/
String renderByInstance(Long instanceId, Map<String, Object> dataMap);
/**
* 根据业务类型渲染 (方式3业务接入渲染)
* 业务系统可根据业务类型自定义数据集和渲染逻辑
*
* @param instanceId 实例ID
* @param businessType 业务类型 (如: 'PURCHASE_ORDER', 'SALES_ORDER' 等)
* @param businessDataMap 业务数据Map (由业务系统自己组织)
* @return 渲染后的HTML
*/
String renderByBusinessType(Long instanceId, String businessType, Map<String, Object> businessDataMap);
/**
* 根据直接内容渲染 (方式4前端预览)
* 用于前端编辑时的实时预览,使用标签默认值
*
* @param content 模板内容 (HTML/Velocity语法)
* @param dataMap 数据Map (标签默认值)
* @return 渲染后的HTML
*/
String renderByContent(String content, Map<String, Object> 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<String, Object> dataMap, String fileName);
}

View File

@@ -0,0 +1,490 @@
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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> dataMap, String fileName) {
// 先渲染获取HTML
String html = renderByInstance(instanceId, dataMap);
// 再导出为Word
return exportToWord(html, fileName);
}
/**
* 将 HTML 转换为 Word 文档内容
* 递归处理 HTML 节点,使用 docx4j API 构建完整的 Word 文档
* 增强版本支持CSS样式保留颜色、对齐、字体等
*/
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" -> {
// 使用带样式的段落处理,而不是使用预定义样式
addStyledParagraph(mainDocumentPart, element1, true);
}
case "p" -> {
addStyledParagraph(mainDocumentPart, element1, false);
}
case "br" -> {
mainDocumentPart.addParagraphOfText("");
}
case "table" -> {
processTable(mainDocumentPart, (Element) node);
}
case "ul", "ol" -> {
processHtmlToWord(mainDocumentPart, element1);
}
case "li" -> {
addStyledParagraph(mainDocumentPart, element1, false);
}
case "strong", "b" -> {
addStyledParagraph(mainDocumentPart, element1, false);
}
case "i", "em" -> {
addStyledParagraph(mainDocumentPart, element1, false);
}
case "div", "section", "article" -> {
processHtmlToWord(mainDocumentPart, element1);
}
default -> {
processHtmlToWord(mainDocumentPart, element1);
}
}
}
}
}
/**
* 添加带样式的段落支持CSS样式解析
* @param mainDocumentPart Word文档主体部分
* @param element HTML元素
* @param isHeading 是否是标题
*/
private void addStyledParagraph(MainDocumentPart mainDocumentPart, Element element, boolean isHeading) throws Exception {
ObjectFactory factory = new ObjectFactory();
P paragraph = factory.createP();
// 创建段落属性
PPr pPr = factory.createPPr();
paragraph.setPPr(pPr);
// 解析并应用段落级样式(对齐方式)
String styleAttr = element.attr("style");
if (StrUtil.isNotBlank(styleAttr)) {
// 解析对齐方式
String textAlign = extractCssProperty(styleAttr, "text-align");
if (StrUtil.isNotBlank(textAlign)) {
Jc jc = factory.createJc();
switch (textAlign.toLowerCase()) {
case "center" -> jc.setVal(JcEnumeration.CENTER);
case "right" -> jc.setVal(JcEnumeration.RIGHT);
case "justify" -> jc.setVal(JcEnumeration.BOTH);
default -> jc.setVal(JcEnumeration.LEFT);
}
pPr.setJc(jc);
}
}
// 创建文本运行
R run = factory.createR();
Text text = factory.createText();
text.setValue(element.text());
run.getContent().add(text);
// 创建文本运行属性
RPr rPr = factory.createRPr();
run.setRPr(rPr);
// 应用字体样式
if (StrUtil.isNotBlank(styleAttr)) {
// 解析颜色
String color = extractCssProperty(styleAttr, "color");
if (StrUtil.isNotBlank(color)) {
Color colorObj = factory.createColor();
String hexColor = convertColorToHex(color);
if (hexColor != null) {
colorObj.setVal(hexColor);
rPr.setColor(colorObj);
}
}
// 解析字体大小
String fontSize = extractCssProperty(styleAttr, "font-size");
if (StrUtil.isNotBlank(fontSize)) {
HpsMeasure hpsMeasure = factory.createHpsMeasure();
// 将px转换为half-points (1px ≈ 1.5半点)
try {
int pxValue = Integer.parseInt(fontSize.replaceAll("[^0-9]", ""));
int halfPoints = pxValue * 2; // 简化转换
hpsMeasure.setVal(java.math.BigInteger.valueOf(halfPoints));
rPr.setSz(hpsMeasure);
rPr.setSzCs(hpsMeasure); // 复杂脚本字体大小
} catch (NumberFormatException e) {
log.warn("无法解析字体大小: {}", fontSize);
}
}
// 解析字体
String fontFamily = extractCssProperty(styleAttr, "font-family");
if (StrUtil.isNotBlank(fontFamily)) {
RFonts rFonts = factory.createRFonts();
fontFamily = fontFamily.replaceAll("['\"]", "").split(",")[0].trim();
rFonts.setAscii(fontFamily);
rFonts.setHAnsi(fontFamily);
rFonts.setCs(fontFamily);
rFonts.setEastAsia(fontFamily);
rPr.setRFonts(rFonts);
}
// 解析粗体
String fontWeight = extractCssProperty(styleAttr, "font-weight");
if ("bold".equalsIgnoreCase(fontWeight) || "700".equals(fontWeight) || "800".equals(fontWeight) || "900".equals(fontWeight)) {
BooleanDefaultTrue bold = factory.createBooleanDefaultTrue();
bold.setVal(true);
rPr.setB(bold);
rPr.setBCs(bold);
}
// 解析斜体
String fontStyle = extractCssProperty(styleAttr, "font-style");
if ("italic".equalsIgnoreCase(fontStyle) || "oblique".equalsIgnoreCase(fontStyle)) {
BooleanDefaultTrue italic = factory.createBooleanDefaultTrue();
italic.setVal(true);
rPr.setI(italic);
rPr.setICs(italic);
}
// 背景色支持 - 暂时注释掉因为 docx4j API 兼容性问题
// 如果需要可以后续使用 CTShd 重新实现
}
// 处理标签自身的样式strong, b, i, em
String tagName = element.tagName().toLowerCase();
if ("strong".equals(tagName) || "b".equals(tagName)) {
BooleanDefaultTrue bold = factory.createBooleanDefaultTrue();
bold.setVal(true);
rPr.setB(bold);
rPr.setBCs(bold);
}
if ("i".equals(tagName) || "em".equals(tagName)) {
BooleanDefaultTrue italic = factory.createBooleanDefaultTrue();
italic.setVal(true);
rPr.setI(italic);
rPr.setICs(italic);
}
// 标题默认加粗且增大字号
if (isHeading) {
BooleanDefaultTrue bold = factory.createBooleanDefaultTrue();
bold.setVal(true);
rPr.setB(bold);
rPr.setBCs(bold);
// 根据标题级别设置字号(如果没有显式指定)
if (rPr.getSz() == null) {
HpsMeasure hpsMeasure = factory.createHpsMeasure();
String tag = element.tagName().toLowerCase();
int size = switch (tag) {
case "h1" -> 32; // 16pt
case "h2" -> 28; // 14pt
case "h3" -> 24; // 12pt
case "h4" -> 22; // 11pt
case "h5" -> 20; // 10pt
case "h6" -> 18; // 9pt
default -> 22;
};
hpsMeasure.setVal(java.math.BigInteger.valueOf(size));
rPr.setSz(hpsMeasure);
rPr.setSzCs(hpsMeasure);
}
}
paragraph.getContent().add(run);
mainDocumentPart.getContent().add(paragraph);
}
/**
* 从CSS样式字符串中提取指定属性的值
* @param styleAttr CSS样式字符串例如 "color: black; text-align: center;"
* @param property 要提取的属性名,例如 "color"
* @return 属性值,例如 "black"如果不存在则返回null
*/
private String extractCssProperty(String styleAttr, String property) {
if (StrUtil.isBlank(styleAttr) || StrUtil.isBlank(property)) {
return null;
}
// 分割样式声明
String[] declarations = styleAttr.split(";");
for (String declaration : declarations) {
String[] parts = declaration.split(":", 2);
if (parts.length == 2) {
String key = parts[0].trim().toLowerCase();
String value = parts[1].trim();
if (key.equals(property.toLowerCase())) {
return value;
}
}
}
return null;
}
/**
* 将CSS颜色值转换为十六进制格式用于Word
* 支持:#rrggbb, rgb(r,g,b), 颜色名称
* @param cssColor CSS颜色值
* @return 十六进制颜色(不带#),例如 "000000"如果无法解析则返回null
*/
private String convertColorToHex(String cssColor) {
if (StrUtil.isBlank(cssColor)) {
return null;
}
cssColor = cssColor.trim().toLowerCase();
// 已经是十六进制格式
if (cssColor.startsWith("#")) {
return cssColor.substring(1);
}
// rgb/rgba格式
if (cssColor.startsWith("rgb")) {
try {
String rgbValues = cssColor.substring(cssColor.indexOf('(') + 1, cssColor.indexOf(')'));
String[] parts = rgbValues.split(",");
int r = Integer.parseInt(parts[0].trim());
int g = Integer.parseInt(parts[1].trim());
int b = Integer.parseInt(parts[2].trim());
return String.format("%02X%02X%02X", r, g, b);
} catch (Exception e) {
log.warn("无法解析RGB颜色: {}", cssColor);
return null;
}
}
// 颜色名称映射
return switch (cssColor) {
case "black" -> "000000";
case "white" -> "FFFFFF";
case "red" -> "FF0000";
case "green" -> "008000";
case "blue" -> "0000FF";
case "yellow" -> "FFFF00";
case "cyan" -> "00FFFF";
case "magenta" -> "FF00FF";
case "gray", "grey" -> "808080";
case "silver" -> "C0C0C0";
case "maroon" -> "800000";
case "olive" -> "808000";
case "lime" -> "00FF00";
case "aqua" -> "00FFFF";
case "teal" -> "008080";
case "navy" -> "000080";
case "fuchsia" -> "FF00FF";
case "purple" -> "800080";
default -> null;
};
}
/**
* 处理表格
* 使用 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());
}
}
}
}

View File

@@ -0,0 +1,60 @@
package com.zt.plat.module.base.service.doctemplate.render;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 默认SQL配置数据提供者实现
* 这是一个临时的实现实际项目中应该根据具体的SQL配置管理来实现
*
* @author hwc
*/
@Component
@Slf4j
public class DefaultSqlConfigProvider implements ISqlConfigProvider {
@Override
public List<Map<String, Object>> querySqlConfig(String sqlConfigId) {
log.debug("查询SQL配置: {}", sqlConfigId);
// TODO: 实际实现应该:
// 1. 从数据库中根据 sqlConfigId 查询SQL脚本
// 2. 执行SQL脚本并返回结果
// 3. 处理异常情况
// 临时实现:返回一些示例数据
List<Map<String, Object>> result = new ArrayList<>();
switch (sqlConfigId) {
case "CONTRACT_INFO":
Map<String, Object> contractData = new HashMap<>();
contractData.put("contractName", "示例合同名称");
contractData.put("supplier", "示例供应商");
contractData.put("amount", "1000000");
contractData.put("deliveryDate", "2025-12-31");
result.add(contractData);
break;
case "ORDER_INFO":
Map<String, Object> orderData = new HashMap<>();
orderData.put("orderNumber", "ORD-2025-001");
orderData.put("customer", "示例客户");
orderData.put("totalAmount", "500000");
orderData.put("orderDate", "2025-01-15");
result.add(orderData);
break;
default:
log.warn("未知的SQL配置ID: {}", sqlConfigId);
break;
}
log.debug("SQL配置查询结果数量: {}", result.size());
return result;
}
}

View File

@@ -0,0 +1,186 @@
package com.zt.plat.module.base.service.doctemplate.render;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.module.base.controller.admin.doctemplate.vo.DocTemplateInstanceRespVO;
import com.zt.plat.module.base.dal.dataobject.doctemplate.DocTemplateTagDO;
import com.zt.plat.module.base.dal.dao.doctemplate.DocTemplateTagMapper;
import com.zt.plat.module.base.service.doctemplate.DocTemplateInstanceService;
import com.zt.plat.module.base.service.doctemplate.render.export.DocTemplateExportStrategyFactory;
import com.zt.plat.module.base.service.doctemplate.render.export.IExportStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 增强版模板实例渲染服务(核心服务)
* 基于现有 DocTemplateRenderService 扩展,支持多数据源和策略模式
*
* @author hwc
*/
@Service
@Slf4j
public class DocTemplateAdvancedRenderService {
@Resource
private DocTemplateInstanceService docTemplateInstanceService;
@Resource
private DocTemplateTagMapper docTemplateTagMapper;
@Resource
private IVelocityRenderEngine velocityRenderEngine;
@Resource
private DocTemplateDataSourceProviderFactory dataSourceProviderFactory;
@Resource
private DocTemplateExportStrategyFactory exportStrategyFactory;
/**
* 渲染模板实例并导出
*/
public Object renderAndExport(TemplateRenderRequest request) {
log.info("开始渲染和导出模板实例ID: {}, 导出类型: {}",
request.getTemplateInstanceId(), request.getExportType());
// 1. 加载模板实例
DocTemplateInstanceRespVO instance = docTemplateInstanceService
.getTemplateInstance(request.getTemplateInstanceId());
if (instance == null) {
throw new ServiceException(HttpStatus.INTERNAL_SERVER_ERROR.value(),"模板实例不存在: " + request.getTemplateInstanceId());
}
// 2. 加载标签定义(用于获取标签的默认值)
List<DocTemplateTagDO> tags = getTemplateTags(instance);
if (tags == null || tags.isEmpty()) {
log.warn("模板实例 {} 没有关联的标签定义", request.getTemplateInstanceId());
}
Map<String, DocTemplateTagDO> tagMap = tags.stream()
.collect(Collectors.toMap(DocTemplateTagDO::getTagCode, t -> t));
// 3. 收集所有需要填充的字段值
Map<String, Object> fillData = new HashMap<>();
for (DocTemplateTagDO tag : tags) {
Object value = getFieldValue(tag, request, instance.getInstanceName());
fillData.put(tag.getTagCode(), value != null ? value : "");
}
// 4. Velocity 渲染
String templateContent = instance.getEditedContent();
if (templateContent == null || templateContent.trim().isEmpty()) {
throw new ServiceException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "模板实例内容为空");
}
String htmlContent = velocityRenderEngine.render(templateContent, fillData);
// 5. 获取导出策略并执行导出
IExportStrategy exportStrategy = exportStrategyFactory
.createStrategy(request.getExportType());
Object result = exportStrategy.export(htmlContent, instance.getInstanceName());
log.info("渲染和导出完成模板实例ID: {}", request.getTemplateInstanceId());
return result;
}
/**
* 获取单个字段的值
* 优先级:调用方覆盖 > 默认值
*/
private Object getFieldValue(DocTemplateTagDO tag, TemplateRenderRequest request,
String templateName) {
String fieldName = tag.getTagCode();
// 判断是否有字段级的数据源覆盖
DocTemplateDataSourceType sourceType = null;
if (request.getFieldDataSources() != null) {
sourceType = request.getFieldDataSources().get(fieldName);
}
// 如果有覆盖,从指定的数据源获取值
if (sourceType != null) {
try {
IDataSourceProvider provider = dataSourceProviderFactory
.createProvider(
sourceType,
request.getDataSourceContext(),
templateName,
request.getBusinessType() != null ?
request.getBusinessType() : templateName
);
Map<String, Object> data = provider.getData(
request.getDataSourceContext(),
java.util.List.of(fieldName)
);
if (data.containsKey(fieldName)) {
Object value = data.get(fieldName);
if (value != null) {
log.debug("从数据源 {} 获取字段值: {} = {}", sourceType, fieldName, value);
return value;
}
}
} catch (Exception e) {
log.warn("从数据源获取字段值失败,使用默认值: {}, 数据源: {}", fieldName, sourceType, e);
}
}
// 使用默认值从标签的defaultValue字段获取
String defaultValue = extractDefaultValue(tag);
log.debug("使用标签默认值: {} = {}", fieldName, defaultValue);
return defaultValue;
}
/**
* 从标签定义中提取默认值
*/
private String extractDefaultValue(DocTemplateTagDO tag) {
if (tag.getDefaultValue() != null && !tag.getDefaultValue().trim().isEmpty()) {
return tag.getDefaultValue();
}
return "";
}
/**
* 获取模板实例关联的标签列表
* 通过实例的 categoryId 查询该分类下的所有标签定义
*/
private List<DocTemplateTagDO> getTemplateTags(DocTemplateInstanceRespVO instance) {
if (instance.getCategoryId() == null) {
log.warn("模板实例 {} 没有关联分类ID", instance.getId());
return List.of();
}
try {
// 通过 categoryId 查询该分类下的所有标签
List<DocTemplateTagDO> tags = docTemplateTagMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DocTemplateTagDO>()
.eq(DocTemplateTagDO::getCategoryId, instance.getCategoryId())
.eq(DocTemplateTagDO::getEnabled, "1") // 只查询启用的标签
.orderByAsc(DocTemplateTagDO::getSort)
);
if (tags == null || tags.isEmpty()) {
log.warn("分类ID {} 下没有找到标签定义", instance.getCategoryId());
return List.of();
}
log.debug("从分类ID {} 获取到 {} 个标签定义", instance.getCategoryId(), tags.size());
return tags;
} catch (Exception e) {
log.error("查询分类 {} 的标签列表失败", instance.getCategoryId(), e);
return List.of();
}
}
}

View File

@@ -0,0 +1,111 @@
package com.zt.plat.module.base.service.doctemplate.render;
import com.zt.plat.module.base.service.doctemplate.render.provider.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
/**
* 文档模板数据源提供者工厂(工厂模式)
*
* @author hwc
*/
@Component
@Slf4j
public class DocTemplateDataSourceProviderFactory {
@Resource
private ApplicationContext applicationContext;
@Resource
private ISqlConfigProvider sqlConfigProvider;
/**
* 创建数据源提供者
*
* @param sourceType 数据源类型
* @param context 数据源上下文含义由sourceType决定
* @param templateName 模板名称(用于查找业务数据提供者)
* @param businessType 业务类型当sourceType=BUSINESS时使用如"contract"、"order"等)
* @return 数据源提供者实例
*/
public IDataSourceProvider createProvider(
DocTemplateDataSourceType sourceType,
Object context,
String templateName,
String businessType) {
log.debug("创建数据源提供者,类型: {}, 业务类型: {}", sourceType, businessType);
switch (sourceType) {
case DEFAULT:
return new DocTemplateDefaultFieldDataSourceProvider();
case BUSINESS:
return createBusinessDataProvider(businessType);
case REQUEST:
// context 应该是 Map<String, Object> 类型的请求数据
return new DocTemplateRequestFieldDataSourceProvider((java.util.Map<String, Object>) context);
case SQL_CONFIG:
// context 应该是 sqlConfigId
return new DocTemplateSqlConfigFieldDataSourceProvider(sqlConfigProvider, (String) context);
default:
throw new IllegalArgumentException("不支持的数据源类型: " + sourceType);
}
}
/**
* 创建业务数据提供者
*
* @param businessType 业务类型
* @return 数据源提供者
*/
private IDataSourceProvider createBusinessDataProvider(String businessType) {
if (businessType == null || businessType.trim().isEmpty()) {
throw new IllegalArgumentException("使用BUSINESS数据源时businessType不能为空");
}
// 业务数据提供者的Bean名称约定{businessType}DataProvider
// 示例contractDataProvider、orderDataProvider、purchaseOrderDataProvider等
String providerBeanName = businessType + "DataProvider";
try {
IBusinessDataProvider businessDataProvider = applicationContext.getBean(
providerBeanName,
IBusinessDataProvider.class
);
log.debug("找到业务数据提供者: {}", providerBeanName);
return new DocTemplateBusinessFieldDataSourceProvider(businessDataProvider);
} catch (NoSuchBeanDefinitionException e) {
throw new IllegalArgumentException(
"未找到业务数据提供者: " + providerBeanName +
"(业务类型:" + businessType + "" +
"。请确保实现了 " + IBusinessDataProvider.class.getSimpleName() +
" 接口并注册为名为 '" + providerBeanName + "' 的Spring Bean。", e);
}
}
/**
* 重载方法不指定businessType时的调用
* 此时将使用templateName作为businessType向后兼容
*
* @param sourceType 数据源类型
* @param context 数据源上下文
* @param templateName 模板名称
* @return 数据源提供者实例
*/
public IDataSourceProvider createProvider(
DocTemplateDataSourceType sourceType,
Object context,
String templateName) {
return createProvider(sourceType, context, templateName, templateName);
}
}

View File

@@ -0,0 +1,38 @@
package com.zt.plat.module.base.service.doctemplate.render;
/**
* 数据源类型枚举
*
* @author hwc
*/
public enum DocTemplateDataSourceType {
/**
* 标签默认值
*/
DEFAULT("标签默认值"),
/**
* 业务策略
*/
BUSINESS("业务策略"),
/**
* 前端入参
*/
REQUEST("前端入参"),
/**
* SQL配置
*/
SQL_CONFIG("SQL配置");
private final String description;
DocTemplateDataSourceType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}

View File

@@ -0,0 +1,30 @@
package com.zt.plat.module.base.service.doctemplate.render;
import java.util.Map;
/**
* 业务数据提供者接口
* 不同的业务类型实现此接口来提供数据
*
* <p>命名约定:{业务类型}DataProvider
* <p>示例:
* <ul>
* <li>ContractDataProvider (合同数据提供者)</li>
* <li>OrderDataProvider (订单数据提供者)</li>
* <li>PurchaseOrderDataProvider (采购单数据提供者)</li>
* <li>SalesOrderDataProvider (销售单数据提供者)</li>
* </ul>
*
* @author hwc
*/
public interface IBusinessDataProvider {
/**
* 获取业务数据
*
* @param context 业务上下文通常是业务ID如contractId、orderId等
* @param fieldNames 需要填充的字段名列表
* @return 字段值映射表
*/
Map<String, Object> provideData(Object context, java.util.List<String> fieldNames);
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.base.service.doctemplate.render;
import java.util.Map;
/**
* 文档模板数据源提供者接口(策略模式)
* 所有数据源实现都遵循此接口
*
* @author hwc
*/
public interface IDataSourceProvider {
/**
* 获取指定字段的值
*
* @param context 上下文信息(内容因数据源类型而异)
* @param fieldNames 需要获取的字段名列表
* @return 字段值映射表
*/
Map<String, Object> getData(Object context, java.util.List<String> fieldNames);
/**
* 获取此数据源的类型
*
* @return 数据源类型
*/
DocTemplateDataSourceType getType();
}

View File

@@ -0,0 +1,20 @@
package com.zt.plat.module.base.service.doctemplate.render;
import java.util.List;
import java.util.Map;
/**
* SQL配置数据提供者接口
*
* @author hwc
*/
public interface ISqlConfigProvider {
/**
* 查询SQL配置结果
*
* @param sqlConfigId SQL配置ID
* @return 查询结果列表每个Map代表一行数据
*/
List<Map<String, Object>> querySqlConfig(String sqlConfigId);
}

View File

@@ -0,0 +1,20 @@
package com.zt.plat.module.base.service.doctemplate.render;
import java.util.Map;
/**
* Velocity 渲染引擎接口
*
* @author hwc
*/
public interface IVelocityRenderEngine {
/**
* 渲染模板内容
*
* @param templateContent 模板内容
* @param data 数据映射
* @return 渲染后的内容
*/
String render(String templateContent, Map<String, Object> data);
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.module.base.service.doctemplate.render;
import com.zt.plat.module.base.service.doctemplate.render.export.ExportType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TemplateRenderRequest {
private Long templateInstanceId;
private Map<String, DocTemplateDataSourceType> fieldDataSources;
private Object dataSourceContext;
private ExportType exportType = ExportType.WORD;
private String businessType;
}

View File

@@ -0,0 +1,99 @@
package com.zt.plat.module.base.service.doctemplate.render;
import lombok.extern.slf4j.Slf4j;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.io.StringWriter;
import java.util.Map;
import java.util.Properties;
/**
* Velocity 渲染引擎实现
*/
@Component
@Slf4j
public class VelocityRenderEngineImpl implements IVelocityRenderEngine {
private static volatile boolean velocityInitialized = false;
@PostConstruct
public synchronized void initVelocity() {
if (velocityInitialized) {
return;
}
try {
Properties props = new Properties();
props.setProperty("input.encoding", "UTF-8");
props.setProperty("output.encoding", "UTF-8");
props.setProperty("resource.loader", "string");
props.setProperty("string.resource.loader.class", StringResourceLoader.class.getName());
props.setProperty("runtime.log.logsystem.class",
"org.apache.velocity.runtime.log.NullLogChute");
props.setProperty("eventhandler.referenceinsertion.class",
"org.apache.velocity.app.event.implement.EscapeHtmlReference");
props.setProperty("eventhandler.escape.html.match", "true");
Velocity.init(props);
velocityInitialized = true;
log.info("Velocity 模板引擎初始化成功");
} catch (Exception e) {
log.error("Velocity 模板引擎初始化失败", e);
throw new RuntimeException("Velocity 模板引擎初始化失败", e);
}
}
@Override
public String render(String templateContent, Map<String, Object> data) {
if (templateContent == null || templateContent.trim().isEmpty()) {
log.warn("模板内容为空");
return "";
}
if (!velocityInitialized) {
initVelocity();
}
// 转换模板语法:{{variableName}} -> ${variableName}
// 支持Mustache/Handlebars风格的模板兼容Velocity语法
String velocityTemplate = convertToVelocitySyntax(templateContent);
VelocityContext context = new VelocityContext();
if (data != null && !data.isEmpty()) {
data.forEach(context::put);
}
try {
StringWriter writer = new StringWriter();
Velocity.evaluate(context, writer, "TemplateRender", velocityTemplate);
String result = writer.toString();
log.debug("模板渲染成功,数据量: {}, 内容长度: {}",
data != null ? data.size() : 0, result.length());
return result;
} catch (Exception e) {
log.error("模板渲染失败,模板长度: {}", templateContent.length(), e);
throw new RuntimeException("模板渲染失败: " + e.getMessage(), e);
}
}
/**
* 将 Mustache/Handlebars 语法 {{variableName}} 转换为 Velocity 语法 ${variableName}
*
* @param template 原始模板内容
* @return 转换后的 Velocity 语法模板
*/
private String convertToVelocitySyntax(String template) {
if (template == null) {
return null;
}
// 使用正则表达式替换 {{variable}} 为 ${variable}
// 匹配 {{ 和 }} 之间的内容(非贪婪模式)
return template.replaceAll("\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\}\\}", "\\${$1}");
}
}

View File

@@ -0,0 +1,79 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
import lombok.extern.slf4j.Slf4j;
/**
* 抽象导出策略(模板方法模式)
* 定义导出的标准流程框架
*
* @author hwc
*/
@Slf4j
public abstract class DocTemplateAbstractExportStrategy implements IExportStrategy {
/**
* 模板方法:定义导出流程骨架
*/
@Override
public final Object export(String htmlContent, String templateName) {
// 1. 验证输入
validateInput(htmlContent, templateName);
// 2. 生成文件名
String fileName = generateFileName(templateName);
// 3. 执行具体的导出逻辑
Object result = doExport(htmlContent, fileName);
// 4. 后处理(如日志、监控等)
postProcess(fileName, result);
return result;
}
/**
* 验证输入
*/
protected void validateInput(String htmlContent, String templateName) {
if (htmlContent == null || htmlContent.isEmpty()) {
throw new IllegalArgumentException("HTML内容为空");
}
if (templateName == null || templateName.isEmpty()) {
throw new IllegalArgumentException("模板名称为空");
}
}
/**
* 生成文件名
*/
protected String generateFileName(String templateName) {
return templateName + "_" + System.currentTimeMillis() + getFileExtension();
}
/**
* 具体的导出逻辑(由子类实现)
*
* @param htmlContent HTML内容
* @param fileName 文件名
* @return 导出结果
*/
protected abstract Object doExport(String htmlContent, String fileName);
/**
* 获取文件扩展名
*
* @return 文件扩展名
*/
protected abstract String getFileExtension();
/**
* 后处理逻辑(可选,子类可覆盖)
*
* @param fileName 文件名
* @param result 导出结果
*/
protected void postProcess(String fileName, Object result) {
// 默认实现:记录日志
log.info("导出完成: {}, 类型: {}", fileName, getExportType().getDescription());
}
}

View File

@@ -0,0 +1,58 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
/**
* 文档模板导出策略工厂(工厂模式)
*
* @author hwc
*/
@Component
@Slf4j
public class DocTemplateExportStrategyFactory {
@Resource(name = "docTemplateWordExportTemplateStrategy")
private IExportStrategy wordExportStrategy;
@Resource(name = "docTemplateHtmlPreviewTemplateStrategy")
private IExportStrategy htmlPreviewStrategy;
// TODO: 添加其他导出策略的注入
// @Resource(name = "docTemplateExcelExportTemplateStrategy")
// private IExportStrategy excelExportStrategy;
//
// @Resource(name = "docTemplatePdfExportTemplateStrategy")
// private IExportStrategy pdfExportStrategy;
/**
* 创建导出方式策略
*
* @param exportType 导出类型
* @return 导出策略实例
*/
public IExportStrategy createStrategy(ExportType exportType) {
log.debug("创建导出策略,类型: {}", exportType);
switch (exportType) {
case WORD:
return wordExportStrategy;
case EXCEL:
// TODO: 实现Excel导出策略
throw new UnsupportedOperationException("Excel导出功能尚未实现");
case PDF:
// TODO: 实现PDF导出策略
throw new UnsupportedOperationException("PDF导出功能尚未实现");
case HTML_PREVIEW:
return htmlPreviewStrategy;
default:
throw new IllegalArgumentException("不支持的导出方式: " + exportType);
}
}
}

View File

@@ -0,0 +1,38 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
/**
* 导出方式枚举
*
* @author hwc
*/
public enum ExportType {
/**
* Word文档
*/
WORD("Word"),
/**
* Excel表格
*/
EXCEL("Excel"),
/**
* PDF文档
*/
PDF("PDF"),
/**
* HTML预览
*/
HTML_PREVIEW("HTML预览");
private final String description;
ExportType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}

View File

@@ -0,0 +1,36 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
import lombok.*;
/**
* HTML预览结果
*
* @author hwc
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class HtmlPreviewResult {
/**
* HTML内容
*/
private String htmlContent;
/**
* 文件名
*/
private String fileName;
/**
* 内容类型
*/
@Builder.Default
private String contentType = "text/html";
@Override
public String toString() {
return htmlContent;
}
}

View File

@@ -0,0 +1,39 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* HTML预览策略实现
* 直接返回HTML内容用于前端预览
*
* @author hwc
*/
@Component("docTemplateHtmlPreviewTemplateStrategy")
@Slf4j
public class HtmlPreviewTemplateStrategyDocTemplate extends DocTemplateAbstractExportStrategy {
@Override
protected Object doExport(String htmlContent, String fileName) {
log.debug("HTML预览内容长度: {}", htmlContent.length());
// 直接返回HTML内容
HtmlPreviewResult result = HtmlPreviewResult.builder()
.htmlContent(htmlContent)
.fileName(fileName)
.build();
log.debug("HTML预览完成");
return result;
}
@Override
protected String getFileExtension() {
return ".html";
}
@Override
public ExportType getExportType() {
return ExportType.HTML_PREVIEW;
}
}

View File

@@ -0,0 +1,26 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
/**
* 导出方式策略接口(策略模式)
* 定义所有导出方式的标准接口
*
* @author hwc
*/
public interface IExportStrategy {
/**
* 执行导出
*
* @param htmlContent 渲染后的HTML内容
* @param templateName 模板名称(用于生成文件名)
* @return 导出结果
*/
Object export(String htmlContent, String templateName);
/**
* 获取导出方式类型
*
* @return 导出类型
*/
ExportType getExportType();
}

View File

@@ -0,0 +1,48 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
import lombok.*;
import java.io.InputStream;
/**
* Word 导出结果
*
* @author hwc
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "fileContent")
public class WordExportResult {
/**
* 文件路径(如果保存到服务器)
*/
private String filePath;
/**
* 文件流(如果直接返回)
*/
private InputStream fileStream;
/**
* 文件内容(字节数组)
*/
private byte[] fileContent;
/**
* 文件名
*/
private String fileName;
/**
* 文件大小(字节)
*/
private Long fileSize;
/**
* 内容类型
*/
@Builder.Default
private String contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
}

View File

@@ -0,0 +1,68 @@
package com.zt.plat.module.base.service.doctemplate.render.export;
import com.zt.plat.module.base.service.doctemplate.DocumentRenderApiService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.zt.plat.module.base.enums.ErrorCodeConstants.*;
/**
* Word 导出策略实现
* 基于现有的 DocumentRenderApiService 进行扩展
*
* @author hwc
*/
@Component("docTemplateWordExportTemplateStrategy")
@Slf4j
public class WordExportTemplateStrategyDocTemplate extends DocTemplateAbstractExportStrategy {
@Resource
private DocumentRenderApiService documentRenderApiService;
@Override
protected Object doExport(String htmlContent, String fileName) {
try {
log.debug("开始导出Word文档文件名: {}", fileName);
// 调用 DocumentRenderApiService 将HTML转换为Word
byte[] wordContent = documentRenderApiService.exportToWord(htmlContent, fileName);
// 构建导出结果
WordExportResult result = WordExportResult.builder()
.fileName(fileName + getFileExtension())
.fileContent(wordContent)
.fileSize((long) wordContent.length)
.contentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
.build();
log.info("Word导出完成文件名: {}, 大小: {} bytes", fileName, wordContent.length);
return result;
} catch (Exception e) {
log.error("Word导出失败文件名: {}", fileName, e);
throw exception(TEMPLATE_EXPORT_FAILED);
}
}
@Override
protected String getFileExtension() {
return ".docx";
}
@Override
public ExportType getExportType() {
return ExportType.WORD;
}
@Override
protected void postProcess(String fileName, Object result) {
// Word导出特定的后处理逻辑
if (result instanceof WordExportResult) {
WordExportResult wordResult = (WordExportResult) result;
log.info("Word导出后处理完成: {}, 文件大小: {}字节", fileName, wordResult.getFileSize());
}
}
}

View File

@@ -0,0 +1,90 @@
package com.zt.plat.module.base.service.doctemplate.render.provider;
import com.zt.plat.module.base.service.doctemplate.render.IBusinessDataProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 合同数据提供者
* 提供合同相关的业务数据
*
* @author hwc
*/
@Component("contractDataProvider")
@Slf4j
public class ContractDataProvider implements IBusinessDataProvider {
@Override
public Map<String, Object> provideData(Object context, List<String> fieldNames) {
Long contractId = null;
if (context instanceof Long) {
contractId = (Long) context;
} else if (context instanceof String) {
try {
contractId = Long.valueOf((String) context);
} catch (NumberFormatException e) {
log.warn("无效的合同ID格式: {}", context);
return Map.of();
}
}
if (contractId == null) {
log.warn("合同ID为空");
return Map.of();
}
log.debug("获取合同数据合同ID: {}, 请求字段: {}", contractId, fieldNames);
// TODO: 实际实现应该:
// 1. 根据合同ID从数据库或服务中查询合同信息
// 2. 根据请求的字段名返回对应的数据
// 3. 处理数据类型转换
// 临时实现:返回一些示例数据
Map<String, Object> data = new HashMap<>();
// 模拟合同数据
for (String fieldName : fieldNames) {
switch (fieldName) {
case "contractName":
case "contract_name":
data.put(fieldName, "中铜采购合同-" + contractId);
break;
case "supplier":
case "supplier_name":
data.put(fieldName, "上海金属贸易有限公司");
break;
case "amount":
case "total_amount":
data.put(fieldName, 5000000.00);
break;
case "deliveryDate":
case "delivery_date":
data.put(fieldName, "2025-06-30");
break;
case "contractNumber":
case "contract_number":
data.put(fieldName, "HT-2025-" + String.format("%04d", contractId % 10000));
break;
case "signDate":
case "sign_date":
data.put(fieldName, "2025-01-15");
break;
case "paymentTerms":
case "payment_terms":
data.put(fieldName, "预付30%发货付60%验收付10%");
break;
default:
log.debug("未处理的合同字段: {}", fieldName);
break;
}
}
log.debug("返回合同数据: {}", data.keySet());
return data;
}
}

View File

@@ -0,0 +1,49 @@
package com.zt.plat.module.base.service.doctemplate.render.provider;
import com.zt.plat.module.base.service.doctemplate.render.DocTemplateDataSourceType;
import com.zt.plat.module.base.service.doctemplate.render.IBusinessDataProvider;
import com.zt.plat.module.base.service.doctemplate.render.IDataSourceProvider;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* 业务数据源提供者实现
* 包装业务数据提供者,提供统一的数据源接口
*
* @author hwc
*/
@Slf4j
public class DocTemplateBusinessFieldDataSourceProvider implements IDataSourceProvider {
private final IBusinessDataProvider businessDataProvider;
public DocTemplateBusinessFieldDataSourceProvider(IBusinessDataProvider businessDataProvider) {
this.businessDataProvider = businessDataProvider;
log.debug("创建业务数据源提供者: {}", businessDataProvider.getClass().getSimpleName());
}
@Override
public Map<String, Object> getData(Object context, List<String> fieldNames) {
if (businessDataProvider == null) {
log.warn("业务数据提供者为空");
return Map.of();
}
try {
log.debug("调用业务数据提供者获取数据,上下文: {}, 字段数量: {}", context, fieldNames.size());
Map<String, Object> data = businessDataProvider.provideData(context, fieldNames);
log.debug("业务数据提供者返回数据: {}", data.keySet());
return data;
} catch (Exception e) {
log.error("业务数据提供者获取数据失败", e);
return Map.of();
}
}
@Override
public DocTemplateDataSourceType getType() {
return DocTemplateDataSourceType.BUSINESS;
}
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.module.base.service.doctemplate.render.provider;
import com.zt.plat.module.base.service.doctemplate.render.DocTemplateDataSourceType;
import com.zt.plat.module.base.service.doctemplate.render.IDataSourceProvider;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 标签默认值数据源实现
* 返回空数据,表示使用字段自身的默认值
*
* @author hwc
*/
@Slf4j
public class DocTemplateDefaultFieldDataSourceProvider implements IDataSourceProvider {
@Override
public Map<String, Object> getData(Object context, List<String> fieldNames) {
// 默认值由调用方在外层处理,这里返回空,表示不提供覆盖数据
log.debug("默认值数据源被调用,字段数量: {}", fieldNames != null ? fieldNames.size() : 0);
return new HashMap<>();
}
@Override
public DocTemplateDataSourceType getType() {
return DocTemplateDataSourceType.DEFAULT;
}
}

View File

@@ -0,0 +1,52 @@
package com.zt.plat.module.base.service.doctemplate.render.provider;
import com.zt.plat.module.base.service.doctemplate.render.DocTemplateDataSourceType;
import com.zt.plat.module.base.service.doctemplate.render.IDataSourceProvider;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 前端入参数据源提供者实现
* 从传入的请求数据中提取字段值
*
* @author hwc
*/
@Slf4j
public class DocTemplateRequestFieldDataSourceProvider implements IDataSourceProvider {
private final Map<String, Object> requestData;
public DocTemplateRequestFieldDataSourceProvider(Map<String, Object> requestData) {
this.requestData = requestData != null ? requestData : Map.of();
log.debug("创建前端入参数据源提供者,数据量: {}", this.requestData.size());
}
@Override
public Map<String, Object> getData(Object context, List<String> fieldNames) {
Map<String, Object> result = new HashMap<>();
if (requestData == null || requestData.isEmpty()) {
log.debug("前端入参数据为空");
return result;
}
for (String fieldName : fieldNames) {
if (requestData.containsKey(fieldName)) {
Object value = requestData.get(fieldName);
result.put(fieldName, value);
log.debug("从前端入参获取字段值: {} = {}", fieldName, value);
}
}
log.debug("前端入参数据源提供结果: {}", result.keySet());
return result;
}
@Override
public DocTemplateDataSourceType getType() {
return DocTemplateDataSourceType.REQUEST;
}
}

View File

@@ -0,0 +1,60 @@
package com.zt.plat.module.base.service.doctemplate.render.provider;
import com.zt.plat.module.base.service.doctemplate.render.DocTemplateDataSourceType;
import com.zt.plat.module.base.service.doctemplate.render.IDataSourceProvider;
import com.zt.plat.module.base.service.doctemplate.render.ISqlConfigProvider;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* SQL配置数据源提供者实现
* 执行SQL配置查询并返回结果
*
* @author hwc
*/
@Slf4j
public class DocTemplateSqlConfigFieldDataSourceProvider implements IDataSourceProvider {
private final ISqlConfigProvider sqlConfigProvider;
private final String sqlConfigId;
public DocTemplateSqlConfigFieldDataSourceProvider(ISqlConfigProvider sqlConfigProvider, String sqlConfigId) {
this.sqlConfigProvider = sqlConfigProvider;
this.sqlConfigId = sqlConfigId;
log.debug("创建SQL配置数据源提供者: {}", sqlConfigId);
}
@Override
public Map<String, Object> getData(Object context, List<String> fieldNames) {
if (sqlConfigProvider == null) {
log.warn("SQL配置提供者为空");
return Map.of();
}
try {
log.debug("执行SQL配置查询: {}", sqlConfigId);
List<Map<String, Object>> results = sqlConfigProvider.querySqlConfig(sqlConfigId);
if (results == null || results.isEmpty()) {
log.debug("SQL配置查询无结果");
return Map.of();
}
// 返回第一行结果通常SQL配置查询应该返回单行
Map<String, Object> result = results.get(0);
log.debug("SQL配置查询结果字段: {}", result.keySet());
return result;
} catch (Exception e) {
log.error("SQL配置查询失败: {}", sqlConfigId, e);
return Map.of();
}
}
@Override
public DocTemplateDataSourceType getType() {
return DocTemplateDataSourceType.SQL_CONFIG;
}
}

View File

@@ -0,0 +1,90 @@
package com.zt.plat.module.base.service.doctemplate.render.provider;
import com.zt.plat.module.base.service.doctemplate.render.IBusinessDataProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 订单数据提供者
* 提供订单相关的业务数据
*
* @author hwc
*/
@Component("orderDataProvider")
@Slf4j
public class OrderDataProvider implements IBusinessDataProvider {
@Override
public Map<String, Object> provideData(Object context, List<String> fieldNames) {
Long orderId = null;
if (context instanceof Long) {
orderId = (Long) context;
} else if (context instanceof String) {
try {
orderId = Long.valueOf((String) context);
} catch (NumberFormatException e) {
log.warn("无效的订单ID格式: {}", context);
return Map.of();
}
}
if (orderId == null) {
log.warn("订单ID为空");
return Map.of();
}
log.debug("获取订单数据订单ID: {}, 请求字段: {}", orderId, fieldNames);
// TODO: 实际实现应该:
// 1. 根据订单ID从数据库或服务中查询订单信息
// 2. 根据请求的字段名返回对应的数据
// 3. 处理数据类型转换
// 临时实现:返回一些示例数据
Map<String, Object> data = new HashMap<>();
// 模拟订单数据
for (String fieldName : fieldNames) {
switch (fieldName) {
case "orderNumber":
case "order_number":
data.put(fieldName, "ORD-2025-" + String.format("%06d", orderId % 100000));
break;
case "customer":
case "customer_name":
data.put(fieldName, "中铜制造有限公司");
break;
case "totalAmount":
case "total_amount":
data.put(fieldName, 2500000.00);
break;
case "orderDate":
case "order_date":
data.put(fieldName, "2025-01-20");
break;
case "deliveryAddress":
case "delivery_address":
data.put(fieldName, "上海市浦东新区金桥路123号");
break;
case "contactPerson":
case "contact_person":
data.put(fieldName, "张经理");
break;
case "contactPhone":
case "contact_phone":
data.put(fieldName, "13800138000");
break;
default:
log.debug("未处理的订单字段: {}", fieldName);
break;
}
}
log.debug("返回订单数据: {}", data.keySet());
return data;
}
}