MVP 完成到期单词复习,答题结果只记账一次 #8

Closed
opened 2026-09-10 17:09:12 +08:00 by ila · 8 comments
Owner

来源与目标

2026-09-10 用户确认 F01–F12、原型通过,并要求按四阶段建议推进。原型验收:#1 评论 7498。阶段 3;覆盖 F10 单词、B02。

学习者进入本人到期列表,显示答案、答对/答错或重学,完成后更新复习时间,形成首条完整学习闭环。

验收标准

  • 先记录固定间隔、等级上限、时区/UTC 存储、重学及已知/忽略是否入队的决策表与固定时间样例;不做策略配置 UI 或 FSRS。
  • 到期查询仅返回本人/当前语言条目;空队列、答案、评分、重学和完成状态符合原型。
  • 重复请求、双标签页并发、网络重发不重复更新次数和间隔;答案提交归属由服务端验证。
  • 使用可控时钟验证到期边界和每个状态转换;运行登录→导入→查词→保存→复习端到端场景。

依赖与执行

前置:#7。状态:已完成(2026-09-11 用户验收通过,验收对象 85b8e24,PR #27 已合入 main,验收记录见最新评论)。前置尚未通过时不得标进行中;每单完成停在待验收,由用户验收后关闭。技术验证可与不依赖其结论的工作分工,但不提前冻结未验证契约。

参考模块与设计证据

go-admin 身份、事务与通用响应;复习调度为新增领域逻辑,不直接复用 SysJob 执行任意任务。

沿用已验收复习原型;规则表作为后端设计证据。

后端不为建表/API 单独画页面原型;先写数据、接口、状态、隔离与幂等契约。学习端采用 #1 已验收 v1,未做的 LinguaCafe 对照不标完成。

工作量、范围与风险

预计 5~8 人日(8 小时/人日),包含本单设计、前后端实现、相关测试、修正和文档;是规划估算,不是交付日期或 AI 运行时间。仅 F 范围,X 系列不纳入。数据变更先在隔离测试库验证迁移/回退,保留既有数据和原型。

文档与证据

实施时按影响更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification;需求变化更新 Product-Requirements-Overview,交付单补实际部署恢复文档。Wiki 先写再回读同步;结束评论记录测试、未验证内容、提交及 revision。工单正文保留基线,重要变化追加评论。

Gitea MCP 指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅进入进程。

## 来源与目标 2026-09-10 用户确认 F01–F12、原型通过,并要求按四阶段建议推进。原型验收:#1 评论 7498。阶段 3;覆盖 F10 单词、B02。 学习者进入本人到期列表,显示答案、答对/答错或重学,完成后更新复习时间,形成首条完整学习闭环。 ## 验收标准 - [ ] 先记录固定间隔、等级上限、时区/UTC 存储、重学及已知/忽略是否入队的决策表与固定时间样例;不做策略配置 UI 或 FSRS。 - [ ] 到期查询仅返回本人/当前语言条目;空队列、答案、评分、重学和完成状态符合原型。 - [ ] 重复请求、双标签页并发、网络重发不重复更新次数和间隔;答案提交归属由服务端验证。 - [ ] 使用可控时钟验证到期边界和每个状态转换;运行登录→导入→查词→保存→复习端到端场景。 ## 依赖与执行 前置:#7。状态:已完成(2026-09-11 用户验收通过,验收对象 85b8e24,PR #27 已合入 main,验收记录见最新评论)。前置尚未通过时不得标进行中;每单完成停在待验收,由用户验收后关闭。技术验证可与不依赖其结论的工作分工,但不提前冻结未验证契约。 ## 参考模块与设计证据 go-admin 身份、事务与通用响应;复习调度为新增领域逻辑,不直接复用 SysJob 执行任意任务。 沿用已验收复习原型;规则表作为后端设计证据。 后端不为建表/API 单独画页面原型;先写数据、接口、状态、隔离与幂等契约。学习端采用 #1 已验收 v1,未做的 LinguaCafe 对照不标完成。 ## 工作量、范围与风险 预计 5~8 人日(8 小时/人日),包含本单设计、前后端实现、相关测试、修正和文档;是规划估算,不是交付日期或 AI 运行时间。仅 F 范围,X 系列不纳入。数据变更先在隔离测试库验证迁移/回退,保留既有数据和原型。 ## 文档与证据 实施时按影响更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification;需求变化更新 Product-Requirements-Overview,交付单补实际部署恢复文档。Wiki 先写再回读同步;结束评论记录测试、未验证内容、提交及 revision。工单正文保留基线,重要变化追加评论。 Gitea MCP 指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅进入进程。
Author
Owner

#8 方案确认与实施启动(2026-09-11)

用户在当前会话确认「都按你说的」,同意本单决策表 D1~D6。前置 #7 已通过用户验收并关闭(PR #26 已合入 main);本单分支 feat/8-due-word-review 从 main 527d8af 创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。

决策表(F10 单词复习,后端设计证据)

D1 等级与间隔(答对后按新等级排期)

等级 1 2 3 4 5 6 7(上限)
下次复习 1 天 2 天 4 天 7 天 15 天 30 天 60 天

新词(level 0) 答对 → 学习中 level 1,排 1 天后;答错 → 保持 新词,立即回队。学习中 level N 答对 → N+1(封顶 7,到 7 后答对仍为 7 并排 60 天);答错 → N−1(最低 1)。

D2 入队范围:仅 新词/学习中 且 due_at ≤ 现在;已知/忽略 不入队。新保存的词立即到期(due_at = 保存时刻);手动把词条改为「学习中 level N」时下次复习 = 现在 + 间隔[N]。

D3 评分动作:correct 答对 → 升级并排新间隔、离开本轮;wrong 答错 → 降级并立即重新入队;again 再学一次 → 不改等级、立即重新入队。完成页与空队列文案沿用已验收原型 v1。

D4 时区与到期边界:due_at 以 UTC 绝对时刻存储,到期判定为 due_at ≤ now,不引入本地日边界。理由:MVP 没有用户时区设置(属 X 系列边界),绝对时刻在多账号自托管下语义一致且无 DST 陷阱;代价是复习时刻随首次作答时间漂移,已记入业务规则页。原型「今日到期 1/2」按当前到期队列的位置显示。

D5 幂等与并发:客户端每次作答生成 answerId(UUID),服务端以 UNIQUE(owner_id, answer_id) 保证网络重发、双击、重放只记一次;作答必须回传它看到的 dueAt,服务端锁行比对,不匹配说明另一标签页已推进 → 不改等级/间隔/次数并返回 result=stale。归属由服务端验证:他人或不存在词条 404,状态已变为已知/忽略 409。每次尝试落一条 lexgo_review_answers(等级/状态/间隔前后值 + result + 次数),作为「只记账一次」的证据并供 #13 使用。

D6 正面挖空句:取第一条个人例句,把该词(规范化词形、整词、忽略大小写)替换为 _____;没有例句则只显示词本身。复习不依赖共享词典可用性,不在复习时回查词典。

数据与接口

schema v6:lexgo_terms 增加 due_at DATETIME(3) NULL、review_count、correct_count、wrong_count、last_reviewed_at 与到期索引,并将既有行 due_at = created_at(已保存词汇立即进入队列);新增 lexgo_review_answers(所有者、answer 摘要唯一键、词条、评分、状态/等级/间隔前后值、result、时间)。纯新增列与新增表,不修改既有语义。

接口 权限与输入/输出
GET /api/v1/reviews/queue 本人+当前语言,仅 新词/学习中 且到期;按 due_at,id 排序、上限 50;不接受查询参数
POST /api/v1/reviews/:termId/answers {answerId, grade: correct|wrong|again, expectedDueAt};首次应用 201、重复或 stale 200;返回本次 result、词条新状态与是否重新入队

学习端新增 /review 路由与顶栏「到期复习」入口(#12 词汇库尚未实现,需要直达入口)、stores/review.ts、复习卡片与完成/空态;键盘可聚焦并用 Enter 操作,切换账号或退出清空队列。

测试、迁移与回退

  • Go 固定时钟单测:间隔表决策样例、到期边界(due_at = now 到期、due_at > now 不到期)、每个状态与等级转换、等级上下限。
  • MySQL 集成(专用库 lexgo_test_issue8):两账号隔离、幂等重发只记一次、双标签页 stale 不重复推进、他人词条 404、已知/忽略 409、v5→v6 保留既有词条与计数。
  • 学习端单测与构建;默认 E2E;真实 API+MySQL+浏览器完成「登录→导入→查词→保存→复习」端到端。
  • 回退:停止 lexgo-api,恢复 .local/lexgo-pre-issue8.exe,把 lexgo_schema 标记改回 5 后启动;新增列均有默认值,旧二进制仍可写入,保留新表与新列、不删除既有数据。
  • 文档:实施后更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification,需求变化更新 Product-Requirements-Overview;Wiki 先写后回读再同步镜像。

非目标:策略配置 UI/X11、FSRS、短语复习(#11)、进度页(#13)、练习模式(X08)。

回退说明:本会话 pi 无可用 Gitea MCP 工具,沿用本单既定回退,使用目标 git.ilapage.cn API;凭据仅从既有安全配置读入进程。

## #8 方案确认与实施启动(2026-09-11) 用户在当前会话确认「都按你说的」,同意本单决策表 D1~D6。前置 #7 已通过用户验收并关闭(PR #26 已合入 main);本单分支 `feat/8-due-word-review` 从 main `527d8af` 创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。 ### 决策表(F10 单词复习,后端设计证据) **D1 等级与间隔**(答对后按新等级排期) | 等级 | 1 | 2 | 3 | 4 | 5 | 6 | 7(上限) | |---|---|---|---|---|---|---|---| | 下次复习 | 1 天 | 2 天 | 4 天 | 7 天 | 15 天 | 30 天 | 60 天 | `新词`(level 0) 答对 → `学习中` level 1,排 1 天后;答错 → 保持 `新词`,立即回队。`学习中` level N 答对 → N+1(封顶 7,到 7 后答对仍为 7 并排 60 天);答错 → N−1(最低 1)。 **D2 入队范围**:仅 `新词`/`学习中` 且 `due_at ≤ 现在`;`已知`/`忽略` 不入队。新保存的词立即到期(`due_at = 保存时刻`);手动把词条改为「学习中 level N」时下次复习 = 现在 + 间隔[N]。 **D3 评分动作**:`correct` 答对 → 升级并排新间隔、离开本轮;`wrong` 答错 → 降级并立即重新入队;`again` 再学一次 → 不改等级、立即重新入队。完成页与空队列文案沿用已验收原型 v1。 **D4 时区与到期边界**:`due_at` 以 UTC 绝对时刻存储,到期判定为 `due_at ≤ now`,不引入本地日边界。理由:MVP 没有用户时区设置(属 X 系列边界),绝对时刻在多账号自托管下语义一致且无 DST 陷阱;代价是复习时刻随首次作答时间漂移,已记入业务规则页。原型「今日到期 1/2」按当前到期队列的位置显示。 **D5 幂等与并发**:客户端每次作答生成 `answerId`(UUID),服务端以 `UNIQUE(owner_id, answer_id)` 保证网络重发、双击、重放只记一次;作答必须回传它看到的 `dueAt`,服务端锁行比对,不匹配说明另一标签页已推进 → 不改等级/间隔/次数并返回 `result=stale`。归属由服务端验证:他人或不存在词条 404,状态已变为已知/忽略 409。每次尝试落一条 `lexgo_review_answers`(等级/状态/间隔前后值 + result + 次数),作为「只记账一次」的证据并供 #13 使用。 **D6 正面挖空句**:取第一条个人例句,把该词(规范化词形、整词、忽略大小写)替换为 `_____`;没有例句则只显示词本身。复习不依赖共享词典可用性,不在复习时回查词典。 ### 数据与接口 schema v6:`lexgo_terms` 增加 `due_at DATETIME(3) NULL`、`review_count`、`correct_count`、`wrong_count`、`last_reviewed_at` 与到期索引,并将既有行 `due_at = created_at`(已保存词汇立即进入队列);新增 `lexgo_review_answers`(所有者、answer 摘要唯一键、词条、评分、状态/等级/间隔前后值、result、时间)。纯新增列与新增表,不修改既有语义。 | 接口 | 权限与输入/输出 | |---|---| | GET /api/v1/reviews/queue | 本人+当前语言,仅 `新词`/`学习中` 且到期;按 due_at,id 排序、上限 50;不接受查询参数 | | POST /api/v1/reviews/:termId/answers | `{answerId, grade: correct\|wrong\|again, expectedDueAt}`;首次应用 201、重复或 stale 200;返回本次 result、词条新状态与是否重新入队 | 学习端新增 `/review` 路由与顶栏「到期复习」入口(#12 词汇库尚未实现,需要直达入口)、`stores/review.ts`、复习卡片与完成/空态;键盘可聚焦并用 Enter 操作,切换账号或退出清空队列。 ### 测试、迁移与回退 - Go 固定时钟单测:间隔表决策样例、到期边界(`due_at = now` 到期、`due_at > now` 不到期)、每个状态与等级转换、等级上下限。 - MySQL 集成(专用库 `lexgo_test_issue8`):两账号隔离、幂等重发只记一次、双标签页 stale 不重复推进、他人词条 404、已知/忽略 409、v5→v6 保留既有词条与计数。 - 学习端单测与构建;默认 E2E;真实 API+MySQL+浏览器完成「登录→导入→查词→保存→复习」端到端。 - 回退:停止 lexgo-api,恢复 `.local/lexgo-pre-issue8.exe`,把 `lexgo_schema` 标记改回 5 后启动;新增列均有默认值,旧二进制仍可写入,保留新表与新列、不删除既有数据。 - 文档:实施后更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification,需求变化更新 Product-Requirements-Overview;Wiki 先写后回读再同步镜像。 非目标:策略配置 UI/X11、FSRS、短语复习(#11)、进度页(#13)、练习模式(X08)。 回退说明:本会话 pi 无可用 Gitea MCP 工具,沿用本单既定回退,使用目标 `git.ilapage.cn` API;凭据仅从既有安全配置读入进程。
Author
Owner

#8 实施完成,待用户验收(2026-09-11)

用户确认的方案与决策表见评论 7769。分支 feat/8-due-word-review 从 main 527d8af 创建,实现提交 0328505,已推送。PR #27 未合并,工单不关闭,停在待用户验收。

实现与差异

  • schema v6(server/app/lexgo/database.go):新增 lexgo_term_reviews(每词条一行排期:due_at、review_count、correct_count、wrong_count、last_reviewed_at)与 lexgo_review_answers(每次作答一行:answer_key、评分、状态/等级/间隔前后值、result、requeued)。语句全部是 CREATE TABLE IF NOT EXISTS 与 INSERT IGNORE ... SELECT,因此迁移仍是「可重试的加法迁移」,没有 ALTER 个人词条表;既有 4 个已保存词条按 due_at = created_at 进入队列。
  • server/app/lexgo/review.go:固定间隔表 1/2/4/7/15/30/60 天、applyGrade 状态转换、队列查询、按 answerId 去重、按 expectedDueAt 判定 stale、syncTermReview 维护排期行。
  • 接口:GET /api/v1/reviews/queue(本人+当前语言,仅新词/学习中且到期,total 为全部到期数、单页最多 50)、POST /api/v1/reviews/:termId/answers(应用 201、重放或 stale 200,返回前后状态与是否回队)。
  • 学习端:/review 路由与书库、阅读器顶栏「到期复习」入口;stores/review.ts、ReviewCard.vue、ReviewView.vue;正面显示词与挖空例句、答案面显示个人释义、完成页与空队列页;键盘焦点在换卡与显示答案后落到下一个操作;一个评分动作对应一个 answerId,失败重试复用同一个。管理端无改动。

实际验证

验证 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue8 python scripts/server.py test-integration 41 个顶层用例全部通过、0 跳过(原 34,新增 6 个复习用例与 1 个 v5→v6 迁移用例)
learner npx vitest --run / vue-tsc --build / pnpm run build / playwright test 71 项单测、类型检查、构建、5 项 E2E 通过
admin pnpm test / pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / harness.py check --strict 56 项与严格检查通过
Wiki sync --check 一致,5 个页面先写后回读

覆盖:间隔表与全部状态转换、到期边界(due_at = now 到期、早 1 毫秒不到期)、已知/忽略不入队、两账号队列互不可见、重复提交只记一次、同一词条双标签页写入 stale 不重复推进、他人词条 404、已知词条 409、未知评分/缺少到期时间/未知字段 400、显式等级排期规则、唯一键约束、v5→v6 保留既有数据。

本机开发库 lexgo_dev 已显式 v5→v6:升级前后 sys_user 6、lexgo_spaces 6、lexgo_sessions 5、lexgo_books 5、lexgo_chapters 10、lexgo_ingest_jobs 10、lexgo_dictionaries 1、lexgo_terms 4 全部不变,新增 lexgo_term_reviews 4 行(due_at = created_at)与空的 lexgo_review_answers;旧二进制备份在忽略的 .local/lexgo-pre-issue8.exe。本次只重启 lexgo-api 与 lexgo-learner。

真实链路:真实 Go API+真实 MySQL 共 38 项检查通过(两账号 issue5_a/issue5_b,脚本可重复运行),随后用临时 Playwright 用例在真实学习端完成「登录→粘贴导入→点词查义→保存个人释义→到期复习作答→回阅读器确认状态」完整闭环,并复核 390×844 窄屏无横向溢出。临时用例运行后删除,截图作为附件上传。

未验证与边界

  • 真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器窄屏检查,不当作真机结果。
  • 并发只覆盖「双标签页同一词条」这一层,没有做多用户压力测试;单实例部署。
  • 到期判定用 UTC 绝对时刻而非本地日边界,复习时刻会随首次作答时间漂移,已写入业务规则页。
  • 短语复习(#11)、词汇库(#12)、进度统计(#13)、策略配置 UI(X11)与练习模式(X08)不在本单。
  • 运维记录:学习端常驻 Vite 在长时间运行后一度对 /src/style.css 返回空样式表(E2E 观察到 white-space: normal),重启后恢复,未改动代码或配置。

文档

Wiki 先写后回读,再同步镜像(镜像随本次提交):

  • Architecture-and-Code-Map: 0d6fb43a2662e263fc6f2fb5426b9a8fafaa4868
  • Business-Rules-and-Glossary: e59184a923910cee9283771154349397e7063fbd
  • Local-Development-and-Verification: e01464ad570bbd037e813985d78b137462bca241
  • Product-Requirements-Overview: faafbd4c7ec98dfead97a5f1d8ead8ac7ffc9853
  • Home: e44e179cbad8e28133bc2b97d1b6f57b0bb77063
  • Project-Profile 本单未变化,仍为 91a2eb47ca73e41a329bccd6ceeb7498c691529c

未提交凭据、账号密码、测试数据库数据或截图;.local/ 保持忽略。

回退

停止 lexgo-api,恢复 .local/lexgo-pre-issue8.exe,把 lexgo_schema 标记改回 5 后启动;新表不影响旧二进制写入个人词条,保留 lexgo_term_reviews、lexgo_review_answers 与全部既有数据,不删除数据、不重新 bootstrap。

Gitea MCP 仍指向其他站点,沿用已记录的目标站点 API 回退;凭据仅从既有安全配置读入进程。

附件:issue8-reader-saved.png、issue8-review-revealed.png、issue8-review-summary.png、issue8-reader-reviewed.png、issue8-review-mobile-390.png(本会话模型不能读取图片,截图未经 Agent 目视检查,功能断言来自程序化检查)。

## #8 实施完成,待用户验收(2026-09-11) 用户确认的方案与决策表见评论 [7769](https://git.ilapage.cn/OPC/lexgo/issues/8#issuecomment-7769)。分支 `feat/8-due-word-review` 从 main `527d8af` 创建,实现提交 `0328505`,已推送。PR [#27](https://git.ilapage.cn/OPC/lexgo/pulls/27) 未合并,工单不关闭,停在待用户验收。 ### 实现与差异 - schema v6(`server/app/lexgo/database.go`):新增 `lexgo_term_reviews`(每词条一行排期:`due_at`、`review_count`、`correct_count`、`wrong_count`、`last_reviewed_at`)与 `lexgo_review_answers`(每次作答一行:`answer_key`、评分、状态/等级/间隔前后值、`result`、`requeued`)。语句全部是 `CREATE TABLE IF NOT EXISTS` 与 `INSERT IGNORE ... SELECT`,因此迁移仍是「可重试的加法迁移」,没有 ALTER 个人词条表;既有 4 个已保存词条按 `due_at = created_at` 进入队列。 - `server/app/lexgo/review.go`:固定间隔表 1/2/4/7/15/30/60 天、`applyGrade` 状态转换、队列查询、按 `answerId` 去重、按 `expectedDueAt` 判定 stale、`syncTermReview` 维护排期行。 - 接口:`GET /api/v1/reviews/queue`(本人+当前语言,仅新词/学习中且到期,`total` 为全部到期数、单页最多 50)、`POST /api/v1/reviews/:termId/answers`(应用 201、重放或 stale 200,返回前后状态与是否回队)。 - 学习端:`/review` 路由与书库、阅读器顶栏「到期复习」入口;`stores/review.ts`、`ReviewCard.vue`、`ReviewView.vue`;正面显示词与挖空例句、答案面显示个人释义、完成页与空队列页;键盘焦点在换卡与显示答案后落到下一个操作;一个评分动作对应一个 `answerId`,失败重试复用同一个。管理端无改动。 ### 实际验证 | 验证 | 结果 | |---|---| | `go vet ./...` | 通过 | | `LEXGO_TEST_DB_NAME=lexgo_test_issue8 python scripts/server.py test-integration` | 41 个顶层用例全部通过、0 跳过(原 34,新增 6 个复习用例与 1 个 v5→v6 迁移用例) | | learner `npx vitest --run` / `vue-tsc --build` / `pnpm run build` / `playwright test` | 71 项单测、类型检查、构建、5 项 E2E 通过 | | admin `pnpm test` / `pnpm lint` | 31 项与 lint 通过;管理端本单无代码改动 | | `python -m unittest discover -s tests` / `harness.py check --strict` | 56 项与严格检查通过 | | Wiki `sync --check` | 一致,5 个页面先写后回读 | 覆盖:间隔表与全部状态转换、到期边界(`due_at = now` 到期、早 1 毫秒不到期)、已知/忽略不入队、两账号队列互不可见、重复提交只记一次、同一词条双标签页写入 stale 不重复推进、他人词条 404、已知词条 409、未知评分/缺少到期时间/未知字段 400、显式等级排期规则、唯一键约束、v5→v6 保留既有数据。 本机开发库 `lexgo_dev` 已显式 v5→v6:升级前后 sys_user 6、lexgo_spaces 6、lexgo_sessions 5、lexgo_books 5、lexgo_chapters 10、lexgo_ingest_jobs 10、lexgo_dictionaries 1、lexgo_terms 4 全部不变,新增 `lexgo_term_reviews` 4 行(`due_at = created_at`)与空的 `lexgo_review_answers`;旧二进制备份在忽略的 `.local/lexgo-pre-issue8.exe`。本次只重启 lexgo-api 与 lexgo-learner。 真实链路:真实 Go API+真实 MySQL 共 38 项检查通过(两账号 issue5_a/issue5_b,脚本可重复运行),随后用临时 Playwright 用例在真实学习端完成「登录→粘贴导入→点词查义→保存个人释义→到期复习作答→回阅读器确认状态」完整闭环,并复核 390×844 窄屏无横向溢出。临时用例运行后删除,截图作为附件上传。 ### 未验证与边界 - 真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器窄屏检查,不当作真机结果。 - 并发只覆盖「双标签页同一词条」这一层,没有做多用户压力测试;单实例部署。 - 到期判定用 UTC 绝对时刻而非本地日边界,复习时刻会随首次作答时间漂移,已写入业务规则页。 - 短语复习(#11)、词汇库(#12)、进度统计(#13)、策略配置 UI(X11)与练习模式(X08)不在本单。 - 运维记录:学习端常驻 Vite 在长时间运行后一度对 `/src/style.css` 返回空样式表(E2E 观察到 `white-space: normal`),重启后恢复,未改动代码或配置。 ### 文档 Wiki 先写后回读,再同步镜像(镜像随本次提交): - Architecture-and-Code-Map: `0d6fb43a2662e263fc6f2fb5426b9a8fafaa4868` - Business-Rules-and-Glossary: `e59184a923910cee9283771154349397e7063fbd` - Local-Development-and-Verification: `e01464ad570bbd037e813985d78b137462bca241` - Product-Requirements-Overview: `faafbd4c7ec98dfead97a5f1d8ead8ac7ffc9853` - Home: `e44e179cbad8e28133bc2b97d1b6f57b0bb77063` - Project-Profile 本单未变化,仍为 `91a2eb47ca73e41a329bccd6ceeb7498c691529c` 未提交凭据、账号密码、测试数据库数据或截图;`.local/` 保持忽略。 ### 回退 停止 lexgo-api,恢复 `.local/lexgo-pre-issue8.exe`,把 `lexgo_schema` 标记改回 5 后启动;新表不影响旧二进制写入个人词条,保留 `lexgo_term_reviews`、`lexgo_review_answers` 与全部既有数据,不删除数据、不重新 bootstrap。 Gitea MCP 仍指向其他站点,沿用已记录的目标站点 API 回退;凭据仅从既有安全配置读入进程。 附件:issue8-reader-saved.png、issue8-review-revealed.png、issue8-review-summary.png、issue8-reader-reviewed.png、issue8-review-mobile-390.png(本会话模型不能读取图片,截图未经 Agent 目视检查,功能断言来自程序化检查)。
Author
Owner

补充提交 ec5ec2d(docs: 记录 #8 已确认的复习决策口径):按本项目既有惯例,把用户本轮确认的复习口径写入仓库根 AGENTS.md 的项目决策清单——固定间隔表与三个评分动作、入队范围、UTC 绝对时刻的到期判定、按 answerId 去重与 expectedDueAt 判定过期标签页、计数归属,以及短语/进度/策略 UI 的范围边界。仅新增一行文档,未改动代码、数据或接口;功能提交 0328505 保持不变,分支已同步推送,治理严格检查通过。

补充提交 `ec5ec2d`(docs: 记录 #8 已确认的复习决策口径):按本项目既有惯例,把用户本轮确认的复习口径写入仓库根 `AGENTS.md` 的项目决策清单——固定间隔表与三个评分动作、入队范围、UTC 绝对时刻的到期判定、按 `answerId` 去重与 `expectedDueAt` 判定过期标签页、计数归属,以及短语/进度/策略 UI 的范围边界。仅新增一行文档,未改动代码、数据或接口;功能提交 `0328505` 保持不变,分支已同步推送,治理严格检查通过。
Author
Owner

#8 代码审核:暂不达标,需修复后再验收(2026-09-11,Claude Code)

审核对象:提交 0328505、ec5ec2d(PR #27)。只读审阅代码与测试,没有重跑测试;实施评论 7776 中的测试结果本次没有复核。决策表 D1、D3、D4、D6 的实现与评论 7769 一致。以下问题影响验收标准第 2、3 条(D2、D5)。

必须修复

R1 网络重发后,学习端丢失本轮计数,可能误显示「今天没有到期词条」(验收标准:网络重发)

  • 位置:learner/src/stores/review.ts applyResult,只有 result === 'applied' 时才累加 answered、correctCount、wrongCount、wordsReviewed。
  • 场景:第一次提交已经在服务端生效,但响应丢失 → 用户点「重试提交」,复用同一个 answerId → 服务端返回 duplicate → 卡片被移出,本轮计数没有增加。如果这是本轮唯一一次作答,answered === 0,于是 empty 为真,页面显示「今天没有到期词条」,完成页无法出现。
  • 建议:同一 store 实例只会在自己重试时发送相同的 answerId,所以 duplicate 就是本客户端的首次结果,应按 applied 计数(根据 result.grade 分类)。stale 继续不计数。补一个 Vitest:第一次请求抛网络错误,重试返回 duplicate,断言计数为 1 并显示完成页。

R2 同一个 answerId 的并发重发会返回 500,而不是首次结果(验收标准:重复请求并发)

  • 位置:server/app/lexgo/review.go AnswerReview。按 answer_key 查询是在给词条加锁之前用普通读执行的。
  • 场景:两个带同一个 answerId 的请求几乎同时到达,两者都查不到记录。A 先拿到锁并执行 applied,B 等锁释放后发现 due_at 已变,进入 staleAnswer,插入相同的 (owner_id, answer_key),触发唯一键冲突 → 500。计数不会重复,这一点满足要求,但重发没有得到幂等结果,客户端会看到错误。
  • 建议:给词条加锁之后,再用加锁读(FOR UPDATE,避免 REPEATABLE READ 快照读到旧数据)检查一次 answer_key,命中则走 replayedAnswer。另一种做法是插入时遇到唯一键冲突就转为 replay。补一个 MySQL 集成用例,用两个 goroutine 并发提交同一个 answerId,断言都返回 2xx、review_count == 1、只有一条 answers 记录。现有 TestMySQLReviewAnswerIdempotencyAndStaleTabs 只覆盖了顺序重发。

R3 编辑任何字段都会重排复习时间,与 D2 不符

  • 位置:terms.go saveTerm 每次保存都调用 syncTermReview,而这个函数会无条件覆盖 due_at。
  • D2 只规定了两种情况:新保存的词立即到期;**手动改为「学习中 level N」**时,排到 now+间隔[N]。
  • 场景 1:一个学习中 3 级、已经逾期的词,只修改释义或例句 → due_at 变成 now+4 天,这个词从今天的队列中消失,但没有经过复习。
  • 场景 2:一个刚答对、排到 60 天后的 7 级词,只改一个错字 → 重新排为 now+60 天,时间也被改动了。
  • 建议:只有在新建时,或 status/level 与保存前的值不同时,才更新 due_at;只修改释义、例句或原形时保留原有排期。为此需要在 upsert 之前读取旧状态,并且应在同一事务内加锁读取。补一个用例:只改释义后,due_at 不变。

建议修复(不阻塞,但建议一并处理)

  • R4 过期标签页的结果没有任何提示:返回 stale 时卡片直接消失。建议显示一行 role="status" 提示,例如「该词已在其他页面复习」,避免用户以为自己的评分已经记录。
  • R5 注释与实现不一致:review.ts 的注释写着「One answer id per card and grade」,但 answerIdFor 只按卡片生成 id。报错后如果换一个评分重试,会沿用原来的 id,服务端按首次评分返回。这个行为可以接受,但需要修正注释,或者报错后只允许「重试提交」。
  • R6 方案偏差没有先记录变更:评论 7769 确认的方案是「在 lexgo_terms 上增加列」,实际实现改成了独立表 lexgo_term_reviews。评论 7776 事后说明了原因,这项技术变更风险更低,可以接受,但按规则应该先追加变更评论。回退说明里「新增列均有默认值」也应改为「新表」。

验收结论

R1~R3 修复并补齐对应测试之前,#8 保持「待验收」,不建议合并 PR #27。修复后,需要重跑 test-integration(lexgo_test_issue8)和 learner 的 vitest/E2E,并在本工单追加差异与测试结果。本次审核没有长期文档影响;R3 如果改变了保存时的排期口径,只需要在 Business-Rules-and-Glossary 中把 D2 写得更明确。

Gitea MCP 仍然指向其他站点,本次沿用已记录的目标站点 API 回退;凭据只从 git 凭据管理器读入进程。

## #8 代码审核:暂不达标,需修复后再验收(2026-09-11,Claude Code) 审核对象:提交 `0328505`、`ec5ec2d`(PR #27)。只读审阅代码与测试,没有重跑测试;实施评论 7776 中的测试结果本次没有复核。决策表 D1、D3、D4、D6 的实现与评论 7769 一致。以下问题影响验收标准第 2、3 条(D2、D5)。 ### 必须修复 **R1 网络重发后,学习端丢失本轮计数,可能误显示「今天没有到期词条」**(验收标准:网络重发) - 位置:`learner/src/stores/review.ts` `applyResult`,只有 `result === 'applied'` 时才累加 `answered`、`correctCount`、`wrongCount`、`wordsReviewed`。 - 场景:第一次提交已经在服务端生效,但响应丢失 → 用户点「重试提交」,复用同一个 `answerId` → 服务端返回 `duplicate` → 卡片被移出,本轮计数没有增加。如果这是本轮唯一一次作答,`answered === 0`,于是 `empty` 为真,页面显示「今天没有到期词条」,完成页无法出现。 - 建议:同一 store 实例只会在自己重试时发送相同的 `answerId`,所以 `duplicate` 就是本客户端的首次结果,应按 `applied` 计数(根据 `result.grade` 分类)。`stale` 继续不计数。补一个 Vitest:第一次请求抛网络错误,重试返回 `duplicate`,断言计数为 1 并显示完成页。 **R2 同一个 `answerId` 的并发重发会返回 500,而不是首次结果**(验收标准:重复请求并发) - 位置:`server/app/lexgo/review.go` `AnswerReview`。按 `answer_key` 查询是在给词条加锁**之前**用普通读执行的。 - 场景:两个带同一个 `answerId` 的请求几乎同时到达,两者都查不到记录。A 先拿到锁并执行 `applied`,B 等锁释放后发现 `due_at` 已变,进入 `staleAnswer`,插入相同的 `(owner_id, answer_key)`,触发唯一键冲突 → 500。计数不会重复,这一点满足要求,但重发没有得到幂等结果,客户端会看到错误。 - 建议:给词条加锁之后,再用加锁读(`FOR UPDATE`,避免 REPEATABLE READ 快照读到旧数据)检查一次 `answer_key`,命中则走 `replayedAnswer`。另一种做法是插入时遇到唯一键冲突就转为 replay。补一个 MySQL 集成用例,用两个 goroutine 并发提交同一个 `answerId`,断言都返回 2xx、`review_count == 1`、只有一条 answers 记录。现有 `TestMySQLReviewAnswerIdempotencyAndStaleTabs` 只覆盖了顺序重发。 **R3 编辑任何字段都会重排复习时间,与 D2 不符** - 位置:`terms.go` `saveTerm` 每次保存都调用 `syncTermReview`,而这个函数会无条件覆盖 `due_at`。 - D2 只规定了两种情况:新保存的词立即到期;**手动改为「学习中 level N」**时,排到 now+间隔[N]。 - 场景 1:一个学习中 3 级、已经逾期的词,只修改释义或例句 → `due_at` 变成 now+4 天,这个词从今天的队列中消失,但没有经过复习。 - 场景 2:一个刚答对、排到 60 天后的 7 级词,只改一个错字 → 重新排为 now+60 天,时间也被改动了。 - 建议:只有在新建时,或 `status`/`level` 与保存前的值不同时,才更新 `due_at`;只修改释义、例句或原形时保留原有排期。为此需要在 upsert 之前读取旧状态,并且应在同一事务内加锁读取。补一个用例:只改释义后,`due_at` 不变。 ### 建议修复(不阻塞,但建议一并处理) - **R4 过期标签页的结果没有任何提示**:返回 `stale` 时卡片直接消失。建议显示一行 `role="status"` 提示,例如「该词已在其他页面复习」,避免用户以为自己的评分已经记录。 - **R5 注释与实现不一致**:`review.ts` 的注释写着「One answer id per card and grade」,但 `answerIdFor` 只按卡片生成 id。报错后如果换一个评分重试,会沿用原来的 id,服务端按首次评分返回。这个行为可以接受,但需要修正注释,或者报错后只允许「重试提交」。 - **R6 方案偏差没有先记录变更**:评论 7769 确认的方案是「在 `lexgo_terms` 上增加列」,实际实现改成了独立表 `lexgo_term_reviews`。评论 7776 事后说明了原因,这项技术变更风险更低,可以接受,但按规则应该先追加变更评论。回退说明里「新增列均有默认值」也应改为「新表」。 ### 验收结论 R1~R3 修复并补齐对应测试之前,#8 保持「待验收」,不建议合并 PR #27。修复后,需要重跑 `test-integration`(lexgo_test_issue8)和 learner 的 vitest/E2E,并在本工单追加差异与测试结果。本次审核没有长期文档影响;R3 如果改变了保存时的排期口径,只需要在 Business-Rules-and-Glossary 中把 D2 写得更明确。 Gitea MCP 仍然指向其他站点,本次沿用已记录的目标站点 API 回退;凭据只从 git 凭据管理器读入进程。
Author
Owner

#8 审核意见分析与 R1~R3 整改(2026-09-11)

审核评论见 7781。我没有直接采信,而是先为每条「必须修复」写复现用例,在专用库 lexgo_test_issue8 观察到失败后再修。结论:R1~R3 全部成立,R4/R5 是真实小问题,R6 属实;同时 R3 的复现顺带暴露了一处比审核描述更严重的问题。整改提交 3258498,已推送。

逐条核对

条目 我的核对结果 结论
R1 重放丢失本轮计数 成立。applyResult 只对 applied 计数,旧契约下重放返回 result=duplicate,client 自己重试后 answered 仍为 0;若这是本轮最后一张卡,empty 为真 → 显示「今天没有到期词条」,完成页不出现。但修复不能只是「按 applied 计数」:若首次尝试实际被判定为 stale,重放同样返回 duplicate,直接计数会把未计分的作答算成一次复习 需修,方案调整
R2 并发同键返回 500 成立且已复现:并发用例打印 status 500,另一个 201,review_count = 1。原因是 answer_key 查询在词条加锁前用普通读,两个请求都查不到;后者在 stale 分支插入相同唯一键 需修,采纳建议
R3 编辑字段重排复习 成立且已复现:3 级逾期词的 due_at 从 2026-09-06 被改成 2026-09-15(now+4 天),逾期词条被挤出当天队列 需修,采纳建议
R3 附带问题(审核未提) 复现过程中发现更严重的一层:阅读器面板保存只提交状态、不提交等级,termLevel 对缺省等级一律返回 1,于是4 级词改一个错字会掉回 1 级(不是只有排期变化,是学习进度被重置) 一并修复
R4 无过期提示 成立。stale 时卡片直接消失,学习者无法判断评分是否被记录 采纳
R5 注释与实现不一致 成立。注释写「per card and grade」,answerIdFor 实际只按卡片生成 采纳,改注释并让提示区分两种情况
R6 变更未先记录 属实。方案评论 7769 写的是在 lexgo_terms 加列,实现改成独立表 lexgo_term_reviews;虽然 7776 与 Wiki 说明了原因、且该选择确实更安全(加法迁移可重试、不对既有表 ALTER),但按「数据结构变化先更新工单」的要求应当在实施前追加变更评论。方案评论中的「新增列均有默认值」以独立表实现为准 认可,已记入文档

审核文本中两处细节需要更正:一是 R1 的结论表述为「完成页无法出现」应与「显示成空队列」并列,实际影响是完成页被空队列页取代,数据本身没有重复计分(这一点审核也承认);二是审核自述「没有重跑测试」,因此 R1 的后半段(首次为 stale 的重放不能计数)需要修正建议方案,我已按此实现。

整改内容

  • R2:取得词条行锁后再用加锁读复查一次答案键,命中即返回已记录结果;stale 插入遇到 1062 也转为返回记录。新增 TestMySQLReviewConcurrentReplayOfOneAnswer(两个 goroutine 同键提交,两个都 2xx、review_count = 1、只有一条答案记录)。
  • R3:保存前加锁读取旧行,只有新建或状态/等级实际变化才移动 due_at;缺排期行时补建。新增 TestMySQLReviewEditKeepsSchedule(逾期 3 级词只改释义:due_at 与队列不变;改等级重排到 now+15 天)。
  • R3 附带:保存未提及等级时保留已获得等级,只有进入 学习中 才从 1 开始。新增 TestMySQLReviewPanelSaveKeepsLevel(4 级词面板式保存后仍为 4 级且排期不变;退出再进入则从 1 开始)。
  • R1 契约:作答响应 result 只取 applied/stale,另加 duplicate 布尔标记;重放返回首次结果并置 duplicate=true(已应用仍返回 201,重放与 stale 返回 200)。客户端因此按首次结果计数,网络丢响应后重试不会丢计数;同时用 resolved 区分「本轮解决过卡片」与「一开始就没有到期词条」,只解决卡片而无新计分时显示「本轮没有新的计分:N 个词条已在其他页面复习」。
  • R4/R5:stale 显示「该词已在其他页面复习,本次未计分。」,重放且首次为 stale 显示「该词已按上一次的评分记录,未重复计分。」,均为 role="status";answerId 注释改为「每张卡片一个,失败重试复用」。

整改后复测

验证 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue8 python scripts/server.py test-integration 44 个顶层用例全部通过、0 跳过(原 41,新增 3 个复现/回归用例,覆盖 R2 并发、R3 排期、R3 等级保留)
learner npx vitest --run / vue-tsc --build / pnpm run build / playwright test 73 项单测、类型检查、构建、5 项 E2E 通过(新增「丢响应后重试按首次结果计数」与「stale 提示」用例)
admin pnpm test / pnpm lint 31 项与 lint 通过
python -m unittest discover -s tests / harness.py check --strict / sync --check 56 项、严格检查、镜像一致
真实 API+MySQL 42 项检查通过(原 38,新增 4 项:显式等级落库、面板保存保留等级、复习后面板保存保留等级、文本编辑不移动复习日期)
真实浏览器 面板保存→到期复习→完成页闭环复测通过,截图 .local/evidence/issue8-fixed-summary.png

本机 lexgo-api 已用整改后二进制重启(schema 仍为 v6,无需迁移),/healthz 200。

文档

  • Business-Rules-and-Glossary:1d90013d883de9339865694e0bb0026f9edd3acc(入队范围补「只有新建或状态/等级变化才移动复习时间」与「缺省等级保留已获得等级」;幂等段改为 result + duplicate 契约)
  • Architecture-and-Code-Map:8add5553222f7f5235c0b3fe45a0ad46d37bbd46(作答响应契约与加锁复查)
  • Local-Development-and-Verification:1192d23a8198e981961adfc6066f0b3bc3b84058(本整改小节与复测结果)

R6 记录:本次数据结构从「改 lexgo_terms 列」变为「独立表 lexgo_term_reviews」,理由是可重试的加法迁移与不对既有表 ALTER;方案评论 7769 中「新增列均有默认值」的表述作废,以 7776 与 Wiki 为准。此项不阻塞验收。

工单仍停在「待验收」,PR #27 未合并,请复核 R1~R3 的修复与测试。

Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。

附件:issue8-fixed-summary.png(整改后真实浏览器复测截图;本会话模型不能读取图片,功能断言来自程序化检查)。

## #8 审核意见分析与 R1~R3 整改(2026-09-11) 审核评论见 [7781](https://git.ilapage.cn/OPC/lexgo/issues/8#issuecomment-7781)。我没有直接采信,而是先为每条「必须修复」写复现用例,在专用库 `lexgo_test_issue8` 观察到失败后再修。结论:**R1~R3 全部成立,R4/R5 是真实小问题,R6 属实**;同时 R3 的复现顺带暴露了一处比审核描述更严重的问题。整改提交 `3258498`,已推送。 ### 逐条核对 | 条目 | 我的核对结果 | 结论 | |---|---|---| | R1 重放丢失本轮计数 | 成立。`applyResult` 只对 `applied` 计数,旧契约下重放返回 `result=duplicate`,client 自己重试后 `answered` 仍为 0;若这是本轮最后一张卡,`empty` 为真 → 显示「今天没有到期词条」,完成页不出现。**但修复不能只是「按 applied 计数」**:若首次尝试实际被判定为 stale,重放同样返回 duplicate,直接计数会把未计分的作答算成一次复习 | 需修,方案调整 | | R2 并发同键返回 500 | 成立且已复现:并发用例打印 `status 500`,另一个 201,`review_count = 1`。原因是 `answer_key` 查询在词条加锁前用普通读,两个请求都查不到;后者在 stale 分支插入相同唯一键 | 需修,采纳建议 | | R3 编辑字段重排复习 | 成立且已复现:3 级逾期词的 `due_at` 从 `2026-09-06` 被改成 `2026-09-15`(now+4 天),逾期词条被挤出当天队列 | 需修,采纳建议 | | R3 附带问题(审核未提) | 复现过程中发现更严重的一层:阅读器面板保存只提交状态、不提交等级,`termLevel` 对缺省等级一律返回 1,于是**4 级词改一个错字会掉回 1 级**(不是只有排期变化,是学习进度被重置) | 一并修复 | | R4 无过期提示 | 成立。`stale` 时卡片直接消失,学习者无法判断评分是否被记录 | 采纳 | | R5 注释与实现不一致 | 成立。注释写「per card and grade」,`answerIdFor` 实际只按卡片生成 | 采纳,改注释并让提示区分两种情况 | | R6 变更未先记录 | 属实。方案评论 7769 写的是在 `lexgo_terms` 加列,实现改成独立表 `lexgo_term_reviews`;虽然 7776 与 Wiki 说明了原因、且该选择确实更安全(加法迁移可重试、不对既有表 ALTER),但按「数据结构变化先更新工单」的要求应当在实施前追加变更评论。方案评论中的「新增列均有默认值」以独立表实现为准 | 认可,已记入文档 | 审核文本中两处细节需要更正:一是 R1 的结论表述为「完成页无法出现」应与「显示成空队列」并列,实际影响是**完成页被空队列页取代**,数据本身没有重复计分(这一点审核也承认);二是审核自述「没有重跑测试」,因此 R1 的后半段(首次为 stale 的重放不能计数)需要修正建议方案,我已按此实现。 ### 整改内容 - **R2**:取得词条行锁后再用加锁读复查一次答案键,命中即返回已记录结果;`stale` 插入遇到 1062 也转为返回记录。新增 `TestMySQLReviewConcurrentReplayOfOneAnswer`(两个 goroutine 同键提交,两个都 2xx、`review_count = 1`、只有一条答案记录)。 - **R3**:保存前加锁读取旧行,只有新建或状态/等级实际变化才移动 `due_at`;缺排期行时补建。新增 `TestMySQLReviewEditKeepsSchedule`(逾期 3 级词只改释义:`due_at` 与队列不变;改等级重排到 now+15 天)。 - **R3 附带**:保存未提及等级时保留已获得等级,只有进入 `学习中` 才从 1 开始。新增 `TestMySQLReviewPanelSaveKeepsLevel`(4 级词面板式保存后仍为 4 级且排期不变;退出再进入则从 1 开始)。 - **R1 契约**:作答响应 `result` 只取 `applied`/`stale`,另加 `duplicate` 布尔标记;重放返回首次结果并置 `duplicate=true`(已应用仍返回 201,重放与 stale 返回 200)。客户端因此按首次结果计数,网络丢响应后重试不会丢计数;同时用 `resolved` 区分「本轮解决过卡片」与「一开始就没有到期词条」,只解决卡片而无新计分时显示「本轮没有新的计分:N 个词条已在其他页面复习」。 - **R4/R5**:`stale` 显示「该词已在其他页面复习,本次未计分。」,重放且首次为 stale 显示「该词已按上一次的评分记录,未重复计分。」,均为 `role="status"`;`answerId` 注释改为「每张卡片一个,失败重试复用」。 ### 整改后复测 | 验证 | 结果 | |---|---| | `go vet ./...` | 通过 | | `LEXGO_TEST_DB_NAME=lexgo_test_issue8 python scripts/server.py test-integration` | **44 个顶层用例全部通过、0 跳过**(原 41,新增 3 个复现/回归用例,覆盖 R2 并发、R3 排期、R3 等级保留) | | learner `npx vitest --run` / `vue-tsc --build` / `pnpm run build` / `playwright test` | **73 项单测**、类型检查、构建、5 项 E2E 通过(新增「丢响应后重试按首次结果计数」与「stale 提示」用例) | | admin `pnpm test` / `pnpm lint` | 31 项与 lint 通过 | | `python -m unittest discover -s tests` / `harness.py check --strict` / `sync --check` | 56 项、严格检查、镜像一致 | | 真实 API+MySQL | **42 项检查通过**(原 38,新增 4 项:显式等级落库、面板保存保留等级、复习后面板保存保留等级、文本编辑不移动复习日期) | | 真实浏览器 | 面板保存→到期复习→完成页闭环复测通过,截图 `.local/evidence/issue8-fixed-summary.png` | 本机 lexgo-api 已用整改后二进制重启(schema 仍为 v6,无需迁移),`/healthz` 200。 ### 文档 - Business-Rules-and-Glossary:`1d90013d883de9339865694e0bb0026f9edd3acc`(入队范围补「只有新建或状态/等级变化才移动复习时间」与「缺省等级保留已获得等级」;幂等段改为 `result` + `duplicate` 契约) - Architecture-and-Code-Map:`8add5553222f7f5235c0b3fe45a0ad46d37bbd46`(作答响应契约与加锁复查) - Local-Development-and-Verification:`1192d23a8198e981961adfc6066f0b3bc3b84058`(本整改小节与复测结果) R6 记录:本次数据结构从「改 `lexgo_terms` 列」变为「独立表 `lexgo_term_reviews`」,理由是可重试的加法迁移与不对既有表 ALTER;方案评论 7769 中「新增列均有默认值」的表述作废,以 7776 与 Wiki 为准。此项不阻塞验收。 工单仍停在「待验收」,PR [#27](https://git.ilapage.cn/OPC/lexgo/pulls/27) 未合并,请复核 R1~R3 的修复与测试。 Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。 附件:issue8-fixed-summary.png(整改后真实浏览器复测截图;本会话模型不能读取图片,功能断言来自程序化检查)。
Author
Owner

工单状态行与仓库决策清单已同步整改结果,无需新增分析:

  • 工单 #8 正文状态行改为「实现提交 0328505,审核整改提交 3258498,PR #27;实施证据见评论 7776,审核分析与整改见评论 7784」,避免只指向整改前的提交。
  • 提交 85b8e24(docs: 记录 #8 审核整改后的复习口径)把本轮整改明确的两条规则补进仓库根 AGENTS.md 的项目决策清单:只有新建或状态/等级实际变化才移动复习时间、编辑释义保留原排期、保存未提及等级时保留已获得等级;作答响应 result 只取 applied/stale 并用 duplicate 标记重放。

仅文档改动,未改代码、数据或接口;治理严格检查通过,分支已同步推送。

工单状态行与仓库决策清单已同步整改结果,无需新增分析: - 工单 #8 正文状态行改为「实现提交 0328505,审核整改提交 3258498,PR #27;实施证据见评论 7776,审核分析与整改见评论 7784」,避免只指向整改前的提交。 - 提交 `85b8e24`(docs: 记录 #8 审核整改后的复习口径)把本轮整改明确的两条规则补进仓库根 `AGENTS.md` 的项目决策清单:只有新建或状态/等级实际变化才移动复习时间、编辑释义保留原排期、保存未提及等级时保留已获得等级;作答响应 `result` 只取 applied/stale 并用 `duplicate` 标记重放。 仅文档改动,未改代码、数据或接口;治理严格检查通过,分支已同步推送。
Author
Owner

#8 复核整改:R1~R3 达标(2026-09-11,Claude Code)

复核对象:整改提交 3258498、85b8e24(评论 7784)。只读审阅代码与测试差异,没有重跑测试,结论基于代码逻辑核对。

逐条复核

  • R2(并发同键):AnswerReview 现在拿到词条行锁之后,再用 lockedAnswerKey(SELECT ... FOR UPDATE)复查一次答案键;staleAnswer 插入遇到 MySQL 1062 也会转成 replayedAnswer。两条路径都不再返回 500。新增的 TestMySQLReviewConcurrentReplayOfOneAnswer 用两个 goroutine 同键提交,断言两次都是 2xx、review_count == 1、只有一条 lexgo_review_answers 记录,复现了原问题并验证了修复。达标。
  • R3(编辑重排复习时间):syncTermReview 增加 reschedule 参数,只有新建或 saveTerm 判断出状态/等级实际变化时才更新 due_at;否则只在缺行时补建。TestMySQLReviewEditKeepsSchedule 验证逾期词只改释义后 due_at 和队列位置都不变,改等级才重排。达标,并且额外发现并修复了一个我审核时没有覆盖的问题:阅读器面板保存不带 level 字段时,原实现会把已获得的等级重置为 1(termLevel 对 level == nil 一律返回 1)。整改后 termLevel 在缺省时保留 previous.Level,只有真正进入 学习中 状态才从 1 开始,TestMySQLReviewPanelSaveKeepsLevel 覆盖了这个场景。处理得比我的审核意见更完整。
  • R1(重发丢计数):没有照搬我给的「按 applied 计数」建议,而是改了契约:result 只剩 applied/stale,重放(duplicate=true)复述首次结果;finished/empty 的判定从「是否有新计分」改成「本轮是否解决过卡片」(resolved),首次是 stale 的重放不会被误记为一次新复习。这个方案比我建议的更严谨,避免了我在审核意见里没考虑到的边界(首次本来就是 stale,重放也不该计分)。notice 文案区分「已按上一次评分记录」和「已在其他页面复习」,完成页在没有新计分时显示专门的提示而不是空队列页。达标,且指出并修正了我审核意见的一个疏漏(评论 7784 已说明)。
  • R4/R5:stale/重放提示已加 role="status";answerId 注释改为「每张卡片一个,失败重试复用」,与实现一致。达标。
  • R6:已在 AGENTS.md 决策清单和评论中记录方案偏差(独立表而非加列),说明沿用理由。达标。

复核中发现的次要问题(不阻塞)

完成页在「本轮没有新计分」的分支下,仍然显示「已更新复习计划」(ReviewView.vue 第 51 行),但这种情况下复习计划其实没有变化,文案不准确。属于文案细节,不影响功能正确性,建议顺手改一下,不需要为此单独返工。

结论

R1~R3 达标,可以进入用户验收流程。 本次复核为只读代码审阅,没有重跑 test-integration、learner vitest/E2E 等,pi 报告的复测结果(44 项集成用例、73 项前端单测等)未被本次复核重复验证;如需更高把握,建议在验收前独立重跑一次。上述完成页文案是唯一遗留的小问题,不构成阻塞项。

## #8 复核整改:R1~R3 达标(2026-09-11,Claude Code) 复核对象:整改提交 `3258498`、`85b8e24`(评论 [7784](https://git.ilapage.cn/OPC/lexgo/issues/8#issuecomment-7784))。只读审阅代码与测试差异,没有重跑测试,结论基于代码逻辑核对。 ### 逐条复核 - **R2(并发同键)**:`AnswerReview` 现在拿到词条行锁之后,再用 `lockedAnswerKey`(`SELECT ... FOR UPDATE`)复查一次答案键;`staleAnswer` 插入遇到 MySQL 1062 也会转成 `replayedAnswer`。两条路径都不再返回 500。新增的 `TestMySQLReviewConcurrentReplayOfOneAnswer` 用两个 goroutine 同键提交,断言两次都是 2xx、`review_count == 1`、只有一条 `lexgo_review_answers` 记录,复现了原问题并验证了修复。**达标**。 - **R3(编辑重排复习时间)**:`syncTermReview` 增加 `reschedule` 参数,只有新建或 `saveTerm` 判断出状态/等级实际变化时才更新 `due_at`;否则只在缺行时补建。`TestMySQLReviewEditKeepsSchedule` 验证逾期词只改释义后 `due_at` 和队列位置都不变,改等级才重排。**达标**,并且额外发现并修复了一个我审核时没有覆盖的问题:阅读器面板保存不带 `level` 字段时,原实现会把已获得的等级重置为 1(`termLevel` 对 `level == nil` 一律返回 1)。整改后 `termLevel` 在缺省时保留 `previous.Level`,只有真正进入 `学习中` 状态才从 1 开始,`TestMySQLReviewPanelSaveKeepsLevel` 覆盖了这个场景。**处理得比我的审核意见更完整**。 - **R1(重发丢计数)**:没有照搬我给的「按 applied 计数」建议,而是改了契约:`result` 只剩 `applied`/`stale`,重放(`duplicate=true`)复述首次结果;`finished`/`empty` 的判定从「是否有新计分」改成「本轮是否解决过卡片」(`resolved`),首次是 stale 的重放不会被误记为一次新复习。这个方案比我建议的更严谨,避免了我在审核意见里没考虑到的边界(首次本来就是 stale,重放也不该计分)。`notice` 文案区分「已按上一次评分记录」和「已在其他页面复习」,完成页在没有新计分时显示专门的提示而不是空队列页。**达标,且指出并修正了我审核意见的一个疏漏**(评论 7784 已说明)。 - **R4/R5**:`stale`/重放提示已加 `role="status"`;`answerId` 注释改为「每张卡片一个,失败重试复用」,与实现一致。**达标**。 - **R6**:已在 `AGENTS.md` 决策清单和评论中记录方案偏差(独立表而非加列),说明沿用理由。**达标**。 ### 复核中发现的次要问题(不阻塞) 完成页在「本轮没有新计分」的分支下,仍然显示「已更新复习计划」(`ReviewView.vue` 第 51 行),但这种情况下复习计划其实没有变化,文案不准确。属于文案细节,不影响功能正确性,建议顺手改一下,不需要为此单独返工。 ### 结论 **R1~R3 达标,可以进入用户验收流程。** 本次复核为只读代码审阅,没有重跑 `test-integration`、learner vitest/E2E 等,pi 报告的复测结果(44 项集成用例、73 项前端单测等)未被本次复核重复验证;如需更高把握,建议在验收前独立重跑一次。上述完成页文案是唯一遗留的小问题,不构成阻塞项。
Author
Owner

用户验收与合并收尾

验收时间:2026-09-11 22:54 +0800(Asia/Shanghai,与 PR 合入时刻一致)。用户在会话中明确确认「#8 通过验收」。结论:#8 验收通过,关闭工单;PR #27 已 fast-forward-only 合入 main。

验收对象是 PR #27 的完整头部 85b8e24,包含功能提交 0328505、决策记录 ec5ec2d、审核整改 3258498 与整改后的口径记录 85b8e24,四个提交逐个进入 main,没有合并提交、没有改写历史,与用户验收版本逐字节一致。验收状态文档提交 cd2b893 已推送。分支 feat/8-due-word-review 保留;需要撤销时可在新工单中使用 revert。

验收前的完整证据见评论 7776(实施与测试),独立审核意见见 7781,审核分析与 R1~R3 整改见 7784,本次不重复覆盖。合并后复核:harness.py check --strict 通过、sync --verify 通过、治理测试 56 项通过,工作区干净且与远端同步。

长期文档 Wiki revisions:

  • Project-Profile: ee9d2b8cc69c17bc83b2f33fc69527ee23ab5f0f
  • Architecture-and-Code-Map: f7b41a473c138bedf0baff1544e6480872b92d4d
  • Product-Requirements-Overview: e6a1f2037516203e9144159af77b25e87638b6fd
  • Home: 85c66bb518fcbb166b916549aa6e655215e0ed85
  • Business-Rules-and-Glossary 与 Local-Development-and-Verification 本单验收未变化,仍为整改后的 1d90013d883de9339865694e0bb0026f9edd3acc / 1192d23a8198e981961adfc6066f0b3bc3b84058

未增加产品范围或数据库变更;schema 保持 v6,本机开发库已完成 v5→v6 迁移,lexgo-api 运行整改后二进制。

第 3 阶段「首条学习闭环」#5~#8 至此全部通过用户验收,完整闭环(导入→查词→保存个人词义与状态→到期复习)可用。剩余 #9~#15 与 #21、#24 待实施;真机触屏详细证据与完整备份恢复演练缺口保留,由 #14/#15 承接。

Gitea MCP 仍指向其他站点,沿用已记录的目标 git.ilapage.cn API 回退,凭据仅在进程环境中使用。

## 用户验收与合并收尾 验收时间:2026-09-11 22:54 +0800(Asia/Shanghai,与 PR 合入时刻一致)。用户在会话中明确确认「#8 通过验收」。结论:#8 验收通过,关闭工单;PR [#27](https://git.ilapage.cn/OPC/lexgo/pulls/27) 已 fast-forward-only 合入 main。 验收对象是 PR #27 的完整头部 `85b8e24`,包含功能提交 `0328505`、决策记录 `ec5ec2d`、审核整改 `3258498` 与整改后的口径记录 `85b8e24`,四个提交逐个进入 main,没有合并提交、没有改写历史,与用户验收版本逐字节一致。验收状态文档提交 `cd2b893` 已推送。分支 `feat/8-due-word-review` 保留;需要撤销时可在新工单中使用 revert。 验收前的完整证据见评论 [7776](https://git.ilapage.cn/OPC/lexgo/issues/8#issuecomment-7776)(实施与测试),独立审核意见见 [7781](https://git.ilapage.cn/OPC/lexgo/issues/8#issuecomment-7781),审核分析与 R1~R3 整改见 [7784](https://git.ilapage.cn/OPC/lexgo/issues/8#issuecomment-7784),本次不重复覆盖。合并后复核:`harness.py check --strict` 通过、`sync --verify` 通过、治理测试 56 项通过,工作区干净且与远端同步。 长期文档 Wiki revisions: - Project-Profile: `ee9d2b8cc69c17bc83b2f33fc69527ee23ab5f0f` - Architecture-and-Code-Map: `f7b41a473c138bedf0baff1544e6480872b92d4d` - Product-Requirements-Overview: `e6a1f2037516203e9144159af77b25e87638b6fd` - Home: `85c66bb518fcbb166b916549aa6e655215e0ed85` - Business-Rules-and-Glossary 与 Local-Development-and-Verification 本单验收未变化,仍为整改后的 `1d90013d883de9339865694e0bb0026f9edd3acc` / `1192d23a8198e981961adfc6066f0b3bc3b84058` 未增加产品范围或数据库变更;schema 保持 v6,本机开发库已完成 v5→v6 迁移,lexgo-api 运行整改后二进制。 第 3 阶段「首条学习闭环」#5~#8 至此全部通过用户验收,完整闭环(导入→查词→保存个人词义与状态→到期复习)可用。剩余 #9~#15 与 #21、#24 待实施;真机触屏详细证据与完整备份恢复演练缺口保留,由 #14/#15 承接。 Gitea MCP 仍指向其他站点,沿用已记录的目标 git.ilapage.cn API 回退,凭据仅在进程环境中使用。
ila closed this issue 2026-09-11 22:56:38 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/lexgo#8