温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系名片!
一、项目背景与意义
合同是企业经营活动中最核心的法律载体,从起草、审批、签署、履约到归档,每一个环节都涉及法务、财务、业务和采购等多个部门。传统依靠邮件、共享盘和纸质台账管理合同的方式,普遍存在版本混乱、审批链路不透明、履约节点容易遗漏、到期续签无人跟进等问题,一旦出现纠纷,往往难以快速定位历史版本和关键条款。
合同生命周期管理(Contract Lifecycle Management,CLM)系统通过数字化手段把合同的全流程纳入统一平台,实现模板标准化、审批流程化、履约可视化、归档规范化。本文基于 Spring Boot + Vue3 前后端分离架构,设计并实现一套面向中小企业的合同生命周期管理系统,覆盖合同起草、审批、签署、履约跟踪、变更、续签与归档的完整闭环。
二、系统需求分析
2.1 角色与权限
系统采用 RBAC 模型,主要角色划分如下:
| 角色 | 核心职责 | 主要权限 |
|---|---|---|
| 业务人员 | 发起合同、填写合同信息 | 合同起草、提交审批、查看本人合同 |
| 部门主管 | 业务初审 | 审批本部门合同、查看部门合同台账 |
| 法务人员 | 条款合规审查 | 法务审批、模板维护、风险条款标注 |
| 财务人员 | 金额与付款条款审核 | 财务审批、付款计划查看 |
| 系统管理员 | 系统配置与运维 | 用户管理、角色管理、流程配置、日志审计 |
2.2 功能性需求
- 合同起草:支持从模板生成合同,填写相对方、金额、起止日期、付款方式等关键字段,支持附件上传。
- 审批流程:支持多级串行审批与条件分支,审批人可同意、驳回、转办、加签,并填写审批意见。
- 签署管理:记录签署方式(线上电子签或线下纸质签)、签署时间与签署文件。
- 履约跟踪:按合同生成付款计划与关键节点提醒,支持履约进度登记。
- 变更与续签:合同变更生成新版本并保留历史版本;到期前自动提醒续签。
- 归档与检索:合同完成后归档,支持按编号、相对方、金额区间、状态等多条件检索。
- 统计报表:合同数量趋势、金额分布、审批时效、到期预警等可视化看板。
2.3 非功能性需求
系统需满足接口响应时间在常规查询场景下不超过 500ms,支持 200 人左右并发在线;合同附件与关键操作日志需长期留存,满足审计要求;敏感字段需脱敏展示,操作行为需可追溯。
三、系统总体架构设计
3.1 技术选型
| 层次 | 技术栈 | 说明 |
|---|---|---|
| 前端 | Vue3 + TypeScript + Vite + Element Plus + Pinia + Vue Router | 组合式 API 开发,按需加载,状态集中管理 |
| 后端 | Spring Boot 3 + Spring Security + MyBatis-Plus | 提供 RESTful 接口,JWT 无状态认证 |
| 流程引擎 | Flowable | 驱动合同多级审批流程 |
| 数据库 | MySQL 8 | 业务数据与流程数据存储 |
| 缓存 | Redis | Token、字典、到期提醒任务缓存 |
| 文件存储 | MinIO | 合同附件与签署文件对象存储 |
| 定时任务 | Spring Task / XXL-Job | 到期提醒、状态流转扫描 |
3.2 分层架构
flowchart TD A[Vue3 前端应用] -->|HTTP/JSON| B[网关与安全层 Spring Security + JWT] B --> C[Controller 接口层] C --> D[Service 业务层] D --> E[Mapper 持久层 MyBatis-Plus] D --> F[Flowable 流程引擎] D --> G[MinIO 文件服务] D --> H[Redis 缓存] E --> I[(MySQL)] F --> I后端采用经典三层结构,Controller 只做参数校验与响应封装,Service 承载合同状态机与流程编排逻辑,Mapper 负责数据访问。流程引擎与业务表通过 businessKey 关联,保证审批状态与合同状态一致。
四、数据库设计
核心表围绕合同主体、版本、审批、履约四条主线设计。
| 表名 | 说明 | 关键字段 |
|---|---|---|
| sys_user | 用户表 | id、username、password、dept_id、status |
| sys_role | 角色表 | id、role_code、role_name |
| contract_info | 合同主表 | id、contract_no、title、party_b、amount、start_date、end_date、status、current_version |
| contract_version | 合同版本表 | id、contract_id、version_no、content、file_url、create_time |
| contract_approval | 审批记录表 | id、contract_id、task_id、approver_id、result、opinion、approve_time |
| contract_payment | 付款计划表 | id、contract_id、period_no、plan_amount、plan_date、pay_status |
| contract_attachment | 附件表 | id、contract_id、file_name、file_url、file_size |
| contract_log | 操作日志表 | id、contract_id、operator_id、action、detail、create_time |
合同状态使用枚举统一管理,流转路径为:草稿 → 审批中 → 待签署 → 履约中 → 已完成 → 已归档,任一环节可驳回至草稿,履约中可发起变更生成新版本。
五、核心功能实现
5.1 合同状态机
合同状态流转集中在 Service 层统一控制,避免多处修改导致状态不一致。下面给出状态枚举与流转校验的核心代码。
public enum ContractStatus { DRAFT("草稿"), APPROVING("审批中"), TO_SIGN("待签署"), PERFORMING("履约中"), COMPLETED("已完成"), ARCHIVED("已归档"); private final String label; ContractStatus(String label) { this.label = label; } public String getLabel() { return label; } }@Service public class ContractStateService { private static final Map<ContractStatus, Set<ContractStatus>> TRANSITIONS = Map.of( ContractStatus.DRAFT, Set.of(ContractStatus.APPROVING), ContractStatus.APPROVING, Set.of(ContractStatus.TO_SIGN, ContractStatus.DRAFT), ContractStatus.TO_SIGN, Set.of(ContractStatus.PERFORMING), ContractStatus.PERFORMING, Set.of(ContractStatus.COMPLETED), ContractStatus.COMPLETED, Set.of(ContractStatus.ARCHIVED) ); @Transactional(rollbackFor = Exception.class) public void transfer(Long contractId, ContractStatus target) { ContractInfo contract = contractMapper.selectById(contractId); if (contract == null) { throw new BizException("合同不存在"); } ContractStatus current = contract.getStatus(); if (!TRANSITIONS.getOrDefault(current, Set.of()).contains(target)) { throw new BizException("非法的状态流转:" + current + " -> " + target); } contract.setStatus(target); contractMapper.updateById(contract); logService.record(contractId, "STATUS_CHANGE", current + " -> " + target); } }5.2 审批流程集成
合同提交审批时启动 Flowable 流程实例,将合同 ID 作为 businessKey,审批任务完成后回调业务层更新合同状态。
@Service @RequiredArgsConstructor public class ContractApprovalService { private final RuntimeService runtimeService; private final TaskService taskService; private final ContractStateService stateService; public void submit(Long contractId, Long initiatorId) { Map<String, Object> vars = new HashMap<>(); vars.put("initiator", initiatorId); vars.put("contractId", contractId); ProcessInstance instance = runtimeService.startProcessInstanceByKey( "contractApproval", String.valueOf(contractId), vars); stateService.transfer(contractId, ContractStatus.APPROVING); } public void complete(Long taskId, boolean approved, String opinion) { Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); if (task == null) { throw new BizException("审批任务不存在"); } Map<String, Object> vars = new HashMap<>(); vars.put("approved", approved); vars.put("opinion", opinion); taskService.complete(taskId, vars); } }5.3 到期提醒与履约跟踪
通过定时任务扫描即将到期的合同,提前 30 天、7 天分别推送提醒,并同步生成待办消息。
@Component @RequiredArgsConstructor public class ContractExpireJob { private final ContractInfoMapper contractMapper; private final MessageService messageService; @Scheduled(cron = "0 0 8 * * ?") public void scanExpiringContracts() { LocalDate today = LocalDate.now(); List<ContractInfo> list = contractMapper.selectList( new LambdaQueryWrapper<ContractInfo>() .in(ContractInfo::getStatus, ContractStatus.PERFORMING, ContractStatus.COMPLETED) .between(ContractInfo::getEndDate, today, today.plusDays(30))); for (ContractInfo contract : list) { long days = ChronoUnit.DAYS.between(today, contract.getEndDate()); messageService.push(contract.getOwnerId(), "合同【" + contract.getTitle() + "】将在 " + days + " 天后到期,请及时处理续签"); } } }5.4 前端页面与接口封装
前端使用 Vite 构建,按模块拆分路由,接口统一封装在src/api目录,配合 Axios 拦截器处理 Token 与错误提示。
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) request.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = Bearer ${token} } return config }) request.interceptors.response.use( (response) => { const { code, data, message } = response.data if (code !== 200) { ElMessage.error(message || '请求失败') return Promise.reject(new Error(message)) } return data }, (error) => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request<script setup lang="ts"> import { ref, onMounted } from 'vue' import { getContractPage } from '@/api/contract' import type { ContractItem } from '@/types/contract' const loading = ref(false) const list = ref<ContractItem[]>([]) const total = ref(0) const query = ref({ pageNum: 1, pageSize: 10, keyword: '', status: '' }) const loadData = async () => { loading.value = true try { const res = await getContractPage(query.value) list.value = res.records total.value = res.total } finally { loading.value = false } } onMounted(loadData) </script>六、关键设计要点
- 状态与流程解耦:合同状态由业务层统一维护,流程引擎只负责驱动审批节点,通过回调同步状态,避免双写不一致。
- 版本化管理:每次变更生成新的版本记录,主表只保留当前版本号,历史版本可追溯对比。
- 权限细粒度控制:接口层使用注解校验权限码,数据层按部门与创建人过滤,防止越权查看合同。
- 操作留痕:关键操作写入日志表,记录操作人、动作、变更前后内容,满足审计要求。
- 文件安全:附件通过 MinIO 预签名 URL 访问,链接带有效期,避免文件地址被随意传播。
七、系统测试与部署
测试阶段重点覆盖状态流转的非法路径、审批驳回后的回退、并发审批的幂等处理以及大附件上传的稳定性。接口层使用 JUnit 5 + MockMvc 编写单元测试,前端使用 Vitest 覆盖核心工具函数。
部署采用 Docker Compose 编排,前端构建产物由 Nginx 托管并反向代理后端接口,后端以 Jar 包形式运行,MySQL、Redis、MinIO 独立部署,通过环境变量注入配置,便于在测试与生产环境之间切换。
services: backend: image: contract-server:latest ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/contract?useUnicode=true&characterEncoding=utf8 SPRING_REDIS_HOST: redis depends_on: - mysql - redis frontend: image: nginx:alpine ports: - "80:80" volumes: - ./dist:/usr/share/nginx/html mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: contract redis: image: redis:7-alpine八、总结与展望
本文围绕企业合同管理的实际痛点,基于 Spring Boot + Vue3 前后端分离架构完成了一套合同生命周期管理系统的设计与核心实现,重点解决了合同状态一致性、多级审批流转、履约到期提醒和版本追溯等问题。系统结构清晰、扩展性良好,能够满足中小企业日常合同管理需求。
后续可从三个方向继续演进:一是接入电子签章服务,实现签署环节全线上化;二是引入合同条款智能审查,利用大模型辅助识别风险条款;三是完善数据看板,从合同金额、履约率、审批时效等维度为管理层提供决策支持。