知识库目录树与共享功能实现全记录
概述
本文记录了为作业批改系统实现知识库模块的全过程,包括目录树浏览、文件管理、共享知识库协作功能,以及期间遇到的各种问题和解决方案。
最终效果:用户可以创建个人知识库目录树,上传文件、管理文件夹;同时支持创建共享知识库,多人协作上传和管理文档。
一、目录树知识库(初版)
需求
将知识库管理页面改为类似腾讯 ima 的目录树形式,支持:
- 文件夹/文件的无限层级嵌套
- 展开折叠
- 右键菜单(新建子文件夹、重命名、删除)
- 拖拽排序
- 搜索过滤
- 右侧文档内容预览
技术选型
项目已集成 Element Plus,直接使用 el-tree 组件而非手写递归组件。
| 功能 | 实现 |
|---|---|
| 目录树渲染 | el-tree + children 递归 |
| 右键菜单 | @node-contextmenu + 自定义浮层 |
| 拖拽排序 | draggable + @node-drop |
| 搜索过滤 | filter-node-method |
| 行内重命名 | 双击 → el-input 内联编辑 |
| 文件上传 | el-upload 拖拽模式 |
| 文档预览 | 后端 Tika 解析 + 前端 Markdown 渲染 |
后端设计
新增 directory_node 表:
1 | CREATE TABLE directory_node ( |
API 归入 DocumentController,不另建 Controller:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/documents/directory/tree |
获取目录树 |
| POST | /api/documents/directory/folder |
创建文件夹 |
| PUT | /api/documents/directory/{id}/rename |
重命名 |
| DELETE | /api/documents/directory/{id} |
删除(递归) |
| PUT | /api/documents/directory/{id}/move |
拖拽移动 |
遇到的问题
1. 编码导致读取乱码
Files.readString() 默认 UTF-8,Windows 中文文本为 GBK 编码。改用 Apache Tika(FileStorageService.readFileContent())解析文件,同时回退路径按 UTF-8 → GBK → GB18030 依次尝试解码。
2. 删除文件不删 chunk
deleteDocument 只删了文件和 DB 记录,没清理 document_chunk。修复:调用 VectorStoreService.deleteDocument(docId)。
3. 上传到根目录不显示
parentNodeId 为 null 时跳过了 directory_node 创建。修复:无论有无 parentNodeId,都创建目录节点。
二、共享知识库
需求
参考腾讯 ima 的协作模式:
- 创建共享知识库(多人可读写)
- 通过邀请链接加入
- 成员管理(踢人、改角色)
- 文件归共享库所有(成员退出后文件保留)
数据模型
新增 shared_kb 和 shared_kb_member 表,同时 document 和 directory_node 加 kb_id 字段:
1 | CREATE TABLE shared_kb ( |
API
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/shared-kb/create |
创建知识库 |
| GET | /api/shared-kb/my |
我创建的 |
| GET | /api/shared-kb/joined |
我加入的 |
| PUT | /api/shared-kb/{id} |
编辑名称/简介 |
| DELETE | /api/shared-kb/{id} |
解散(仅 owner) |
| POST | /api/shared-kb/{id}/invite |
生成邀请链接 |
| POST | /api/shared-kb/join?token= |
通过邀请加入 |
| GET | /api/shared-kb/{id}/members |
成员列表 |
| DELETE | /api/shared-kb/{id}/members/{userId} |
踢人 |
| GET | /api/documents/directory/tree?kbId= |
获取指定知识库目录树 |
前端布局
1 | 📁 我的知识库 ← 始终可见,完全可编辑 |
权限模型
| 操作 | 个人库 | 共享库(成员) | 共享库(非成员) |
|---|---|---|---|
| 查看 | ✅ 拥有者 | ✅ | ❌ |
| 上传/新建文件夹 | ✅ | ✅ | ❌ |
| 重命名/删除/移动 | ✅ | ✅ | ❌ |
| 管理成员/邀请 | — | owner + admin | ❌ |
| 解散 | — | owner only | ❌ |
遇到的重大问题
1. @Update + WITH RECURSIVE 不生效
共享文件夹时需要递归更新所有子节点的 is_shared。最初用 @Update 注解 + PostgreSQL 递归 CTE:
1 |
|
运行时只更新了文件夹本身,子节点未更新。原因:MyBatis 的 @Update 对 WITH RECURSIVE ... UPDATE 语句的执行行为不确定。
解决方案: 改用 selectDescendants(@Select + WITH RECURSIVE,已验证可用)查出所有子节点,再逐个 updateById。
2. 编辑链失败导致代码不一致
多次修改 PageThree.vue 时,其中一个编辑因 oldText 不匹配失败,导致后续编辑被跳过。结果:
- 模板里用了
myTreeData但 script 里声明的是treeData computed和ElMessage导入丢失TreeNode接口缺少isShared字段myKbs、joinedKbs、treeProps声明被误删
教训: 连续 edit 操作应该独立验证,不应该批量链式提交。
3. @node-contextmenu 事件中 data 变量作用域错误
1 | @node-contextmenu="currentKbId == null ? handleContextMenu($event, data) : null" |
data 是 #default 插槽的作用域变量,但 @node-contextmenu 绑定在 el-tree 组件上,不在插槽内,data 不可用。
解决方案: 始终绑定 @node-contextmenu="handleContextMenu",在函数内部判断 currentKbId。
4. 共享库权限校验过严
createFolder、renameNode、deleteDirectoryNode、moveNode 都校验 node.getUserId().equals(currentUser)。共享库下 A 创建的文件夹,B 无法操作。
解决方案: 加上判断——如果节点 kbId != null(属于共享库),跳过 userId 校验。
5. 共享库上传文件显示到个人树
右键”上传文件到此”只设置了 uploadParentNode,没有同步 uploadKbId。submitUpload 只看 uploadKbId 来决定是否带 kbId 参数,导致文件被归入个人库。
解决方案: contextUploadFile 从文件夹节点读取 kbId 并同步设置 uploadKbId。
6. TypeScript 类型问题
FlatNode缺少isShared字段 → 后端返回了但前端读不到TreeNode缺少isShared、sharedBy、userIdauth()返回{}时类型不兼容HeadersInit
解决方案: 补全接口定义,auth() 加显式返回类型 Record<string, string>。
三、踩坑总结
- MyBatis
@Update不支持复杂 CTE → 改 Java 层面循环更新 Files.readString()不处理 GBK → 用 Apache Tika 替代- Vue slot scope 和组件事件不在同一作用域 → 别在组件事件里引 slot 变量
- Vite 热更新有缓存 → 出问题时清
.vite缓存重启 - 多次
edit操作互相影响 → 大改用write整个文件更可靠 - 共享库权限设计要提前规划 → 后期到处改 userId 校验很麻烦
- FormData 上传不带 Content-Type → 浏览器自动设置 multipart boundary
四、代码量统计
| 模块 | 文件数 | 约行数 |
|---|---|---|
| 后端新增 | 8 | ~700 |
| 后端修改 | 6 | ~200 |
| 前端重写 | 1 (PageThree.vue) | ~550 |
| 数据库迁移 | 3 | ~90 |
| 合计 | 18 | ~1540 |



