☰
Spring Boot集团合同管理系统开发实战:全流程设计与部署
2026/10/7 16:44:28 网站建设 项目流程

做集团合同管理系统那阵子,法务部的同事隔三差五就在群里喊“帮我拉一下下个月到期的合同清单”,业务部门的Excel表格每个季度换一版,版本号早晚对不上。我就在想,与其让信息继续散落在个人电脑和微信聊天记录里,不如干脆用Spring Boot把这套东西彻底线上化。于是就有了这套完整的集团合同管理系统——它不是一个空壳演示项目,而是真正能跑通“合同登记-审批-生效-到期提醒-归档查询”全流程的工程化系统,自带源码、数据库脚本、开发环境配置和调试部署说明,还附带覆盖设计说明、数据库设计、核心代码解析的论文文档,对做毕设或者企业信息化刚起步的技术团队来说,是很好的参考样板。

整个系统从技术选型到数据库建模,从权限设计到部署上线,每一步都有值得复盘的地方。下面按我的实际开发顺序,把核心设计思路、关键代码实现、踩坑实录和调试部署细节完整拆开讲。

1. 项目定位与技术选型解析

1.1 集团合同管理到底在管什么

很多刚接触这类业务的人会有一个误区,觉得合同管理不就是建个表把合同信息录进去吗?真做起来完全不是这么回事。集团型企业的合同管理,表面看是“登记一份合同”的简单动作,实际牵扯到合同的主体信息、相对方信息、业务部门归属、金额条款、收付款计划、审批流转、附件归档、到期预警等多个维度。

我最初梳理需求时,把核心诉求归纳成四类:

  • 台账统一:所有合同不再分散在个别人的Excel里,统一收录到系统,按合同编号、类型、部门、时间多个维度查询统计。
  • 流程规范:合同不是录入就算完,要经过业务员发起、部门主管审批、法务复核等环节,每一步都有痕迹。
  • 权限隔离:子公司只能看子公司的合同,总部能看全部;普通员工不能删改他人的合同记录。
  • 风险提醒:合同快到到期日时自动预警,避免忘记续签造成被动局面。

这套Spring Boot合同管理系统,就是围绕这四类需求做的。合同的核心生命周期包括起草、审批、生效、履行中、到期、归档,每个环节都有对应的代码实现。

1.2 为什么选Spring Boot这一套技术组合

技术选型上我几乎没有犹豫,直接定了Spring Boot + MyBatis Plus + MySQL + Spring Security + Vue这套组合。原因很实际:

Spring Boot本身简化了配置和部署,内嵌Tomcat,一个jar包就能跑起来,不用像SSH时代那样折腾一堆XML配置文件。MyBatis Plus在单表CRUD场景下几乎不用写SQL,内置分页插件、条件构造器,开发效率肉眼可见地提升。MySQL依然是中小型项目和毕设场景下最普及、最好找资料的关系型数据库,不管是表结构设计还是问题排查,网上都有大量方案可以借鉴。

权限和安全性这块,Spring Security是Java生态里绕不开的成熟框架,配合JWT做无状态认证,前端拿到token后每次请求带上,后端通过拦截器验证,实现登录鉴权和接口级别的权限控制。前端选择Vue + Element UI,是因为Vue的中文文档和社区资料丰富,Element UI的组件风格也非常适合做后台管理系统——表格、表单、弹窗、分页这些全是现成的,不需要从零造轮子。

实际开发时这套组合还有个隐藏优势:如果做毕设,论文里写“系统基于Spring Boot框架,采用前后端分离架构,后端使用Spring Security进行安全管控”,这一句的含金量就非常足。如果做企业内部系统,这套技术栈的后续维护成本也很低,招聘Java开发基本都会。

1.3 项目结构与目录规划

项目采用前后端分离结构,后端一个Spring Boot工程,前端一个Vue工程。后端目录按经典分层组织:

src/main/java/com/group/contract/ ├── controller/ # 控制层,接收前端请求 ├── service/ # 业务逻辑层,核心代码都在这里 │ └── impl/ ├── mapper/ # MyBatis Plus的Mapper接口 ├── entity/ # 数据库实体类 ├── dto/ # 请求/响应数据结构 ├── vo/ # 前端展示对象 ├── config/ # 配置类(Security、CORS等) ├── common/ # 统一返回结果、异常处理、常量类 └── ContractApplication.java # 启动类

前端Vue工程按下述结构组织:

src/ ├── api/ # 接口请求封装 ├── views/ # 页面组件(合同列表、审批、统计等) ├── router/ # 前端路由 ├── store/ # 状态管理 └── main.js

分层的意义在于:后续如果要把单机部署改成分布式,或者把MySQL换成别的数据库,改动被限制在某一层内,不会牵一发动全身。对学习者来说,这种结构也很容易顺着请求链路把流程读通:页面按钮 → controller接口 → service业务逻辑 → mapper操作数据库。

2. 数据库设计:合同系统的地基工程

2.1 核心表设计与字段规划

数据库设计是这套系统最花心思的部分。合同管理涉及的数据实体,远比想象中多。我把核心表定为这几张:

合同主表(contract)

字段名类型说明
idbigint主键,自增
contract_novarchar(50)合同编号,如HT-2024-001
contract_namevarchar(200)合同名称
contract_typevarchar(50)合同类型(采购/销售/租赁/劳务等)
party_avarchar(200)甲方名称
party_bvarchar(200)乙方名称
contract_amountdecimal(15,2)合同金额
sign_datedate签订日期
start_datedate生效日期
end_datedate到期日期
dept_idbigint所属部门ID,用于数据隔离
statusint合同状态(0草稿 1审批中 2已生效 3已驳回 4已归档 5已终止)
create_byvarchar(50)创建人
create_timedatetime创建时间
update_timedatetime更新时间
remarkvarchar(500)备注

附件表(contract_attachment):存合同扫描件、补充协议,字段包括附件名称、附件路径、上传人、上传时间、关联合同ID。

审批记录表(contract_approval):记录每一步审批意见,字段包括合同ID、审批人、审批节点等级、审批结果(通过/驳回)、审批意见、审批时间。

收付款计划表(contract_payment):记录合同的付款/收款节点,字段包括合同ID、款项名称、计划金额、计划日期、实际完成日期、状态。

用户表(sys_user)和角色表(sys_role):标准的RBAC权限模型,用户和角色多对多,通过中间表关联。

2.2 为什么状态字段用数字而不是字符串

设计时我特意把status字段定义成int型而不是varchar存“草稿”“审批中”这种中文。原因有两个:

第一,数据库检索效率高,整数等值比较比字符串匹配快得多,合同表数据量上来之后,这个差异会越来越明显。第二,状态控制在Java端用枚举实现,代码里写ContractStatusEnum.EFFECTIVE.getCode(),语义清晰,不会出现“草稿”和“草 稿”这种因空格导致的脏数据。

状态流转我用了一个简单的状态机概念来约束:

草稿(DRAFT) → 审批中(PENDING) → 已生效(EFFECTIVE) → 已归档(ARCHIVED) ↓ ↓ 驳回(REJECTED) 终止(TERMINATED)

状态机的核心思路是:不是每个状态之间都可以互相跳转,比如一份已归档的合同不能直接被改成草稿。在Service层写状态流转校验方法,如果目标状态不在允许转换的集合里,直接抛出业务异常。

2.3 数据权限怎么隔离

集团系统最核心的权限需求是“子公司之间不能互看数据”。我在用户表里增加了dept_id字段(部门ID),然后在所有数据查询的Service层强制拼接数据权限条件。

实现方式很简单:获取当前登录用户,判断其角色是“集团管理员”还是“子公司普通用户”。集团管理员查询不限制dept_id,普通用户查询自动加上WHERE dept_id = 当前用户部门ID。这个逻辑统一封装在BaseService或者公共查询方法里,保证不会出现某个查询接口漏加权限条件导致越权。

接口权限则依靠Spring Security的@PreAuthorize("hasRole('ADMIN')")注解,在controller方法上声明访问所需的角色。前端根据用户角色动态生成菜单,普通业务员看不到“系统管理”这个入口。

3. 核心功能模块的代码实现

3.1 合同录入与分页查询

合同录入页面对应的后端接口,核心逻辑无非是参数校验、数据落库、附件处理三步。我习惯用DTO接收前端参数,避免前端多传字段直接灌到实体类里。字段校验用@NotBlank、@NotNull这些注解,金额用@DecimalMin(value = "0.01")。

分页查询用的MyBatis Plus的Page对象:

public PageResult<ContractVO> pageContracts(ContractQueryDTO dto) { LambdaQueryWrapper<Contract> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(dto.getContractName()), Contract::getContractName, dto.getContractName()) .eq(dto.getContractType() != null, Contract::getContractType, dto.getContractType()) .eq(dto.getStatus() != null, Contract::getStatus, dto.getStatus()) .between(dto.getStartDate() != null && dto.getEndDate() != null, Contract::getSignDate, dto.getStartDate(), dto.getEndDate()) .orderByDesc(Contract::getCreateTime); Page<Contract> page = contractMapper.selectPage(new Page<>(dto.getPageNum(), dto.getPageSize()), wrapper); // 转VO返回 }

这里有个经验点:列表查询不要SELECT *把大字段(比如remark、存储路径)也查出来,徒增网络传输压力。转VO的时候按需组装字段即可。

3.2 审批流的简化实现

很多初学者提到审批流就想到Activiti、Flowable这类重量级工作流引擎。但集团合同管理的审批场景,绝大多数是固定链路:业务员提交 → 部门主管审批 → 法务或总经理审批,层级固定、节点清晰,用工作流引擎反而是杀鸡用牛刀,部署复杂度高,学习成本也不低。

我的方案是用审批记录表自己控制流转。提交审批时创建一条审批记录,level表示当前审批层级,status表示待审批/已通过/已驳回。审批人提交意见时更新记录:

public void approve(ApprovalDTO dto) { ContractApproval approval = approvalMapper.selectById(dto.getApprovalId()); if (!approval.getApproverId().equals(currentUserId())) { throw new BusinessException("当前用户无权处理该审批任务"); } approval.setResult(dto.getResult()); // 1通过 2驳回 approval.setComment(dto.getComment()); approval.setApprovalTime(new Date()); approvalMapper.updateById(approval); // 如果当前是最后一级审批,合同状态改为已生效 if (dto.getResult() == 1 && isLastLevel(approval.getLevel())) { Contract contract = contractMapper.selectById(approval.getContractId()); contract.setStatus(ContractStatusEnum.EFFECTIVE.getCode()); contractMapper.updateById(contract); } else if (dto.getResult() == 2) { Contract contract = contractMapper.selectById(approval.getContractId()); contract.setStatus(ContractStatusEnum.REJECTED.getCode()); contractMapper.updateById(contract); } }

判断最后一级的方式是在合同类型表或者合同类型枚举里配置approval_levels字段,比如销售合同需要2级审批,那么level=2就是最后一级。这样写代码逻辑直观,数据库也多不了几张表,而且完全满足业务需求。如果你面向的毕设题目明确要求用工作流引擎,那另说,但那是为框架而框架,不值当。

3.3 到期提醒:定时任务怎么做到不遗漏

合同到期提醒是法务同事最看重的功能。我的实现方案很简单:Spring Boot的@Scheduled注解写一个任务,每天凌晨扫描所有end_date在30天内且status为“已生效”的合同,把提醒记录插入message表,用户登录后在系统首页看到醒目的待办提醒。

@Component public class ContractExpireTask { @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void scanExpiringContracts() { // 查询30天内到期且状态生效的合同 List<Contract> list = contractMapper.selectExpiringContracts(); for (Contract contract : list) { messageService.notifyContractExpire(contract); } } }

注意,@Scheduled是单机任务调度,如果系统后续部署多个实例做负载均衡,同一个任务会被每个实例执行一遍,产生重复提醒。解决思路有两种:一种是用Redis的分布式锁,抢到锁的实例才执行;另一种是直接用XXL-Job这类分布式任务调度平台。对当前阶段来说,单机版本足够,但我在代码注释里做了说明,这样别人看代码时不会以为这是个疏漏。

3.4 附件上传:路径别写死

系统允许上传合同扫描件PDF,我用的是本地磁盘存储:配置一个file.upload-path属性,用MultipartFile.transferTo()把文件写入指定目录,数据库附件表保存相对路径。

上传这块实际踩过一个坑:一开始把存储路径写死成D:/contract-files/,后来项目从Windows开发机器迁到Linux测试服务器,路径直接失效,所有历史附件全部访问不了。后来改成file.upload-path配置项,区分开发环境和生产环境,才彻底解决。这个点也在下面第5章专门展开。

4. 调试部署与开发环境配置

4.1 本地开发环境这样搭不会乱

先把环境按版本锁死,防止出现“在我电脑上能跑”的问题:

  • JDK 1.8+(我用的8,稳妥)
  • Maven 3.6+
  • MySQL 5.7+(8.0也可以,但驱动和连接串要对应)
  • Node.js 14+(前端编译用)
  • IntelliJ IDEA(后端) + VSCode或IDEA(前端)

MySQL导入数据库脚本时,注意先创建数据库再导入:

mysql -uroot -p create database contract_system default character set utf8mb4; use contract_system; source /path/to/contract_system.sql;

4.2 后端启动参数与常见配置项

后端application.yml中有几个配置项是必须检查的:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/contract_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver

这里有个特别容易被坑的点:MySQL 8.x的驱动必须要加allowPublicKeyRetrieval=true,否则连不上会报Public Key Retrieval is not allowed。很多用户卡在数据库连接失败,十有八九是这个问题。

后端启动直接在IDEA里运行ContractApplication类,控制台出现Started ContractApplication in x.xx seconds就说明启动成功。也可以命令行方式:

mvn spring-boot:run

4.3 前端启动与跨域处理

前端在项目根目录执行:

npm install npm run serve

启动后默认端口是8080,和后端冲突。我习惯把前端端口设定成8081,在vue.config.js里修改,并配置代理转发解决跨域:

module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };

这样前端请求/api/contract/list会被代理到后端8080/api/contract/list,浏览器无跨域问题,后端也不需要额外配置CORS。但如果前后端分开部署(比如前端Nginx、后端独立jar),CORS配置就得在后端加上,否则浏览器会拦截响应。

4.4 服务器部署:jar包方式最省心

部署到服务器时,我不推荐在服务器上装Maven现场编译,而是本地打包好再上传:

mvn clean package -DskipTests

打包产物在target目录下是xxx.jar,上传到服务器后:

nohup java -jar contract-system.jar > run.log 2>&1 &

用nohup后台运行,日志重定向到run.log方便排查问题。前端打包:

npm run build

产物是dist目录,放到Nginx的html目录下,配置一个反向代理把/api转发到后端即可。

5. 常见问题排查与踩坑记录

5.1 启动闪退或者端口被占用

后端启动直接闪退,最常见的原因就是端口被占用。Spring Boot的启动日志会显示Port 8080 was already in use,这时分两步排查:先看是谁占了端口,再决定是kill掉占用进程还是改端口。

# Linux netstat -tlnp | grep 8080 # Windows netstat -ano | findstr 8080

找到占用进程后,如果是无关进程直接kill。如果想改端口,直接改application.yml里的server.port,注意前端代理配置也要同步修改。

5.2 数据库连接失败:百分之八十是URL问题

我自己排查过很多次数据库连接问题,经验是:先确认MySQL服务启动,再确认账号密码,最后检查连接串。

Access denied for user 'root'@'localhost'就是账号密码错误或者本地host认证方式不对,去MySQL重新授权。

Unknown database 'contract_system'就是数据库没创建成功,回到第4.1节确认。

Public Key Retrieval is not allowed就是MySQL 8的认证插件问题,加allowPublicKeyRetrieval=true。

The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized就是时区问题,连接串加serverTimezone=Asia/Shanghai。

5.3 前端请求接口一直401

登录页能打开,但调接口报401,通常不是密码错了,而是JWT token没带上或者token过期。前端在请求拦截器里统一加Authorization头:

config.headers.Authorization = 'Bearer ' + getToken()

Spring Security默认放行的路径要配置好:登录接口、验证码接口放行,其他接口都要校验。如果某个接口测试时不用token能直接访问,说明Security配置里这个路径被放行了,需要检查。

还有个隐蔽问题:Spring Security开启csrf后,POST请求不带csrfToken会被拦截,报403。前后端分离项目用JWT认证,基本可以关掉csrf,不然每次请求都要额外处理token,麻烦但又没实际收益。

5.4 文件上传后访问404

本地开发上传文件没问题,部署到服务器后上传成功但访问图片/PDF提示404。核心原因大多是上传目录和访问路径映射不一致。

我最终的解决方案是:上传目录用配置项指定,然后写一个资源映射配置,把URL路径映射到物理目录:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("file:" + fileUploadPath + "/"); }

这样前端访问/files/xxx.pdf就能直接读取本地文件。注意file:前缀的路径最后要加一个斜杠,丢了斜杠映射会失效。

5.5 定时任务不执行

@Scheduled注解在类上不生效,最常见的原因是启动类或配置类忘了加@EnableScheduling。加了也会遇到不执行的情况,那就是cron表达式写错了。0 0 2 * * ?是每天凌晨2点整,如果写成0 0 2 * * *(秒位多一位),Spring Boot的解析器直接报错启动失败。

6. 这套系统做完后的几点经验

系统整个开发周期大约三周,数据的规范化管理和权限的严格隔离是最耗精力的部分。我在实际使用中发现,无论功能写得再好,如果数据录入不规范——比如合同类型随意填、日期格式混乱,后端的统计功能就全是废的。所以真正上线前,数据清洗和录入规范培训远比写代码重要。

如果你拿这套系统做毕设,我建议在论文里重点写三个部分:数据库为什么这么设计、Spring Security如何实现前后端分离认证、合同状态机如何保障数据一致性。这三个点既是你代码里的亮点,也是答辩老师最爱问的方向。

最后再分享一个扩展思路:当前系统用的是本地文件存储,后续可以对接对象存储服务做持久化,合同附件就不怕服务器磁盘损坏了;电子签章接口也可以对接,签完章自动归档。走上这条路之后,这套系统从内部管理工具升级成完整的合同全生命周期管理平台,也就是水到渠成的事了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询