资讯动态

开源企业文档管理系统实战:Spring Boot + Vue + Elasticsearch 从部署到二次开发

发布时间:2026/10/16 2:57:52 来源:尧图企业网站定制
简介这是一套面向企业IT团队与Java开发者的开源企业文档管理系统源码适合需要自建文档存储、权限管控与二次开发的技术人员学习研究。系统以源代码开放为特点支持PDF、DOC、TXT等多种文件格式的存储、管理与分享并通过角色与文件级别的细粒度权限控制保障信息安全。压缩包共1981个文件约8.43MB以1037个java源文件为核心辅以103个jsp页面、78个js脚本、38个css样式及37个xml配置另有gif、png等界面素材与少量jar依赖整体呈现典型Java Web项目结构。资源中可见build.bat、startup.bat等构建与启动脚本以及proguard.conf代码混淆配置、doxyfile.cfg文档生成配置和utility.log日志文件便于读者理解权限管理、多格式解析、构建自动化与代码保护等工程实践。目前已有5228人学习下载适合作为企业文档管理方案选型与Java项目实战的参考素材。1. 从一堆散落的合同和制度文件说起这个开源企业文档管理系统到底能干什么如果你待过那种几十号人、业务跑得比 IT 建设快的公司大概率见过这样的场景合同在财务的共享盘里制度文件在行政的电脑上项目验收单躺在某个离职同事的邮箱附件里。想找一份三个月前签的采购协议得在群里问一圈最后发现是某个同事用微信传的文件名还叫「新建 Microsoft Word 文档(3).docx」。这不是段子是我在某公司做内部工具时真实遇到的事。这套开源企业文档管理系统解决的就是这个问题。它把文档的存储、分类、权限、版本、检索这几件事收拢到一个自部署的服务里让「找文件」从人肉问人变成一次搜索。技术栈上后端是 Spring Boot 提供 REST 接口前端用 Vue 做管理界面文件实体走本地磁盘或对象存储元数据落 MySQL全文检索交给 Elasticsearch。适合谁适合中小团队里那个被临时抓来「搞个文档库」的后端或全栈开发也适合想拿一个完整 CRUD 权限 检索项目练手的人。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把它拆开讲。2. 技术选型与目录结构先看清骨架再动手2.1 为什么是 Spring Boot Vue MySQL Elasticsearch 这套组合企业文档管理这个场景需求其实很朴素上传、下载、分类、按角色控权限、按关键词搜内容。它不需要高并发秒杀那套东西但对「稳定」和「可维护」要求高因为一旦上线全公司都指着它找文件。后端选 Spring Boot是因为它把 Spring 生态里那些繁琐的 XML 配置全干掉了一个RestController就能暴露接口配合 Spring Security 做权限拦截也顺手。前端选 Vue是因为管理后台这种表单密集、列表密集的界面Vue 的响应式和组件化写起来比手撸 jQuery 舒服太多而且 Element Plus 这类组件库能直接拿来用省掉大量样式工作。数据库分两块MySQL 存用户、角色、文档元数据文件名、大小、分类、上传人、版本号Elasticsearch 存文档正文的倒排索引。为什么不全塞 MySQL因为 MySQL 的LIKE %关键词%在几万份文档面前会慢到让人怀疑人生而且没法做分词。Elasticsearch 天生干这个中文分词挂个 IK 插件就能按词搜而不是按字符匹配。文件实体本身不进数据库只存一个存储路径或对象存储的 key。常见做法是本地磁盘存一份同时留一个 S3 兼容对象存储的适配接口小团队先用本地盘量大了再切对象存储代码不用大改。2.2 目录结构长什么样一个典型的后端工程目录大致是这样组织的doc-management/ ├── doc-common/ # 通用工具、常量、统一返回体 ├── doc-auth/ # 认证授权模块JWT Spring Security ├── doc-core/ # 核心业务文档 CRUD、分类、版本 ├── doc-search/ # Elasticsearch 检索封装 ├── doc-storage/ # 存储抽象层本地盘 / 对象存储实现 ├── doc-admin/ # 启动模块聚合以上依赖 └── pom.xml # Maven 父 POM统一版本管理前端单独一个仓库或放在web/目录下web/ ├── src/ │ ├── api/ # 所有后端接口的封装 │ ├── views/ │ │ ├── document/ # 文档列表、上传、详情 │ │ ├── category/ # 分类管理 │ │ └── system/ # 用户、角色、权限 │ ├── router/ # 路由与权限守卫 │ └── store/ # Pinia 状态管理 └── vite.config.js这种多模块拆法的好处是职责清晰doc-storage只关心「文件怎么存怎么取」doc-search只关心「怎么建索引怎么查」业务模块doc-core调用它们互不污染。坏处是模块间依赖要理清楚否则容易出现循环依赖Maven 直接报错。2.3 核心数据表设计元数据这块几张关键表撑起整个系统表名作用关键字段sys_user用户id, username, password, dept_idsys_role角色id, role_code, role_namesys_user_role用户角色关联user_id, role_iddoc_category文档分类树形id, name, parent_id, sortdoc_file文档主表id, name, category_id, storage_key, size, uploader_id, current_versiondoc_version版本历史id, file_id, version_no, storage_key, create_timedoc_permission细粒度权限id, file_id, role_id, can_read, can_writedoc_category用parent_id自关联做树形分类这是最省事的做法查询时递归或一次性拉全表在内存里拼树。doc_file和doc_version分开是为了支持「同一份文档多次修改保留历史」current_version指向最新版本下载旧版本时去doc_version里捞。提示分类表如果层级很深别用递归 SQL 查直接全表拉出来在 Java 里用 Map 拼树几千个分类也就几毫秒的事。3. 把项目跑起来从环境准备到第一个文档上传3.1 环境依赖与启动顺序这套系统跑起来需要四样东西JDK 17、MySQL 8、Elasticsearch 8带 IK 分词插件、Node 18。启动顺序有讲究先起基础设施再起后端最后起前端。# 1. 启动 MySQL 和 Elasticsearch用 Docker 最省事 docker run -d --name doc-mysql -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEdoc_manage \ mysql:8.0 docker run -d --name doc-es -p 9200:9200 \ -e discovery.typesingle-node \ -e ES_JAVA_OPTS-Xms512m -Xmx512m \ elasticsearch:8.11.0 # 2. 进入 ES 容器装 IK 分词插件中文检索必须 docker exec -it doc-es bin/elasticsearch-plugin install \ https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.11.0/elasticsearch-analysis-ik-8.11.0.zip docker restart doc-es这里有个血泪经验IK 插件的版本必须和 Elasticsearch 版本严格对应8.11 的 ES 装 7.x 的 IK启动直接报插件加载失败。装完插件一定要重启 ES 容器否则不生效。数据库建好后导入项目doc-admin/src/main/resources/db/下的schema.sql和data.sql前者建表后者塞初始管理员账号和几个默认分类。3.2 后端配置与启动后端配置集中在application.yml几个关键项必须改spring: datasource: url: jdbc:mysql://localhost:3306/doc_manage?useUnicodetruecharacterEncodingutf8 username: root password: root123 elasticsearch: uris: http://localhost:9200 doc: storage: type: local # local 或 s3 local-path: /data/doc-files # 文件实体存放目录 jwt: secret: your-secret-key-at-least-32-chars expire: 7200 # token 有效期秒storage.type决定文件往哪存本地盘就填locallocal-path指向一个真实存在且有写权限的目录。JWT 的secret别用默认值长度至少 32 字符否则启动时校验不过。配置改完在项目根目录执行mvn clean package -DskipTests java -jar doc-admin/target/doc-admin-1.0.0.jar看到控制台打印Started DocAdminApplication就算起来了默认端口 8080。第一次启动会自动往 Elasticsearch 里创建doc_index索引如果 ES 没起来这一步会报连接超时先检查 ES 是否正常。3.3 前端启动与联调前端进web/目录先装依赖再起开发服务器cd web npm install npm run devVite 默认跑在 5173 端口需要在vite.config.js里配代理把/api转发到后端 8080否则跨域请求会被浏览器拦// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })浏览器打开http://localhost:5173用data.sql里的初始账号一般是admin/admin123登录进「文档管理」页面点上传选一个 PDF 或 Word填好分类提交。上传成功后列表里能看到这条记录点搜索框输入文档里的某个词能搜出来就说明 Elasticsearch 索引也通了。3.4 上传接口的完整链路上传这个动作后端走了一条挺长的链路值得单独看一眼。核心 Controller 大概长这样RestController RequestMapping(/api/document) public class DocumentController { Autowired private DocumentService documentService; PostMapping(/upload) public ResultDocumentVO upload( RequestParam(file) MultipartFile file, RequestParam(categoryId) Long categoryId) { // 1. 校验文件类型和大小 // 2. 计算文件 MD5做秒传判断 // 3. 存储文件实体拿到 storageKey // 4. 写 doc_file 和 doc_version 记录 // 5. 异步发消息触发 ES 建索引 DocumentVO vo documentService.upload(file, categoryId); return Result.success(vo); } }MultipartFile是 Spring 对上传文件的封装categoryId决定文档归到哪个分类下。Service 层里先算 MD5 是为了秒传——如果同一个文件已经有人传过直接复用已有的storageKey不重复占磁盘。存储这一步调doc-storage模块的接口本地实现就是把字节流写到local-path下按日期分目录返回一个相对路径作为storageKey。最后一步「异步建索引」很关键。如果同步做用户上传一个大文件后要等 ES 索引完才能看到成功提示体验很差。常见做法是丢一个消息到队列RabbitMQ 或 Redis 的 list 都行后台消费者慢慢建索引。小团队嫌麻烦用 Spring 的Async开个线程池异步跑也够用。注意异步建索引意味着「上传成功」和「能搜到」之间有几秒延迟测试时别刚传完就搜搜不到不一定是 bug。4. 权限与检索两个最容易翻车的模块4.1 基于角色的文档权限控制企业文档和网盘最大的区别就是权限。网盘是「我的文件我做主」企业文档是「你的文件可能只有你领导能看」。这套系统用 RBAC 打底再叠一层文档级权限。RBAC 部分用户挂角色角色挂菜单和接口权限Spring Security 在过滤器里校验。文档级权限则查doc_permission表一份文档可以针对某个角色单独授权读或写。判断逻辑是「用户的所有角色里只要有一个角色对该文档有读权限就能读」。public boolean canRead(Long fileId, Long userId) { // 1. 上传者本人和超管直接放行 DocFile file docFileMapper.selectById(fileId); if (file.getUploaderId().equals(userId) || isSuperAdmin(userId)) { return true; } // 2. 查用户所有角色 ListLong roleIds userRoleMapper.selectRoleIdsByUserId(userId); if (roleIds.isEmpty()) { return false; } // 3. 查文档权限表任一角色有读权限即可 Long count docPermissionMapper.countReadable(fileId, roleIds); return count ! null count 0; }这段逻辑看着简单但坑在「分类继承」。实际业务里给一个分类授权往往希望它下面的子分类和文档都继承这个权限。如果只做文档级授权管理员得给几百份文档挨个点会疯。常见做法是在doc_category上也加权限字段查询时先看文档自身有没有显式权限没有就往上找父分类直到根节点。4.2 Elasticsearch 检索的索引设计检索这块索引结构设计得好不好直接决定搜索体验。doc_index的 mapping 大致这样{ mappings: { properties: { fileId: { type: long }, name: { type: text, analyzer: ik_max_word }, content: { type: text, analyzer: ik_max_word }, categoryId:{ type: long }, uploaderId:{ type: long }, createTime:{ type: date } } } }name和content用ik_max_word分词器它会把「企业文档管理系统」切成「企业」「文档」「管理」「系统」等多个词召回率高。如果更在意精确度可以换成ik_smart切得粗一些。categoryId和uploaderId用long而不是text是为了能做过滤和聚合比如「只看某个分类下的文档」。建索引的代码在doc-search模块public void indexDocument(DocFile file, String content) { IndexRequest request new IndexRequest(doc_index) .id(String.valueOf(file.getId())) .source(Map.of( fileId, file.getId(), name, file.getName(), content, content, categoryId, file.getCategoryId(), uploaderId, file.getUploaderId(), createTime, file.getCreateTime() ), XContentType.JSON); try { esClient.index(request, RequestOptions.DEFAULT); } catch (IOException e) { // 索引失败不能影响主流程记日志后由补偿任务重试 log.error(索引文档失败, fileId{}, file.getId(), e); } }content从哪来上传的是 PDF 或 Word得先把文本抽出来。PDF 用 Apache PDFBoxWord 用 POI抽出来的纯文本塞进content字段。这一步是检索能不能搜到正文的关键如果只索引文件名那和直接查数据库没区别。提示PDFBox 抽扫描件 PDF 是抽不出文字的因为那是图片。要支持扫描件得接 OCR这是另一个量级的工程别在初期就陷进去。4.3 搜索接口的查询构造搜索接口要同时支持关键词、分类过滤、上传人过滤用 Elasticsearch 的 bool query 组合public SearchResult search(String keyword, Long categoryId, int page, int size) { BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); // 关键词匹配文件名或正文命中任一即可 if (StringUtils.hasText(keyword)) { boolQuery.must(QueryBuilders.multiMatchQuery(keyword, name, content) .operator(Operator.OR)); } // 分类过滤 if (categoryId ! null) { boolQuery.filter(QueryBuilders.termQuery(categoryId, categoryId)); } SearchSourceBuilder builder new SearchSourceBuilder() .query(boolQuery) .from((page - 1) * size) .size(size) .highlighter(new HighlightBuilder().field(name).field(content)); // 执行查询并解析高亮结果... }multiMatchQuery让关键词同时匹配name和contentoperator(OR)表示只要命中一个词就算匹配。filter和must的区别在于 filter 不参与打分纯过滤性能更好。高亮部分把命中的关键词用em包起来前端渲染时显示成红色用户一眼能看到为什么这条被搜出来。分页用from和size但要注意 ES 默认from size超过 10000 会报错。文档量大的话得改用search_after游标分页这是后话初期几千份文档用 from/size 完全够。5. 避坑与常见问题排查5.1 上传大文件报 413 或超时现象传一个 50MB 的 PDF浏览器报 413 Request Entity Too Large或者转圈半天最后失败。原因Spring Boot 默认的单文件大小限制是 1MB请求大小限制 10MB超过就拒。另外 Nginx 如果做了反向代理它自己也有client_max_body_size限制默认 1MB。解决Spring 侧在application.yml里放开spring: servlet: multipart: max-file-size: 100MB max-request-size: 200MBNginx 侧在server块里加client_max_body_size 200m;。两边都要改只改一边照样翻车。5.2 中文搜索搜不到现象文档里明明有「采购合同」四个字搜「采购」却搜不出来。原因Elasticsearch 默认分词器对中文是按单字切的或者干脆把整句当一个词导致搜不到。没装 IK 插件或者 mapping 里analyzer没指定。解决确认 IK 插件装好且版本匹配然后检查索引 mappingname和content字段的analyzer必须是ik_max_word或ik_smart。已经建好的索引改 mapping 不生效得删了重建重新灌数据。5.3 下载文件时文件名乱码现象下载下来的文件中文名变成一串百分号编码或者干脆是乱码。原因HTTP 响应头里Content-Disposition的filename没做 URL 编码不同浏览器解析方式不一样。解决后端设置响应头时统一编码String encodedName URLEncoder.encode(file.getName(), StandardCharsets.UTF_8) .replace(, %20); response.setHeader(Content-Disposition, attachment; filename\ encodedName \; filename*UTF-8 encodedName);filename*是 RFC 5987 规定的写法现代浏览器优先认它兼容性最好。5.4 权限改了但用户还能访问现象管理员把某个角色对某文档的读权限取消了但该角色下的用户刷新页面还能下载。原因权限判断结果被缓存了或者前端路由守卫只校验了菜单权限没校验文档级权限。解决检查是否有 Redis 缓存了权限结果改权限时要主动清缓存。另外文档下载接口必须在后端再校验一次权限不能只靠前端隐藏按钮——前端隐藏只是体验后端拦截才是安全。5.5 服务重启后 ES 索引丢失现象重启 Elasticsearch 容器后之前建的索引和文档全没了。原因Docker 启动 ES 时没挂数据卷容器一删数据就没了。解决启动时挂载数据目录docker run -d --name doc-es -p 9200:9200 \ -v /data/es-data:/usr/share/elasticsearch/data \ -e discovery.typesingle-node \ elasticsearch:8.11.0生产环境更要挂卷而且最好配快照仓库定期备份。索引丢了虽然能从 MySQL 重新灌但几万份文档重新抽文本建索引够等一阵。6. 二次开发从能跑到好用我一般改这几个地方项目跑通只是起点真正落地到团队里总得按自己的业务改。我拿这套系统做过几次二次开发有几个改造点性价比最高分享下思路。第一个是存储层扩展。默认的本地盘存储单机跑没问题但一旦多实例部署就废了——A 实例传的文件B 实例读不到。改造方法是实现doc-storage模块里的StorageService接口写一个 S3 兼容的实现把文件往对象存储扔。接口就三个方法store、load、delete实现起来不到一百行。改完在配置里把storage.type切成s3填上 endpoint、accessKey、secretKey、bucket 就行。这样后端可以水平扩多个实例文件共享问题自然解决。第二个是权限模型细化。RBAC 加文档级权限对大多数团队够用但有些场景要求「部门隔离」——销售部的人看不到研发部的文档。做法是在doc_file上加一个dept_id字段上传时记录上传者所属部门查询时强制拼一个部门过滤条件。这个改动涉及查询、下载、搜索三个入口每个入口都要加漏一个就是越权漏洞。我一般会写一个统一的PermissionChecker组件所有入口都调它避免散落在各处。第三个是检索体验优化。默认搜索是「关键词匹配」但用户经常搜一句话比如「去年的采购合同」。这时候可以接一个大模型做查询改写把自然语言转成结构化查询条件时间范围 分类 关键词。改造点在搜索接口前面加一层 query 解析解析结果再拼 ES 查询。这块不用做太复杂先用规则匹配时间词和分类词覆盖八成场景就够。验证改造是否成功我习惯用一套固定的回归用例传一份带中文正文的 PDF等几秒搜正文里的词能搜到用不同角色的账号登录确认该看到的能看到、不该看到的看不到下载文件确认文件名不乱码、内容完整。这套用例每次改完权限或存储相关代码都跑一遍比事后救火省心得多。从那以后我每次动权限和存储这两块代码都强制走一遍这套回归哪怕只改了一行。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑