1. 项目概述与核心价值
这个基于SpringBoot的合同信息管理系统是我在去年为一家中型企业实施的内部管理平台,主要解决他们纸质合同管理混乱、审批流程不透明、历史记录查询困难等问题。系统上线后,合同审批周期从平均7天缩短到2天,合同归档错误率降低了85%,成为他们日常运营中不可或缺的工具。
系统采用经典的MVC架构,前端使用Thymeleaf模板引擎,后端基于SpringBoot 2.7.3构建,数据库选用MySQL 8.0,通过Spring Security实现细粒度的权限控制。特别值得一提的是,我们针对合同文件设计了双重存储机制——数据库存元数据,文件系统存扫描件,既保证了查询效率又确保了文件安全。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计解析
2.1 技术栈选型考量
选择SpringBoot作为基础框架主要基于三个实际考量:首先,企业原有Java技术团队可以快速上手;其次,SpringBoot的自动配置特性让我们能快速集成MyBatis、Redis等组件;最重要的是,其内嵌Tomcat的特性简化了部署复杂度。在数据库选型时,我们对比了MySQL和PostgreSQL,最终选择MySQL是因为企业已有DBA团队对其有丰富运维经验。
2.2 核心模块划分
系统分为六个核心模块:
- 合同录入模块:支持PDF/Word上传和在线表单两种方式
- 审批流引擎:基于Activiti 7实现多级审批
- 归档管理:采用"年份-类型-编号"的三级目录结构
- 统计报表:集成ECharts实现可视化分析
- 提醒服务:通过Quartz定时任务实现到期提醒
- 系统管理:包括用户、角色、菜单等基础配置
特别在审批流设计中,我们实现了会签(所有审批人同意)和或签(任一审批人同意)两种模式,通过配置即可切换,这在处理不同金额的合同时特别实用。
3. 关键实现细节
3.1 合同文件处理方案
我们遇到的最大挑战是大文件上传问题。测试时发现超过50MB的PDF上传经常超时,最终解决方案是:
- 前端采用分片上传,每片2MB
- 后端用Apache Tika进行文件类型校验
- 存储时生成MD5值避免重复文件
- 建立文件索引表关联合同元数据
核心代码片段:
java复制@PostMapping("/upload")
public Result uploadContract(@RequestParam MultipartFile file) {
// 校验文件类型
String contentType = tika.detect(file.getInputStream());
if(!ALLOWED_TYPES.contains(contentType)){
return Result.error("仅支持PDF/DOCX格式");
}
// 生成存储路径
String filePath = "/contracts/" + DateUtil.today() + "/" + IdUtil.simpleUUID();
File dest = new File(uploadPath + filePath);
dest.getParentFile().mkdirs();
file.transferTo(dest);
// 记录文件信息
ContractFile contractFile = new ContractFile();
contractFile.setFileMd5(DigestUtil.md5Hex(dest));
fileService.save(contractFile);
return Result.ok(filePath);
}
3.2 动态审批流实现
审批流程配置采用了数据库驱动的方式,主要表结构包括:
- ACT_RE_MODEL:存储流程定义
- ACT_RU_TASK:运行中的任务
- ACT_HI_TASKINST:历史任务记录
我们扩展了Activiti的Behavior类来实现自定义审批逻辑:
java复制public class CustomUserTaskBehavior extends UserTaskActivityBehavior {
@Override
public void execute(DelegateExecution execution) {
// 获取动态指定的审批人
String assignee = (String)execution.getVariable("approver");
TaskEntity task = (TaskEntity)taskService.createTaskQuery()
.executionId(execution.getId()).singleResult();
task.setAssignee(assignee);
}
}
4. 部署实践与优化
4.1 多环境部署方案
我们建立了三套环境:
- 开发环境:使用H2内存数据库,方便快速测试
- 测试环境:与生产环境1:1配置,用于压力测试
- 生产环境:采用Nginx+双Tomcat集群部署
生产环境的关键配置:
properties复制# application-prod.properties
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-active=20
spring.redis.timeout=3000
spring.servlet.multipart.max-file-size=100MB
4.2 性能优化措施
通过JProfiler分析发现两个性能瓶颈:
- 合同列表查询慢:添加了type和status字段的联合索引
- 审批记录查询频繁:引入Redis缓存审批状态
优化前后的对比:
| 操作类型 | 优化前响应时间 | 优化后响应时间 |
|---|---|---|
| 合同列表查询 | 1200ms | 280ms |
| 审批状态查询 | 450ms | 80ms |
5. 典型问题排查实录
5.1 内存泄漏问题
系统运行两周后出现频繁Full GC,通过以下步骤定位:
- 使用jmap生成堆转储文件
- 用MAT分析发现Activiti的历史表查询未关闭ResultSet
- 检查代码发现缺少try-with-resources块
- 修复后增加连接池监控
关键修复代码:
java复制try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// 处理结果集
}
5.2 并发审批冲突
当多人同时审批时会偶发状态不一致,解决方案:
- 在合同表添加version字段
- 使用乐观锁控制更新
sql复制UPDATE contract
SET status = 'APPROVED', version = version + 1
WHERE id = ? AND version = ?
6. 安全防护实践
6.1 接口安全加固
除了Spring Security的基础配置外,我们还:
- 对敏感接口添加@PreAuthorize注解
- 使用AOP记录所有管理操作
- 合同下载链接设置10分钟过期
安全审计配置示例:
java复制@Aspect
@Component
public class AuditLogAspect {
@AfterReturning("execution(* com..admin..*(..))")
public void logAdminOperation(JoinPoint jp) {
String operation = jp.getSignature().getName();
String username = SecurityUtils.getCurrentUsername();
auditLogService.save(new AuditLog(username, operation));
}
}
6.2 数据备份策略
采用三重备份机制:
- 每日凌晨1点全量备份到本地NAS
- 每周日同步到阿里云OSS
- 数据库binlog实时同步到备机
备份脚本关键部分:
bash复制#!/bin/bash
DATE=$(date +%Y%m%d)
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME > /backup/full_$DATE.sql
rclone copy /backup/full_$DATE.sql oss:contract-backup
7. 扩展与二次开发建议
根据实际使用反馈,建议在以下方面进行扩展:
- 增加电子签章集成:可以考虑对接e签宝或法大大
- 合同模板引擎:集成Apache POI-TL实现动态模板
- 移动端适配:开发微信小程序版本
- 智能提醒:基于NLP解析合同关键日期
电子签章集成示例伪代码:
java复制public class ESignService {
public void signContract(Long contractId) {
Contract contract = contractService.getById(contractId);
String pdfPath = fileService.getPath(contract.getFileId());
// 调用SDK生成签署链接
EsignResponse resp = esignClient.createSignFlow(
contract.getTitle(),
pdfPath,
contract.getParties()
);
// 存储签署记录
SignRecord record = new SignRecord();
record.setFlowId(resp.getFlowId());
signRecordService.save(record);
}
}
在项目部署过程中,我特别建议做好以下准备工作:
- 提前申请SSL证书配置HTTPS
- 准备至少4核8G的服务器资源
- 规划好数据库定期维护窗口
- 对关键用户进行系统操作培训
这个系统最让我自豪的是它的灵活性——通过配置就能适应不同组织的审批流程变化,而不用修改代码。有家企业甚至用它管理了从采购合同到员工劳动合同等12类不同的合同流程。如果你在实施过程中遇到任何具体问题,我很乐意分享更多实现细节。
