R4 过期标签页的结果没有任何提示:返回 stale 时卡片直接消失。建议显示一行 role="status" 提示,例如「该词已在其他页面复习」,避免用户以为自己的评分已经记录。
R5 注释与实现不一致:review.ts 的注释写着「One answer id per card and grade」,但 answerIdFor 只按卡片生成 id。报错后如果换一个评分重试,会沿用原来的 id,服务端按首次评分返回。这个行为可以接受,但需要修正注释,或者报错后只允许「重试提交」。
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
来源与目标
2026-09-10 用户确认 F01–F12、原型通过,并要求按四阶段建议推进。原型验收:#1 评论 7498。阶段 3;覆盖 F10 单词、B02。
学习者进入本人到期列表,显示答案、答对/答错或重学,完成后更新复习时间,形成首条完整学习闭环。
验收标准
依赖与执行
前置:#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 回退;凭据仅进入进程。
#8 方案确认与实施启动(2026-09-11)
用户在当前会话确认「都按你说的」,同意本单决策表 D1~D6。前置 #7 已通过用户验收并关闭(PR #26 已合入 main);本单分支
feat/8-due-word-review从 main527d8af创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。决策表(F10 单词复习,后端设计证据)
D1 等级与间隔(答对后按新等级排期)
新词(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、时间)。纯新增列与新增表,不修改既有语义。新词/学习中且到期;按 due_at,id 排序、上限 50;不接受查询参数{answerId, grade: correct|wrong|again, expectedDueAt};首次应用 201、重复或 stale 200;返回本次 result、词条新状态与是否重新入队学习端新增
/review路由与顶栏「到期复习」入口(#12 词汇库尚未实现,需要直达入口)、stores/review.ts、复习卡片与完成/空态;键盘可聚焦并用 Enter 操作,切换账号或退出清空队列。测试、迁移与回退
due_at = now到期、due_at > now不到期)、每个状态与等级转换、等级上下限。lexgo_test_issue8):两账号隔离、幂等重发只记一次、双标签页 stale 不重复推进、他人词条 404、已知/忽略 409、v5→v6 保留既有词条与计数。.local/lexgo-pre-issue8.exe,把lexgo_schema标记改回 5 后启动;新增列均有默认值,旧二进制仍可写入,保留新表与新列、不删除既有数据。非目标:策略配置 UI/X11、FSRS、短语复习(#11)、进度页(#13)、练习模式(X08)。
回退说明:本会话 pi 无可用 Gitea MCP 工具,沿用本单既定回退,使用目标
git.ilapage.cnAPI;凭据仅从既有安全配置读入进程。#8 实施完成,待用户验收(2026-09-11)
用户确认的方案与决策表见评论 7769。分支
feat/8-due-word-review从 main527d8af创建,实现提交0328505,已推送。PR #27 未合并,工单不关闭,停在待用户验收。实现与差异
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-integrationnpx vitest --run/vue-tsc --build/pnpm run build/playwright testpnpm test/pnpm lintpython -m unittest discover -s tests/harness.py check --strictsync --check覆盖:间隔表与全部状态转换、到期边界(
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_reviews4 行(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 窄屏无横向溢出。临时用例运行后删除,截图作为附件上传。
未验证与边界
/src/style.css返回空样式表(E2E 观察到white-space: normal),重启后恢复,未改动代码或配置。文档
Wiki 先写后回读,再同步镜像(镜像随本次提交):
0d6fb43a2662e263fc6f2fb5426b9a8fafaa4868e59184a923910cee9283771154349397e7063fbde01464ad570bbd037e813985d78b137462bca241faafbd4c7ec98dfead97a5f1d8ead8ac7ffc9853e44e179cbad8e28133bc2b97d1b6f57b0bb7706391a2eb47ca73e41a329bccd6ceeb7498c691529c未提交凭据、账号密码、测试数据库数据或截图;
.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 目视检查,功能断言来自程序化检查)。
补充提交
ec5ec2d(docs: 记录 #8 已确认的复习决策口径):按本项目既有惯例,把用户本轮确认的复习口径写入仓库根AGENTS.md的项目决策清单——固定间隔表与三个评分动作、入队范围、UTC 绝对时刻的到期判定、按answerId去重与expectedDueAt判定过期标签页、计数归属,以及短语/进度/策略 UI 的范围边界。仅新增一行文档,未改动代码、数据或接口;功能提交0328505保持不变,分支已同步推送,治理严格检查通过。#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.tsapplyResult,只有result === 'applied'时才累加answered、correctCount、wrongCount、wordsReviewed。answerId→ 服务端返回duplicate→ 卡片被移出,本轮计数没有增加。如果这是本轮唯一一次作答,answered === 0,于是empty为真,页面显示「今天没有到期词条」,完成页无法出现。answerId,所以duplicate就是本客户端的首次结果,应按applied计数(根据result.grade分类)。stale继续不计数。补一个 Vitest:第一次请求抛网络错误,重试返回duplicate,断言计数为 1 并显示完成页。R2 同一个
answerId的并发重发会返回 500,而不是首次结果(验收标准:重复请求并发)server/app/lexgo/review.goAnswerReview。按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.gosaveTerm每次保存都调用syncTermReview,而这个函数会无条件覆盖due_at。due_at变成 now+4 天,这个词从今天的队列中消失,但没有经过复习。status/level与保存前的值不同时,才更新due_at;只修改释义、例句或原形时保留原有排期。为此需要在 upsert 之前读取旧状态,并且应在同一事务内加锁读取。补一个用例:只改释义后,due_at不变。建议修复(不阻塞,但建议一并处理)
stale时卡片直接消失。建议显示一行role="status"提示,例如「该词已在其他页面复习」,避免用户以为自己的评分已经记录。review.ts的注释写着「One answer id per card and grade」,但answerIdFor只按卡片生成 id。报错后如果换一个评分重试,会沿用原来的 id,服务端按首次评分返回。这个行为可以接受,但需要修正注释,或者报错后只允许「重试提交」。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 审核意见分析与 R1~R3 整改(2026-09-11)
审核评论见 7781。我没有直接采信,而是先为每条「必须修复」写复现用例,在专用库
lexgo_test_issue8观察到失败后再修。结论:R1~R3 全部成立,R4/R5 是真实小问题,R6 属实;同时 R3 的复现顺带暴露了一处比审核描述更严重的问题。整改提交3258498,已推送。逐条核对
applyResult只对applied计数,旧契约下重放返回result=duplicate,client 自己重试后answered仍为 0;若这是本轮最后一张卡,empty为真 → 显示「今天没有到期词条」,完成页不出现。但修复不能只是「按 applied 计数」:若首次尝试实际被判定为 stale,重放同样返回 duplicate,直接计数会把未计分的作答算成一次复习status 500,另一个 201,review_count = 1。原因是answer_key查询在词条加锁前用普通读,两个请求都查不到;后者在 stale 分支插入相同唯一键due_at从2026-09-06被改成2026-09-15(now+4 天),逾期词条被挤出当天队列termLevel对缺省等级一律返回 1,于是4 级词改一个错字会掉回 1 级(不是只有排期变化,是学习进度被重置)stale时卡片直接消失,学习者无法判断评分是否被记录answerIdFor实际只按卡片生成lexgo_terms加列,实现改成独立表lexgo_term_reviews;虽然 7776 与 Wiki 说明了原因、且该选择确实更安全(加法迁移可重试、不对既有表 ALTER),但按「数据结构变化先更新工单」的要求应当在实施前追加变更评论。方案评论中的「新增列均有默认值」以独立表实现为准审核文本中两处细节需要更正:一是 R1 的结论表述为「完成页无法出现」应与「显示成空队列」并列,实际影响是完成页被空队列页取代,数据本身没有重复计分(这一点审核也承认);二是审核自述「没有重跑测试」,因此 R1 的后半段(首次为 stale 的重放不能计数)需要修正建议方案,我已按此实现。
整改内容
stale插入遇到 1062 也转为返回记录。新增TestMySQLReviewConcurrentReplayOfOneAnswer(两个 goroutine 同键提交,两个都 2xx、review_count = 1、只有一条答案记录)。due_at;缺排期行时补建。新增TestMySQLReviewEditKeepsSchedule(逾期 3 级词只改释义:due_at与队列不变;改等级重排到 now+15 天)。学习中才从 1 开始。新增TestMySQLReviewPanelSaveKeepsLevel(4 级词面板式保存后仍为 4 级且排期不变;退出再进入则从 1 开始)。result只取applied/stale,另加duplicate布尔标记;重放返回首次结果并置duplicate=true(已应用仍返回 201,重放与 stale 返回 200)。客户端因此按首次结果计数,网络丢响应后重试不会丢计数;同时用resolved区分「本轮解决过卡片」与「一开始就没有到期词条」,只解决卡片而无新计分时显示「本轮没有新的计分:N 个词条已在其他页面复习」。stale显示「该词已在其他页面复习,本次未计分。」,重放且首次为 stale 显示「该词已按上一次的评分记录,未重复计分。」,均为role="status";answerId注释改为「每张卡片一个,失败重试复用」。整改后复测
go vet ./...LEXGO_TEST_DB_NAME=lexgo_test_issue8 python scripts/server.py test-integrationnpx vitest --run/vue-tsc --build/pnpm run build/playwright testpnpm test/pnpm lintpython -m unittest discover -s tests/harness.py check --strict/sync --check.local/evidence/issue8-fixed-summary.png本机 lexgo-api 已用整改后二进制重启(schema 仍为 v6,无需迁移),
/healthz200。文档
1d90013d883de9339865694e0bb0026f9edd3acc(入队范围补「只有新建或状态/等级变化才移动复习时间」与「缺省等级保留已获得等级」;幂等段改为result+duplicate契约)8add5553222f7f5235c0b3fe45a0ad46d37bbd46(作答响应契约与加锁复查)1192d23a8198e981961adfc6066f0b3bc3b84058(本整改小节与复测结果)R6 记录:本次数据结构从「改
lexgo_terms列」变为「独立表lexgo_term_reviews」,理由是可重试的加法迁移与不对既有表 ALTER;方案评论 7769 中「新增列均有默认值」的表述作废,以 7776 与 Wiki 为准。此项不阻塞验收。工单仍停在「待验收」,PR #27 未合并,请复核 R1~R3 的修复与测试。
Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。
附件:issue8-fixed-summary.png(整改后真实浏览器复测截图;本会话模型不能读取图片,功能断言来自程序化检查)。
工单状态行与仓库决策清单已同步整改结果,无需新增分析:
85b8e24(docs: 记录 #8 审核整改后的复习口径)把本轮整改明确的两条规则补进仓库根AGENTS.md的项目决策清单:只有新建或状态/等级实际变化才移动复习时间、编辑释义保留原排期、保存未提及等级时保留已获得等级;作答响应result只取 applied/stale 并用duplicate标记重放。仅文档改动,未改代码、数据或接口;治理严格检查通过,分支已同步推送。
#8 复核整改:R1~R3 达标(2026-09-11,Claude Code)
复核对象:整改提交
3258498、85b8e24(评论 7784)。只读审阅代码与测试差异,没有重跑测试,结论基于代码逻辑核对。逐条复核
AnswerReview现在拿到词条行锁之后,再用lockedAnswerKey(SELECT ... FOR UPDATE)复查一次答案键;staleAnswer插入遇到 MySQL 1062 也会转成replayedAnswer。两条路径都不再返回 500。新增的TestMySQLReviewConcurrentReplayOfOneAnswer用两个 goroutine 同键提交,断言两次都是 2xx、review_count == 1、只有一条lexgo_review_answers记录,复现了原问题并验证了修复。达标。syncTermReview增加reschedule参数,只有新建或saveTerm判断出状态/等级实际变化时才更新due_at;否则只在缺行时补建。TestMySQLReviewEditKeepsSchedule验证逾期词只改释义后due_at和队列位置都不变,改等级才重排。达标,并且额外发现并修复了一个我审核时没有覆盖的问题:阅读器面板保存不带level字段时,原实现会把已获得的等级重置为 1(termLevel对level == nil一律返回 1)。整改后termLevel在缺省时保留previous.Level,只有真正进入学习中状态才从 1 开始,TestMySQLReviewPanelSaveKeepsLevel覆盖了这个场景。处理得比我的审核意见更完整。result只剩applied/stale,重放(duplicate=true)复述首次结果;finished/empty的判定从「是否有新计分」改成「本轮是否解决过卡片」(resolved),首次是 stale 的重放不会被误记为一次新复习。这个方案比我建议的更严谨,避免了我在审核意见里没考虑到的边界(首次本来就是 stale,重放也不该计分)。notice文案区分「已按上一次评分记录」和「已在其他页面复习」,完成页在没有新计分时显示专门的提示而不是空队列页。达标,且指出并修正了我审核意见的一个疏漏(评论 7784 已说明)。stale/重放提示已加role="status";answerId注释改为「每张卡片一个,失败重试复用」,与实现一致。达标。AGENTS.md决策清单和评论中记录方案偏差(独立表而非加列),说明沿用理由。达标。复核中发现的次要问题(不阻塞)
完成页在「本轮没有新计分」的分支下,仍然显示「已更新复习计划」(
ReviewView.vue第 51 行),但这种情况下复习计划其实没有变化,文案不准确。属于文案细节,不影响功能正确性,建议顺手改一下,不需要为此单独返工。结论
R1~R3 达标,可以进入用户验收流程。 本次复核为只读代码审阅,没有重跑
test-integration、learner vitest/E2E 等,pi 报告的复测结果(44 项集成用例、73 项前端单测等)未被本次复核重复验证;如需更高把握,建议在验收前独立重跑一次。上述完成页文案是唯一遗留的小问题,不构成阻塞项。用户验收与合并收尾
验收时间: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:
ee9d2b8cc69c17bc83b2f33fc69527ee23ab5f0ff7b41a473c138bedf0baff1544e6480872b92d4de6a1f2037516203e9144159af77b25e87638b6fd85c66bb518fcbb166b916549aa6e655215e0ed851d90013d883de9339865694e0bb0026f9edd3acc/1192d23a8198e981961adfc6066f0b3bc3b84058未增加产品范围或数据库变更;schema 保持 v6,本机开发库已完成 v5→v6 迁移,lexgo-api 运行整改后二进制。
第 3 阶段「首条学习闭环」#5~#8 至此全部通过用户验收,完整闭环(导入→查词→保存个人词义与状态→到期复习)可用。剩余 #9~#15 与 #21、#24 待实施;真机触屏详细证据与完整备份恢复演练缺口保留,由 #14/#15 承接。
Gitea MCP 仍指向其他站点,沿用已记录的目标 git.ilapage.cn API 回退,凭据仅在进程环境中使用。