概述

本文记录了为作业批改系统实现知识库模块的全过程,包括目录树浏览、文件管理、共享知识库协作功能,以及期间遇到的各种问题和解决方案。

最终效果:用户可以创建个人知识库目录树,上传文件、管理文件夹;同时支持创建共享知识库,多人协作上传和管理文档。


一、目录树知识库(初版)

需求

将知识库管理页面改为类似腾讯 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
2
3
4
5
6
7
8
9
10
11
CREATE TABLE directory_node (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
parent_id BIGINT DEFAULT NULL,
label VARCHAR(255) NOT NULL,
node_type VARCHAR(20) NOT NULL, -- folder | file
doc_id VARCHAR(64) DEFAULT NULL,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);

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_kbshared_kb_member 表,同时 documentdirectory_nodekb_id 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CREATE TABLE shared_kb (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description VARCHAR(500) DEFAULT '',
owner_id BIGINT NOT NULL,
invite_token VARCHAR(64) UNIQUE,
invite_expires_at TIMESTAMP,
invite_max_uses INT DEFAULT 0,
invite_use_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE shared_kb_member (
id BIGSERIAL PRIMARY KEY,
kb_id BIGINT NOT NULL REFERENCES shared_kb(id),
user_id BIGINT NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'member',
joined_at TIMESTAMP DEFAULT NOW(),
UNIQUE(kb_id, user_id)
);

ALTER TABLE document ADD COLUMN kb_id BIGINT DEFAULT NULL;
ALTER TABLE directory_node ADD COLUMN kb_id BIGINT DEFAULT NULL;

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
2
3
4
5
6
7
8
9
10
11
📁 我的知识库              ← 始终可见,完全可编辑
├─ 数学
└─ 英语

⭐ 我创建的
▶ 📂 高一数学备课组 ⚙️ ← 点击展开/折叠
├─ 课件
└─ 教案

🔗 我加入的
▶ 📂 英语教研组

权限模型

操作 个人库 共享库(成员) 共享库(非成员)
查看 ✅ 拥有者
上传/新建文件夹
重命名/删除/移动
管理成员/邀请 owner + admin
解散 owner only

遇到的重大问题

1. @Update + WITH RECURSIVE 不生效

共享文件夹时需要递归更新所有子节点的 is_shared。最初用 @Update 注解 + PostgreSQL 递归 CTE:

1
2
3
4
5
6
@Update("""
WITH RECURSIVE sub_tree AS (...)
UPDATE directory_node SET is_shared = #{shared}
WHERE id IN (SELECT id FROM sub_tree)
""")
void updateSharedRecursive(...);

运行时只更新了文件夹本身,子节点未更新。原因:MyBatis 的 @UpdateWITH RECURSIVE ... UPDATE 语句的执行行为不确定。

解决方案: 改用 selectDescendants@Select + WITH RECURSIVE,已验证可用)查出所有子节点,再逐个 updateById

2. 编辑链失败导致代码不一致

多次修改 PageThree.vue 时,其中一个编辑因 oldText 不匹配失败,导致后续编辑被跳过。结果:

  • 模板里用了 myTreeData 但 script 里声明的是 treeData
  • computedElMessage 导入丢失
  • TreeNode 接口缺少 isShared 字段
  • myKbsjoinedKbstreeProps 声明被误删

教训: 连续 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. 共享库权限校验过严

createFolderrenameNodedeleteDirectoryNodemoveNode 都校验 node.getUserId().equals(currentUser)。共享库下 A 创建的文件夹,B 无法操作。

解决方案: 加上判断——如果节点 kbId != null(属于共享库),跳过 userId 校验。

5. 共享库上传文件显示到个人树

右键”上传文件到此”只设置了 uploadParentNode,没有同步 uploadKbIdsubmitUpload 只看 uploadKbId 来决定是否带 kbId 参数,导致文件被归入个人库。

解决方案: contextUploadFile 从文件夹节点读取 kbId 并同步设置 uploadKbId

6. TypeScript 类型问题

  • FlatNode 缺少 isShared 字段 → 后端返回了但前端读不到
  • TreeNode 缺少 isSharedsharedByuserId
  • auth() 返回 {} 时类型不兼容 HeadersInit

解决方案: 补全接口定义,auth() 加显式返回类型 Record<string, string>


三、踩坑总结

  1. MyBatis @Update 不支持复杂 CTE → 改 Java 层面循环更新
  2. Files.readString() 不处理 GBK → 用 Apache Tika 替代
  3. Vue slot scope 和组件事件不在同一作用域 → 别在组件事件里引 slot 变量
  4. Vite 热更新有缓存 → 出问题时清 .vite 缓存重启
  5. 多次 edit 操作互相影响 → 大改用 write 整个文件更可靠
  6. 共享库权限设计要提前规划 → 后期到处改 userId 校验很麻烦
  7. FormData 上传不带 Content-Type → 浏览器自动设置 multipart boundary

四、代码量统计

模块 文件数 约行数
后端新增 8 ~700
后端修改 6 ~200
前端重写 1 (PageThree.vue) ~550
数据库迁移 3 ~90
合计 18 ~1540