
做了大半年的电子合同项目最大的感受是这个系统表面上就是“把纸质合同搬到手机上传一下签个字”真做起来才发现一整条链路全是细节。实名认证、证书签发、哈希校验、防篡改、多端集成、权限隔离每一块拆开都够写好几篇文章。今天分享的这个Java技术栈的电子合同电子签名系统源码后端是Spring Boot MyBatis-Plus前端用uni-app一套代码打包微信小程序、公众号H5、原生APP和普通H5四个端直接部署就能跑通业务闭环。适合正在做合同管理、法务数字化或者想快速搭建电子签约平台的同学参考。1. 系统定位与架构拆解1.1 电子合同到底解决了什么痛点先回忆一下传统纸质合同的签署流程起草合同、打印、走内部审批、邮寄给对方、对方盖章或签字、再寄回来、最后人力归档。顺利的话三五天碰上跨地区或者对方审批流程长一个月都不奇怪。电子合同系统做的事情就是把这条链路整体搬到线上创建合同、设置签署方和签署顺序、通过短信或微信通知各方、各方在线完成实名认证后手写签名或上传印章、系统固化文件存档。整个流程从“天”缩短到“分钟”。单看功能清单并不复杂但合同签署是一个强信任场景。系统要确保三件事签署人确实是本人签署内容在签署后没有被改过签署行为不可否认。这三点落到技术实现上分别对应实名认证、哈希校验与数字签名、操作日志与证据链留痕。这也是我认为这类系统真正值钱的地方而不是几张页面。1.2 核心技术选型与原因后端选用Java不是因为它有什么很华丽的特性而是生态成熟、招人容易、出了问题网上能找到大量资料。项目里常用的组合是Spring Boot负责接口和业务编排MyBatis-Plus做持久层Redis处理缓存和分布式锁MySQL存业务数据MinIO或阿里云OSS存合同文件和签章图片。MyBatis-Plus在这类业务里的优势很明显。合同、签署方、签章记录这些表的CRUD操作高度重复用它的BaseMapper和IService可以直接省掉大量样板代码。再加上分页插件、代码生成器团队可以把手写Mapper的工作量压到最低。前端选uni-app是另一个务实的决定。合同签署是社会化协作场景你没法要求客户都装同一个APP微信小程序覆盖熟人社交场景公众号适合嵌入企业内部OA流程原生APP服务大客户H5方便对接第三方平台。用uni-app一套Vue代码编译四个端业务逻辑共享平台差异用条件编译单独处理维护成本比维护四套原生工程低一个数量级。1.3 系统能力边界与适用场景这套源码覆盖的签署场景主要是三类个人签署例如劳动合同、保密协议、借款协议企业间签署例如采购合同、对账单、合作协议以及平台托管签署例如租赁平台、招聘平台代发合同。从源码角度看它适合用来学习以下内容Spring Boot的业务建模方式、MyBatis-Plus的持久层设计、状态机在业务流转中的应用、uni-app多端工程的组织方式、微信小程序登录与手机号获取的完整链路以及电子签名背后哈希与加密算法的工程化落地。2. 电子签名的技术原理与安全体系2.1 数字签名给合同内容加一把专属锁电子签名不是让你在屏幕上画个名字然后把图片贴上去核心是数字签名。我给团队讲这个概念的时候用的类比是“指纹加锁”合同原文先做哈希运算得到一个固定长度的摘要这个摘要是合同内容的“指纹”哪怕只改一个标点指纹也会完全变化。然后再用签名者的私钥对这个摘要加密得到签名值。合同、签名值、签名者的公钥三者一起发布。验证的时候只需要做两件事用公钥解密签名值得到原始摘要再对当前合同内容重新计算摘要两个摘要一致就说明合同没有被篡改签名者的身份也没问题。整套体系的根基是私钥必须只有签名者自己持有这叫“不可否认性”。项目里我一般用RSA 2048及以上或者ECDSA私钥不落库用KMS或者独立密钥服务管理避免数据库泄露导致所有历史签署全部失效。2.2 防篡改与时间戳固化很多初学者以为合同签完存个PDF就完事了这远远不够。PDF文件本身是可以被编辑的如果只存文件将来有人改了合同内容双方各执一词系统根本没法自证清白。所以签署完成的合同必须做哈希固化也就是把最终PDF的SHA-256值存进数据库后续任何字节层面的改动都会导致哈希不一致。更严谨的做法是引入可信时间戳服务。把“文件哈希 当前时间”打包发给时间戳机构由机构用它的私钥签名后返回一个时间戳证书。这样即使系统自己的服务器时间被篡改时间戳证书仍然能证明合同在某个时刻已经存在且内容未被改动。我们在项目里对严肃性要求高的合同都会启用时间戳普通的B端内部协议至少也会做服务端哈希留痕。需要提醒的是如果客户要求做司法举证只有时间戳完整操作日志实名认证记录才能组成一条有说服力的证据链三者缺一不可。2.3 实名认证与签署意愿确认实名认证是整个系统的信任入口。个人实名一般做三要素校验姓名、身份证号、人脸识别判断是不是本人操作企业实名要校验营业执照信息再通过对公打款随机金额或法人人脸来确认企业意志。这里我的建议是直接对接第三方实名认证服务不要自建人脸识别。活体检测模型训练、对抗攻击防御、身份证识别每一项都是重投入。实名认证完成后系统为用户生成一对签名密钥并把“用户信息、密钥公钥、实名状态”绑定在一起。每次签署动作都要重新校验用户登录状态并对“我已阅读并同意”这种动作单独留日志。签署意愿这一点经常被忽略很多系统把“登录即代表同意”当成默认行为这是不对的。真正的电子合同系统里用户必须在进入签署页时明确点击确认并且这个点击行为要记录设备信息、IP、时间。后期一旦有纠纷这些都是证明“本人自愿签署”的重要材料。2.4 国密算法的取舍国内部分行业项目对密码算法有国密要求例如SM2、SM3、SM4。但这并不意味着所有商业项目都必须上国密实际项目里大部分SaaS类的电子合同系统用的还是RSA/SHA-256体系性能和兼容性更好。我的建议是把加密、摘要、签名这些底层操作封装成统一接口例如定义一个CryptoService里面放generateKeyPair、sign、verify方法底层用RSA还是SM2可以配置切换。这样项目初期跑得快后期客户提出国密合规要求只需要替换底层实现不需要改上层业务代码。3. 后端核心模块与数据库落地3.1 核心表设计与自动建表方案持久层还是先看表结构。这里以合同主表为例CREATE TABLE contract ( id BIGINT PRIMARY KEY COMMENT 主键, contract_no VARCHAR(64) NOT NULL COMMENT 合同编号, title VARCHAR(255) NOT NULL COMMENT 合同标题, file_url VARCHAR(500) COMMENT PDF文件存储地址, file_hash VARCHAR(128) COMMENT 合同文件SHA-256哈希, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1待签署 2部分签署 3已完成 4已撤销, creator_id BIGINT NOT NULL COMMENT 发起人用户ID, expire_time DATETIME COMMENT 签署截止时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_creator (creator_id), INDEX idx_status (status) ) COMMENT 合同主表;配套的表还包括contract_signatory即签署方表存合同ID、用户ID、签署顺序、签署状态contract_sign_record签署记录表存每次签章的图片、哈希、请求IP、设备信息、时间戳。用户表和证书表按常规设计即可。关于“用MyBatis-Plus实体类自动生成建表SQL”这个点网上有很多方案我实践下来比较稳的做法是写一个启动时的建表组件扫描指定包下的实体类读取TableName注解拿到表名读取字段上的TableField注解和Java类型映射到MySQL类型拼接CREATE TABLE IF NOT EXISTS语句并执行。这个方案对早期开发和演示环境很方便但生产环境我建议换用Flyway或者Liquibase做数据库版本管理。自动建表虽然省事一旦字段变更没有脚本记录生产环境升级时会很痛苦。3.2 合同模板与动态PDF渲染合同的条文结构通常是固定的差异点集中在甲方乙方名称、金额、日期、产品条款编号这些变量上。我用FreeMarker做模板引擎合同原文保存成模板文件变量位置用占位符代替后端组装JSON数据渲染出完整HTML再通过OpenPDF生成PDF。这里有一个必须提前解决的坑PDF中文字体。OpenPDF默认字体不支持中文生成出来的PDF中文全是方块。解决方法是把宋体或思源宋体的TTF文件放到classpath下在生成PDF时注册BaseFont bf BaseFont.createFont( /fonts/simsun.ttc,0, BaseFont.IDENTITY_H, BaseFont.EMBEDDED ); Font font new Font(bf, 12, Font.NORMAL);字体文件不建议从系统目录动态读取打包部署后路径不可控直接打进jar包是最省心的做法。3.3 签署流程状态机设计合同签署不是简单的“草稿变已签”中间有并行和顺序两种模式还涉及撤回和拒绝。我在代码里用枚举定义状态和允许的动作避免业务代码里到处写if else草稿创建人可以编辑、删除、发送待签署至少一方未签署所有人均可查看部分签署存在按顺序签署时前序签署方已完成等待后续签署方已完成所有签署方均已签署已撤销创建人撤回或超过截止时间状态机的核心逻辑是每次变更前先校验当前状态是否允许该动作。比如撤回只能发生在没有任何签署方完成签署之前一旦有人签过字合同只能走线下协商修改流程再以新合同重新发起。这个限制很多人会忽略导致线上合同被无限制撤回存在严重安全隐患。我把状态和动作的映射关系放在一个Map里集中管理再用单测把所有流转路径覆盖一遍后续维护省心很多。3.4 行级数据权限与隔离合同数据天然敏感普通用户不可能也不应该看到公司内所有合同。行级权限我推荐两种实现方式。第一种是查询条件主动带上权限约束例如在SQL里拼接exists子查询判断当前用户是否存在于这张合同的签署方表中。优势是直观、容易排查缺点是每个查询都要记得写容易漏。第二种是MyBatis拦截器方案自定义一个DataScope注解标注在Mapper方法上拦截器在SQL执行前解析方法参数中的当前用户ID自动注入权限片段。这种方案业务代码最干净但排查问题时对拦截器逻辑的把关非常考验团队水平。对于这套具有参考价值的源码我更推荐先用第一种方式把逻辑跑通等理解了权限拼接的语义再考虑做拦截器抽象。权限永远先保证正确再追求优雅。4. 多端集成与微信生态踩坑记录4.1 uni-app工程结构与条件编译前端工程按uni-app标准组织pages目录放业务页面api目录统一封装请求store管理登录态utils放公共工具。所有接口路径和后端约定好统一前缀请求封装里根据运行平台自动处理请求头差异。多端差异用条件编译处理。例如小程序端修改导航栏标题// #ifdef MP-WEIXIN uni.setNavigationBarTitle({ title: 合同签署 }); // #endif // #ifdef H5 document.title 合同签署; // #endif这类平台差异化代码尽量收敛在少数工具文件里不要散落在业务组件中。否则一旦新端上线排查成本会很高。亲测在四个端跑下来除了iOS的WebView对某些CSS属性渲染有差异大部分业务代码完全可以共用。4.2 小程序登录与手机号获取完整流程微信小程序的用户体系接入大概是这个项目里和新手同学交流最多的话题。流程分两步第一步wx.login拿到临时code传给后端后端用code加AppID、AppSecret调用微信接口换取openid和session_key同时生成系统自己的token返回给前端。后续所有接口都带这个token。获取手机号是另一个高频踩坑点。2023年以后微信调整了规则不能再通过用户信息授权接口拿到手机号唯一的正规方式是页面放一个button设置open-type为getPhoneNumber用户点击后通过bindgetphonenumber事件的回调拿到动态code再把这个code发给后端后端用code换手机号信息。注意这个能力按次收费开发调试强烈建议用真机开发者工具模拟器基本拿不到真实返回。这一步也是电子合同签署流程中实名信息填充的重要来源。4.3 手写签名面板实现要点手写签名用canvas实现核心逻辑并不复杂但有两个细节非常影响体验。一是笔画平滑度直接用touchmove坐标连线会有折角感我用二阶贝塞尔曲线对中间点做平滑处理效果会好很多。二是导出图片的清晰度canvas默认按CSS像素渲染小屏设备上导出图片会非常糊。必须将canvas宽高乘以devicePixelRatioconst dpr uni.getSystemInfoSync().pixelRatio; canvas.width canvasWidth * dpr; canvas.height canvasHeight * dpr; ctx.scale(dpr, dpr);签名完成后调用toDataURL导出透明背景PNGbase64传到后端存起来。真机上还要给签名区域设置touch-action: none否则页面滚动和画线手势会互相冲突导致笔画断断续续。这个坑我在真机测试时才意识到模拟器上完全复现不了。4.4 公众号、H5与APP的接入方式公众号内嵌H5和普通H5的基础差异在登录体系。公众号页面需要走微信OAuth授权用户在微信浏览器里打开时跳转授权页拿code后后端换取openid。H5部署在自有站点时账号密码登录即可或者是短信验证码登录。APP端我采用的做法是保留原生壳负责推送、扫码和外部唤起核心签署页面全部用WebView内嵌H5。这样小程序、H5、APP三端都复用同一套签署流程页面唯一的代价是APP离线状态无法签署但合同签署本身就是强网络依赖场景这个取舍很合理。深度链接这块APP需要在前端工程里配置URL SchemeH5页面在APP内做支付或签署跳转时通过Scheme唤起对应原生页面。如果公司同时有iOS和Android的包这里还需要额外处理应用未安装时的Fallback逻辑。5. 落地过程中的常见问题排查5.1 PDF签名后打不开或乱码OpenPDF生成PDF时尽量用1.3.30以上版本旧版本和部分打印机的PDF兼容性有问题。另外中文乱码问题核心就是字体没有注册解决方案参考3.2节。我再补充一个点如果模板里有特殊符号比如不换行空格、特殊引号部分字体渲染会报错建议上线前用批量合同样本做一轮PDF渲染验证。5.2 签名坐标偏移PDF页面坐标系的原点在左下角前端canvas坐标原点是左上角这两个坐标系不能直接对应。叠加签章图片时要做换算pdfY pageHeight - imageY - imageHeight。很多新手做出来的签章位置总是偏上一截或者偏下一截基本都是忽略了坐标系转换。5.3 并发签署导致状态错乱并行签署时两个人可能同时提交签署如果合同主表状态更新不加锁可能出现状态覆盖从部分签署直接跳回待签署。我在签署接口里加了Redis分布式锁key设计为contract:sign:{contractId}加锁后再校验状态、写入签署记录、更新合同主状态。锁粒度只到合同维度不阻塞其他合同操作。5.4 高频问题速查表问题常见原因处理方式小程序请求失败request域名未加入白名单开发工具勾选“不校验合法域名”生产环境配置合法域名手机号获取不到使用了模拟器或旧接口换真机调试使用button组件的getPhoneNumberPDF中文全部变方块缺少中文字体注册Classpath中加入TTF字体并注册手写签名导出白屏绘图未完成就导出draw回调结束后再调用toDataURL签章位置偏移坐标系未转换按pageHeight - imageY - imageHeight计算重复点击签署生成了多条记录未做幂等前端按钮防重复提交后端加业务幂等键最后说一点我做这类项目的体会。电子合同系统最容易被人低估的是底层可信数据的设计界面做得再花哨都不如把证据链做扎实重要。真正考验功力的地方在于一旦双方对一份合同产生争议系统能不能拿出完整的证据链谁在什么时间查看了合同、做了实名认证、确认了哪些条款、签了什么内容、当时的文件哈希是多少。这套源码把基础骨架都搭好了后续可以沿着电子签名证据链的方向继续打磨比如把签署结果摘要做外部存证、对接企业微信审批流、增加多语言合同能力。先把闭环跑通再逐步优化信任体系是我一贯的做法。