站点架构导入1537 字·2026/11/14·被检索 0 次

词汇站 kaoyan-vocab · 单词记忆与复习算法

词汇站是 Next.js 14 + Prisma + SQLite 的单词记忆应用,跑在 3001、basePath 为 /vocab,经门户反代到 cag.quest/vocab/。实测其 16 张表、528 个单词、复习队列的真实逻辑:按 mastery<4 取最近更新 100 词,答对出队、答错排队尾,并非间隔重复。文中同时写明它不校验门户身份、不能脱离门户直连使用的耦合。

挂靠在《四、网站架构总览》之下

版本
使用说明
v1reused-preview2026/11/14 11:16:36

词汇站 kaoyan-vocab · 单词记忆与复习算法

一句话:词汇站是一个 Next.js 14 的单词记忆应用,跑在 127.0.0.1:3001(Next 实际监听 ::3001),basePath 为 /vocab,由门户反代到 https://cag.quest/vocab/;它自己不做登录,是一个「全局单用户」的词库。

一、定位与入口

项实测值
公网地址https://cag.quest/vocab/(经 portal 3000 反代)
监听端口3001(实测 Get-NetTCPConnection 显示 :: 3001,PID 21496)
进程命令node node_modules\next\dist\bin\next start -p 3001
basePath/vocab(next.config.js)
计划任务KaoyanNextServer,Running;动作 ops\launch.ps1 -Name vocab
框架Next.js 14.2.15 App Router、React 18、Prisma 5.20、SQLite
数据库kaoyan-vocab/prisma/dev.db(实测 16 张表,528 个单词)
样式Tailwind CSS,暗色模式用 html.dark 类切换

next.config.js 同时开启 trailingSlash: true 和 serverComponentsExternalPackages: ["@prisma/client"]。前者意味着所有页面和 API 都要带尾斜杠;后者避免 Prisma 引擎被打包进 server bundle。

二、技术栈与目录结构

kaoyan-vocab/
  app/
    page.tsx               首页:搜索框 + 统计卡 + 白皮书知识库入口
    layout.tsx             全局布局、主题脚本、导航、工具栏
    word/[word]/           单词详情(主版本正文 + 版本/收藏/掌握度)
    review/                复习卡片页
    history/               搜索历史
    roots/                 词根词缀库
    confusables/           易混词组
    associate/             联想气泡
    associate/[id]/        联想结果详情
    api/
      search/              POST 检索
      words/[word]/        单词详情
      words/[word]/favorite
      words/[word]/mastery 掌握度 0-5
      words/[word]/regenerate
      review/              待复习列表
      random/              随机复习一个词
      history/ stats/ roots/ confusables/ compare/ notes/ association/
  components/              SearchBox、Toolbar、WordClickProvider、MarkdownView 等
  lib/
    db.ts                  Prisma 单例
    session.ts             仅生成 localStorage 匿名 sessionId
    deepseek.ts            AI 调用
    generate.ts            AI 生成单词材料
    formats.ts             生成格式模板
    whitebook.ts / whitebook-parse.ts / whitebook-updater.ts
    ratelimit.ts           进程内限流
  prisma/
    schema.prisma          16 个模型
    seed.ts                从 whitebook.md 3.1–3.7 灌库
    dev.db

目录里没有 middleware.ts,也没有任何鉴权模块。lib/session.ts 的注释写得很清楚:它生成的是「客户端匿名会话 ID,用于搜索历史归组」,与门户的 pf_sid 毫无关系。

三、数据模型

dev.db 实测 16 张表。核心模型如下:

表用途关键字段关系
Word单词主表word 唯一、normalized 唯一、phonetic、coreMeaning、mastery(0-5)、favorite、inWhitebook、frequencyRank一对多 WordSense、WordVersion、WordRelation、WordMorpheme
WordSense义项 / 熟词僻义meaningZh、label、isRare、prosody、orderwordId → Word,级联删除
WordVersion单词内容的版本formatType(standard/brief/root/confuse/example/translation/custom)、contentMarkdown、isPrimary、model、tokens多对一 Word;一个词只有一个 isPrimary
Morpheme词根 / 前缀 / 后缀slug 唯一、form、type、meaning多对多 Word,经 WordMorpheme 带 slot
WordMorpheme词与词根关联slot(prefix/root/suffix)、note复合唯一 (wordId, morphemeId, slot)
ConfusableGroup易混词组title 唯一、keyPoint、category一对多 ConfusableMember
ConfusableMember组内成员wordText、normalized、distinction、ordergroupId → 组
PronunciationItem音标辨析kind(symbol/word)、symbol、ipa、note唯一 (kind, symbol)
TranslationNote翻译 / 语义支线keyText、body唯一 (keyText, body)
WordRelation词间关系relationType(同根/易混/反义/搭配/相关)、toWordText自关联 Word
SearchHistory搜索流水wordText、normalized、hit、sessionId可关联 Word
Note工具栏临时笔记content、pinned、wordText可关联 Word
CompareItem对比工具收藏normalized 唯一、order可关联 Word
Association / AssociationItemAI 联想批次与条目seedWords、similarity、reason一对多

实测行数:Word 528、WordSense 145、Morpheme 23、WordMorpheme 85、ConfusableGroup 52、ConfusableMember 390、PronunciationItem 20、TranslationNote 19、WordVersion 41、SearchHistory 39、Association 13 / AssociationItem 138。词库规模不大,属于个人精读型,不是背单词大厂的全量词表。

四、请求链路

  1. 浏览器访问 https://cag.quest/vocab/。
  2. Cloudflare → cloudflared → 127.0.0.1:3000。
  3. portal 命中 /vocab 前缀,proxy(req,res,3001,...),原样转发,只把裸 /vocab 补成 /vocab/。
  4. Next 以 basePath=/vocab 匹配页面,服务端组件直接 prisma 查 SQLite,渲染 HTML。
  5. 浏览器里的客户端组件发 fetch("/api/search")。注意这是绝对根路径,不是 /vocab/api/search。它必须再经过 portal:portal 的 /api/* 规则把它重写成 /vocab/api/* 再转给 3001。
  6. 因此词汇站不能脱离门户直接使用:直连 http://127.0.0.1:3001/vocab/ 虽然能打开页面,但页面里的 /api/* 请求会 404。这是 basePath 与前端绝对路径共同造成的耦合。

五、关键机制

1)所谓「复习算法」其实是掌握度队列,不是间隔重复。

app/api/review/route.ts 的完整筛选条件是:

where: { mastery: { lt: 4 } }
orderBy: { updatedAt: "desc" }
take: 100

没有 dueAt、没有 interval、没有 easeFactor、没有 SM-2 / FSRS 的遗忘曲线。前端 app/review/page.tsx 的行为是:

  • 点「不会」→ POST /api/words/:word/mastery,值 0;
  • 点「模糊」→ 值 3;
  • 点「已掌握」→ 值 5;
  • mastery >= 4 的卡片从当前队列移除并计入「本轮已掌握」;
  • < 4 的卡片被 splice 出来 push 到队尾,本轮再来一遍;
  • 全部过完或用户点「重新加载列表」时,重新拉一次 mastery < 4 的词。

所以它是一个「按最近更新倒序、最多 100 张、答对即出队、答错排队尾」的 Leitner 式简化队列。优点是实现极简、用户理解成本低;缺点是同一批词每次顺序固定、没有时间维度、无法表达「三天后再复习」。文中必须明确:这不是间隔重复算法,只是掌握度驱动的循环队列。

2)掌握度是唯一记忆状态。 Word.mastery 是 0-5 的整数,只由用户点按钮写入,没有自动衰减。/api/stats 把 mastery < 4 的数量直接当作「待复习」。这意味着系统的「记忆模型」实际上是一个手工标签,而不是可计算的记忆强度。

3)内容生成与版本。 搜索一个不存在的词会走 deepseek 生成,结果写入 WordVersion,isPrimary=true 的那一版作为详情页主内容。一个词可以有多版(标准/简版/词根版/易混版/例句版/翻译版/自定义),旧版永不覆盖。生成是同步请求,受 lib/ratelimit.ts 的进程内限流约束。

4)白皮书驱动的知识库。 prisma/seed.ts 解析项目根 whitebook.md 的第 3.1–3.7 节,灌入 Morpheme、WordSense、ConfusableGroup、PronunciationItem、TranslationNote。首页的「白皮书知识库」四张卡直接读这些表的计数。更新白皮书后要手动 npm run db:seed,没有增量同步机制。

5)与门户的鉴权衔接:几乎没有。 实测对 app/ 与 lib/ 全量搜索 x-portal-secret、x-user-、getViewer,零命中。词汇站不读门户注入的身份头,也不校验共享密钥。门户对它只是「网络层入口」:所有公网流量必须经过 3000,但站内没有「谁在访问」的概念。任何能访问 /vocab/ 的人都能改掌握度、收藏、生成内容。这是当前架构里权限最弱的一站。

六、发布与运维

标准流程:

cd C:\Users\cag\WorkBuddy\2026-10-11-06-00-16\kaoyan-vocab
npm run build
Stop-ScheduledTask  -TaskName KaoyanNextServer
Start-ScheduledTask -TaskName KaoyanNextServer

launch.ps1 -Name vocab 会先杀掉 3001 的监听进程,再以隐藏窗口执行 next start -p 3001,日志写 next-server.log 与 .err。手工调试可以用 start-next.bat,它同样先 taskkill 占用 3001 的旧进程——脚本注释里明确写了「3000 留给 portal」。

常见故障:

  • 页面能开但所有操作报 404 / 405:八成是绕过了门户直连 3001,/api/* 没有被重写。改用 http://127.0.0.1:3000/vocab/。
  • ChunkLoadError 或静态资源 404:.next 构建产物与当前进程不匹配,重新 next build 后重启任务。
  • Prisma 报无法打开数据库:确认 DATABASE_URL="file:./dev.db" 是相对 prisma/ 解析的,实际文件在 prisma/dev.db。
  • 端口被占:launch.ps1 会处理;若是 SYSTEM 孤儿进程,需要管理员权限。

七、现状与改进点

结构债一:没有用户维度。 全站共享一份 mastery、favorite、Note、CompareItem。多人使用会互相覆盖,这与门户已经有的 5 个账号完全脱节。

结构债二:复习模型名不副实。 如果产品目标是「抗遗忘」,需要引入 dueAt、intervalDays、ease 或至少 lastReviewedAt,并按到期日取词;当前只按 updatedAt 倒序,复习顺序会随任何写操作漂移。

结构债三:前端绝对路径依赖门户。 fetch("/api/...") 与 basePath 不一致,是历史兼容写法。要独立部署,应统一改成 /vocab/api/... 或走 lib/api.ts 常量。

风险:写接口无鉴权。 公网任何人可清空/覆盖掌握度,也可触发 AI 生成消耗 token。至少应校验 x-portal-secret 并要求登录,写操作再加一层 viewer 判断。

建议:先做「最小身份接入」——读 X-User-*,给 Word 增加 ownerId,把掌握度与收藏按用户隔离;再把复习队列升级为「到期优先 + 新词配额」;最后把前端 API 前缀统一,去掉对门户 /api/* 重写的隐式依赖。