做合同信息管理系统,最烦的不是写代码,而是需求看着简单,一动工全是细节。最近刚带着这套基于SpringBoot的合同信息管理系统从框架搭建跑到上线交付,后端接口用SpringBoot,管理页面走Vue单页应用,合同台账、收付款计划、附件存储、到期提醒、多角色权限这些模块全都过了一遍。如果你正准备做类似的项目,或者刚拿到一套源码加配套文档和部署说明的项目包,想搞明白代码怎么和文档对起来、怎么真正跑通整个流程,这篇东西应该能帮你少走不少弯路。我会把设计思路、核心实现、部署经过和踩过的坑都摊开讲,照着走一遍基本就能交付。
1. 合同信息管理到底在管什么——需求拆解与技术选型
1.1 传统合同管理方式的痛点
在没有系统之前,大部分公司管合同靠的是Excel加网盘,甚至还有直接用文件夹按时间堆的。合同编号编到一半发现重号,审核状态得挨个问人,付款节点到了没人提醒,想查跟某家客户签过哪些合同得翻半小时聊天记录。这些场景看着不起眼,但一旦合同数量超过几百份,整个流程就开始失控。
所以我做这套系统时,第一件事不是写代码,而是先把管理对象拆清楚。合同管理系统的核心不是“一张表存着合同”,而是把合同的完整生命周期管起来:从相对方登记、合同拟定、审批归档,到履约阶段的收付款计划、到期提醒,再到后续的变更和续签。只有把这些环节都纳入一个闭环,系统才有实际价值。
1.2 系统的核心功能模块拆分
我将这个项目拆成四大块:基础数据层、合同业务层、履约提醒层、系统管理层。基础数据层负责公司内部机构、用户账号、往来单位(客户和供应商)的维护;合同业务层负责合同的新增编辑、审批状态流转、附件上传和全文关键字检索;履约提醒层负责收付款节点和合同到期时间扫描,生成待办事项;系统管理层负责角色权限、菜单配置和操作日志。
这里特别说一下往来单位模块。很多初写者容易忽略它,直接把客户名称塞进合同表里,结果后面统计“某客户所有合同”时只能写like去匹配,数据稍微一乱就查不全。我做了一张独立的customer表,合同表里只存customer_id,这样既方便核验单位统一信用代码,也方便后续做更多维度的统计分析。
1.3 为什么选择SpringBoot这套技术组合
技术选型上,后端选了SpringBoot 2.7.x加MyBatis-Plus,前端用了Vue2加ElementUI,数据库用的MySQL 5.7。这套组合不是最炫的,但胜在稳定和资料多,尤其适合需要快速交付、后续还要交给别人维护的项目。
SpringBoot的好处是省掉了大量XML配置,内嵌Tomcat,打个jar包就能跑;MyBatis-Plus则把单表CRUD、分页查询、条件构造器都封装好了,代码量能少三分之一。前端用Vue组件化开发,表单校验、弹窗、分页表格这些场景ElementUI都有现成组件,开发效率很高。
注意:如果你的目标环境是Java 8,SpringBoot请选2.x系列,不要图新鲜上SpringBoot 3。SpringBoot 3必须配Java 17,很多机器上部署会出问题,后续维护也是麻烦。
整套项目前后端分离部署:后端打jar包运行,前端打包成静态文件,再用Nginx做反向代理。这种方式的好处是接口和页面解耦,以后要加移动端或者对接其他系统,直接复用后端接口即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与权限模型——核心实现的底层支撑
2.1 合同主表字段设计
合同主表的设计直接决定系统能不能扩展。我建了一张contract_info表,下面是核心字段,实际开发时可以按需增减:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| contract_no | varchar | 合同编号,业务上唯一 |
| contract_name | varchar | 合同名称 |
| customer_id | bigint | 往来单位id,关联customer表 |
| contract_amount | decimal | 合同金额 |
| start_date | date | 开始日期 |
| end_date | date | 结束日期 |
| sign_date | date | 签订日期 |
| status | tinyint | 状态:0草稿、1执行中、2已完成、3已终止 |
| owner_id | bigint | 主办人id |
| file_path | varchar | 合同源文件存储路径 |
合同编号这里要注意,不能直接用自增id展示给用户,一般会拼一个规则:前缀加日期加序号,比如HT20250112-001。我用了一个独立编号生成方法,先查当天最大序号再加一,加唯一索引防并发重复。
金额字段用decimal(18,2),不要用float或double,否则对账时会出现莫名其妙的精度问题。
2.2 子表和关联表的规划
除了主表,至少要设计三张子表:合同阶段表、附件表、操作日志表。
合同阶段表存储合同从起草到归档的审批记录,包括节点名称、操作人、审批意见、操作时间,这样后续审计时能看到完整轨迹。附件表存附件名和存储路径,同时关联对应的合同id。操作日志表可以做成统一的系统日志,记录谁在什么时间新增、修改、导出了合同。
如果系统要做收付款计划,再加一张payment_plan表,字段包含计划名称、计划金额、计划日期、实际完成日期、状态。每次扫描时,判断“当前日期距离计划日期小于等于7天且状态未完成”的记录,生成提醒数据。
2.3 角色权限设计思路
权限这块是管理系统的重头戏。我没有引入Spring Security那套复杂的权限框架,而是用JWT加拦截器的方式自己做了一层轻量级鉴权。
系统里规划了四种角色:管理员、商务人员、财务人员、法务人员。管理员可以查看和管理全部合同;商务人员只能操作自己创建的合同;财务主要看收付款阶段的数据;法务负责审批对内的合同条款。角色不同,数据范围也不同。
JWT的设计很简单:用户登录成功后,后端生成一个token,里面带上userId、userName、roleCode,过期时间设为两小时。前端每次请求把token放到请求头里,后端拦截器先校验token是否存在,再解析出角色信息,放行到Controller里供业务逻辑使用。
java复制public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
Claims claims = JwtUtil.parseToken(token.replace("Bearer ", ""));
if (claims != null) {
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("roleCode", claims.get("roleCode"));
return true;
}
}
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"登录状态已过期,请重新登录\"}");
return false;
}
}
数据隔离上,商务人员查询合同时,SQL里强制带owner_id等于当前用户id这一条件;财务和法务则不受这个限制。这样的设计既简单又够用,比起把每个按钮的权限都做成一张权限表,这个体量的项目完全够用。
3. 实操过程——从搭建工程到功能落地的完整记录
3.1 开发环境与工程结构规划
我的开发环境是JDK 8、Maven 3.6.3、MySQL 5.7,IDE用的社区版IDEA。工程结构按标准分包:controller、service、mapper、entity、common、config、utils。
- entity:对应数据库表实体
- mapper:MyBatis-Plus的Mapper接口
- service:业务逻辑层
- controller:接口层,只做参数接收和结果封装
- common:统一返回值、异常处理、常量类
- config:跨域配置、拦截器注册、静态资源映射
- utils:JWT工具类、日期工具类等
建立工程后第一件事是配置数据源。注意application.yml里要加上时区参数serverTimezone=Asia/Shanghai,否则连接MySQL 8时会出现时间报错。另外,建议配置mybatis-plus的map-underscore-to-camel-case为true,这样数据库字段的下划线命名能自动映射到Java的驼峰属性。
3.2 统一返回结果与异常处理
前后端分离项目最忌讳的是每个接口返回的JSON格式不一致。我在common包里封装了Result对象,Code分别为200(成功)、400(参数错误)、401(未登录)、500(服务器异常),前端根据code做统一处理。
同时写了一个全局异常处理器,用@RestControllerAdvice注解拦截所有Controller层抛出的异常。业务异常手动抛出,系统异常统一记录日志并返回友好提示,避免把堆栈信息直接抛给前端。
这样做的收益在联调时特别明显。前端不用每种报错都看控制台了,接口返回结构固定,表格组件、弹窗提示都能统一处理,开发效率高不少。
3.3 合同列表分页和关键字检索实现
合同列表是使用频率最高的页面,必须做得顺手。我使用的是MyBatis-Plus的分页插件,先配置一个MybatisPlusInterceptor,注册PaginationInnerInterceptor,然后service层里通过lambdaQuery条件构造器拼装查询条件。
java复制public Page<ContractInfo> pageContract(int current, int size, String keyword, Integer status, Long customerId) {
Page<ContractInfo> page = new Page<>(current, size);
LambdaQueryWrapper<ContractInfo> wrapper = Wrappers.lambdaQuery();
if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w
.like(ContractInfo::getContractNo, keyword)
.or().like(ContractInfo::getContractName, keyword)
.or().like(ContractInfo::getRemark, keyword));
}
if (status != null) {
wrapper.eq(ContractInfo::getStatus, status);
}
if (customerId != null) {
wrapper.eq(ContractInfo::getCustomerId, customerId);
}
wrapper.orderByDesc(ContractInfo::getCreateTime);
return contractMapper.selectPage(page, wrapper);
}
前端表格用了ElementUI的el-table,分页用el-pagination,每次切换页码或搜索条件时重新请求接口。搜索框做了防抖,输入停止500毫秒后才发起请求,能有效减轻数据库压力。
需要提醒的是,列表查询不要用select *把所有大字段都查出来,比如合同文本内容如果存在数据库里,会导致列表加载很慢。我一般会把文件存到磁盘,数据库只存路径,列表查询也只select需要的字段。
3.4 合同到期提醒的后台任务实现
合同到期提醒是这类系统里特别拉好感的功能,实现起来也不难。我直接用Spring自带的Scheduled注解做定时扫描任务,每天早上8点执行一次。
java复制@Component
public class ContractRemindTask {
@Resource
private ContractInfoMapper contractInfoMapper;
@Scheduled(cron = "0 0 8 * * ?")
public void scanExpiringContract() {
List<ContractInfo> list = contractInfoMapper.selectList(new LambdaQueryWrapper<ContractInfo>()
.eq(ContractInfo::getStatus, 1)
.le(ContractInfo::getEndDate, DateUtil.offsetDay(new Date(), 30)));
for (ContractInfo contract : list) {
// 生成提醒记录,或调用消息通知接口
}
}
}
这里有两个细节。第一,别在定时器里直接发短信或者企业微信消息,万一扫描逻辑还没写完就先在测试环境炸了,容易打扰到真实用户。我建议先把待办记录插到remind表,用户登录后在首页“待办中心”看到,再接消息推送。第二,定时任务要和SpringBoot启动类上的@EnableScheduling搭配,别漏了注解。
如果是稍微复杂一点的场景,比如要求支持动态配置提醒周期,可以换成Quartz或xxl-job,但当前项目用Spring原生调度完全足够了。
3.5 合同附件的上传与下载
附件上传要考虑目录规划和文件重命名。我在配置里指定了一个上传根目录,按月份分子目录。文件名用UUID加原始扩展名,避免中文名在不同系统上乱码。
java复制String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString().replace("-", "") + ext;
String monthDir = new SimpleDateFormat("yyyyMM").format(new Date());
Path dir = Paths.get(basePath, monthDir);
Files.createDirectories(dir);
file.transferTo(dir.resolve(fileName).toFile());
上传成功后,返回文件id和访问路径给前端。前端download接口会根据文件id去数据库查路径,再把文件以流的形式写回浏览器。下载时要注意设置Content-Disposition响应头,避免浏览器把PDF或Word直接打开,用户想要的是下载保存。
4. 部署文档不是摆设——从打包到Linux服务器上线的实战记录
4.1 后端打包需要注意的坑
打包后端项目时,很多人直接双击Maven的package,等到build成功后发现jar包特别大,运行起来还报“没有主清单文件”。这通常是因为pom.xml里没有配置spring-boot-maven-plugin的repackage。
我在pom里加了如下配置:
xml复制<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.18</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
打包前还要检查application.yml里的数据库地址、文件上传路径是否改成服务器环境的值。我见过最惨的情况是打包时连本地数据库地址都没改,部署到服务器后接口一直报数据库连接失败,排查了一小时才发现是配置没覆盖。
4.2 数据库脚本导入与初始化数据
部署前一定要准备好建表SQL和基础数据SQL,并且要测试过一遍,不只是能执行成功,还要保证数据之间能关联起来。
我踩过的坑是,在本地开发时数据库用了utf8mb4字符集,但导出的SQL文件在服务器执行时没指定字符集,结果所有中文注释和字典数据都变成乱码。后来在导入命令里强制加了参数:
bash复制mysql -uroot -p --default-character-set=utf8mb4 contract_db < contract_db.sql
另外,建议把基础角色数据、管理员账号数据、菜单数据都写进SQL文件。这样重新部署或换一台服务器时,一条命令就能把初始化数据全部导入,不用手动再前台点一遍。
4.3 Linux部署与Nginx配置
我把项目打成了jar包放到了/data/contract目录下,用nohup后台启动。
bash复制nohup java -jar contract-system.jar --spring.profiles.active=prod >/data/contract/logs/run.log 2>&1 &
注意端口占用问题。我默认后端端口是8080,如果服务器上已经跑了别的服务,就修改server.port配置。部署完成后先curl一下127.0.0.1:8080/health看是否通,再配置Nginx做反向代理。
前端打包后的dist目录上传到/data/contract-web,Nginx配置两个location:一个是根路径指向前端静态文件,一个是/api代理到后端8080接口。
nginx复制location / {
root /data/contract-web;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
这段配置里try_files很关键。Vue是单页应用,前端路由用的是history模式,如果不加try_files,用户直接访问某个子路由比如/contract/list就会报404,加上之后会统一回退到index.html,由前端路由接管。
4.4 跨域问题为什么在部署后消失
开发环境最烦的就是跨域。前端调试地址是localhost:8081,后端是localhost:8080,端口不同所以浏览器会拦截。我在config里加了全局跨域配置,允许localhost:8081的访问。
但部署后用同一个域名加路径就没了跨域问题,因为Nginx把/api的请求反代到了同域下的后端接口,浏览器看来都是同一个域名。如果你在服务器上配置完还出现跨域报错,大概率是代理地址写错了,或者Nginx没有重启成功。
bash复制nginx -t
nginx -s reload
这两条命令是排查Nginx配置的好帮手。配置完先检查语法,再平滑重载,基本不会影响线上请求。
5. 交付时必须盯紧的细节——源码、文档与讲解材料的质量把控
5.1 文档和代码对不上是最致命的问题
拿到一套项目包以后,最怕的不是功能少,而是文档写的是旧版本,代码已经迭代了好几轮。比如文档里说用redis缓存用户信息,实际代码里根本没有引入redis依赖;或者数据库脚本里的表名和实体类注解对不上,启动时直接报表不存在。
我强烈建议在交付前做一次“从零搭建”验证。拿一台干净的机器,严格按照文档步骤执行:安装JDK和MySQL、导入SQL、打包前后端、启动服务、登录系统。任何一个步骤如果按文档做不通,就要么改文档,要么改代码,绝对不要留到客户自己部署时再暴露。
我自己的做法是写部署文档时顺手截了几张关键操作成功的截图,比如jar启动成功日志、前端页面登录成功画面、数据库表列表。这些截图不是形式,是给接手人吃定心丸——“照着我这步骤走,就是能跑起来”。
5.2 演示数据要精心准备,而不是随便造几行
很多人为了演示方便,在系统里随手插几条“测试1”“测试2”的数据,结果客户一登录满眼都是垃圾数据,看到第一眼体验就垮了。演示数据应该像真实业务数据一样具备参考意义:合同编号有规律,客户名称是贴近行业场景的示例,金额是合理数值,合同状态有分布(执行中、已完成、已终止),收付款计划也有前后逻辑。
我把演示数据做成了一组“模拟项目X”公司相关的场景数据:有和A客户签订的年度采购合同,有和B供应商签署的框架服务协议,还有一份即将到期的合同用来展示提醒机制。这样不管是功能演示还是自己测试,都能明显看出系统的业务逻辑。
5.3 “讲解”环节重点讲什么,评审/交接时才有说服力
标题里提到的“讲解”,我认为是整套资料里最容易被低估的部分。真正有价值的讲解不是把代码逐行念一遍,而是要讲清楚几个核心问题:
- 为什么用SpringBoot而不用SSH或者纯Servlet?从成本、开发效率、维护难度回答。
- 权限控制是怎么实现的?尤其要能讲清token校验、数据权限隔离的逻辑,这是面试/评审高频点。
- 合同到期提醒是怎么触发的?要能说出定时任务的cron表达式、扫描哪张表、为什么用状态字段过滤。
- 附件上传的存储策略?是存数据库好还是存磁盘好,各自利弊。
我准备的讲解稿是围绕这套系统的业务流程图展开的:登录进来之后,不同角色分别能看到哪些菜单,新增一单合同从填写表单到上传附件再到推送提醒,这个完整链路走下来,听的人立马就明白系统干了什么。然后再深入讲某个具体模块的接口实现和SQL逻辑,就比只对着PPT念有说服力得多。
6. 上线后还要做的几件事——补全体验和长期可维护性
系统跑起来只是开始,真正让人用得顺手的往往是那些边角功能。做完主流程后,我补了个首页统计面板,用ECharts展示合同金额月趋势、不同状态合同的占比、未来30天到期待办数量。这个面板看起来不复杂,但对用户感知度的提升很大,领导打开系统第一眼看到的就是数据价值。
另外我加了登录日志和操作日志。操作日志记录的是用户每次新增、编辑、删除、导出的动作,后续万一数据出了问题,可以按操作日志回溯是谁在什么时候动过数据。这个功能不需要设计得很复杂,一张log表加一个切面即可,却能极大降低维护责任风险。
密码安全也要提上日程。初始化管理员账号的密码一定要修改,数据库中不能存明文密码,我用的是BCrypt加密,每次登录把用户输入的密码和数据库密文比对。哪怕数据库被导出,明文密码也不会泄露。
重要提示:不要在配置文件中明文写数据库密码和登录密钥。至少要放到部署环境的系统环境变量里,或者用Jasypt做配置加密。项目包源码如果是公开演示用,务必将敏感信息替换成占位符。
7. 复盘这套系统,我最想留下的几个建议
类似合同管理这种偏业务的管理系统,技术难点真的不多,最大的挑战是“你怎么理解业务”。如果只是照着需求文档把页面搭出来,不去思考合同的状态流转、数据权限、提醒逻辑,那做完的系统很可能只是个花架子,一进入真实使用场景就会露怯。
动手前先画清楚业务流程图。什么角色在什么节点做什么操作,合同状态如何从草稿走到归档,中间哪些数据必须留痕。流程图画顺了,数据库表结构基本就出来了,写代码只是时间问题。
第二个建议是,一定要保留一套可以重复执行的初始化脚本。数据库表结构改了,脚本就要同步更新,让任何一台新机器都能一键建好库。很多项目做到后面,开发库和测试库结构都不一样了,部署新环境全靠手工改,这种项目接手的人会非常痛苦,作为原始作者也会被反复询问。
第三个建议和心态有关。做这类系统交付,优先级排序应该是:把流程跑通比炫技术重要,把数据存规范比页面好看重要,把文档写清楚比代码注释多重要。技术栈就好比工具箱里的一把螺丝刀,能拧动螺丝就是好工具。真要让这套系统长期稳住,核心是数据模型合理、逻辑边界清晰、交接资料不挖坑。
如果你也是第一次做SpringBoot相关管理系统,我的体会是:优先把“新增合同→上传附件→维护收付款计划→扫描到期提醒”这条主链路打通,先把核心问题解决掉,再去打磨统计报表和权限细节。不要一开始就想着把所有功能都做到完美,很多时候是在真实验证之后,才知道哪些功能用户根本不用、哪些功能又缺了不行。先把系统的骨架立起来,后面什么问题都有迹可循。
