Spring Boot+Vue合同管理系统源码解析:从审批流到到期提醒
简介一套面向企业合同管理场景的完整源码资源适合需要搭建或二次开发现代化合同管理系统的开发人员及企业信息化负责人。资源以合同信息管理模块为核心覆盖合同录入、审核、签署、履行、续签终止等环节同时提供合同扫描电子化、设备信息管理及年度统计等功能可帮助组织提升合同处理效率、降低纸质文件风险。压缩包大小47.99MB共654个文件其中以C#源码92个cs、程序集103个dll、可执行文件26个exe为主另含resources/resx界面资源、png/bmp图标、xml/config配置及数据库文件等类型齐全便于直接编译运行或按需改造。源码模块划分清晰从数据库接口到UI界面均有对应实现读者可借此理解系统后台逻辑、合同扫描图像处理及报表模块的编写思路。目前已有927人学习下载适合希望以完整项目为参考快速落地合同管理系统的中高级开发者。1. 合同管理系统源码先解决“到期没人管”而不是“录入”拿到这套合同管理系统源码时我第一反应是它把重心放在了“审批流”和“到期提醒”上而不是停留在合同登记台账。很多团队的合同管理其实不是缺录入工具而是缺一条能自动流转的链路谁起草、谁审批、到期谁负责续签。这套源码正好覆盖了从合同起草、审批、归档、提醒到台账统计的完整闭环。后端是Spring Boot MyBatis-Plus前端是Vue数据库MySQL代码结构清楚拿来就能跑。适合谁看一类是正在做企业内部系统二次开发的开发者想找一个业务完整、可改可扩展的样例另一类是刚接触前后端分离项目的初级工程师想看懂一个真实业务系统是怎么组织的。这个项目没有复杂的分布式架构也没有花哨的微服务它做的就是一件事把合同管理这条业务链路用最稳妥的方式落地。学习它能快速搞懂状态机、定时任务、聚合查询这些在企业系统里天天用的东西。2. 整体架构与项目结构从审批流到台账的数据闭环2.1 技术选型为什么是 Spring Boot Vue MySQL这套源码采用前后端分离方案后端用Spring Boot系列框架配合MyBatis-Plus做数据持久层前端用Vue框架配合Element组件库数据库选用MySQL认证用的是JWT。我跑完后最大的感受是选型没有追新但每个组件都站在了企业级应用最不容易出错的位置上。Spring Boot负责提供REST API内置Tomcat打包成jar就能跑MyBatis-Plus把单表CRUD、分页查询、条件构造器都封装好了省掉大量重复的XML配置Vue负责页面交互通过axios调用接口MySQL存储业务数据InnoDB引擎保证事务。JWT做无状态登录避免了Session共享问题。这套组合对合同管理这种中后台系统来说属于“稳妥牌”经典搭配。为什么不用更重的微服务架构合同系统通常内部使用并发量不大单体应用部署简单、排错直接后端代码都在一个工程里二次开发时改一处就能全局生效。如果需要横向扩展后端接口已经做到无状态后续加一层Nginx负载均衡就能接住更大压力。2.2 目录结构一个可移植的工程骨架后端工程目录清晰按功能模块分包而不是按技术分层分包。这种组织方式在业务系统里非常实用你要改合同相关的功能直接进contract包不用在controller、service、mapper之间来回跳。contract-system/ ├── backend/ │ ├── src/main/java/com/company/contract/ │ │ ├── controller/ # REST接口层 │ │ │ ├── ContractController.java │ │ │ ├── ApproveController.java │ │ │ └── UserController.java │ │ ├── service/ # 业务逻辑层 │ │ │ ├── ContractService.java │ │ │ ├── ApproveService.java │ │ │ └── RemindService.java │ │ ├── mapper/ # MyBatis-Plus数据访问层 │ │ │ ├── ContractMapper.java │ │ │ └── ApproveRecordMapper.java │ │ ├── entity/ # 数据库实体 │ │ │ ├── Contract.java │ │ │ └── ApproveRecord.java │ │ ├── common/ # 通用返回结果、异常处理 │ │ ├── config/ # 跨域、拦截器、定时任务配置 │ │ └── ContractApplication.java │ └── src/main/resources/ │ ├── application.yml │ └── mapper/ # 复杂SQL的XML文件 ├── frontend/ │ ├── src/ │ │ ├── views/ │ │ │ ├── ContractList.vue │ │ │ ├── ContractEdit.vue │ │ │ └── ApproveList.vue │ │ ├── api/ # 接口封装 │ │ ├── router/ # 前端路由 │ │ └── store/ # 登录状态管理 │ └── package.json └── sql/ └── init.sql # 建库建表及初始化数据前端工程按页面维度划分ContractList负责台账展示ContractEdit负责新建和编辑ApproveList负责审批待办。api目录里统一封装了axios调用router控制页面跳转store保存登录用户信息。这种结构对想定制功能的人来说非常友好找文件的时间成本很低。2.3 数据模型设计合同主表、审批记录表、提醒任务表核心数据模型围绕三个表展开合同主表保存合同基础信息审批记录表保存每一步流转历史提醒任务表保存到期预警状态。我一般把这三张表比作“主数据 流水 任务”的铁三角。-- 合同主表 CREATE TABLE contract ( id bigint NOT NULL AUTO_INCREMENT, contract_no varchar(64) NOT NULL COMMENT 合同编号, title varchar(255) NOT NULL COMMENT 合同标题, party_a varchar(128) DEFAULT NULL COMMENT 甲方名称, party_b varchar(128) DEFAULT NULL COMMENT 乙方名称, amount decimal(12,2) DEFAULT NULL COMMENT 合同金额, status varchar(20) NOT NULL DEFAULT DRAFT COMMENT 状态: DRAFT/APPROVING/APPROVED/REJECTED/ARCHIVED, sign_date date DEFAULT NULL COMMENT 签订日期, expire_date date DEFAULT NULL COMMENT 到期日期, owner_id bigint NOT NULL COMMENT 负责人ID, file_url varchar(500) DEFAULT NULL COMMENT 附件地址, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_expire_date (expire_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段存储当前合同状态从DRAFT草稿开始提交后进入APPROVING审批中通过后变为APPROVED已生效到期或履行完毕后归档为ARCHIVED已归档。idx_status和idx_expire_date是高频查询索引清单页按状态筛选提醒任务按到期日扫表这两个索引能避免全表扫描。-- 审批记录表 CREATE TABLE approve_record ( id bigint NOT NULL AUTO_INCREMENT, contract_id bigint NOT NULL COMMENT 合同ID, action varchar(20) NOT NULL COMMENT 动作: SUBMIT/APPROVE/REJECT/ARCHIVE, from_status varchar(20) DEFAULT NULL, to_status varchar(20) NOT NULL, operator_id bigint NOT NULL COMMENT 操作人ID, comment varchar(255) DEFAULT NULL COMMENT 审批意见, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_contract_id (contract_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;审批记录只追加、不修改完整保留合同生命周期里的每一次操作。from_status和to_status两个字段记录了状态跳转配合操作人ID方便审计追溯。提醒任务表则用来去重标记某个合同在某一天是否已经发送过临近到期通知避免重复打扰。3. 核心模块拆解一个合同从起草到归档的完整链路3.1 合同起草与保存后端校验 附件上传合同起草是整个流程的入口。前端ContractEdit.vue收集表单数据附件上传走独立接口后端保存时做必填校验、编号生成和状态初始化。这里隐藏一个细节编号生成要保证唯一常见做法是“前缀 年月日 四位随机数”但更好的做法是直接查库取当天的最大序号避免并发下撞号。PostMapping(/contract) public Result save(RequestBody ContractDTO dto) { if (StringUtils.isBlank(dto.getTitle())) { return Result.error(合同标题不能为空); } if (dto.getExpireDate() null) { return Result.error(到期日期不能为空); } Contract contract new Contract(); BeanUtils.copyProperties(dto, contract); contract.setContractNo(generateContractNo()); contract.setStatus(DRAFT); contract.setOwnerId(LoginUser.getCurrentUserId()); contractService.save(contract); return Result.ok(contract.getId()); }这里有几个容易忽略的点BeanUtils.copyProperties把DTO的属性拷贝到实体省去手动set但要注意日期类型格式必须一致状态置为DRAFT是硬编码如果后续要扩展“待提交”等中间态可以把状态初始化放到配置里。返回结果只返回了合同ID前端拿到后可以继续走上传附件接口。附件上传单独做原因是文件体量大不适合和表单数据一起走JSON。常见做法是后端接收MultipartFile保存到本地或OSS然后把file_url写回合同表。如果不上传文件合同也能保存但会少一个重要信息源。3.2 审批流引擎状态机的实现审批流是这个系统最值得看的模块。实现思路是先用状态枚举固定住合法跳转再通过Service层控制流转动作。这种实现比用工作流引擎轻得多适合内部合同审批这种固定流程。public enum ContractStatus { DRAFT, // 草稿 APPROVING, // 审批中 APPROVED, // 已通过 REJECTED, // 已驳回 ARCHIVED; // 已归档 public static boolean canTransition(ContractStatus from, ContractStatus to) { switch (from) { case DRAFT: return to APPROVING || to REJECTED; case APPROVING: return to APPROVED || to REJECTED; case APPROVED: return to ARCHIVED; default: return false; } } }调用审批接口时第一步先校验当前状态是否允许跳转。这一步很多人会漏导致前端传一个非法状态直接把数据写乱了。PostMapping(/approve/{contractId}) public Result approve(PathVariable Long contractId, RequestBody ApproveRequest req) { Contract contract contractService.getById(contractId); ContractStatus from ContractStatus.valueOf(contract.getStatus()); ContractStatus to ContractStatus.valueOf(req.getTargetStatus()); if (!ContractStatus.canTransition(from, to)) { return Result.error(非法状态流转: from - to); } ApproveRecord record new ApproveRecord(); record.setContractId(contractId); record.setAction(req.getAction()); record.setFromStatus(from.name()); record.setToStatus(to.name()); record.setOperatorId(LoginUser.getCurrentUserId()); record.setComment(req.getComment()); approveService.save(record); contract.setStatus(to.name()); contractService.updateById(contract); return Result.ok(); }这段代码要配合数据库事务使用在Service方法上加Transactional保证审批记录写入和合同状态更新要么同时成功要么同时失败。我遇到过只更新了状态却没写记录的翻车情况后来统一把“记录流水”和“更新主表”放在一个事务方法内问题就消失了。3.3 到期提醒定时任务怎么扫表到期提醒使用Spring自带的 Scheduled 定时任务每天早上8点扫描一次合同表找出未来30天内到期且未归档、未提醒的合同然后给负责人发送通知。这个模块不难但定时任务有几个坑值得单独说。Scheduled(cron 0 0 8 * * ?) public void remindExpiringContracts() { LocalDate today LocalDate.now(); LocalDate expireEnd today.plusDays(30); ListContract contracts contractMapper.findExpiring( today, expireEnd, APPROVED); for (Contract contract : contracts) { boolean exists remindMapper.exists(contract.getId(), today); if (!exists) { sendRemindMessage(contract); remindMapper.insert(contract.getId(), today); } } }cron表达式0 0 8 * * ?表示每天8点整触发一次。查询条件里过滤了状态是APPROVED的合同因为草稿和审批中的合同还没生效不需要提醒。remind表的存在是为了防止同一天内重复发送也防止重启服务后扫表又扫到前一天处理过的数据。这里有个需要注意的场景如果定时任务跑挂了比如数据库连接池耗尽那么当天的提醒会全部丢失。常见做法是加一个简单的执行日志表任务启动时先查上次执行时间如果发现超过一天没执行就触发补偿逻辑。源码里没有做补偿但预留了提醒表结构二次开发时可以直接在此基础上加。3.4 合同台账与搜索聚合查询的SQL写法台账列表页需要展示分页数据、状态统计、金额汇总。这些查询如果用Java代码遍历内存列表效率低且不优雅。源码里直接用聚合SQL把统计结果从数据库算出来接口响应速度在万级数据量下依然能稳定在100毫秒以内。-- 按状态统计合同数量 SELECT status, COUNT(*) AS count FROM contract WHERE owner_id #{ownerId} GROUP BY status; -- 合同金额汇总 SELECT SUM(amount) AS total_amount, COUNT(*) AS total_count FROM contract WHERE expire_date BETWEEN #{start} AND #{end} AND status APPROVED;第一段SQL用于首页状态卡片展示草稿几份、审批中几份、已通过几份。第二段SQL用于统计未来某个时间段内到期合同的总金额帮助法务同事预估续签工作量。注意SUM函数在amount为NULL时返回NULL所以前端读取total_amount时要加判空。搜索功能用的是MyBatis-Plus的LambdaQueryWrapper条件拼接放在Service层。常见做法是标题用like模糊匹配合同编号用eq精确匹配日期范围用ge和le过滤状态用in支持多选。前端传什么参数后端就拼什么条件没有参数就查全部这样列表页的筛选交互很灵活。4. 常见问题排查从启动到上线我踩过的坑4.1 环境准备与启动步骤别漏这三个配置先把项目跑起来需要的环境是JDK 8或11、MySQL 5.7或8.0、Node.js 14以上。后端直接执行Application启动类前端在frontend目录下运行npm install然后npm run dev。但启动前有三个配置必须确认否则大概率翻车。第一MySQL连接串里的时区参数不能漏。在application.yml里jdbc连接url要加上空字符时区配置否则日期字段会差8小时到期提醒的日期计算全乱。spring: datasource: url: jdbc:mysql://localhost:3306/contract_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai第二前端接口代理要指向后端端口。Vue开发服务器默认8080后端接口也是8080所以前端配置里要开启代理把/home转发到后端地址。如果直接写死ip调试时会非常痛苦。// vue.config.js module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }第三数据库初始化需要手动执行sql/init.sql。源码不自带自动建库建库语句里要选择合适的字符集建议utf8mb4这样中文全文搜索和特殊字符都能存。4.2 高频问题清单现象、原因、解决以下5条是实际运行中高频出现的问题每条都按现象、原因、解决来写方便对照排查。问题1启动时端口被占用现象后端启动日志报Tomcat已经使用8080端口应用起不来。原因本地另一个进程占用了8080或者上一次启动的服务没有停止。解决改后端端口或者找到占用进程杀掉。我一般先在application.yml里换成server.port8081快速验证问题再回头清理旧进程。问题2数据库插入中文乱码现象合同标题和甲方名称存进去变成问号。原因数据库连接串没指定characterEncoding或者表字符集不是utf8mb4。解决先改连接串重启服务看是否正常如果还乱码把库和表都改成utf8mb4。执行SQLALTER TABLE contract CONVERT TO CHARACTER SET utf8mb4。问题3前端请求接口跨域现象浏览器控制台报CORS错误请求发不出去。原因前后端分离后前端端口是3000后端是8080浏览器默认拦截跨源请求。解决后端加一个全局CORS配置类允许指定来源、请求头和认证信息。这是个很标准的过滤器配置直接加到config包里即可。问题4审批通过后状态没有变现象在界面上点击通过返回成功但再次刷新后合同状态还是审批中。原因大概率是事务没提交或者状态更新的SQL条件带上了旧状态。解决先看后端日志里有没有报错再检查service方法上是否有Transactional。如果更新语句用了UPDATE ... WHERE status APPROVING而并发场景下状态已经被其他操作改了就会更新0行前端却显示成功。这个坑很隐蔽我在测试并发时遇到过解决方法是更新前先查一次当前状态并在更新SQL里带上id主键。问题5定时任务完全不执行现象日志里看不到任何提醒输出。原因启动类上漏了EnableScheduling注解。Spring Boot不会自动开启定时任务需要显式声明。解决在Application类上加这个注解重启服务。如果加了仍不执行检查cron表达式是否写对以及定时任务类是否被Spring扫描到。5. 二次开发把固定流程改成可配置流程5.1 改造思路用流程定义表替换硬编码状态机这套源码的审批流是写死在Java枚举里的内部流程不变时很好用但如果不同部门有不同的审批路径改代码就太麻烦了。我在二次开发时做过的最大改动是把状态机从代码里挪到数据库表里。改造方向是新增一张流程定义表保存每个状态允许流转到哪些目标状态以及每个流转需要什么角色。审批Service启动时把表数据加载到内存缓存每次审批先查缓存中该状态允许的后续状态再校验当前用户角色。// 改造后的审批校验逻辑 ListFlowNode nodes flowConfigService.getByStatus(currentStatus); boolean pass nodes.stream() .anyMatch(n - n.getTargetStatus().equals(req.getTargetStatus()) n.getRole().equals(LoginUser.getCurrentUser().getRole())); if (!pass) { return Result.error(当前角色不允许该操作); }配置表的好处是业务方提出新流程时只用数据库管理工具插入几条记录不用发版重启。这个改造工作量不大但能让系统从“死流程”变成“活流程”。同时审批记录表新增了一个config_id字段用来跟踪每次流转使用的是哪条配置方便回溯。5.2 验证方法从用例到回归改完流程配置后验证不能只看单个接口通不通。我一般会准备一张测试清单覆盖草稿提交、审批通过、驳回、归档、非法流转、超期提醒六个场景。重点验证非法流转比如直接从草稿跳到归档必须被拦截因为这类操作会破坏数据一致性。另一个有效方法是做“配置驱动测试”在测试库插入多条流程配置用同一个接口跑不同角色、不同目标状态的组合断言返回结果符合配置预期。我后来还加了一个小工具每次上线前自动导出配置表和审批记录的比对结果看有没有不一致的脏数据。从那以后我每次调整状态机都会强制走一遍“配置验证 事务边界检查 异常流程回归”确认审批记录和状态更新始终在同一个事务里。这种习惯帮我躲过了好几次“数据错乱”的线上事故。希望这篇解析能帮你在跑通源码和二次开发时少踩几个坑。本文还有配套的精品资源点击获取