ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Thymeleaf手写电子签字合同系统实战解析

SpringBoot+Thymeleaf手写电子签字合同系统实战解析 简介这是基于SpringBoot框架与Thymeleaf模板引擎实现的电子文件签字与合同管理系统Java源码适合需要快速搭建电子签署功能的开发人员、毕业设计者及中小项目团队使用。资源围绕“文件/通知发布签字盖章”业务闭环展开内含数字签名身份验证思路同时可见安卓端安装包与电脑控制端相关文件有助于理解网页后台与移动端协同的完整路径。压缩包共485个文件、63.97MB类型以CSS、HTML、XML、Java、JS、PNG为主前端模板与样式占比最高支撑服务端渲染Java与字节码文件对应后端核心逻辑各类图片提供界面素材整体便于按模块查阅与二次开发。目前已有1313人学习适合电子合同、在线签名方向参考。借助代码可梳理从文件发布、签章触发到结果保存的完整业务链路掌握签署流程、盖章交互及二维码处理等关键环节。 一个项目的价值不在于它用了多牛逼的框架而在于它能不能真正解决手头的问题。我给你说个真实场景部门里合同还是线下打印、盖章、扫描、归档那一套一份合同来回快递至少三五天。后来我做了这套基于 SpringBoot Thymeleaf 的电子签字合同系统把签署流程搬到了线上手写签字、合同归档、签署状态跟踪全都有前后端都不用拆跑起来轻改起来快部署到内网服务器就能用。这篇文章就按我实际开发这套系统的思路把需求分析、技术选型、数据库设计、核心代码、运行演示、踩坑记录完整拆一遍给正在做 Java 项目、准备搞合同签批系统、或者拿 SpringBoot 练手的朋友一个可以直接参考的副本。在动手之前我先说清楚这套系统的定位它不是要去对接 CA 机构做那种具有完整法律效力的电子签章平台而是解决企业内部文件确认签字这个高频场景比如供应商对账单、内部审批单、项目验收单、合同回传确认。在这个前提下技术方案怎么简单、怎么高效、怎么好维护就怎么来这也是我选 SpringBoot Thymeleaf 而不是 Vue SpringBoot 前后端分离的最直接原因。1. 项目需求与技术选型为什么是这种做法1.1 电子签字系统解决什么问题电子签字系统说白了就是把人在纸质文件上签名这个动作搬到网页上并且把签名结果和原始文件绑定形成一份可查询、可追溯的签署记录。它解决的核心问题有三个第一签署效率。传统模式里一份合同从起草到双方签完字中间要经过打印、快递、扫描、归档流程越长不确定性越大。线上签署把分钟级变成现实。第二签署真实性。系统会记录操作人、签署时间、IP地址、签名图片、文件哈希值一旦发生争议可以通过这些信息追溯签署过程。第三归档管理。纸质合同一旦堆起来找一份旧合同翻箱倒柜电子化之后按合同编号、签署人、签署日期随时检索。做这个项目之前我梳理过市面上成熟的电子签平台它们的核心能力是实名认证 数字证书 时间戳。但我们内部场景不需要那么重做一个够用、能用、管用的签字系统性价比最高。1.2 为什么用 SpringBoot Thymeleaf 而不是前后端分离很多 Java 开发者一上来就默认SpringBoot Vue前后端分离我不反对但要看场景。这套系统的用户量是几十人操作界面就是合同列表、签署页面、历史记录这几个页面没有复杂交互。如果用前后端分离你得维护两套工程、处理跨域、设计接口文档、考虑前端打包部署成本直接翻倍。而 Thymeleaf 是服务端渲染模板引擎直接把 Java 对象丢到模板里渲染出 HTML天然支持页面片段复用、表单回显、权限控制对于这种内部管理系统来说它的开发效率和维护成本是最优的。还有一个隐形优势不用额外装 Node.js 环境。项目拿到手只要 JDK 和 Maven 能用就能跑起来。这一点对做毕业设计、对源码学习、对部署到客户内网环境的人来说非常友好。1.3 整体功能模块划分这套系统的功能我拆成了五个模块合同管理模块上传合同文件PDF、图片、查看合同列表、删除合同、查看签署状态。签署模块打开签署页面鼠标或触摸屏手写签名签名图片自动贴到合同指定位置。用户模块登录、退出、当前用户信息。实际项目中可以扩展角色权限。记录模块每次签署操作都会生成签署记录包含操作人、时间、文件哈希等。文件存储模块统一处理合同文件上传、签名图片保存、签署后文件下载。模块划分的思路是一个职责一条线后续加功能或者排查问题都很直接。2. 系统设计与核心原理2.1 数据库表设计合同、签署记录、签名图片如何存储数据库我用的 MySQL表结构设计是这套系统的地基。核心表有四张用户表、合同表、签署记录表、签名图片表。我挑重点说设计思路。合同表contract关键字段CREATE TABLE contract ( id bigint NOT NULL AUTO_INCREMENT, contract_no varchar(64) NOT NULL COMMENT 合同编号, title varchar(255) NOT NULL COMMENT 合同标题, file_path varchar(255) NOT NULL COMMENT 原始文件路径, signed_file_path varchar(255) DEFAULT NULL COMMENT 签署后文件路径, status tinyint NOT NULL DEFAULT 0 COMMENT 0待签署 1签署中 2已完成, create_by varchar(64) NOT NULL COMMENT 创建人, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_contract_no (contract_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT合同表;这里我把status设计成 tinyint是因为后续状态流转复杂了用 int 扩展性更强但 tinyint 足够覆盖目前待签署、签署中、已完成三种状态。contract_no加了唯一索引保证合同编号不重复这在归档查询时很重要。签署记录表sign_record的设计要点是一次签署一条记录CREATE TABLE sign_record ( id bigint NOT NULL AUTO_INCREMENT, contract_id bigint NOT NULL, user_id bigint NOT NULL, sign_name varchar(64) NOT NULL COMMENT 签署人姓名, sign_img_path varchar(255) NOT NULL COMMENT 签名图片路径, sign_position varchar(64) NOT NULL COMMENT 签名位置格式:x,y, file_hash varchar(128) NOT NULL COMMENT 签署时文件哈希, ip_addr varchar(64) DEFAULT NULL COMMENT 签署IP, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_contract_id (contract_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT签署记录表;设计这张表时我特意加了file_hash字段用来记录签署那一刻合同文件的 SHA-256 值。这意味着即使之后文件被改动我们也能通过重新计算哈希来判断文件是否被篡改这是电子签系统很重要的一个可信度设计。签名图片存储方面直接把签名图片作为一张 PNG 文件存到服务器磁盘数据库只存路径。这种做法的好处是数据库保持轻量文件 IO 交给操作系统文件系统备份时只要把数据目录一起备份即可。2.2 手写签字的核心实现思路手写签字的核心是在网页上画一条平滑的曲线然后把这条曲线转换成图片传到后端保存。前端画曲线的方案我选了 HTML5 Canvas。在移动端和 PC 端浏览器上Canvas 的兼容性都很好而且不需要额外引第三方库。实现思路是在 canvas 上监听mousedown、mousemove、mouseup移动端对应touchstart、touchmove、touchend把鼠标轨迹的点坐标收集起来最后用canvas.toDataURL(image/png)导出为 base64 编码的图片数据再通过 AJAX 提交到后端。为了画出来的曲线圆润平滑我在点与点之间用了二次贝塞尔曲线quadraticCurveTo来连接这样不会出现明显的折线锯齿感。签名时还会设置笔触颜色、宽度和圆角样式让签名看起来更接近真实笔迹。这里面有个细节要做canvas 画布尺寸和显示尺寸要一致否则在高 DPI 屏幕上会出现签名图片模糊的问题。我的做法是设置canvas.width和canvas.height为实际像素值再通过 CSS 控制显示尺寸。签名完成后前端还需要把 canvas 里的签名区域裁剪干净避免保存下一大片空白区域。2.3 合同文件与签名合成方案签名图片保存好之后下一步就是把签名叠加到原始合同文件上。这一步我用了两种方式如果合同是图片格式PNG、JPG直接用 Java 的Graphics2D把签名 PNG 绘制到原图指定坐标位置然后输出为新图片。绘制时要注意透明背景处理签名 PNG 的背景必须是透明的否则会盖住合同文字。如果合同是 PDF 格式我会先把 PDF 转成图片再叠加签名。PDF 渲染和图片绘制我用的是 Apache PDFBox它可以把 PDF 每一页渲染成图片我只需要处理签署那一页就可以。转图片的代码大概是PDDocument document PDDocument.load(new File(pdfPath)); PDFRenderer renderer new PDFRenderer(document); BufferedImage image renderer.renderImageWithDPI(signPageIndex, 150); document.close();把 PDF 转成图片再叠加签名虽然会让文件体积变大但好处是所见即所得签署后的效果和用户预览时看到的完全一致适合内部确认场景。如果想要保留 PDF 文字层并输出真正的 PDF 签署文件那就需要更重的方案比如 iText 加数字证书当前系统不做这个原因前面说了。3. 关键代码实现从签名到落库的完整链路3.1 签名捕获接口后端接收签名图片的接口我设计成接收 base64 字符串。核心代码如下PostMapping(/api/sign/upload) ResponseBody public Result uploadSign(RequestBody SignUploadVO vo) { // 1. base64解码 String base64Data vo.getSignData(); String[] parts base64Data.split(,); byte[] signBytes Base64.getDecoder().decode(parts[1]); // 2. 校验签名图片大小防止内存溢出 if (signBytes.length 1024 * 500) { return Result.error(签名图片不能超过500KB); } // 3. 保存签名图片到指定目录文件名用UUID String fileName UUID.randomUUID().toString() .png; File dir new File(signSavePath); if (!dir.exists()) { dir.mkdirs(); } File signFile new File(dir, fileName); Files.write(signFile.toByteArray() null ? new byte[0] : signBytes, signFile.toPath()); // 4. 返回签名图片访问路径 return Result.success(signFile.getName()); }这里有两个容易忽略的点。第一前端传回来的 base64 如果带data:image/png;base64,前缀后端要先 split 处理。第二必须对上传文件大小做限制。我一开始没加限制有人传了一张超大截图直接导致请求超时Tomcat 还抛了MaxUploadSizeExceededException。加上 500KB 限制后既满足手写签名的需求又防止了恶意大文件攻击。3.2 签名图片叠加到合同页面的代码思路签名图片保存成功之后接下来要把它和合同文件合成。我封装了一个ContractSignService内部逻辑分三步public String signContract(Long contractId, String signImgPath, int pageIndex, int x, int y) { // 1. 根据合同ID查出合同文件路径 Contract contract contractMapper.selectById(contractId); // 2. 加载原始文件判断是图片还是PDF File sourceFile new File(contract.getFilePath()); if (sourceFile.getName().endsWith(.pdf)) { return signPdfWithImage(sourceFile, signImgPath, pageIndex, x, y); } return signImageWithImage(sourceFile, signImgPath, x, y); }图片签署的叠加逻辑是BufferedImage contractImage ImageIO.read(sourceFile); BufferedImage signImage ImageIO.read(new File(signImgPath)); Graphics2D g2d contractImage.createGraphics(); g2d.drawImage(signImage, x, y, signWidth, signHeight, null); g2d.dispose(); ImageIO.write(contractImage, png, signedFile);这里有个经验值要给到大家签名图片的缩放比例不要超过原始签名宽度的 1.2 倍否则签名会模糊或者边缘出现锯齿。Graphics2D绘制时我还会开启抗锯齿g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);不然合成的签名边缘在放大后惨不忍睹。3.3 合同状态流转与权限控制当签署记录创建成功我会同步更新合同状态从待签署变成签署中当双方都签完之后变成已完成。这里我建议用状态机枚举来管理而不是在业务代码里散落一堆魔法数字。定义一个枚举类public enum ContractStatus { PENDING(0, 待签署), SIGNING(1, 签署中), COMPLETED(2, 已完成); private final int code; private final String desc; ContractStatus(int code, String desc) { this.code code; this.desc desc; } public static ContractStatus fromCode(int code) { for (ContractStatus status : values()) { if (status.code code) { return status; } } throw new IllegalArgumentException(未知状态: code); } }权限控制方面因为用户量小我没有引入 Spring Security而是用拦截器HandlerInterceptor做登录校验。在WebConfig里注册拦截器排除登录接口和静态资源路径其余接口都校验 sessionregistry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /api/login, /static/**, /favicon.ico);这样做的好处是代码量小、逻辑直观对于内部系统足够。如果后续要接入 RBAC 权限再引入 Spring Security 也来得及不会影响现有设计。4. 本地运行与实操演示从零跑起来4.1 环境准备与配置运行这套系统需要的环境非常基础JDK 8 或 JDK 11我实测 JDK 17 也能跑但有些老版本依赖需要调整Maven 3.6MySQL 5.7 或 MySQL 8.0浏览器建议 Chrome 或 Edge在application.yml里需要配置数据源、文件上传路径、签名保存路径。文件路径这块我强烈建议用绝对路径不要在打包后还依赖相对路径不然换台机器部署就找不到文件了。我这里的配置片段spring: datasource: url: jdbc:mysql://localhost:3306/sign_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver sign: save-path: D:/sign-system/files/ # 合同文件存储路径 sign-path: D:/sign-system/signs/ # 签名图片存储路径数据库初始化脚本我已经写好直接执行schema.sql建表再执行data.sql插入一个测试用户用户名和密码都在里面。4.2 启动步骤启动步骤很简单三步# 1. 创建数据库并导入SQL脚本 mysql -uroot -p schema.sql # 2. 修改配置文件中的数据库账号密码和存储路径 # 3. 启动项目 mvn spring-boot:run启动成功后访问http://localhost:8080/login输入测试账号密码就会进入合同列表页面。整个启动过程耗时取决于 Maven 拉依赖的速度首次拉包可能要多等一会儿。4.3 演示流程新建合同、签署、查看结果进入系统后我建议按下面流程走一遍完整业务点击上传合同选择一张图片或一个 PDF 文件填写合同标题系统会自动生成合同编号。回到合同列表点击去签署进入签署页面。在签名画板上用鼠标写名字或者用手指在触屏上写。写的时候如果觉得笔迹太细可以调整笔触宽度参数。点击确认签署签名图片会被保存并自动叠加到合同右下角指定位置。签署完成后回到合同列表状态会变成签署中或已完成点击查看就能看到带签名的合同效果。下载签署后的合同文件本地打开检查。这个流程走下来整个系统的主链路就通了。整个过程不需要额外装任何前端依赖直接浏览器操作。我本地实测完整签署流程大概 2 秒完成性能瓶颈主要在文件 IO 和图片合成上对于内部使用完全够。5. 常见问题与排查技巧实录这个系统开发过程中我踩了不少坑挑几个有代表性的记录下来大家照着排查能省很多时间。5.1 中文乱码问题上传合同标题和签署人姓名如果是中文保存到数据库后可能变成问号。这个问题的根源有两个一是数据库表字符集不是 utf8mb4我在建表语句里特意加了DEFAULT CHARSETutf8mb4二是 JDBC 连接串没有指定编码解决办法是在application.yml里加上characterEncodingutf8。另外上传 PDF 转图片时如果 PDF 里嵌了特殊字体渲染出来的图片可能中文缺字这种情况需要在服务器安装对应字体比如fonts-wqy-zenhei。5.2 内存溢出问题PDF 转图片和图片合成都是内存密集型操作如果合同文件很大JVM 默认内存不够会直接抛OutOfMemoryError: Insufficient Memory。我的处理方式分两层第一层是限制上传文件大小单个合同不超过 20MB第二层是调整 JVM 启动参数。如果你是用 IDEA 启动在 VM options 里加-Xms256m -Xmx1024m如果打成 jar 包部署启动命令改成java -Xms256m -Xmx1024m -jar sign-system.jar另外图片合成时我用完BufferedImage会立刻flush()并置 null让 GC 尽快回收大对象。不要等到方法结束才释放否则连续签署几份大合同后内存会像滚雪球一样涨。5.3 Thymeleaf 模板渲染问题Thymeleaf 的常见坑有两个。第一个是严格模式默认模板标签没闭合会直接报错不想被这个卡住可以在配置里关掉严格校验spring: thymeleaf: mode: HTML第二个坑是日期格式化。后端传给页面的 Java 8LocalDateTime直接用${contract.createTime}渲染会输出一串对象地址。解决办法是在实体类加注解或使用工具类格式化JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;或者前端在 Controller 里转成格式化字符串再封装给页面。5.4 打包部署到服务器的问题项目在本地跑得很正常打包部署到服务器却起不来我遇到最多的两种情况。一是用高版本 JDK 编译后在低版本 JDK 上运行报UnsupportedClassVersionError解决方案是 Maven 编译时锁定 release 版本。二是 jar 包启动时找不到配置文件里的绝对路径比如D:/xxx在 Linux 上根本不存在所以部署到 Linux 时要改成 Linux 路径比如/home/sign-system/files/。如果是 Docker 部署记得把文件存储目录挂载到宿主机否则容器重建后数据全丢。6. 项目扩展与优化建议6.1 电子签名的可信度与合规性思考这里必须说清楚一件事自己做的手写签名系统并不能等同于有完全法律效力的电子签章。真正的电子签名需要对接权威 CA 机构、数字证书、时间戳服务走《电子签名法》认可的流程。如果是企业内部确认、流程审批、对账回传这种场景这套系统产生的签署记录操作人、时间、IP、哈希值已经足够提供追溯能力但如果要对外签正式商务合同建议对接正规电子签平台或者在自己的系统里增加数字证书验签能力。如果要在现有系统上增强可信度有两个低成本方案一是增加短信验证码二次确认签署人必须输入手机验证码才能完成签署二是在签署记录上增加文件哈希校验定期对已归档合同做完整性校验发现文件被篡改立即告警。6.2 还可以扩展哪些功能这套系统目前是最小可用版本如果要往生产级演进可以从几个方向扩展多级审批流合同发起后需要部门负责人、法务、总经理逐级审批需要引入工作流引擎如 Flowable。批量签署一次导入几十份合同自动把同一人的签名批量盖上。合同模板化做一份标准合同模板替换占位符内容后一键生成合同。消息通知签署完成或合同逾期未签时通过邮件或企业微信通知相关人。对接企业微信/钉钉用户体系直接复用企业组织架构省掉单独账号管理。我自己在后续迭代中先加了消息通知和哈希校验因为这两个功能最能体现电子签的价值。消息通知让签署方不会漏掉待办哈希校验让归档合同的可信度提高了一个档次。最后分享一个开发心得做这种业务系统最忌讳一开始就奔着炫技去。用最稳妥的技术组合把核心业务链路跑通再把安全性和体验细节补上这才是最务实的路线。SpringBoot Thymeleaf 这套组合在中小型内部系统里短期内依然是很能打的方案。如果你在跑这套代码时遇到问题优先检查文件路径、数据库字符集和 JVM 内存这三个点大多数坑都出在这里。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进