news 2026/7/30 6:33:34

SpringBoot中实现生成文档-上传文件-调用外部平台创建合同的通用流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot中实现生成文档-上传文件-调用外部平台创建合同的通用流程

SpringBoot中实现生成文档-上传文件-调用外部平台创建合同的通用流程

一、这个流程在做什么

用一句话概括:将本地的业务数据,渲染成一份正式的PDF文档,上传到文件服务器获取公网链接,然后把这个链接提交给外部签约平台去创建一份电子合同。

类比现实世界:

你写好一份合同 → 打印出来 → 放到共享柜台上 → 把柜台号告诉对方说"去那里签字"

对应到技术流程:

组装数据 → 渲染PDF → 上传OSS获取URL → 调用签约平台API(传入URL) → 平台创建合同


二、为什么要这样设计(而不是直接传PDF内容)

问题:PDF文件可能有几百KB甚至几MB 方案A(不好):把PDF的Base64直接塞到HTTP请求体里发给签约平台 → 请求体太大,容易超时、网关限制 方案B(推荐):把PDF上传到OSS,只传一个URL给签约平台 → 签约平台自己去URL下载,解耦且高效

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

三、通用示例:生成发票PDF → 上传 → 提交到电子发票平台

场景假设

一个电商系统,用户点击"开具发票"后:

  1. 从数据库查出订单和商品数据
  2. 用模板渲染出一份发票PDF
  3. 上传PDF到文件服务器(OSS)
  4. 调用电子发票平台接口,传入PDF的URL,创建一张电子发票
  5. 发票平台返回发票编号,本地保存

步骤1:组装渲染数据

把数据库中分散的数据整合成模板需要的结构:

// 发票渲染所需的数据结构@DatapublicclassInvoiceRenderData{privateStringinvoiceNo;// 发票号privateStringbuyerName;// 购方名称privateStringsellerName;// 销方名称privateBigDecimaltotalAmount;// 总金额privateStringissueDate;// 开票日期privateList<InvoiceItem>items;// 商品明细行}@DatapublicclassInvoiceItem{privateStringproductName;// 商品名privateIntegerquantity;// 数量privateBigDecimalunitPrice;// 单价privateBigDecimalamount;// 金额}

组装逻辑:

// 从数据库查出原始数据Orderorder=orderRepository.findById(orderId);List<OrderItem>orderItems=orderItemRepository.findByOrderId(orderId);Customercustomer=customerRepository.findById(order.getCustomerId());// 转换为渲染数据结构InvoiceRenderDatarenderData=newInvoiceRenderData();renderData.setInvoiceNo(generateInvoiceNo());renderData.setBuyerName(customer.getName());renderData.setSellerName("XX科技有限公司");renderData.setTotalAmount(order.getTotalAmount());renderData.setIssueDate(LocalDate.now().toString());List<InvoiceItem>items=orderItems.stream().map(oi->{InvoiceItemitem=newInvoiceItem();item.setProductName(oi.getProductName());item.setQuantity(oi.getQuantity());item.setUnitPrice(oi.getUnitPrice());item.setAmount(oi.getUnitPrice().multiply(BigDecimal.valueOf(oi.getQuantity())));returnitem;}).collect(Collectors.toList());renderData.setItems(items);

步骤2:用模板引擎渲染PDF

模板引擎的作用:固定的排版格式 + 动态的业务数据 = 最终的PDF文件

/** * 使用JasperReports将数据渲染为PDF. * * @param renderData 业务数据 * @return PDF的字节数组 */publicbyte[]renderPdf(InvoiceRenderDatarenderData){// 1. 加载预编译的模板文件(.jasper 是编译后的二进制模板)// 模板文件定义了页面布局、表格结构、字体、页眉页脚等ClassPathResourcetemplate=newClassPathResource("templates/invoice.jasper");// 2. 设置模板参数(全局变量,如公司Logo路径、子报表路径等)Map<String,Object>parameters=newHashMap<>();parameters.put("COMPANY_LOGO","templates/logo.png");// 3. 将Java对象集合转换为报表数据源// JasperReports 会遍历这个数据源,每条数据生成一行JRDataSourcedataSource=newJRBeanCollectionDataSource(Collections.singletonList(renderData));// 4. 填充模板:模板 + 参数 + 数据 → 内存中的报表对象JasperPrintjasperPrint=JasperFillManager.fillReport(template.getInputStream(),parameters,dataSource);// 5. 导出为PDF字节数组ByteArrayOutputStreamout=newByteArrayOutputStream();JRPdfExporterexporter=newJRPdfExporter();exporter.setExporterInput(newSimpleExporterInput(jasperPrint));exporter.setExporterOutput(newSimpleOutputStreamExporterOutput(out));exporter.exportReport();returnout.toByteArray();}

类比理解

  • .jasper模板文件 = Word模板(定义了样式和占位符)
  • renderData= 要填入的数据
  • JasperFillManager.fillReport()= 邮件合并
  • exportReport()= 另存为PDF

步骤3:上传PDF到文件服务器

/** * 上传文件到OSS,返回公网可访问的URL. * * @param pdfBytes 文件内容 * @return 文件的公网URL */publicStringuploadToOss(byte[]pdfBytes){// 将字节数组包装为输入流ByteArrayInputStreaminputStream=newByteArrayInputStream(pdfBytes);// 调用OSS客户端上传// 内部会:生成唯一文件名 → 上传到OSS Bucket → 返回文件路径UploadResultresult=ossClient.uploadFile(inputStream,"pdf");if(result==null||result.getFilePath()==null){thrownewRuntimeException("文件上传失败");}// 拼接完整URL:域名 + 文件路径// 例如:https://cdn.example.com/files/2026/07/abc123.pdfStringfullUrl=ossBaseUrl+"/"+result.getFilePath();returnfullUrl;}

上传后为什么要返回URL:签约平台不在我们的内网,它需要一个公网可访问的地址去下载这个PDF文件。

步骤4:组装签约平台参数

/** * 组装调用签约平台"创建合同"接口的参数. */publicContractCreateParambuildContractParam(StringpdfUrl,InvoiceRenderDatarenderData){ContractCreateParamparam=newContractCreateParam();// 合同基本信息param.setContractName("电子发票-"+renderData.getInvoiceNo());param.setFileUrl(pdfUrl);// PDF的公网链接(关键!)param.setFileName("发票.pdf");// 签署方A:卖方(我方,先盖章)ContractSignersellerSigner=newContractSigner();sellerSigner.setRole(1);// 甲方sellerSigner.setName(renderData.getSellerName());sellerSigner.setSignPosition("销方签章");// 在PDF中寻找这个关键字定位盖章位置sellerSigner.setSignOrder(1);// 第1个签// 签署方B:买方(客户,后签)ContractSignerbuyerSigner=newContractSigner();buyerSigner.setRole(2);// 乙方buyerSigner.setName(renderData.getBuyerName());buyerSigner.setSignPosition("购方签章");buyerSigner.setSignOrder(2);// 第2个签param.setSigners(Arrays.asList(sellerSigner,buyerSigner));param.setSignOrderly(true);// 顺序签署(A签完B才能签)returnparam;}

签章位置定位原理:PDF中写了"销方签章"这几个字,签约平台解析PDF找到这几个字的坐标,在旁边放上电子印章图片。

步骤5:调用签约平台创建合同

/** * 调用签约平台HTTP接口创建合同. * * @param param 创建参数 * @return 合同ID(签约平台分配) */publicIntegercreateContractOnPlatform(ContractCreateParamparam){// HTTP POST 调用签约平台HttpResponseresponse=httpClient.post("https://sign-platform.example.com/api/contract/create",JsonUtil.toJson(param));PlatformResultresult=JsonUtil.fromJson(response.getBody(),PlatformResult.class);if(result.isSuccess()){returnresult.getContractId();// 签约平台返回的合同ID}else{thrownewRuntimeException("创建合同失败: "+result.getErrorMsg());}}

步骤6:整合——完整流程串联

@ServicepublicclassInvoiceServiceImpl{/** * 完整流程:阶段1(事务内,同步). * 准备数据 + 生成PDF + 上传 + 组装参数 + 注册事务后回调. */@Transactional(rollbackFor=Exception.class)publicvoidissueInvoice(IntegerorderId){// ======== 1. 组装数据 ========InvoiceRenderDatarenderData=this.buildRenderData(orderId);// ======== 2. 渲染PDF ========byte[]pdfBytes=this.renderPdf(renderData);// ======== 3. 上传到OSS ========StringpdfUrl=this.uploadToOss(pdfBytes);// ======== 4. 组装签约平台参数 ========ContractCreateParamcontractParam=this.buildContractParam(pdfUrl,renderData);// ======== 5. 本地写库(状态改为"处理中") ========Invoiceinvoice=newInvoice();invoice.setOrderId(orderId);invoice.setInvoiceNo(renderData.getInvoiceNo());invoice.setStatus(InvoiceStatus.CREATING);// 创建中invoice.setPdfUrl(pdfUrl);invoiceRepository.save(invoice);// ======== 6. 注册事务后回调 → 发MQ ========AfterCommitActionCollectorcollector=newAfterCommitActionCollector();TransactionSynchronizationManager.registerSynchronization(collector);collector.addAction(()->invoiceCreateMqSender.send(contractParam,invoice.getId()));}/** * 完整流程:阶段2(MQ消费后,独立事务). * 调用签约平台 + 更新本地状态. */@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidcallSignPlatform(ContractCreateParamparam,IntegerinvoiceId){Invoiceinvoice=invoiceRepository.findById(invoiceId).orElse(null);if(invoice==null)return;try{// 调用签约平台IntegercontractId=this.createContractOnPlatform(param);// 成功:保存合同ID,状态改为"待签署"invoice.setContractId(contractId);invoice.setStatus(InvoiceStatus.WAITING_SIGN);}catch(Exceptione){// 失败:状态回退invoice.setStatus(InvoiceStatus.CREATE_FAILED);log.warn("创建合同失败",e);}invoiceRepository.save(invoice);}}

四、各步骤之间的依赖关系

组装数据 ──→ 渲染PDF ──→ 上传OSS ──→ 组装平台参数 ──→ 发MQ ──→ 调用平台 │ │ │ │ │ │ │ │ │ │ │ │ 需要数据库 需要步骤1 需要步骤2 需要步骤3的URL 需要步骤4 需要步骤5 查询的数据 的数据 的字节数组 作为参数字段 的参数 的消息

每一步的输出是下一步的输入,形成流水线:

步骤输入输出可能失败的原因
组装数据数据库记录结构化DTO数据不存在、字段为空
渲染PDFDTO + 模板byte[]模板文件损坏、数据格式不匹配
上传OSSbyte[]URL字符串网络超时、OSS服务不可用
组装平台参数URL + 业务数据请求参数DTO纯内存操作,几乎不会失败
调用平台请求参数合同ID网络超时、平台校验不通过

五、为什么"渲染PDF + 上传OSS"放在事务内

你可能有疑问:PDF生成和OSS上传不是数据库操作,为什么放在事务里?

原因:它们是"组装MQ消息参数"的前置步骤 如果放在事务外: 事务内写库(状态=处理中) → 提交 → 生成PDF → 上传失败! → 数据库已经是"处理中"了,但没有PDF也没有发MQ → 系统卡在中间状态 放在事务内: 事务内:写库 + 生成PDF + 上传OSS → 任何一步失败都回滚 → 要么全部准备就绪然后提交,要么全部回滚当作什么都没发生 代价:事务持有时间稍长(多了PDF生成和OSS上传的时间) 收益:数据一致性有保障

但注意:调用签约平台这步没有放在同一个事务内,因为它耗时更长且有重试需求,所以通过MQ异步处理。


六、状态流转

┌─────────────────────────────────────────────────────────────┐ │ 事务内(同步) │ │ │ │ 初始状态 事务提交时状态 │ │ DRAFT ──[生成PDF+上传+写库]──→ CREATING │ │ │ └───────────────────────────────┬─────────────────────────────┘ │ (事务提交后发MQ) ▼ ┌─────────────────────────────────────────────────────────────┐ │ MQ消费后(异步) │ │ │ │ CREATING ──[调用平台成功]──→ WAITING_SIGN (待签署) │ │ │ │ │ └──[调用平台失败]──→ CREATE_FAILED (创建失败,可重试) │ │ │ └─────────────────────────────────────────────────────────────┘

七、总结:这个流程的本质

这个流程本质上是一个文档生成 + 外部系统注册的通用模式:

本地数据 → 渲染成文件 → 上传到公共存储 → 把文件地址告诉外部系统 → 外部系统据此创建任务

现实中的其他场景也是相同模式:

场景渲染上传外部系统
月对账单签章JasperReports生成对账PDF阿里云OSS签约中台
电子合同签署模板填充生成合同PDF文件服务器电子签章平台
报关单申报生成报关单PDF/ExcelFTP/OSS海关系统
营销邮件HTML模板渲染CDN邮件发送服务
电子发票发票版式文件生成税务文件服务器税务平台

核心步骤永远是:组装数据 → 生成文件 → 存到可访问的地方 → 告诉外部系统去哪里取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/30 6:29:53

Unity VR投掷游戏开发实战:SteamVR集成与桌面模拟模式详解

1. 项目概述与核心价值最近在整理过往项目资料时&#xff0c;翻出了一个基于Unity 2021 LTS开发的VR投掷小游戏完整源码包。这个项目麻雀虽小&#xff0c;五脏俱全&#xff0c;它不仅完整实现了核心的投掷玩法&#xff0c;更关键的是&#xff0c;它从一开始就设计为支持SteamVR…

作者头像 李华
网站建设 2026/7/30 6:26:04

Vue3+Python构建教育成绩分析系统实战

1. 项目概述&#xff1a;双体综合考试成绩分析系统这个项目是一个典型的"前后端分离数据分析"的教育信息化解决方案。前端采用Vue3构建响应式管理界面&#xff0c;后端使用Python处理成绩数据的统计分析&#xff0c;最终通过可视化图表呈现考试结果。系统名称中的&qu…

作者头像 李华
网站建设 2026/7/30 6:25:58

Spring框架核心原理与实战应用全解析

1. Spring框架全景透视 Spring框架作为Java企业级开发的基石&#xff0c;已经走过了近二十年的演进历程。从最初的轻量级IoC容器发展到如今涵盖云原生、响应式编程、AI集成的全栈生态&#xff0c;其核心设计理念始终保持着惊人的一致性。让我们先看一组关键数据&#xff1a;根据…

作者头像 李华
网站建设 2026/7/30 6:25:23

Arduino TFT彩屏驱动与交互开发全攻略:从硬件选型到实战项目

1. 项目概述&#xff1a;为什么Arduino玩家需要一块TFT彩屏&#xff1f; 如果你玩Arduino有一段时间了&#xff0c;点亮过LED&#xff0c;驱动过舵机&#xff0c;也通过串口监视器看过数据&#xff0c;那你可能会开始觉得&#xff0c;那个黑白的、只能显示文本的串口窗口有点“…

作者头像 李华
网站建设 2026/7/30 6:24:57

Go语言跨平台开发实战:从原理到部署

1. 为什么Go语言天生适合跨平台开发 十年前我第一次尝试将一个Windows服务移植到Linux时&#xff0c;整整花了三周时间处理各种平台差异。如今用Go重写同样的功能&#xff0c;从Windows编译到Linux只需要在命令行加个参数。这种体验让我深刻理解了Go语言设计者对跨平台开发的深…

作者头像 李华
网站建设 2026/7/30 6:12:56

Python字典数据格式化:从json.dumps到rich库的完整实践指南

1. 从“一团乱麻”到“赏心悦目”&#xff1a;为什么我们需要格式化字典在Python的日常开发中&#xff0c;字典&#xff08;dict&#xff09;几乎无处不在。无论是从API接口获取的JSON数据&#xff0c;还是数据库查询返回的记录集&#xff0c;亦或是我们自己构建的配置信息&…

作者头像 李华