
简介这是一套基于Java Web技术构建的企业级合同管理系统的完整源码包面向Java初学者、Web开发学习者及中小型企业管理者旨在解决合同创建、审批、执行、到期提醒与审计追踪等全生命周期管理难题。资源共941个文件涵盖253个HTML页面前端界面、203个CSS样式表UI美化、157个PNG图标视觉资源、103个JS脚本交互逻辑、24个JSP动态页服务端渲染以及16个核心Java类如ContractAction、UserInfoDAO、ExportExcelUtil等完整呈现MVC分层架构与典型业务模块实现压缩包大小为16.18MB。已有1007人学习下载提供开箱即用的免费版系统含登录认证、合同增删改查、Excel导出、身份证校验、验证码生成等实用功能代码结构清晰、注释规范适合作为Java Web课程设计、毕业项目参考或企业轻量级合同管理落地原型。 做合同管理系统这事是被一次扎帐事故逼出来的。当时公司的合同台账就是一张Excel业务、财务、法务各存一份线上签字流程走的是企业微信聊天记录。年底财务扎帐时发现某供应商框架合同已经过期两个月物流部门还在按旧合同结算多付了六万多。领导把这事扔给我让我上一套系统。我当时的方案很直接用Java自己写一套合同管理系统做成免费版本内部先用起来顺手也能开源给同行参考。这套系统解决的是企业合同全生命周期的管理问题从起草、审批、签订、履约到归档全环节线上化配合到期提醒和台账统计把合同散落在个人电脑、续签靠人催、数据全靠Excel的乱象拉到可控范围。如果你正在做Java Web方向的项目或者公司正在选型合同管理开源方案这篇文章的完整思路和代码细节可以直接抄作业。1. 项目背景与整体设计思路1.1 上线前我先拆解出的四个核心痛点做之前我专门和业务、法务、财务三方聊了一圈把需求收敛成了四类。第一合同不统一。有的合同在业务员电脑里有的在钉钉群里有的干脆只有纸质版审计来了找不到原始文件。第二审批流程失控。谁批了、谁没批、卡在哪个环节全靠问一单合同的审批周期经常拖到两周。第三履约节点遗忘。到期续签、分期付款、验收节点没有任何提醒过期了才知道损失只能认。第四统计困难。临到汇报时想知道某供应商今年签了多少合同、总金额多少Excel透视表做得人想哭。这四个痛点直接定义了系统的四大模块合同文件管理、审批流、履约提醒、统计台账。这里有个我从这个项目里悟出来的设计原则做业务系统永远先理痛点再设计功能否则做出的东西没人用。我见过太多系统一上来就堆功能最后业务方根本不买单。1.2 技术选型为什么押注Java现在微服务、Go、Node都能做系统但选Java不是情怀是综合成本最低。原因有以下几点。第一生态成熟招人成本低。公司招聘后端Java简历永远最多出问题找人接手容易。第二面向对象编程的思想和业务模型天然匹配——合同、用户、角色、审批记录这些实体用类和接口去建模非常顺手尤其是后面设计RBAC权限模型时OOP的优势体现得很充分。第三配套方案齐全Spring Security做权限控制、MyBatis做持久层、Redis做缓存、Quartz做定时任务都有成熟方案不用自己造轮子。另外一个现实考量系统要免费开源给同行用Java部署的通用性最好。你发一个Spring Boot的jar包对方只要有JDK就能跑Windows和Linux都行这比很多技术栈的部署门槛低太多。开源社区里Java管理类系统的代码质量和活跃度也最高遇到问题能抄的作业多。1.3 免费版的设计边界什么该做什么不该做免费版不等于功能残缺而是要克制边界。我一开始也想全功能铺开比如电子签章、OCR识别、合同文本比对后来发现这些都涉及第三方收费接口免费版强行做进去别人部署后还要买服务体验很差。所以免费版我圈定了四件事完整的合同全生命周期管理、灵活的审批流配置、到期/付款提醒、多维度统计。电子签章对接、OCR识别、复杂比对这些则预留了接口位但不做主功能。这个取舍很重要——一套免费系统最怕的就是什么都想沾结果每个模块都是半成品。我见过太多开源项目死在功能膨胀上作者一个人维护不过来社区也散掉了。做免费版一定要明确最小可用闭环先让大家用起来。2. 技术选型与架构设计2.1 后端技术栈明细这套系统的后端技术栈清单如下JDK 8正式环境用的1.8.0_202稳定优先不追新Spring Boot 2.7.18免费版用这个版本足够不需要硬上Spring Boot 3MyBatis-Plus 3.5.x单表操作不用写SQL开发效率提升明显MySQL 8.0存储合同元数据文件本体放磁盘Redis 6.x缓存登录token、权限列表、热点数据JWT Spring Security做登录认证Hutool工具库日期、加密、Excel导入导出都用它省去大量工具方法定时任务直接用Spring自带的Scheduled不引入XXL-Job这套组合的特点是不装懂——没有引入微服务、分布式事务、工作流引擎就是一个单体应用加缓存和数据库复杂度可控。免费版用户拿到代码也看得懂有问题能在社区里直接讨论。我在设计时反复提醒自己一个管理系统的核心价值是业务流程清晰不是技术栈炫技。2.2 前端方案前后端分离还是服务端渲染一开始我在前端方案上纠结过。直接用Thymeleaf服务端渲染最简单Spring Boot集成度高但合同管理这个场景要做富交互比如审批流转、批量操作、图表统计服务端渲染会写出一堆难以维护的JS后期改需求非常痛苦。最终选了前后端分离Vue 2 Element UI 做管理后台Nginx做静态资源服务器后端只提供RESTful API。这套方案对Java开发者来说需要额外学一点前端但收益很大——审批页面、统计图表、合同列表这些交互密集的页面用组件化前端做体验好太多。项目里有一个常见误区老想着前后端都自己写结果Vue和Java两头都稀松。我的建议是前端只用Element UI的现成组件不折腾二次封装把精力放在后端业务上这才是Java团队出活最快的方式。2.3 数据库设计合同数据如何建模合同管理系统的核心表不多但设计得好不好直接影响后续开发难度和维护成本。我在这版里一共设计了8张表核心的是下面这四张。第一张是 contract 表合同主表字段包括合同编号、合同名称、合同类型采购/销售/租赁/框架、甲方乙方单位名称、合同金额、签订日期、生效日期、到期日期、负责人ID、状态草稿/审批中/已生效/已归档/已作废、附件路径等。金额用 decimal(18,2)日期用 date状态用 tinyint。这里有个Java基础层面的坑很多新手会把金额用double存等出现精度问题、结算对不上时哭都来不及金额类字段一律用BigDecimal这是面试和实战都会考的基本功。第二张是 contract_template 表合同模板表字段有模板名称、模板类型、模板文件路径、版本号、创建人、创建时间。模板用文件方式存储不做在线编辑成本低、实现简单对业务完全够用。第三张是 approval_record 表审批记录表这是审批流的核心合同ID、审批人ID、审批动作同意/驳回、审批意见、审批时间、节点顺序。有了这张表合同的审批历史就能完整还原审计的时候也能说清楚。第四张是 sys_user、sys_role、sys_user_role 三张表用户、角色、关联表标准RBAC模型。用户表只存基本信息角色表存角色名称菜单权限挂在角色上通过关联表把关系串起来。2.4 RBAC权限模型一个能写进java面试题的案例权限设计上我用了最经典的RBAC模型用户 - 角色 - 菜单权限。这里的核心思路是用户和角色是典型的多对多关系用两个实体类加一个关联实体完美映射数据库三张表。Java面向对象编程里的继承、接口、组合这些特性在权限模型中用组合的方式最合适——用户拥有角色角色拥有权限关联关系用List字段表达业务代码里直接通过角色集合去判断是否有菜单权限非常清晰。这套权限模型其实也是一个很经典的java面试题如何设计一套权限系统你在面试时把表结构、实体类关系、接口鉴权流程说清楚再提一句我用组合而不是继承来实现用户和角色的关系比单纯背八股文有说服力得多。很多Java八股文里写的组合优于继承在这个场景里就是活生生的案例。3. 核心功能模块实现3.1 合同模板与起草模块起草合同我做了两个入口从模板创建和直接新建空白合同。从模板创建的流程是用户选择模板文件Word系统复制模板文件到当天日期目录同时生成一条合同记录状态置为草稿。新建合同时前端做一个合同编号预生成的按钮避免用户手输编号因为手输一定会出现重号这是我在老系统里踩过的坑。合同编号生成规则我做了个可配置的生成器默认规则是HT 年月日 3位流水号如HT20250112001。流水号不能用SQL查表count因为并发下一定会出问题我直接用Redis的INCR做自增序列一天一个key过期时间24小时既保证当天流水从1开始又避免并发冲突。3.2 审批流设计状态机比工作流引擎更实用审批流是这类系统的灵魂。免费版我没有上Activiti、Flowable这类工作流引擎原因是它们对业务人员来说太重配置复杂、学习成本高而且会把系统的部署和维护门槛拉高。我用一张审批记录表加代码里的状态机来驱动完全够用。状态机逻辑很清晰草稿 - 提交审批 - 法务审批 - 财务审批 - 生效。每一步都对应一个状态码审批动作只有两个同意进入下一个状态或驳回回到草稿状态并记录驳回意见。我用枚举类定义状态和可执行动作这比在业务代码里堆if-else清晰太多。approve()方法的核心是先校验当前用户是否有当前节点的审批权限再根据动作更新状态最后插入一条审批记录整个方法用Transactional包住保证数据一致性。这个精简版状态机面试时特别好讲。面试官问状态机怎么设计你把这个场景讲清楚再补充一下为什么不用工作流引擎、状态和动作怎么建模比单纯背概念强得多。3.3 履约提醒定时任务与消息中心履约提醒是用户真正离不开的功能。我在系统里建了一张 reminder_config 表用户可以对每份生效合同设置提前N天提醒比如提前30天提醒续签、提前7天提醒付款。后端用Spring自带的Scheduled注解每小时的整点跑一次任务扫描所有状态为已生效且到期日期 - 提醒天数 当前日期的合同给合同负责人生成一条系统消息同时可以配置邮件通知。用JavaMail发邮件免费版只做邮件短信要花钱接第三方不做。消息表是 sys_message接收人ID、内容、是否已读、创建时间。这里有个特别值得说的坑一开始我用Scheduled(fixedDelay 60 * 60 * 1000)固定间隔跑部署后发现服务器时区是UTC导致提醒时间整体偏了8个小时。后来统一在数据库连接串和JVM启动参数里都指定了时区同时把任务设计成幂等同一合同同一天只提醒一次这个问题才彻底解决。上线后最容易出问题的往往不是功能本身而是环境差异。3.4 统计台账聚合查询与排序的真实场景财务和法务最爱用的是台账统计模块。按供应商维度统计合同金额、按类型统计数量、按月份统计签署趋势这些本质上都是SQL聚合查询MyBatis-Plus的QueryWrapper加groupBy就能搞定不需要上OLAP或者大数据那一套。我在这里提一个很有意思的细节。曾经有人在技术群里问合同列表按金额排序自己写个冒泡排序实现行不行这其实是个典型的java面试题误区。冒泡排序在面试题里练手完全没问题但真实业务里合同列表几万条数据用数据库ORDER BY加金额字段索引是毫秒级而冒泡排序的O(n^2)复杂度会直接卡死页面。排序这种事永远交给数据库不要自己写算法。这也是我在开发中反复强调的原则能用基础设施解决的绝不让业务代码去扛。统计报表我用ECharts画柱状图和饼图后端返回聚合后的List前端绑定数据即可开发快、效果也够用。4. 实操过程从零搭建的完整步骤4.1 环境准备JDK安装与java环境变量配置详细教程先说环境准备这是整个项目第一个拦路虎也是网上提问最多的地方。这套系统要求JDK 8以上如果你是从零开始JDK安装和环境变量配置是第一个坎这里给出一份详细教程。下载JDK 8的Windows版本以Win10为例安装到一个纯英文路径比如 D:\Java\jdk1.8.0_202。记住不要装到带空格或中文的路径这是很多新手踩坑的地方。安装完后很多人的第一反应是直接双击运行java但根本运行不了因为环境变量还没配。配置环境变量要动手操作打开系统属性 - 高级 - 环境变量在系统变量里新建 JAVA_HOME值为JDK安装路径比如 D:\Java\jdk1.8.0_202。然后找到Path变量新增两个值%JAVA_HOME%\bin 和 %JAVA_HOME%\jre\bin。注意这里是在列表里新增条目不是把值追加到原来的字符串末尾Win10以上系统都支持这种操作。配置完打开命令行输入 java -version能看出版本信息说明第一步成功再输入 javac -version有输出才是真正配置对了。很多人只配了java能运行但javac不识别就是因为Path里没加bin目录或者机器上装过多个JDK导致环境变量冲突。Linux环境更简单解压JDK到 /opt/java/jdk1.8.0_202编辑 /etc/profile 添加 export JAVA_HOME...export PATH$JAVA_HOME/bin:$PATH然后执行 source /etc/profile 生效。这一步值得截图存档。评论区里隔三差五就有人问java -version有输出但javac没反应基本都是Path变量配置的问题。4.2 创建Spring Boot工程骨架环境OK后用Spring Initializr创建工程。我这里用的是Spring Boot 2.7.18GroupID填 com.companyArtifactID填 contract-system依赖勾选 Web、MyBatis、MySQL Driver、Validation、Security。工程创建完第一件事是改配置文件 application.yml把数据库连接、Redis连接、文件上传目录这些基础配置写清楚。这里有一个关键点数据库连接串里一定要加上 serverTimezoneAsia/Shanghai不然部署后时间字段会差8个小时。这也是继环境变量之后新手最常遇到的环境类问题。配置完成后我的习惯是先写一个最简单的测试接口确认Spring Boot能启动、MySQL能连上再进入业务功能开发。这样排查问题时范围小很多——如果连测试接口都调不通就别急着开发业务功能。4.3 核心代码实现细节这里给三块核心代码的实现思路你拿到可以直接落地。第一块是合同编号生成。用Redis INCR实现并发安全的自增编号public String generateContractNo(String prefix) { String dateStr LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String key contract:no: dateStr; Long incr redisTemplate.opsForValue().increment(key); redisTemplate.expire(key, Duration.ofHours(24)); return prefix dateStr String.format(%03d, incr); }一天一个key过期时间24小时流水号每天自动重置。这一步解决了并发下的重号问题也彻底告别了先查表取最大编号再加1的隐患。第二块是审批状态机。我用枚举定义状态和允许的转移动作public enum ContractStatus { DRAFT(草稿), APPROVING(审批中), EFFECTIVE(已生效), ARCHIVED(已归档), CANCELED(已作废); private final String desc; ContractStatus(String desc) { this.desc desc; } }提交审批、同意、驳回这三个方法都先查当前状态再执行对应转移不符合转移规则就抛出业务异常。状态机的核心逻辑统一放在一个服务类里管理千万别在Controller里写业务判断不然代码烂得飞快。第三块是文件上传。合同附件是最重要的原始凭证上传后直接存到本地磁盘路径数据库只存相对路径。上传接口用MultipartFile接收限制后缀名为doc、docx、pdf大小限制10MB文件名用UUID重命名避免中文名乱码。文件路径按日期分目录比如 /data/contract-files/2025/01/12/xxx.pdf方便后续归档和备份。4.4 打包部署与Nginx配置开发完成后打包用Maven的package命令生成可执行jar。服务器上我用 nohup java -jar contract-system.jar --spring.profiles.activeprod 启动日志重定向到 contract.log方便后面排错。前端打包后的dist目录部署到Nginx。关键是配置反向代理将 /api 开头的请求转发到后端8080端口。前后端分离有跨域问题用Nginx代理解决比在后端写跨域配置更干净还能顺便做负载均衡和HTTPS终止。location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }部署全套下来我实测一台2核4G的云服务器跑这套系统毫无压力合同数量在10万条以内页面响应都在毫秒级。这套部署方案很轻但对免费版来说绰绰有余。5. 常见问题与排查技巧实录5.1 启动报错 java.lang.OutOfMemoryError: Insufficient Memory这个问题在免费版开源后收到最多的反馈。很多人部署后第一次启动就报 java.lang.OutOfMemoryError: insufficient memory第一反应是加内存参数但真正原因往往是服务器物理内存不够或者JVM堆和元空间配置不合理。我的排查流程是先用 free -h 看服务器剩余内存如果总内存不到2G先把 -Xmx 从默认值调小比如 -Xms256m -Xmx512m如果内存充足但还报错用 jps 看看是不是有多个Java进程在抢内存。还有一种是云服务器默认的swap过小可以临时加一个2G的swap文件兜底。这套排查思路在Java面试里也常作为JVM调优的经典题目出现核心就是不要一上来就乱加参数先确认瓶颈是物理内存还是JVM配置。5.2 审批并发导致的状态错乱上线后遇到一个真实问题两个审批人同时打开同一份合同一个点了同意另一个也点了同意结果状态被覆盖审批记录多了一条。根因是没有做并发控制。解决办法是在审批方法里加乐观锁——contract表加一个version字段update时带上 where version ?更新成功version加1更新失败提示合同状态已变更请刷新后重试。这是成本最低的并发方案也是面试里乐观锁最有说服力的实战案例。如果你在简历里写我解决过并发状态覆盖问题最好能把这个场景讲清楚。5.3 合同编号在并发下的重复问题这个问题跟审批并发类似但更隐蔽。一开始我用的是查表取最大值加1部署到多人使用后出现了重号排查了很久才定位到是并发问题。后来改成Redis INCR后解决。如果线上没有Redis一个替代方案是用数据库的唯一索引加INSERT ... ON DUPLICATE KEY做兜底或者用数据库自增序列。核心原则一句话在分布式多实例环境下永远不要用先查后插的方式生成唯一编号。5.4 文件上传失败和中文乱码文件上传失败最常见的原因是上传目录不存在以及Nginx配置的 client_max_body_size 太小。我给用户的排查建议是先手动创建上传目录并检查磁盘权限再用 curl 测试上传接口最后检查Nginx的 client_max_body_size 是否覆盖了文件大小。中文乱码问题同样常见。合同文件名如果是中文下载时Content-Disposition要设置 filename*UTF-8加上URLEncoder编码否则浏览器会乱码。数据库连接串里的 characterEncodingutf8 也要检查这是中文乱码问题的标准排查路径。我后来把上传的文件名统一改成UUID直接从根上绕过了中文文件名乱码的问题。6. 从这套系统反推Java学习路线与面试点6.1 合同管理系统覆盖的Java基础知识点做完这套系统你会发现它把Java核心知识串起来了集合框架用来做数据缓存和分页IO流处理文件上传下载多线程应对审批并发的场景JDBC通过MyBatis的封装体现在每个数据操作里面向对象编程贯穿权限模型和状态机的设计。如果你正在走java学习路线我建议不是按部就班啃完所有基础再动手而是带着这些具体的系统需求去学遇到哪个知识盲区就补哪个。比如我在写审批状态机时才真正理解枚举的用法在写合同编号生成器时才彻底明白事务和并发控制的关系。这种业务驱动学习的方式效率比纯看书高太多。6.2 面试中怎么讲这个项目如果你准备java面试这个项目完全可以作为简历上的主力项目。面试官大概率会问审批流怎么设计的为什么不用工作流引擎权限模型怎么实现的合同编号并发问题怎么解决的文件是存数据库还是磁盘每一个问题你都能拿出真实方案和踩坑经历这比背一百道java面试题都有用。这里分享一个我面试别人时最看重的点讲项目时主动说出来我用了Redis的自增序列解决合同编号并发问题这句话比简历上写精通Redis更有说服力因为它是真实的取舍过程不是八股文式的背诵。项目里的每一次技术选型背后都要有业务原因这才是面试官想听到的。6.3 后面还能怎么扩展免费版做完了线上稳定运行了三四个版本后续我在考虑几个扩展方向一是接入在线PDF预览用户不用下载就能看合同二是把Excel批量导入做完善帮客户把旧系统的合同数据一次性迁移过来三是加上合同文本比对功能这对租赁、采购场景特别刚需。做免费版开源项目最大的收获不是代码量而是用真实业务反推技术的经验积累。遇到一个业务问题去选择一个最合适的技术方案反复打磨这种实战能力不是靠刷题能得来的。如果你也在做类似的管理系统欢迎留言交流你最头疼的模块——审批流、提醒还是数据导入我们可以一起踩坑一起填坑。本文还有配套的精品资源点击获取