开场:一颗定时炸弹

事情从一个看似简单的测试开始——

学生 QQ 里问”龙空灵喜欢什么?”,结果 RAG 检索到了龙空灵私人知识库里”爱玩 CS2、想吃 Java 实习”的内容。

私人知识库居然对学生可见。 这不叫知识库,这叫朋友圈。


第一关:全库检索,来者不拒

问题

检查 VectorStoreService 的 SQL:

1
SELECT ... FROM document_chunk ORDER BY embedding_vec <=> ?::vector LIMIT ?

没有 WHERE,没有 user_id,没有 kb_id。所有人敞开大门,随便搜。

解决

  1. document_chunkuser_id 列,迁移脚本回填存量数据
  2. VectorStoreService.similaritySearch() / keywordSearch() 加权限过滤 SQL:
1
2
WHERE (user_id = ? AND kb_id IS NULL)  -- 私人文档
OR kb_id IN (?) -- 已加入的共享知识库
  1. DocumentServiceImpl.searchRelevantContent()userId 参数,查 shared_kb_member 获取用户加入的共享库

  2. ChatController 从 JWT 取 userId 传入

改完后 Web 端隔离完毕。但 OneBot 那路呢?


第二关:学生≠老师

问题

OneBot 学生 QQ 提问,没有登录态,传了个 null,还是全库检索。

最初脑子短路想建 QQ→userId 映射,把学生当用户。被纠正:知识库是老师上传的,学生提问不应该碰老师的私人库。

解决

ActiveTeacherService,Redis 存当前活跃老师:

1
2
老师前端操作 → ChatController → Redis: "rag:active_teacher" = userId (TTL 10min)
学生QQ提问 → OnebotRagController → 读Redis → 以老师身份检索

切换老师立即覆盖,10 分钟没人刷新自动过期。


第三关:僵尸消息永动机

问题

日志反复刷 认领僵尸消息: count=1,每 30 秒一次。查代码发现 GradingStreamConsumer.claimPendingMessages() 里:

1
2
stream.claim(group, consumerName, idleTime, TimeUnit.MILLISECONDS, msgId);
// ↑ 返回值被丢弃了!

claim 只是改了消息的 owner,但 consumeLoopneverDelivered() 模式读不到已投递过的消息。于是消息在 Pending 队列里来回改姓但永远不被处理——Redis Stream 版孤儿怨。

解决

1
2
3
4
Map<StreamMessageId, Map<String, String>> claimed = stream.claim(...);
for (entry : claimed) {
executor.submit(() -> processOne(entry.getKey(), entry.getValue()));
}

claim 返回被认领的消息数据,直接提交处理 → 处理完 ACK → 消息从 PEL 移除。


第四关:柱状图去哪了

问题

TaskDetail 页面的成绩分布 ECharts 柱状图怎么都不显示。Console 没报错、接口有数据、echarts 已安装。

排查

加诊断日志后发现 chartRef 容器找不到。

模板结构:

1
2
3
4
<div v-if="loading">加载中...</div>
<template v-else-if="task">
<div ref="chartRef">图表</div> ← 这个!
</template>

renderChart()try 块里用 $nextTick 调用时,loading 还是 truev-if 优先级高于 v-else-ifchartRef 根本没渲染。

解决

1
2
3
4
finally {
this.loading = false
this.$nextTick(() => this.renderChart()) // 等 loading 变 false 后再渲染
}

Y 轴也加了 min: 0 + minInterval: 1,数据再少柱子也看得见。


第五关:新页签的各种毛病

需求

点”查看”按钮看学生提交的原文件。

选型

弹窗 vs 新页签——选了新页签,代码作业通常长,全屏舒坦。

踩坑

  1. submissionIdundefined:后端 TaskController 没返回这个字段。补上 si.put("submissionId", s.getId()),前端加空值兜底

  2. window.close() 报错:Vue 模板里不能直接调 window.close(),改成 methods 里的 closeWindow()


今日战果

问题 根因 改了几处
私人KB对学生可见 VectorStoreService 全表扫描 6 个文件
僵尸消息反复报警 claim 返回值被丢弃 1 个文件
OneBot 无身份 没有活跃教师机制 3 个文件
柱状图不显示 v-if/v-else-if 顺序bug 1 个文件
文件查看 新需求 4 个文件
window.close 报错 Vue模板限制 1 个文件

总计:16 个文件改动,0 次回滚到初始方案。

教训:写代码时觉得自己在造火箭,debug 时发现自己在拆盲盒。