简介:面向企业信息化开发者与.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.js(Express、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 Map<ContractStatus, Set<ContractStatus>> 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); Set<ContractStatus> 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_scope(1 本人、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路径串,查询用前缀匹配,或者用递归 CTE(MySQL 8 支持),别在代码里循环查库。
5.4 到期提醒重复发送
现象:同一个合同一天收到好几条提醒。原因:定时任务没加幂等,或者任务发送后没更新状态就抛异常回滚了。解决:发送前用UPDATE ... SET status = 1 WHERE id = ? AND status = 0抢占,影响行数为 1 才发;发送失败单独记失败表重试,别回滚状态。
5.5 逻辑删除后唯一约束冲突
现象:删掉一个合同编号,新建同编号提示重复。原因:唯一索引建在contract_no上,逻辑删除的数据还在。解决:唯一索引改成(contract_no, deleted)联合唯一,或者删除时把编号改写加后缀。前者更干净,但要注意deleted字段类型和默认值一致。
6. 二次开发与验证:把源码改成自己业务的三个动作
拿到源码直接交付的不多,多数要按自家流程改。改动集中在三处:审批流节点、字段扩展、状态枚举。改完必须验证,不然上线就是开盲盒。
先说审批流。默认多半是“部门负责人 → 法务 → 归档”三级串行,实际业务常见会签和或签。改的时候不要动transit的核心校验,而是在审批节点表里加approve_type(1 或签 2 会签)和sort,服务层按节点顺序取待办,会签节点要求全部通过才流转。这里有个容易忽略的点:驳回后回到哪个节点,默认回起草人,但很多公司要求回上一审批节点,这个字段要在节点配置里留出来,别写死。
字段扩展要给合同主表加列,同时记得同步三处:实体类、Mapper XML 的resultMap、前端表单。漏了resultMap是最常见的,接口返回里字段永远是 null,查半天以为是数据库没存上。加列时给默认值,别加NOT NULL又没默认值,老数据会直接报错。
状态枚举扩展要同步改FLOW表和前端状态字典,两边不一致会出现“前端显示已归档、后端认为是履行中”的错位。验证方法很简单:造一条测试合同,把每个状态流转跑一遍,故意发一次非法流转看是否被拦截;再用两个浏览器同时点审批,确认乐观锁生效。
# 冒烟验证清单,改完跑一遍 # 1. 新建合同 -> 提交 -> 审批通过 -> 归档,全链路走通 # 2. 同一合同两个窗口同时点通过,只有一个成功 # 3. 上传 pdf 后下载,文件大小与哈希一致 # 4. 把合同 end_date 改成明天,手动触发提醒任务,确认只发一次 # 5. 用普通员工账号登录,确认看不到其他部门合同这套源码的价值不在代码量,在于它把合同系统最容易出错的几个点——状态流转、数据范围、附件路径、提醒幂等——都用可读的方式实现了。我自己的习惯是,拿到任何业务源码,先跑通、再挑一个模块按上面清单改一遍,改不动的部分才是真正需要读懂的部分。从那以后我每次接这类项目,都强制先把状态机和权限过滤这两块单独拎出来验证一遍,再谈界面和报表。希望帮到你。
本文还有配套的精品资源,点击获取