2. 电子合同的业务本质与多端交互设计在拆解这套Java电子合同源码之前先聊两句业务层面的事。电子合同本质上是把线下面对面签字盖章这个动作搬到线上同时保证法律效力。它不只是做个PDF、画个签名图片那么简单核心要解决的是怎么证明签名的人是他本人、签名动作是他真实意愿、签完之后内容没被篡改。这个需求放到Java技术栈下整体方案通常是这样后端统一提供合同模板管理、签署流程编排、签名计算、存证归档能力前端按场景拆分成小程序、公众号H5、APP、PC浏览器四个入口。不同端解决的是不同用户的签署习惯——ToB企业采购人员习惯在PC上操作销售外出时用小程序签C端用户在公众号通知里点链接就签了APP端则适配需要离线或强实名场景。四个端共享同一套后端接口和数据库这是多端项目能控住成本的关键。1.1 这套源码到底解决什么问题从用户视角看一个电子合同系统至少要覆盖三段流程发合同、签合同、管合同。发合同指的是创建模板、填写甲乙双方信息、生成待签署文档签合同指的是发起方指定签署位置、接收方完成身份认证和签名动作管合同指的是签署完成后自动归档、生成签署证书、支持验真和下载。往技术底层拆每一个流程背后都有对应实现。发合同阶段系统要做模板解析和字段替换签合同阶段要做摘要计算、时间戳请求、数字签名管合同阶段要做PDF合并、存证数据上链、证据文件包生成。这套源码就是在这些环节上做了工程化落地你可以直接二次开发改造成自己的产品也可以把它当作一套参考实现理解每个环节的实现手法。1.2 为什么选Java技术栈做电子合同客观说市面上电子合同SaaS产品很多但自己用Java从零搭一套仍有意义一方面是对接企业内部系统更方便Java生态里的Spring Boot、MyBatis Plus、Flowable工作流引擎都是现成的跟OA、ERP、CRM对接时能省掉大量适配成本另一方面是Java在服务端稳定性、事务处理、高并发支撑上确实成熟签署高峰期的性能和丢单问题可控。这套系统里有几个基础点会反复用到。MyBatis Plus负责数据持久层的快速落地尤其它根据Java实体类生成建表SQL的能力能在设计阶段把字段名、类型、索引一次对齐省掉大量手写DDL的时间。加密这块用的是Java标准库里的JCA体系SHA-256做摘要、RSA或国密SM2做签名不需要额外引入重量级密码学框架。1.3 多端项目的整体架构设计一个支撑小程序公众号APPH5的电子合同系统后端架构不能拆得太碎。我的建议是初期用单应用多模块的结构公共模块放工具类、常量、统一返回体用户模块管实名认证和登录态合同模块管模板、签署流程、文件存储存证模块管时间戳、哈希上链、证据包。用户端状态管理上四端共用后端签发JWT令牌。小程序通过wx.login拿到code换openid再换令牌公众号通过OAuth2.0拿codeAPP通过账号密码或手机号验证码PC浏览器走账号密码。这个设计的好处是同一个用户可以多端无缝切换——上午在PC上草拟了合同下午在路上用小程序的待办列表里找到它继续签后端不需要重新维护多套身份体系。一个容易被忽略的设计点四端的文件访问地址要统一走后端代理或OSS的私有读URL不能直接把存储桶开放成公共读。合同文件涉及敏感信息一旦公共读出了漏洞等于是把合同内容裸奔在公网上这个坑在早期设计时就要规避。3. 电子签名的核心原理与落地实现电子签名这块是整个系统的技术高地也是很多开发者最摸不着头脑的部分。直接说结论电子签名不是把签名图片贴上去那么简单。一个具备法律效力的电子签名必须满足《电子签名法》里可靠电子签名的三个条件签名人身份真实、签名动作代表本人意愿、签署后任何改动都可被发现。2.1 可靠电子签名的法律与技术三要素展开说这三个条件对应的技术方案。身份真实对应的是实名认证个人用户做身份证OCR人脸识别企业用户做营业执照核验法人身份确认意愿真实对应的是意愿确认常见做法是短信验证码、人脸活体检测或者手写签名时采集压力轨迹数据内容防篡改对应的就是哈希摘要数字签名时间戳的组合。技术链路里最关键的是防篡改验证。签署时系统对PDF文件计算SHA-256摘要用签名者的私钥对这个摘要做加密运算生成数字签名值再把签名值、证书信息、时间戳一起写入PDF的签名域。后续任何人打开这个PDF阅读器都会重新计算摘要并比对只要有一点改动校验就失败。这里个人私钥的保存是个重点不能把私钥明文放在业务数据库里要放到硬件加密机或者至少用密钥管理服务托管起来。2.2 合同文件生成与签名位置定位合同文件生成这个环节实操中我推荐模板占位符的方式但不推荐用简单的字符串替换。原因是合同一般既有动态字段又有固定条款还有可能包含表格、多页签章字符串替换很难处理复杂排版。比较稳的方案是预先做好PDF合同模板用Adobe Acrobat或iText工具加上表单域字段后端只做表单域赋值和动态表格填充。签名位置定位是另一个坑。签署人在小程序上看到的是PDF渲染成的图片或Canvas画布他拖拽签章到某个位置前端拿到的是相对于渲染画布的比例坐标或像素坐标后端必须把这个坐标换算成PDF页面上的绝对坐标。换算公式很简单PDF坐标X 画布像素X / 画布宽度 × PDF页面宽度Y轴要注意PDF坐标系原点在左下角而画布原点在左上角要做一次翻转。这个换算细节如果做错就会出现前端看着签名在右下角下载的PDF里签名跑到左上角的灵异现象。2.3 数字签名、时间戳与防篡改链路先理清一个容易混淆的概念数字签名和时间戳是两件事。数字签名解决这份文件是谁签的问题用签名者的私钥对文件摘要加密时间戳解决这个签名是什么时候签的问题由第三方时间戳服务机构TSA对签名值当前时间做一次签名证明在某个时刻数据已经存在。时间戳协议用的是RFC 3161标准Java侧可以直接用BouncyCastle库生成时间戳请求把PDF签名域的摘要发给TSA拿回时间戳令牌后嵌入PDF。这样做的好处是即便你自己的服务器时钟被人改了时间戳证明的依然是TSA机构的权威时间这在司法举证阶段非常关键。再聊一下国密改造。国内很多项目会要求支持国密算法SM2替代RSA做签名、SM3替代SHA-256做摘要、SM4做对称加密。Java里实现国密推荐用BouncyCastle的国密Provider接入成本不高但要注意改造之后签名证书也要换成国密证书PDF阅读器对国密签名的支持度参差不齐测试时要用WPS、福昕、Adobe三家都过一遍避免出现系统里验签通过客户下载后用某款阅读器打开提示签名无效的纠纷。2.4 手写签名采集的真实操作细节前端的手写签名采集小程序、H5、APP虽然技术栈不同但核心都是Canvas画布捕获手写轨迹生成透明背景PNG。实现上有几个细节直接决定签名质量。第一Canvas要开启触摸事件去抖。用touchstart/touchmove/touchend记录点位两点之间做插值最后通过quadraticCurveTo画出平滑曲线不然手写笔迹会呈现明显的折线感显得很不专业。第二生成的签章PNG背景必须透明。保存时不要用白色填充要直接用canvas的透明通道。如果前端拿到的是白色底图合成到PDF上就是一个白方块盖住合同正文非常难看。第三base64图片要压缩。一张手写签名原图可能1-2MB直接通过接口上传会影响签署页面的加载速度建议前端用Canvas的toDataURL(image/png)输出后配合压缩逻辑把尺寸限制到200KB以内。我试用过toBlob配合canvas尺寸缩放效果稳定。4. 多端场景拆解小程序、公众号、APP与H5四端看着复杂其实核心逻辑高度统一。用户进入系统后看到待签署列表点击进来查看合同详情完成身份验证手写签名或点击确认提交签署。不同端只是在登录方式、信息展示、采集手段上有差异业务接口完全可以复用。下面按端逐个说。3.1 微信小程序端登录、签名、通知小程序端最常用的登录流程是微信手机号快速验证组件。用户在签署页点击手机号快捷验证前端调用button open-typegetPhoneNumber拿到code后端用这个code调微信接口换取真实手机号完成实名手机绑定。这里要注意微信的getPhoneNumber接口现在返回的是加密数据或code不能直接拿手机号明文后端需要先调用phonenumber.getPhoneNumber接口换取手机号而且小程序类目必须包含相应权限否则接口调用直接报错。签名采集在小程序里是用Canvas实现的限制是Canvas画布必须设固定尺寸不能用百分比。原因是小程序Canvas的坐标系跟普通网页不一样绘制前要调wx.createCanvasContext签名区域高度要考虑到iPhone底部安全区建议签名区底部留出env(safe-area-inset-bottom)的适配不然全面屏机型上签名按钮会被home indicator挡住。小程序端还有一个容易被忽略的点合同文件预览。PDF在小程序里没法直接用web-view打开除非配置业务域名最省事的做法是后端把PDF转成图片流小程序端用wx.previewImage预览或者用官方wx.openDocument直接打开PDF文件这个接口支持PDF格式但要求文件必须先下载到本地临时路径注意控制文件大小在10MB以内。3.2 公众号H5与网页端兼容性处理公众号端走的是微信OAuth2.0网页授权。前端跳转微信授权链接后端拿code换openid和用户信息绑定手机号后创建会话。公众号端重点在签署通知推送用户收到模板消息或订阅消息点击跳转H5签署页整体链路是消息模板ID要在微信后台配置跳转URL要用#/sign/xxx这种hash路由避免刷新后404。H5端的兼容性问题是最大的实测下来要注意三个点。一是Canvas签名在部分安卓WebView里触摸事件会丢失解决方案是用Pointer Events替代Touch EventsPointerEvent在微信浏览器和系统WebView里支持率都很高二是iOS Safari的100vh问题签名区域如果用了100vh地址栏收起时底部会被截断建议签名区改用window.innerHeight动态计算高度三是文件下载H5端合同下载不能用window.open直接打开容易被浏览器拦截要用隐藏a标签配合download属性触发下载。3.3 APP端与uni-app打包常见坑APP端很多项目直接用uni-app打包好处是一套代码同时输出iOS和Android。但电子合同APP有几个特殊场景要注意。第一个是离线签署。业务场景里经常有销售在高铁上、地下室信号差时要签合同所以APP端要支持草稿箱模式用户先下载合同到本地完成签名后本地暂存等有网了再自动同步到后端。这个功能小程序和H5基本做不了是APP端区别于其他端的核心竞争力。实现思路也不复杂核心是本地用SQLite或文件缓存签名结果同步时用任务队列做失败重试。第二个是原生插件调用。如果APP端要调起系统的指纹或面容识别做签署确认uni-app需要集成原生插件。这里建议上市场找成熟的三方插件做指纹比对前要明确一下到底是用系统级生物识别做本机鉴权还是接第三方实名认证服务做在线活体检测两者成本和体验差异很大。3.4 多端统一的签署流程设计把四端的流程统一到一张图里看其实就是一个状态机草稿 → 待签署 → 签署中 → 已完成 → 已撤销。后端用一张sign_flow表记录签署状态每个节点存操作人和操作时间。关键在于幂等用户重复点击签署按钮时同一份合同不能生成两条签署记录需要用合同ID签署人ID状态字段做唯一索引数据库层面防重。签署顺序也要提前设计好。简单合同支持任意顺序签署流程复杂一点的要支持先甲后乙或者先发起方签完再发给接收方。我建议在合同模板表里加一个sign_type字段1代表单方签2代表顺序签3代表并签。每次完成一个签署动作后端重新计算下一步签署人是谁把待办事项写入消息中心同时触发短信和模板消息通知。这样设计的好处是后续调整签署规则时不用改表结构只改配置。5. 源码工程实操从数据库到部署这套源码的工程落地方案我按实际开发顺序给大家串一遍方便你拿到源码后快速跑起来也方便你自己从零搭建时有个路线参考。这里分享的是通用做法具体到你拿到的源码包结构可能会有些差异但核心思路是一致的。4.1 工程结构梳理Java后端我用Spring Boot作为底座Maven管理依赖多模块结构大致如下sign-system ├── sign-common // 公共工具、统一返回体、常量 ├── sign-framework // 配置、安全、异常处理 ├── sign-module // 业务模块合集 │ ├── user // 用户、企业、实名认证 │ ├── contract // 合同模板、合同发起、签署流程 │ ├── seal // 签章管理、手写签名上传 │ ├── sign // 数字签名、时间戳、验签 │ └── store // 存证、归档、证据包导出 └── sign-admin // 管理后台接口这个结构不是随便分的核心原则是按业务域隔离每一个模块的代码。电子合同系统最怕改一处崩一处模块间只通过接口调用不直接访问对方的数据表这样带着新人二次开发时新人改合同模块不会误伤用户模块的代码。前端工程通常是一个独立的h5目录跑PC和公众号网页端另一个uniapp目录跑小程序和APP。如果源码里自带前端建议先确认前端编译工具链版本——Vue2和Vue3的编译方式完全不一样uniapp在HBuilderX里打开和用CLI命令行打开的处理流程也不同版本不匹配会浪费半天排查时间。4.2 核心表设计与MyBatis Plus落地数据库设计这块电子合同系统的核心表不会超过十张但每张表的字段都要认真推敲。我列一下最核心的几张表名核心字段说明contract_infoid, contract_no, title, template_id, status, sign_type合同主表contract_no唯一索引contract_field_valueid, contract_id, field_key, field_value合同字段值存模板变量的实际内容sign_flowid, contract_id, signer_id, sign_order, status, sign_time, sign_img签署流程表记录谁在什么时间签了什么位置seal_infoid, user_id, seal_img, seal_type, create_time印章和签名图片库sign_certid, contract_id, cert_no, digest, timestamp, sign_value签署凭证存摘要、时间戳、签名值ca_apply_recordid, user_id, cert_id, apply_statusCA证书申请记录用MyBatis Plus落地时有一个提效技巧实体类写好之后直接用它提供的代码生成器或内置SQL生成器输出建表语句可以把字段名的下划线转驼峰、类型映射、注释一次搞定。这里有一个要注意的点不要把所有表都设计成逻辑删除。合同和签署记录这类表做逻辑删除会有风险——如果业务上允许删除合同但存证数据已经上链了删除之后证据链就不完整。我建议合同表只允许撤销不允许删除撤销只是改状态字段原始签署记录全部保留。4.3 关键代码逻辑合同签署流程串讲签署流程的核心接口逻辑我用代码把关键步骤写出来。假设用户点击确认签署按钮后端实际做的事是public SignResult sign(SignRequest request) { // 1. 查询合同校验状态 ContractInfo contract contractMapper.selectById(request.getContractId()); if (!ContractStatus.PENDING_SIGN.equals(contract.getStatus())) { throw new BizException(合同当前状态不可签署); } // 2. 查询签署人校验是否在签署流程中 SignFlow flow signFlowMapper.selectBySigner(req); if (flow null) { throw new BizException(签署人不在本次签署流程中); } // 3. 校验唯一性同一签署人不能重复签 if (flow.getStatus() SignStatus.SIGNED) { throw new BizException(该签署人已完成签署); } // 4. 生成待签署PDF计算摘要 byte[] pdfBytes renderContractPdf(contract, request.getFieldValues()); String digest DigestUtils.sha256Hex(pdfBytes); // 5. 请求时间戳 String timestamp tsClient.getTimestampToken(digest); // 6. 用私钥对摘要时间戳做签名 byte[] signValue signService.sign(digest.getBytes()); // 7. 把签名图片合成到PDF指定坐标输出正式签署版PDF byte[] signedPdf sealService.attachSeal(pdfBytes, request.getSealImg(), request.getPosition()); // 8. 生成签署凭证记录摘要、时间戳、签名值 saveSignCert(contract.getId(), request.getSignerId(), digest, timestamp, signValue); // 9. 更新合同状态通知下一签署人 contractMapper.updateStatus(contract.getId(), ContractStatus.PART_SIGNED); notifyNextSigner(contract.getId(), flow.getSignOrder()); return SignResult.success(signedPdf, signValue); }这段逻辑看起来简单但有几个细节是要命的。第一步和第二步是判断当前用户能不能签这份合同第三步防重复提交必须依赖数据库的唯一索引兜底第四步到第六步是签名核心链路第七步把签名图片合成到PDF时坐标转换要用前面提到的比例换算方法第九步的通知不能放在事务里操作外部接口否则第三方短信接口超时会导致整个签署事务回滚合同签了但状态没更新就会出现签名成功但系统提示失败的严重事故。正确的做法是主事务只做签名和状态更新通知动作通过Spring的TransactionalEventListener在事务提交后异步执行或者塞进消息队列。这一步踩坑踩得多了你会发现分布式事务的很多最佳实践本质都是在避免这种外部调用拖垮主业务的经典问题。4.4 部署上线要点部署这块没有太多玄学但有几个环境细节建议提前确认。首先是HTTPS证书电子合同系统涉及敏感数据必须全站HTTPS且小程序后台配置的request域名和downloadFile域名要跟实际部署的域名完全一致一个字符都不能差。其次是文件存储路径。合同源文件、签署版PDF、签名图片建议都放对象存储腾讯云COS、阿里云OSS都行内网环境就用MinIO。上线前记得把存储桶的访问权限设置为私有读所有文件访问走后端签名URL或临时凭证有效期控制在10-30分钟。第三是密钥管理。电子签名的私钥、数据库密码、第三方API密钥不要写在application.yml里用环境变量或配置中心管理至少保证代码仓库里不出现明文密钥。这一步是所有合规检查的底线别在这个环节翻车。6. 常见问题与排查技巧实录最后这部分我整理一下实际开发中几乎必踩的坑按问题现象、原因分析、解决方案的格式做一个速查表再补几个常规文档里不会写的独家经验。5.1 问题速查表问题现象根本原因解决方案小程序端合同预览白屏wx.openDocument的fileType没传pdf或临时文件路径不对明确指定fileType: pdf文件要先wx.downloadFile到本地路径签名图片是黑底或白底方块Canvas导出时填充了纯色背景导出前不执行fillRect直接canvas.toDataURL(image/png)保留透明通道PDF里签名位置错乱前端像素坐标没等比换算成PDF坐标且Y轴方向没翻转做坐标换算pdfY pdfHeight - (canvasY / canvasHeight) * pdfHeight验签失败签署后PDF被二次编辑比如有人用工具加了水印摘要变化验签时先比较摘要确认原文未改签名值和证书本身验签逻辑要完善日志第三方时间戳服务偶发超时外部HTTPS调用没有设置超时和重试时间戳请求设置3秒超时失败后重试两次重试仍失败则提示用户稍后再签企业实名认证审核被驳回上传的营业执照照片不够清晰、法人身份证不在有效期内前端拍照时做清晰度检测OpenCV或用第三方SDK上传前给用户预览确认Android APP端签名区域被键盘顶起manifest或页面配置里adjustResize没生效uni-app页面设置adjust-position: true签名区改用自定义导航占位5.2 独家避坑经验第一个经验关于合同模板的版本管理。合同模板上线后不是一劳永逸的业务方一定会改格式、改条款。千万不要直接在原模板上改要建立模板版本表每次修改生成新版本历史合同永远引用生成时的模板版本号。否则会出现半年前的合同现在下载下来用新模板渲染格式全乱的严重事故。第二个经验关于签署环节的录像存证。可靠性要求高的项目会要求在签署时录制一段用户操作的短视频签署人面部、签署动作、点击确认过程存成证据文件。这个功能实现不复杂前端用navigator.mediaDevices.getUserMedia录屏或调起摄像头后端把视频和签署记录绑定就行。但要注意隐私合规录制前必须有明确的授权提示视频文件加密存储访问权限严格控制。第三个经验关于性能测试。电子合同虽然大多时间并发不高但月底、年底会有集中签署高峰。压测时重点看两个接口一个是PDF渲染接口推荐做成异步任务前端轮询查询生成状态别让用户同步等PDF渲染另一个是验签接口热门合同会被多次查看验签要对验签结果做缓存同一个合同同一个文件摘要直接返回缓存结果避免重复计算。第四个经验是Java侧的常见坑。如果你还在用java.util.Date处理时间戳请求的过期判断建议换成java.time.Instant如果PDF模板包含字体文件iText生成PDF时一定要把字体嵌入到BaseFont字体不嵌入会导致PDF在不同设备上显示效果不一致如果签名的私钥是放在数据库里读取的启动时优先做私钥完整性校验别等用户签署到一半才发现私钥文件损坏。7. 写在最后的个人体会这套系统开发到后期我最大的体会是电子合同的技术难点不在密码学而在工程化的细节里。密码学算法全部是现成的库真正耗时间的全是衔接层的活——前端Canvas坐标怎么转成PDF坐标外部时间戳服务超时了怎么处理才不会影响主流程用户重复点击签署怎么从接口层防掉这些才是实际开发中反复打磨的地方。如果你是刚接触这个领域我的建议是不要急着把四端全部铺开先把小程序H5跑通一版让业务用起来再逐步补APP和公众号。四端同时开发最大的风险不是写代码而是每个端都有自己的兼容性怪癖精力分散到四个端上很容易顾此失彼。前端签名一组接口、后端逻辑稳了之后新接入一个端通常只需要一两周的时间。还有一个小技巧分享给做二次开发的朋友拿到这套源码后第一件事先看sign_cert表和sign_flow表的表结构这两张表基本决定了整个系统能走多远。把这两张表吃透了合同的签署状态、证据追溯关系就都能看明白后续扩展电子签章、批量签署、自动提醒之类的功能都会顺手很多。
阅读完成 · 觉得有帮助?