资讯动态

合同管理系统源码:状态机、数据权限与到期提醒

发布时间:2026/10/27 20:54:25 来源:尧图企业网站定制
简介面向企业信息化开发者与.NET技术学习者的合同管理系统源码聚焦合同信息管理、纸质合同扫描录入、设备信息关联管理及年度合同数据统计等模块适合需要搭建合同全流程管理平台或进行二次开发的团队参考。压缩包共654个文件、约47.99MB以103个dll程序集、92个cs源码文件、36个resx资源与21个xml配置为主另有49个png与7个bmp界面素材、18个config配置、6个csproj工程文件及sln解决方案涵盖主程序、数据库接口、UI设计与报表模块可直接还原后台逻辑与前端实现。资源已有927人学习下载源码结构体现清晰的模块化划分读者可据此理解合同录入、审核、签署、履行到续签终止的业务链路掌握扫描图像处理与设备信息的关联方式并参照报表统计部分完成定制化改造适配不同业务场景。1. 合同管理系统源码一套能直接跑起来的业务底座合同这件事几乎每家公司都在做但真正做过合同管理系统的人都知道难的不是“存一份 PDF”而是状态流转、权限边界、到期提醒和附件版本这几件事同时不出错。这套合同管理系统源码就是围绕这几个高频痛点搭建的一套可运行业务底座合同起草、审批流转、归档、到期预警、附件管理、操作留痕基本都是通用型企业后台的标配模块。它适合两类人一类是接私活或做内部工具、想省掉从零搭架子时间的开发者另一类是想拿一套真实业务代码练手、补全权限模型和状态机设计经验的同学。接下来我按“能跑起来 → 能改得动 → 不踩坑”的顺序拆。2. 合同管理系统技术栈与目录结构先看清骨架再动手拿到一套业务源码最忌讳上来就git clone然后npm install一把梭。合同管理这类系统业务规则密集模块之间耦合比 CRUD demo 高得多先花二十分钟把骨架看清楚后面能省掉半天排错。2.1 典型技术栈组合与选型理由这类系统最常见也最稳的组合是前后端分离后端用 Spring Boot 或 Node.jsExpress、NestJS承载业务与权限前端用 Vue 或 React 做管理后台数据库用 MySQL附件走本地磁盘或对象存储。合同管理的数据特点决定了几个选型偏好合同正文、附件、审批意见是典型的长文本和大字段MySQL 的TEXT、LONGTEXT够用但要注意别把附件二进制塞进数据库常见做法是文件落盘、库里只存路径和哈希审批流是典型的有限状态机状态字段用枚举而不是自由文本能避免后期出现“已审批”和“审批通过”两种写法并存的血泪教训。权限这块合同系统几乎必然涉及“部门 → 角色 → 用户”三级结构因为合同天然带归属部门部门负责人要能看本部门全部合同普通员工只能看自己经手的。用 RBAC 做基础角色再叠加数据范围本人 / 本部门 / 全部是这类系统最省事的做法。别一上来就上 ABAC 或者复杂规则引擎合同量在几万条以内数据范围过滤完全够用规则引擎反而是过度设计。2.2 目录结构与模块划分一套结构清晰的源码后端通常按“控制层 → 服务层 → 数据访问层”分层业务模块按领域切分。以 Spring Boot 风格为例目录大致如下contract-system/ ├── backend/ │ ├── src/main/java/com/example/contract/ │ │ ├── controller/ # 接口层合同、审批、附件、用户 │ │ ├── service/ # 业务层状态流转、到期计算、权限校验 │ │ ├── mapper/ # 数据访问层 │ │ ├── entity/ # 实体合同、附件、审批记录、操作日志 │ │ ├── config/ # 权限拦截器、文件上传配置、跨域 │ │ └── common/ # 统一返回体、异常处理、常量枚举 │ └── src/main/resources/ │ ├── application.yml # 数据源、文件路径、token 有效期 │ └── mapper/ # MyBatis XML └── frontend/ ├── src/views/contract/ # 合同列表、详情、编辑 ├── src/views/approval/ # 审批中心 └── src/api/ # 请求封装、错误码处理这个结构本身没什么玄学关键是看service层里状态流转是怎么写的——是集中在一个ContractStatusMachine里还是散落在各个 controller 里。前者后续加审批节点只改一处后者改一次流程要动五六个文件这是判断源码质量最直接的一眼。2.3 跑起来的最小步骤先确认本机有 JDK 17或 Node 18和 MySQL 8然后按下面顺序走别跳步# 1. 建库并导入初始脚本脚本里一般含表结构和菜单权限数据 mysql -u root -p -e CREATE DATABASE contract_db DEFAULT CHARSET utf8mb4; mysql -u root -p contract_db backend/sql/init.sql # 2. 改配置数据源、附件根目录、token 密钥 # application.yml 里重点改这三处附件目录必须是已存在的绝对路径 # file.upload-path: /data/contract/files # spring.datasource.url: jdbc:mysql://127.0.0.1:3306/contract_db # 3. 起后端 cd backend mvn spring-boot:run # 4. 起前端 cd frontend npm install npm run dev参数说明file.upload-path一定要用绝对路径用相对路径在 IDE 里跑和打成 jar 跑工作目录不一样附件会“莫名其妙消失”这是新手最常翻车的地方之一。token有效期按业务定合同系统建议 8 小时太短审批人中途掉线太长权限回收不及时。初始化脚本里的菜单数据一般绑定了角色 ID如果登录后左侧菜单是空的八成是角色和菜单的关联表没数据补一条role_menu记录即可。3. 合同核心业务实现状态机、权限与到期提醒骨架跑通之后真正决定这套源码能不能用的是三块业务合同状态怎么流转、数据权限怎么卡、到期提醒怎么算。这三块任何一块写歪了后面都是补丁摞补丁。3.1 合同状态机把流转收进一个地方合同从起草到归档状态大致是草稿 → 待审批 → 审批中 → 已生效 → 履行中 → 已归档中间还可能有驳回、作废、终止。如果这些状态用if-else散在各处加一个“会签”节点就能让人崩溃。稳妥写法是把合法流转定义成一张表流转前统一校验。public enum ContractStatus { DRAFT, PENDING, APPROVING, EFFECTIVE, PERFORMING, ARCHIVED, REJECTED, VOID } // 合法流转关系key 是当前状态value 是允许到达的下一状态 private static final MapContractStatus, SetContractStatus FLOW Map.of( ContractStatus.DRAFT, Set.of(ContractStatus.PENDING, ContractStatus.VOID), ContractStatus.PENDING, Set.of(ContractStatus.APPROVING, ContractStatus.REJECTED), ContractStatus.APPROVING, Set.of(ContractStatus.EFFECTIVE, ContractStatus.REJECTED), ContractStatus.EFFECTIVE, Set.of(ContractStatus.PERFORMING, ContractStatus.VOID), ContractStatus.PERFORMING, Set.of(ContractStatus.ARCHIVED) ); public void transit(Long contractId, ContractStatus target) { Contract c contractMapper.selectById(contractId); SetContractStatus allowed FLOW.getOrDefault(c.getStatus(), Set.of()); if (!allowed.contains(target)) { throw new BizException(非法状态流转: c.getStatus() - target); } // 乐观锁带上原状态做条件更新避免两个审批人同时点通过 int rows contractMapper.updateStatus(contractId, c.getStatus(), target); if (rows 0) throw new BizException(合同状态已被他人变更请刷新); }逻辑说明FLOW是唯一事实来源新增节点只改这张表updateStatus的 SQL 必须是WHERE id ? AND status ?这是乐观锁的关键两个审批人同时点“通过”时只有一个能成功。参数上target由接口传入但必须经FLOW校验绝不能信任前端传来的状态值。这段代码看起来简单但它是整套系统里最值得先读、也最值得先改的地方。3.2 数据权限三类范围加一层拦截合同系统的权限不是“能不能进这个页面”而是“能看到哪些行”。常见做法是在用户表挂dept_id在角色表挂data_scope1 本人、2 本部门、3 全部查询合同时动态拼条件。-- data_scope 1只看自己经手的 SELECT * FROM contract WHERE creator_id #{userId} AND deleted 0; -- data_scope 2本部门全部含下级部门可用 dept_path LIKE 前缀匹配 SELECT * FROM contract WHERE dept_id IN (SELECT id FROM sys_dept WHERE id #{deptId} OR parent_id #{deptId}) AND deleted 0;参数说明deleted用逻辑删除而不是物理删除合同是法律凭证删了要有据可查顶多加个归档状态。dept_path存成0,1,5,12这种路径串查下级部门时用LIKE 0,1,5%比递归查询快得多这是老项目里很常见的优化。注意这个条件必须在服务层统一注入不能让每个接口自己拼否则迟早有一条 SQL 漏掉范围限制那就不是 bug是事故。3.3 到期提醒别用定时任务扫全表合同到期提醒是这套系统最容易被低估的模块。常见做法是用定时任务每天扫一遍end_date但合同表一大全表扫描加逐条判断会很吃力。更省事的做法是两级建一个提醒任务表合同生效时写入提醒时间点定时任务只查任务表里remind_time now AND status 0的记录。-- 提醒任务表 CREATE TABLE contract_remind ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_id BIGINT NOT NULL, remind_time DATETIME NOT NULL, status TINYINT DEFAULT 0, -- 0 待发 1 已发 2 已忽略 KEY idx_time_status (remind_time, status) ); -- 每天扫描只捞待发的 SELECT id, contract_id FROM contract_remind WHERE status 0 AND remind_time NOW() LIMIT 500;逻辑说明索引建在(remind_time, status)上扫描量跟待发任务数成正比不跟合同总数成正比。LIMIT 500是防止某天积压几万条把内存打爆分批处理更稳。提醒提前天数一般设 30 天、7 天、1 天三个节点在合同生效时一次性写入三条任务比每天算差值省事得多。发送成功后把status置 1别删记录出问题还能查发送历史。4. 附件与留痕合同系统最容易翻车的两块业务逻辑写得再漂亮附件丢了或者操作日志缺了一条这套系统在真实场景里就不合格。合同系统的附件不是“上传下载”这么简单版本、权限、哈希校验都得考虑。4.1 附件上传落盘策略与命名附件千万别用原始文件名直接存盘中文名、重名、特殊字符三座大山。标准做法是生成 UUID 做文件名原始名存库按日期分目录。public String save(MultipartFile file, Long contractId) { String original file.getOriginalFilename(); String ext original.substring(original.lastIndexOf(.)); // 按年月分目录避免单目录文件过多 String dir LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM)); String name UUID.randomUUID().toString().replace(-, ) ext; Path target Paths.get(uploadPath, dir, name); Files.createDirectories(target.getParent()); file.transferTo(target.toFile()); // 库里只存相对路径迁移环境时不用改数据 return dir / name; }参数说明库里存相对路径2025/06/abc.pdf取文件时拼上upload-path这样换服务器只改配置不改数据。扩展名要做白名单校验合同附件一般允许 pdf、doc、docx、jpg、png别图省事用黑名单。文件大小限制在application.yml里配max-file-size前端也要同步限制后端限制是兜底。如果做私有化交付附件目录建议独立挂盘别跟代码放一个分区升级覆盖时会哭。4.2 操作留痕日志表怎么设计合同系统的操作日志不是为了排查 bug是为了应对“谁改了金额”这种追问。日志表至少要有操作人、操作类型、合同 ID、变更前后快照、IP、时间。CREATE TABLE contract_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, action VARCHAR(32) NOT NULL, -- CREATE/UPDATE/APPROVE/ARCHIVE before_json JSON, -- 变更前关键字段快照 after_json JSON, -- 变更后 ip VARCHAR(64), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_contract (contract_id, created_at) );逻辑说明before_json和after_json只存关键字段金额、期限、甲乙方别存整行否则表体积膨胀很快。写入日志建议用 AOP 切面统一处理业务代码里不写日志逻辑避免漏记。注意日志表只增不改权限上只给审计角色读普通用户看不到这既是合规要求也是产品常识。5. 避坑与排查五条踩过的坑5.1 附件上传成功但下载 404现象上传接口返回成功点下载报文件不存在。原因upload-path用了相对路径IDE 启动时工作目录是模块根目录打成 jar 后是 jar 所在目录两次路径不一致。解决配置里统一用绝对路径启动时打印一次实际路径确认容器化部署用 volume 挂载固定目录。5.2 审批通过后状态没变现象审批人点通过提示成功但列表里状态还是待审批。原因状态更新和审批记录写入不在一个事务里或者更新 SQL 没带原状态条件被并发覆盖。解决transit方法加Transactional更新语句必须带AND status 原状态返回影响行数为 0 时提示刷新重试。5.3 部门负责人看不到下属部门合同现象数据范围设了本部门但只能看到直接所属部门的合同。原因部门查询只按parent_id查了一层。解决部门表存dept_path路径串查询用前缀匹配或者用递归 CTEMySQL 8 支持别在代码里循环查库。5.4 到期提醒重复发送现象同一个合同一天收到好几条提醒。原因定时任务没加幂等或者任务发送后没更新状态就抛异常回滚了。解决发送前用UPDATE ... SET status 1 WHERE id ? AND status 0抢占影响行数为 1 才发发送失败单独记失败表重试别回滚状态。5.5 逻辑删除后唯一约束冲突现象删掉一个合同编号新建同编号提示重复。原因唯一索引建在contract_no上逻辑删除的数据还在。解决唯一索引改成(contract_no, deleted)联合唯一或者删除时把编号改写加后缀。前者更干净但要注意deleted字段类型和默认值一致。6. 二次开发与验证把源码改成自己业务的三个动作拿到源码直接交付的不多多数要按自家流程改。改动集中在三处审批流节点、字段扩展、状态枚举。改完必须验证不然上线就是开盲盒。先说审批流。默认多半是“部门负责人 → 法务 → 归档”三级串行实际业务常见会签和或签。改的时候不要动transit的核心校验而是在审批节点表里加approve_type1 或签 2 会签和sort服务层按节点顺序取待办会签节点要求全部通过才流转。这里有个容易忽略的点驳回后回到哪个节点默认回起草人但很多公司要求回上一审批节点这个字段要在节点配置里留出来别写死。字段扩展要给合同主表加列同时记得同步三处实体类、Mapper XML 的resultMap、前端表单。漏了resultMap是最常见的接口返回里字段永远是 null查半天以为是数据库没存上。加列时给默认值别加NOT NULL又没默认值老数据会直接报错。状态枚举扩展要同步改FLOW表和前端状态字典两边不一致会出现“前端显示已归档、后端认为是履行中”的错位。验证方法很简单造一条测试合同把每个状态流转跑一遍故意发一次非法流转看是否被拦截再用两个浏览器同时点审批确认乐观锁生效。# 冒烟验证清单改完跑一遍 # 1. 新建合同 - 提交 - 审批通过 - 归档全链路走通 # 2. 同一合同两个窗口同时点通过只有一个成功 # 3. 上传 pdf 后下载文件大小与哈希一致 # 4. 把合同 end_date 改成明天手动触发提醒任务确认只发一次 # 5. 用普通员工账号登录确认看不到其他部门合同这套源码的价值不在代码量在于它把合同系统最容易出错的几个点——状态流转、数据范围、附件路径、提醒幂等——都用可读的方式实现了。我自己的习惯是拿到任何业务源码先跑通、再挑一个模块按上面清单改一遍改不动的部分才是真正需要读懂的部分。从那以后我每次接这类项目都强制先把状态机和权限过滤这两块单独拎出来验证一遍再谈界面和报表。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑