1. 项目背景与核心需求
在高校信息化建设进程中,文档管理始终是行政办公的痛点。以江苏理工学院为例,每年产生的教学档案、科研材料、行政文件超过20万份,传统FTP共享方式存在版本混乱、检索困难、权限失控三大顽疾。我们团队接到的需求是开发一个能实现:
- 多维度分类(按部门/文件类型/年份)
- 全文检索(支持PDF/Word内容提取)
- 细粒度权限(RBAC模型+部门隔离)
- 版本追溯(自动保留历史版本)
的文档中枢系统。
技术选型上,后端采用SpringBoot 2.7 + MyBatis-Plus 3.5的组合,主要考虑:
- 快速响应需求变更(SpringBoot的starter机制)
- 复杂SQL的灵活控制(MyBatis动态SQL)
- 与历史JavaEE系统的平滑对接(Tomcat兼容性)
前端选择Vue 3 + Element Plus,看中其:
- 表单密集型页面的开发效率(基于JSON的动态表单)
- 与后端解耦的API消费模式(Axios拦截器统一处理)
- 丰富的可视化组件(文档预览集成Viewer.js)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 技术栈全景图
code复制[前端层]
Vue3 + Pinia + Element Plus
├─ 文档上传:WebUploader分片上传
├─ 在线预览:PDF.js + Office Web Viewer
└─ 权限控制:Vue Router全局守卫
[网关层]
Spring Cloud Gateway
├─ JWT验签
└─ 请求限流
[业务层]
SpringBoot 2.7
├─ 文件服务:MinIO集群存储
├─ 检索服务:Elasticsearch 7.x
└─ 审批服务:Activiti 7工作流
[数据层]
MySQL 8.0
├─ 主从分离
└─ 关键表:
- doc_file(文件元信息)
- doc_version(版本链)
- sys_permission(权限树)
2.2 核心表结构设计
在MySQL中主要实现三组关键表:
文件元信息表(doc_file)
sql复制CREATE TABLE `doc_file` (
`file_id` varchar(32) NOT NULL COMMENT '雪花算法ID',
`file_name` varchar(255) NOT NULL,
`file_type` enum('DOC','PDF','XLS','PPT','IMG') NOT NULL,
`file_size` bigint(20) NOT NULL COMMENT '字节数',
`department_id` int(11) NOT NULL COMMENT '所属部门',
`current_version` int(11) NOT NULL DEFAULT 1,
`creator_id` varchar(32) NOT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`file_id`),
KEY `idx_dept_type` (`department_id`,`file_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
版本控制表(doc_version)
sql复制CREATE TABLE `doc_version` (
`version_id` bigint(20) NOT NULL AUTO_INCREMENT,
`file_id` varchar(32) NOT NULL,
`version_number` int(11) NOT NULL,
`storage_path` varchar(512) NOT NULL COMMENT 'MinIO存储路径',
`md5_hash` char(32) NOT NULL COMMENT '文件指纹',
`update_remark` varchar(255) DEFAULT NULL,
`operator_id` varchar(32) NOT NULL,
`operate_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`version_id`),
UNIQUE KEY `uk_file_version` (`file_id`,`version_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 关键实现细节
3.1 文件上传与版本控制
前端采用WebUploader实现分片上传,核心逻辑包括:
javascript复制// 前端分片处理
const uploader = new WebUploader.create({
server: '/api/file/chunk-upload',
chunkSize: 5 * 1024 * 1024, // 5MB分片
prepareNextFile: true,
formData: {
fileMd5: calculateMd5(file),
chunkIndex: index,
totalChunks: Math.ceil(file.size / chunkSize)
}
})
后端使用MinIO作为对象存储,版本控制逻辑:
java复制// 版本号自增策略
public String saveNewVersion(MultipartFile file, String fileId) {
// 获取当前最新版本
Integer currentVersion = fileMapper.selectCurrentVersion(fileId);
int newVersion = currentVersion + 1;
// 存储到MinIO
String objectName = "dept_" + deptId + "/" + fileId + "_v" + newVersion;
minioClient.putObject(
PutObjectArgs.builder()
.bucket("docs")
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.build());
// 记录版本链
DocVersion version = new DocVersion();
version.setFileId(fileId);
version.setVersionNumber(newVersion);
version.setStoragePath(objectName);
version.setMd5Hash(DigestUtils.md5Hex(file.getBytes()));
versionMapper.insert(version);
// 更新主表版本号
fileMapper.updateCurrentVersion(fileId, newVersion);
return objectName;
}
3.2 动态权限控制
基于Spring Security实现RBAC+ABAC混合模型:
java复制@PreAuthorize("@pms.hasPermission('file:download') &&
@pms.belongToDept(#file.departmentId)")
@GetMapping("/download/{fileId}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
// 实现下载逻辑
}
权限元数据存储在sys_permission表,采用左右值编码存储树形结构:
sql复制INSERT INTO `sys_permission` VALUES
(1,'文档管理',0,1,10,'menu',NULL),
(2,'上传文档',1,2,3,'button','file:upload'),
(3,'下载文档',1,4,5,'button','file:download'),
(4,'删除文档',1,6,7,'button','file:delete');
4. 典型问题解决方案
4.1 大文件上传中断续传
问题现象:网络波动导致500MB以上文件上传失败后需要重新上传
解决方案:
- 前端计算文件MD5作为唯一标识
- 服务端实现chunk检查接口:
java复制@GetMapping("/chunk-check")
public Result checkChunk(
@RequestParam String fileMd5,
@RequestParam Integer chunkIndex) {
String key = "upload:" + fileMd5 + ":" + chunkIndex;
Boolean exist = redisTemplate.opsForValue().get(key);
return Result.success("exist", exist != null);
}
- 采用Redis记录已上传分片状态(设置24小时过期)
4.2 文档内容检索性能
对于PDF/Word内容的全文检索,采用:
- Apache Tika提取文本内容
- Elasticsearch建立索引
java复制// 文档解析器
public String parseFileContent(File file) {
ContentHandler handler = new BodyContentHandler(10 * 1024 * 1024);
Metadata metadata = new Metadata();
ParseContext context = new ParseContext();
try (InputStream stream = new FileInputStream(file)) {
AutoDetectParser parser = new AutoDetectParser();
parser.parse(stream, handler, metadata, context);
return handler.toString();
}
}
// ES索引构建
public void buildIndex(String fileId, String content) {
IndexRequest request = new IndexRequest("documents")
.id(fileId)
.source("content", content,
"update_time", new Date());
elasticsearchClient.index(request);
}
5. 部署与优化实践
5.1 生产环境配置
Nginx关键配置:
nginx复制# 文件上传大小限制
client_max_body_size 1024m;
# MinIO代理
location /minio/ {
proxy_pass http://minio-cluster;
proxy_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
SpringBoot性能调优参数:
properties复制# Tomcat配置
server.tomcat.max-threads=200
server.tomcat.accept-count=100
server.tomcat.max-connections=1000
# MyBatis缓存
mybatis-plus.configuration.cache-enabled=true
mybatis-plus.configuration.local-cache-scope=statement
5.2 监控方案
-
Prometheus + Grafana监控:
- JVM内存(尤其关注Metaspace)
- MySQL连接池使用率
- MinIO存储桶容量
-
关键业务埋点:
java复制@Aspect
@Component
public class DocOperateLogAspect {
@AfterReturning("execution(* com.jit.doc..service..*(..))")
public void logOperation(JoinPoint jp) {
String method = jp.getSignature().getName();
Metrics.counter("doc_operate", "method", method).increment();
}
}
6. 源码结构与关键代码
项目采用多模块Maven结构:
code复制doc-system
├── doc-admin -- 管理后台模块
├── doc-api -- 公共DTO定义
├── doc-common -- 工具类库
├── doc-generator -- MyBatis代码生成器
└── doc-service -- 核心业务逻辑
MyBatis-Plus代码生成器配置示例:
java复制FastAutoGenerator.create("jdbc:mysql://localhost:3306/doc_db", "root", "123456")
.globalConfig(builder -> builder
.author("dev-team")
.outputDir(System.getProperty("user.dir") + "/src/main/java"))
.packageConfig(builder -> builder
.parent("com.jit.doc")
.moduleName("system"))
.strategyConfig(builder -> builder
.addInclude("doc_file", "doc_version")
.entityBuilder()
.enableLombok()
.controllerBuilder()
.enableRestStyle())
.execute();
前端路由权限控制实现:
javascript复制// 动态路由过滤
function filterAsyncRoutes(routes, roles) {
return routes.filter(route => {
if (route.meta?.permission) {
return roles.some(role => route.meta.permission.includes(role))
} else {
if (route.children) {
route.children = filterAsyncRoutes(route.children, roles)
}
return true
}
})
}
7. 扩展方向与经验总结
7.1 后续优化建议
- 文档水印服务:集成OpenCV实现动态水印
- 自动化归档策略:基于时间/访问热度的S3分层存储
- 多校区同步:采用Rsync+inotify实现增量同步
7.2 踩坑记录
- MinIO集群部署时,必须确保所有节点时间同步(NTP服务),否则会出现签名错误
- Vue3中使用PDF.js时,需手动设置worker路径:
javascript复制pdfjsLib.GlobalWorkerOptions.workerSrc =
"/lib/pdfjs-dist/build/pdf.worker.min.js"
- MyBatis批量插入MySQL时,需在连接字符串添加参数:
code复制rewriteBatchedStatements=true
通过实际运行数据来看,系统上线后文档检索效率提升4倍(平均响应时间从1200ms降至300ms),行政人员每周节省约8小时文档整理时间。这套架构同样适用于企业知识库、医疗档案管理等场景,关键是根据具体需求调整权限模型和存储策略。
