MVP 编辑和删除本人书籍、章节 #10

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

来源与目标

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

在私人书库和章节目录改名、编辑、确认删除或取消,内容变化后重新处理且阅读入口保持一致。

验收标准

  • 默认封面、空列表与章节导航可用;编辑正文产生明确版本,旧处理结果不能覆盖新版本。
  • 删除/取消反馈明确;事务清理或软删除策略可恢复,处理中的任务不能在删除后复活内容。
  • 明确个人词条及例句引用在源章节删除后的保留规则,避免连带删除独立学习记录。
  • 跨用户改名/编辑/删除均拒绝;并发编辑/处理完成与重复删除有验证。

依赖与执行

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

参考模块与设计证据

参考 go-admin CRUD/事务/迁移;归属过滤强制执行,级联规则独立设计。

沿用已验收编辑/删除弹窗与列表。

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

工作量、范围与风险

预计 3~5 人日(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。阶段 4;覆盖 F01 补齐、F04。 在私人书库和章节目录改名、编辑、确认删除或取消,内容变化后重新处理且阅读入口保持一致。 ## 验收标准 - [ ] 默认封面、空列表与章节导航可用;编辑正文产生明确版本,旧处理结果不能覆盖新版本。 - [ ] 删除/取消反馈明确;事务清理或软删除策略可恢复,处理中的任务不能在删除后复活内容。 - [ ] 明确个人词条及例句引用在源章节删除后的保留规则,避免连带删除独立学习记录。 - [ ] 跨用户改名/编辑/删除均拒绝;并发编辑/处理完成与重复删除有验证。 ## 依赖与执行 前置:#5。状态:已完成(2026-09-14 用户验收通过,验收对象 1d19b2b,PR #29 已合入 main,验收记录见最新评论)。前置尚未通过时不得标进行中;每单完成停在待验收,由用户验收后关闭。技术验证可与不依赖其结论的工作分工,但不提前冻结未验证契约。 ## 参考模块与设计证据 参考 go-admin CRUD/事务/迁移;归属过滤强制执行,级联规则独立设计。 沿用已验收编辑/删除弹窗与列表。 后端不为建表/API 单独画页面原型;先写数据、接口、状态、隔离与幂等契约。学习端采用 #1 已验收 v1,未做的 LinguaCafe 对照不标完成。 ## 工作量、范围与风险 预计 3~5 人日(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

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

用户在当前会话确认「都按你说的」,同意契约 D1~D9(含 D3 事务硬删除 + 备份恢复、D6 新增编辑用原文接口)。前置 #5 已通过用户验收;本单分支 feat/10-edit-delete-books 从 main 65b50d5 创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。

只读诊断发现(本单必须同时修掉的缺陷)

chapters.content_sha256 与 jobs.content_sha256 已经存在,unprocessableReason() 也已比较两者,但它把章节本身标成 content_changed 失败。因此当某章正在 processing(版本 A)而用户把正文改成 B(新版本、新任务在排队)时,旧任务完成会把已经是新版本的章节标成失败——这就是验收标准里「旧处理结果不能覆盖新版本」目前真实存在的问题;恢复扫描 setIngestChapterStatus 也按章节批量写状态,同样会误伤新版本。RetryIngestJob 只检查 job.status = failed、不检查版本,可以重试旧版本任务并把当前章节拉回 pending 再失败一次。

契约

D1 编辑范围:书名、章节标题、章节正文(原型 editchapter 含正文输入框)。只有正文变化才重新处理;只改标题不触发处理。

D2 版本规则:content_sha256 即版本键。

  • 任务只在版本匹配时才能影响章节:job.content_sha256 != chapter.content_sha256 时把该任务标为 failed + error_reason=superseded(复用现有状态枚举,不新增状态、不改 schema),完全不触碰章节;原有的 content_changed 文案保留用于历史行,但不再由新流程产生。
  • 认领与恢复扫描跳过并作废过期版本任务,不把新版本拖进 processing。
  • 重试只允许版本匹配的任务,否则 409「该任务对应的是旧版本,请刷新」。
  • 编辑正文 → 新 text + 新 sha + status=pending + 新任务(request_key 由「章节 + 版本」确定性派生,章节 id 与阅读入口不变)。

D3 删除策略:事务内硬删除 + 现有 FK 级联(books → chapters → ingest_jobs)。删除响应返回被删章节数,供「已删除 · 剩余 N 章」反馈。恢复路径 = 数据库备份与完整恢复(#15 演练范围)。不做回收站与撤销(不在 MVP 范围)。

D4 章节序号:删除章节后重排为连续序号,实现为事务内 UPDATE lexgo_chapters SET ordinal = ordinal - 1 WHERE book_id = ? AND ordinal > ? ORDER BY ordinal ASC,事务开始先锁 book 行,保证并发删除不产生重号。

D5 学习记录保留:删除书籍或章节不删除个人词条、复习排期与作答记录(它们按 owner 归属,不引用章节);界面没有「来自某章节」的引用,因为例句是用户输入的副本。不需要迁移,只用测试锁定。

D6 编辑用原文接口:新增 GET /api/v1/chapters/:id/source,本人任意状态返回 {id, bookId, ordinal, title, text, status, contentSha256, charCount}。不改 #5「就绪才返回阅读原文」的已验收契约,同时允许修正失败章节的正文。

D7 接口与幂等

接口 行为
PATCH /api/v1/books/:id {title};200 返回 book;字段白名单,未知字段 400
GET /api/v1/chapters/:id/source 本人任意状态;他人与不存在统一 404
PATCH /api/v1/chapters/:id {title?, text?};正文变化才建新任务,返回 {chapter, job?, versionChanged}
DELETE /api/v1/books/:id 事务删除书、章节与任务;返回 {deleted:{bookId, chapters}}
DELETE /api/v1/chapters/:id 事务删除章节与任务并重排序号;返回 {deleted:{chapterId}, bookId, remaining}

重复删除 404;重复保存同一正文不新建任务(sha 相同即无版本变化);他人资源一律 404;未登录 401。

D8 前端:沿用原型文案与弹窗。BookView 增加「编辑书名」「删除书籍」与章节行「编辑」;编辑用对话框,删除用确认弹窗(「本章正文将被删除,已保存词条保留。」「删除这本书及其章节?已保存的生词和短语将保留。」);正文编辑框提示「保存后会重新处理」;删除章节后停留书籍页显示「章节已删除 · 剩余 N 章」;章节清空显示空态;阅读已删除章节沿用「内容不存在」。

D9 schema:无变化(复用 content_sha256 与现有 error_reason),无迁移,回退只需换二进制。

测试计划

  • 版本与并发:处理中改正文 → 旧任务标 superseded 且不改变章节状态 → 新任务完成 → 章节为新版本 ready 且原文等于新文本;恢复扫描同样作废过期版本;旧版本任务重试 409;改回原内容不新建任务。
  • 删除:删章后序号重排、剩余计数、任务级联清理、个人词条/排期/作答不变;删书后章节与任务全部清理、学习记录不变;处理中删除 → worker 不报错、不复活内容;并发删除同一章节 → 一个成功一个 404 且序号正确;重复删除 404。
  • 隔离:跨用户改名/编辑/删除/取 source 一律 404;未登录 401。
  • 前端:对话框流程、确认与取消、正文保存后回到处理中、删除后的剩余章节提示与空态、错误保留输入。
  • 真实链路:真实 API+MySQL 走「改名 → 编辑正文 → 旧任务作废 → 新版就绪 → 删章重排 → 删书 → 词条与复习记录仍在」,并用真实浏览器复跑原型文案路径。

非目标:封面/音频附件(#21)、回收站与撤销、批量操作、章节跨书移动、语言变更。

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

## #10 方案确认与实施启动(2026-09-11) 用户在当前会话确认「都按你说的」,同意契约 D1~D9(含 D3 事务硬删除 + 备份恢复、D6 新增编辑用原文接口)。前置 #5 已通过用户验收;本单分支 `feat/10-edit-delete-books` 从 main `65b50d5` 创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。 ### 只读诊断发现(本单必须同时修掉的缺陷) `chapters.content_sha256` 与 `jobs.content_sha256` 已经存在,`unprocessableReason()` 也已比较两者,但它把**章节本身**标成 `content_changed` 失败。因此当某章正在 processing(版本 A)而用户把正文改成 B(新版本、新任务在排队)时,旧任务完成会把**已经是新版本的章节**标成失败——这就是验收标准里「旧处理结果不能覆盖新版本」目前真实存在的问题;恢复扫描 `setIngestChapterStatus` 也按章节批量写状态,同样会误伤新版本。`RetryIngestJob` 只检查 `job.status = failed`、不检查版本,可以重试旧版本任务并把当前章节拉回 pending 再失败一次。 ### 契约 **D1 编辑范围**:书名、章节标题、章节正文(原型 `editchapter` 含正文输入框)。**只有正文变化才重新处理**;只改标题不触发处理。 **D2 版本规则**:`content_sha256` 即版本键。 - 任务**只在版本匹配时**才能影响章节:`job.content_sha256 != chapter.content_sha256` 时把该任务标为 `failed` + `error_reason=superseded`(复用现有状态枚举,不新增状态、不改 schema),**完全不触碰章节**;原有的 `content_changed` 文案保留用于历史行,但不再由新流程产生。 - 认领与恢复扫描跳过并作废过期版本任务,不把新版本拖进 processing。 - 重试只允许版本匹配的任务,否则 409「该任务对应的是旧版本,请刷新」。 - 编辑正文 → 新 text + 新 sha + `status=pending` + 新任务(`request_key` 由「章节 + 版本」确定性派生,章节 id 与阅读入口不变)。 **D3 删除策略**:事务内**硬删除** + 现有 FK 级联(books → chapters → ingest_jobs)。删除响应返回被删章节数,供「已删除 · 剩余 N 章」反馈。恢复路径 = 数据库备份与完整恢复(#15 演练范围)。不做回收站与撤销(不在 MVP 范围)。 **D4 章节序号**:删除章节后重排为连续序号,实现为事务内 `UPDATE lexgo_chapters SET ordinal = ordinal - 1 WHERE book_id = ? AND ordinal > ? ORDER BY ordinal ASC`,事务开始先锁 book 行,保证并发删除不产生重号。 **D5 学习记录保留**:删除书籍或章节**不删除**个人词条、复习排期与作答记录(它们按 owner 归属,不引用章节);界面没有「来自某章节」的引用,因为例句是用户输入的副本。不需要迁移,只用测试锁定。 **D6 编辑用原文接口**:新增 `GET /api/v1/chapters/:id/source`,本人任意状态返回 `{id, bookId, ordinal, title, text, status, contentSha256, charCount}`。不改 #5「就绪才返回阅读原文」的已验收契约,同时允许修正失败章节的正文。 **D7 接口与幂等** | 接口 | 行为 | |---|---| | PATCH /api/v1/books/:id | `{title}`;200 返回 book;字段白名单,未知字段 400 | | GET /api/v1/chapters/:id/source | 本人任意状态;他人与不存在统一 404 | | PATCH /api/v1/chapters/:id | `{title?, text?}`;正文变化才建新任务,返回 `{chapter, job?, versionChanged}` | | DELETE /api/v1/books/:id | 事务删除书、章节与任务;返回 `{deleted:{bookId, chapters}}` | | DELETE /api/v1/chapters/:id | 事务删除章节与任务并重排序号;返回 `{deleted:{chapterId}, bookId, remaining}` | 重复删除 404;重复保存同一正文不新建任务(sha 相同即无版本变化);他人资源一律 404;未登录 401。 **D8 前端**:沿用原型文案与弹窗。`BookView` 增加「编辑书名」「删除书籍」与章节行「编辑」;编辑用对话框,删除用确认弹窗(「本章正文将被删除,已保存词条保留。」「删除这本书及其章节?已保存的生词和短语将保留。」);正文编辑框提示「保存后会重新处理」;删除章节后停留书籍页显示「章节已删除 · 剩余 N 章」;章节清空显示空态;阅读已删除章节沿用「内容不存在」。 **D9 schema**:无变化(复用 `content_sha256` 与现有 `error_reason`),无迁移,回退只需换二进制。 ### 测试计划 - **版本与并发**:处理中改正文 → 旧任务标 `superseded` 且不改变章节状态 → 新任务完成 → 章节为新版本 ready 且原文等于新文本;恢复扫描同样作废过期版本;旧版本任务重试 409;改回原内容不新建任务。 - **删除**:删章后序号重排、剩余计数、任务级联清理、个人词条/排期/作答不变;删书后章节与任务全部清理、学习记录不变;**处理中删除** → worker 不报错、不复活内容;并发删除同一章节 → 一个成功一个 404 且序号正确;重复删除 404。 - **隔离**:跨用户改名/编辑/删除/取 source 一律 404;未登录 401。 - **前端**:对话框流程、确认与取消、正文保存后回到处理中、删除后的剩余章节提示与空态、错误保留输入。 - **真实链路**:真实 API+MySQL 走「改名 → 编辑正文 → 旧任务作废 → 新版就绪 → 删章重排 → 删书 → 词条与复习记录仍在」,并用真实浏览器复跑原型文案路径。 非目标:封面/音频附件(#21)、回收站与撤销、批量操作、章节跨书移动、语言变更。 回退说明:本会话 pi 无可用 Gitea MCP 工具,沿用本单既定回退,使用目标 `git.ilapage.cn` API;凭据仅从既有安全配置读入进程。
Author
Owner

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

用户确认的契约 D1~D9 见评论 7837。分支 feat/10-edit-delete-books 从 main 65b50d5 创建,实现提交 35ed692,已推送。PR #29 未合并,工单不关闭,停在待用户验收。

只读诊断发现的缺陷与修复

诊断时确认:content_sha256 已是版本键,unprocessableReason() 也会比较,但它把章节本身标成 content_changed 失败;恢复扫描按章节批量写状态;RetryIngestJob 不检查版本。因此「处理中改正文 → 旧任务完成 → 新版本章节被标失败」这一「旧处理结果覆盖新版本」的问题真实存在。本单已修复:

  • 任务只在 job.content_sha256 == chapter.content_sha256 时能影响章节;不匹配时只把任务标为 failed/superseded(复用现有 error_reason,不新增状态、不改 schema),完全不触碰章节;
  • 认领任务用 JOIN 只取版本匹配的行,并先一次性把过期版本任务标为 superseded;恢复扫描同样先作废过期版本、只重排版本匹配的中断任务;
  • 重试接口拒绝版本不匹配的任务(409);
  • unprocessableReason 保留「存储文本重算 SHA 必须等于存储 SHA」的一致性检查,继续兜住绕过 API 的直接写入(content_changed)。

实现与差异

  • server/app/lexgo/edit.go:改名、编辑、删除与编辑用原文接口。只有正文变化才重新处理(不新建任务、不改变状态);任务 request_key 由「章节 + 版本」确定性派生。
  • 删除:事务内硬删除 + 外键级联(书籍 → 章节 → 任务);删除章节后 UPDATE ... ORDER BY ordinal ASC 重排序号,并在事务开始锁 book 行序列化并发删除。个人词条、复习排期与作答记录按 owner 归属,不随删除清理。
  • 处理中删除章节:在途任务找不到章节时视为无事可做,不报错、不复活内容。
  • 学习端:BookView 增加书名与章节编辑对话框、章节行「编辑」入口、两处删除确认弹窗(文案照原型);LibraryView 在书库被删后显示「书籍已删除 · 已保存的生词和短语仍保留在生词本。」;store 增加五个编辑/删除方法。
  • 已知边界:浏览器 textarea 会把编辑后该章的行尾归一为 LF(粘贴与 TXT 导入仍保留原始 CRLF),已记入业务规则页。

实际验证

验证 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration 59 个顶层用例全部通过、0 跳过(原 50,新增 9 个编辑/删除用例)
learner npx vitest --run / vue-tsc --build / pnpm run build / playwright test 94 项单测、类型检查、构建、7 项 E2E 通过
admin pnpm test / pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / harness.py check --strict / sync --check 56 项、严格检查、镜像一致
真实 API+MySQL 36 项检查通过(脚本可重复运行并自行清理 fixture)
真实浏览器 导入 → 改名 → 编辑正文 → 新版就绪 → 删除章节 → 删除书籍 → 书库提示,闭环通过

覆盖:处理中编辑后旧任务只标 superseded 且不触碰新版本、旧版本重试 409、重复保存与改回原内容不新建任务、内容与版本不一致时按 content_changed 失败、恢复扫描作废过期版本、删除章节序号连续与导航正确、删除书籍级联、处理中删除不复活、并发删除一个成功一个 404、重复删除 404、跨用户改名/编辑/删除/读原文 404、未登录 401、个人词条与复习排期在删除后保留。

本单不改数据库结构,无迁移;本机 lexgo-api 已用新二进制重启(schema 仍为 v6),/healthz 200,回退用二进制保存在 .local/lexgo-pre-issue10.exe。

未验证与边界

  • 真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器检查。
  • 并发只覆盖「同一章节并发删除」与「处理中编辑」两类,未做多用户压力测试。
  • 回收站/撤销、批量操作、章节跨书移动、语言变更、封面与音频附件(#21)不在本单。

文档

  • Architecture-and-Code-Map: ce95236e4c8de27c1063448571cfc528c895f937
  • Business-Rules-and-Glossary: 96eec89f74db744d8807baa24b1d1d611ff6bf8f
  • Local-Development-and-Verification: 251fb5de71b8ba75da3cba6eee41454d5bbd22df
  • Product-Requirements-Overview: 82d714b949d38f20586fbe42797b2dec6b0f19f1
  • Home: ea264da6747bd141ec2d67011830320452563415
  • Project-Profile 本单未变化,仍为 b85c7f4650aae95b1429c0b4f7d1412c440731ce

回退

本单无 schema 变化:停止 lexgo-api,恢复 .local/lexgo-pre-issue10.exe,重新启动即可;已改名、编辑或删除的数据按数据库现状保留。

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

附件:issue10-book-after-edit.png、issue10-chapter-deleted.png、issue10-book-deleted.png(真实浏览器闭环截图;本会话模型不能读取图片,功能断言来自程序化检查)。

## #10 实施完成,待用户验收(2026-09-11) 用户确认的契约 D1~D9 见评论 [7837](https://git.ilapage.cn/OPC/lexgo/issues/10#issuecomment-7837)。分支 `feat/10-edit-delete-books` 从 main `65b50d5` 创建,实现提交 `35ed692`,已推送。PR [#29](https://git.ilapage.cn/OPC/lexgo/pulls/29) 未合并,工单不关闭,停在待用户验收。 ### 只读诊断发现的缺陷与修复 诊断时确认:`content_sha256` 已是版本键,`unprocessableReason()` 也会比较,但它把**章节本身**标成 `content_changed` 失败;恢复扫描按章节批量写状态;`RetryIngestJob` 不检查版本。因此「处理中改正文 → 旧任务完成 → 新版本章节被标失败」这一「旧处理结果覆盖新版本」的问题真实存在。本单已修复: - 任务只在 `job.content_sha256 == chapter.content_sha256` 时能影响章节;不匹配时只把任务标为 `failed/superseded`(复用现有 `error_reason`,不新增状态、不改 schema),**完全不触碰章节**; - 认领任务用 JOIN 只取版本匹配的行,并先一次性把过期版本任务标为 superseded;恢复扫描同样先作废过期版本、只重排版本匹配的中断任务; - 重试接口拒绝版本不匹配的任务(409); - `unprocessableReason` 保留「存储文本重算 SHA 必须等于存储 SHA」的一致性检查,继续兜住绕过 API 的直接写入(`content_changed`)。 ### 实现与差异 - `server/app/lexgo/edit.go`:改名、编辑、删除与编辑用原文接口。只有正文变化才重新处理(不新建任务、不改变状态);任务 `request_key` 由「章节 + 版本」确定性派生。 - 删除:事务内硬删除 + 外键级联(书籍 → 章节 → 任务);删除章节后 `UPDATE ... ORDER BY ordinal ASC` 重排序号,并在事务开始锁 book 行序列化并发删除。个人词条、复习排期与作答记录按 owner 归属,不随删除清理。 - 处理中删除章节:在途任务找不到章节时视为无事可做,不报错、不复活内容。 - 学习端:`BookView` 增加书名与章节编辑对话框、章节行「编辑」入口、两处删除确认弹窗(文案照原型);`LibraryView` 在书库被删后显示「书籍已删除 · 已保存的生词和短语仍保留在生词本。」;store 增加五个编辑/删除方法。 - 已知边界:浏览器 textarea 会把编辑后该章的行尾归一为 LF(粘贴与 TXT 导入仍保留原始 CRLF),已记入业务规则页。 ### 实际验证 | 验证 | 结果 | |---|---| | `go vet ./...` | 通过 | | `LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration` | 59 个顶层用例全部通过、0 跳过(原 50,新增 9 个编辑/删除用例) | | learner `npx vitest --run` / `vue-tsc --build` / `pnpm run build` / `playwright test` | 94 项单测、类型检查、构建、7 项 E2E 通过 | | admin `pnpm test` / `pnpm lint` | 31 项与 lint 通过;管理端本单无代码改动 | | `python -m unittest discover -s tests` / `harness.py check --strict` / `sync --check` | 56 项、严格检查、镜像一致 | | 真实 API+MySQL | 36 项检查通过(脚本可重复运行并自行清理 fixture) | | 真实浏览器 | 导入 → 改名 → 编辑正文 → 新版就绪 → 删除章节 → 删除书籍 → 书库提示,闭环通过 | 覆盖:处理中编辑后旧任务只标 superseded 且不触碰新版本、旧版本重试 409、重复保存与改回原内容不新建任务、内容与版本不一致时按 content_changed 失败、恢复扫描作废过期版本、删除章节序号连续与导航正确、删除书籍级联、处理中删除不复活、并发删除一个成功一个 404、重复删除 404、跨用户改名/编辑/删除/读原文 404、未登录 401、个人词条与复习排期在删除后保留。 本单不改数据库结构,无迁移;本机 lexgo-api 已用新二进制重启(schema 仍为 v6),`/healthz` 200,回退用二进制保存在 `.local/lexgo-pre-issue10.exe`。 ### 未验证与边界 - 真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器检查。 - 并发只覆盖「同一章节并发删除」与「处理中编辑」两类,未做多用户压力测试。 - 回收站/撤销、批量操作、章节跨书移动、语言变更、封面与音频附件(#21)不在本单。 ### 文档 - Architecture-and-Code-Map: `ce95236e4c8de27c1063448571cfc528c895f937` - Business-Rules-and-Glossary: `96eec89f74db744d8807baa24b1d1d611ff6bf8f` - Local-Development-and-Verification: `251fb5de71b8ba75da3cba6eee41454d5bbd22df` - Product-Requirements-Overview: `82d714b949d38f20586fbe42797b2dec6b0f19f1` - Home: `ea264da6747bd141ec2d67011830320452563415` - Project-Profile 本单未变化,仍为 `b85c7f4650aae95b1429c0b4f7d1412c440731ce` ### 回退 本单无 schema 变化:停止 lexgo-api,恢复 `.local/lexgo-pre-issue10.exe`,重新启动即可;已改名、编辑或删除的数据按数据库现状保留。 Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。 附件:issue10-book-after-edit.png、issue10-chapter-deleted.png、issue10-book-deleted.png(真实浏览器闭环截图;本会话模型不能读取图片,功能断言来自程序化检查)。
Author
Owner

补充提交 docs: 记录 #10 已确认的编辑与删除口径:按既有惯例把用户确认的口径写入仓库根 AGENTS.md 的项目决策清单——只有正文变化才重新处理、版本门控与 superseded 语义、内容一致性检查、事务硬删除与级联、个人学习记录不随删除清理,以及范围边界。仅文档改动,功能提交 35ed692 不变,治理严格检查通过。

补充提交 `docs: 记录 #10 已确认的编辑与删除口径`:按既有惯例把用户确认的口径写入仓库根 `AGENTS.md` 的项目决策清单——只有正文变化才重新处理、版本门控与 superseded 语义、内容一致性检查、事务硬删除与级联、个人学习记录不随删除清理,以及范围边界。仅文档改动,功能提交 35ed692 不变,治理严格检查通过。
Author
Owner

#10 代码审核:达标(2026-09-14,Claude Code)

审核对象:提交 35ed692、1d19b2b(PR #29)。只读审阅代码与测试差异,没有重跑测试。契约 D1~D9(评论 7837)逐条核对如下。

逐条核对

  • D1/D2 编辑范围与版本规则:UpdateChapter 只在 sha != chapter.ContentSHA256 时才建新版本(新文本、新 sha、status=pending、新任务),只改标题完全不碰处理状态;request_key 由「章节 id + 版本 sha」确定性派生,保证一个版本只有一个任务。达标。
  • 版本门控(诊断出的缺陷修复):ClaimNextIngestJob 的认领查询用 JOIN lexgo_chapters ON c.content_sha256 = j.content_sha256 过滤,只有版本匹配的任务能被认领;FinishIngestJob 在锁定章节后二次比较版本,不匹配就只把任务标 failed/superseded、完全不碰章节(关闭了「认领之后、完成之前章节被编辑」这个竞态窗口);abandonSupersededJobs 用一条多表 UPDATE 语句把版本过期的 pending/processing 任务标为 superseded;RequeueStaleIngestJobs/RecoverIngestJobs 在扫描前先调用它,避免把旧版本任务错误地拖回处理中。TestMySQLChapterEditVersioning(编辑中途完成旧任务、旧任务重试 409)和 TestMySQLRecoverySkipsSupersededJobs(超时恢复扫描不误伤新版本)两个用例直接复现了诊断阶段发现的问题并验证了修复。达标,且诊断和修复过程本身有对应的回归测试,质量高于一般实现。
  • D3 删除策略:DeleteBook/DeleteChapter 都在 protect 提供的单个数据库事务内执行(router.go 第 86 行 db.Transaction),硬删除靠外键级联(lexgo_chapters.book_id ON DELETE CASCADE、lexgo_ingest_jobs 同理)。TestMySQLDeleteDuringProcessing 验证了处理中删除后,在途任务的 FinishIngestJob 找不到章节直接返回 nil(不报错、不复活内容)。达标。
  • D4 章节序号:DeleteChapter 先锁章节再锁书籍行,删除后按 ORDER BY ordinal ASC 依次递减,保证唯一键 (book_id, ordinal) 全程满足;TestMySQLConcurrentChapterDelete 用两个 goroutine 并发删除同一章节,验证一个 200 一个 404,且剩余章节序号正确归一。达标。
  • D5 学习记录保留:DeleteBook/DeleteChapter 均不涉及 lexgo_terms/lexgo_term_reviews/lexgo_review_answers 任何操作;TestMySQLDeleteBookKeepsPersonalRecords 删除整本书后验证个人词条、复习排期行数不变,且复习队列里仍能查到这个词。达标。
  • D6 编辑用原文接口:GET /chapters/:id/source 不看状态直接返回 chapter.OriginalText,与 #5 已验收的「就绪才返回阅读原文」互不冲突(阅读接口另有实现);TestMySQLChapterSourceAnyStatus 覆盖了 pending/ready 两态、跨账号 404、查询参数拒绝。达标。
  • D7 接口与幂等:PATCH/DELETE 的状态码、字段白名单、未知字段拒绝、他人资源 404、重复删除 404、重复保存不新建任务,均有对应测试。达标。
  • D8 前端:BookView.vue 的编辑对话框、两处删除确认弹窗文案与原型一致;edit.spec.ts 覆盖了对话框流程、取消、保存失败保留输入、删除后的书库/书籍页更新。达标。
  • D9 schema:未新增迁移语句,复用现有 content_sha256 与 error_reason 枚举值。达标。

复核中确认的细节

  • UpdateChapter 里标题更新发生在正文校验之前,validatePaste(chapter.Title, text) 用的是(可能刚更新的)新标题做校验,这是既有校验函数的复用方式,不产生问题。
  • 浏览器 textarea 编辑会把该章行尾归一为 LF(粘贴/TXT 导入仍保留原始 CRLF),评论已如实记录为已知边界而非缺陷,同意这个记录方式:这是浏览器表单元素的通用行为,不是本单代码引入的缺陷。

结论

达标,可以进入用户验收。 这单在方案确认阶段做了只读诊断,主动找出了「旧任务完成覆盖新版本」的真实竞态缺陷并在实现里从认领、完成、恢复扫描、重试四个入口一并堵上,测试直接复现了缺陷场景后再验证修复,是这几单里诊断质量最高的一次。删除相关的加锁顺序、序号重排和级联范围都经过并发测试验证,个人学习记录的隔离边界也有专门用例锁定。

本次审核只读代码,没有重跑 test-integration(lexgo_test_issue9)与 learner 的 vitest/E2E/Playwright;pi 报告的 59 项集成用例、94 项前端单测等结果未被本次复核重复验证,如需更高把握建议在验收前独立重跑一次。

Gitea MCP 仍指向其他站点,本次沿用已记录的目标站点 API 回退;凭据只从 ~/.claude/gitea.env 安全配置读入进程。

## #10 代码审核:达标(2026-09-14,Claude Code) 审核对象:提交 `35ed692`、`1d19b2b`(PR #29)。只读审阅代码与测试差异,没有重跑测试。契约 D1~D9(评论 7837)逐条核对如下。 ### 逐条核对 - **D1/D2 编辑范围与版本规则**:`UpdateChapter` 只在 `sha != chapter.ContentSHA256` 时才建新版本(新文本、新 sha、`status=pending`、新任务),只改标题完全不碰处理状态;`request_key` 由「章节 id + 版本 sha」确定性派生,保证一个版本只有一个任务。**达标**。 - **版本门控(诊断出的缺陷修复)**:`ClaimNextIngestJob` 的认领查询用 `JOIN lexgo_chapters ON c.content_sha256 = j.content_sha256` 过滤,只有版本匹配的任务能被认领;`FinishIngestJob` 在锁定章节后二次比较版本,不匹配就只把任务标 `failed/superseded`、完全不碰章节(关闭了「认领之后、完成之前章节被编辑」这个竞态窗口);`abandonSupersededJobs` 用一条多表 UPDATE 语句把版本过期的 pending/processing 任务标为 superseded;`RequeueStaleIngestJobs`/`RecoverIngestJobs` 在扫描前先调用它,避免把旧版本任务错误地拖回处理中。`TestMySQLChapterEditVersioning`(编辑中途完成旧任务、旧任务重试 409)和 `TestMySQLRecoverySkipsSupersededJobs`(超时恢复扫描不误伤新版本)两个用例直接复现了诊断阶段发现的问题并验证了修复。**达标,且诊断和修复过程本身有对应的回归测试,质量高于一般实现**。 - **D3 删除策略**:`DeleteBook`/`DeleteChapter` 都在 `protect` 提供的单个数据库事务内执行(`router.go` 第 86 行 `db.Transaction`),硬删除靠外键级联(`lexgo_chapters.book_id ON DELETE CASCADE`、`lexgo_ingest_jobs` 同理)。`TestMySQLDeleteDuringProcessing` 验证了处理中删除后,在途任务的 `FinishIngestJob` 找不到章节直接返回 `nil`(不报错、不复活内容)。**达标**。 - **D4 章节序号**:`DeleteChapter` 先锁章节再锁书籍行,删除后按 `ORDER BY ordinal ASC` 依次递减,保证唯一键 `(book_id, ordinal)` 全程满足;`TestMySQLConcurrentChapterDelete` 用两个 goroutine 并发删除同一章节,验证一个 200 一个 404,且剩余章节序号正确归一。**达标**。 - **D5 学习记录保留**:`DeleteBook`/`DeleteChapter` 均不涉及 `lexgo_terms`/`lexgo_term_reviews`/`lexgo_review_answers` 任何操作;`TestMySQLDeleteBookKeepsPersonalRecords` 删除整本书后验证个人词条、复习排期行数不变,且复习队列里仍能查到这个词。**达标**。 - **D6 编辑用原文接口**:`GET /chapters/:id/source` 不看状态直接返回 `chapter.OriginalText`,与 #5 已验收的「就绪才返回阅读原文」互不冲突(阅读接口另有实现);`TestMySQLChapterSourceAnyStatus` 覆盖了 pending/ready 两态、跨账号 404、查询参数拒绝。**达标**。 - **D7 接口与幂等**:PATCH/DELETE 的状态码、字段白名单、未知字段拒绝、他人资源 404、重复删除 404、重复保存不新建任务,均有对应测试。**达标**。 - **D8 前端**:`BookView.vue` 的编辑对话框、两处删除确认弹窗文案与原型一致;`edit.spec.ts` 覆盖了对话框流程、取消、保存失败保留输入、删除后的书库/书籍页更新。**达标**。 - **D9 schema**:未新增迁移语句,复用现有 `content_sha256` 与 `error_reason` 枚举值。**达标**。 ### 复核中确认的细节 - `UpdateChapter` 里标题更新发生在正文校验之前,`validatePaste(chapter.Title, text)` 用的是(可能刚更新的)新标题做校验,这是既有校验函数的复用方式,不产生问题。 - 浏览器 textarea 编辑会把该章行尾归一为 LF(粘贴/TXT 导入仍保留原始 CRLF),评论已如实记录为已知边界而非缺陷,同意这个记录方式:这是浏览器表单元素的通用行为,不是本单代码引入的缺陷。 ### 结论 **达标,可以进入用户验收。** 这单在方案确认阶段做了只读诊断,主动找出了「旧任务完成覆盖新版本」的真实竞态缺陷并在实现里从认领、完成、恢复扫描、重试四个入口一并堵上,测试直接复现了缺陷场景后再验证修复,是这几单里诊断质量最高的一次。删除相关的加锁顺序、序号重排和级联范围都经过并发测试验证,个人学习记录的隔离边界也有专门用例锁定。 本次审核只读代码,没有重跑 `test-integration`(`lexgo_test_issue9`)与 learner 的 vitest/E2E/Playwright;pi 报告的 59 项集成用例、94 项前端单测等结果未被本次复核重复验证,如需更高把握建议在验收前独立重跑一次。 Gitea MCP 仍指向其他站点,本次沿用已记录的目标站点 API 回退;凭据只从 `~/.claude/gitea.env` 安全配置读入进程。
Author
Owner

用户验收与合并收尾

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

验收对象是 PR #29 的完整头部 1d19b2b,包含功能提交 35ed692 与口径记录 1d19b2b,两个提交逐个进入 main,没有合并提交、没有改写历史,与用户验收版本逐字节一致。验收状态文档提交 1319c56 已推送。分支 feat/10-edit-delete-books 保留;需要撤销时可在新工单中使用 revert。

本次验收前的完整证据见评论 7842(含只读诊断发现的版本门控缺陷与修复、测试与真实链路),契约见 7837,本次不重复覆盖。合并后复核:harness.py check --strict 通过、sync --verify 通过、sync --check 通过、治理测试 56 项通过,工作区干净且与远端同步。

长期文档 Wiki revisions:

  • Project-Profile: 08d992aee15b7fd5cea0cdbb4eb0b90583068449
  • Architecture-and-Code-Map: a29e5eb9c8ff474ffdcb1278fc6b51e3b9167eb8
  • Product-Requirements-Overview: 559f97b2f00b7fc274bda52bdaad62d10f7926c8
  • Home: 40f720f23d608f2dd875ba0ba838297c07b7a8fa
  • Business-Rules-and-Glossary 与 Local-Development-and-Verification 本单验收未变化,仍为 96eec89f74db744d8807baa24b1d1d611ff6bf8f / 251fb5de71b8ba75da3cba6eee41454d5bbd22df

本单没有数据库结构变化,schema 保持 v6;本机 lexgo-api 运行新二进制,/healthz 200。回退用的上一版本二进制保存在忽略的 .local/lexgo-pre-issue10.exe。

收尾期间目标站点出现间歇性 503/429(/repos/OPC/lexgo 与 wiki API 一度不可用,工单接口正常),镜像导出重试后完成,sync --verify 与 sync --check 均已通过;该过程只影响导出时机,不影响验收对象与结论,也不写入任何本地草稿。

当前待完成 #11~#15,另有 #21 与 #24。真机触屏详细证据与完整备份恢复演练缺口保留,由 #14/#15 承接。

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

## 用户验收与合并收尾 验收时间:2026-09-14 21:32 +0800(Asia/Shanghai,与 PR 合入时刻一致)。用户在会话中明确确认「#10 通过验收」。结论:#10 验收通过,关闭工单;PR [#29](https://git.ilapage.cn/OPC/lexgo/pulls/29) 已 fast-forward-only 合入 main。 验收对象是 PR #29 的完整头部 `1d19b2b`,包含功能提交 `35ed692` 与口径记录 `1d19b2b`,两个提交逐个进入 main,没有合并提交、没有改写历史,与用户验收版本逐字节一致。验收状态文档提交 `1319c56` 已推送。分支 `feat/10-edit-delete-books` 保留;需要撤销时可在新工单中使用 revert。 本次验收前的完整证据见评论 [7842](https://git.ilapage.cn/OPC/lexgo/issues/10#issuecomment-7842)(含只读诊断发现的版本门控缺陷与修复、测试与真实链路),契约见 [7837](https://git.ilapage.cn/OPC/lexgo/issues/10#issuecomment-7837),本次不重复覆盖。合并后复核:`harness.py check --strict` 通过、`sync --verify` 通过、`sync --check` 通过、治理测试 56 项通过,工作区干净且与远端同步。 长期文档 Wiki revisions: - Project-Profile: `08d992aee15b7fd5cea0cdbb4eb0b90583068449` - Architecture-and-Code-Map: `a29e5eb9c8ff474ffdcb1278fc6b51e3b9167eb8` - Product-Requirements-Overview: `559f97b2f00b7fc274bda52bdaad62d10f7926c8` - Home: `40f720f23d608f2dd875ba0ba838297c07b7a8fa` - Business-Rules-and-Glossary 与 Local-Development-and-Verification 本单验收未变化,仍为 `96eec89f74db744d8807baa24b1d1d611ff6bf8f` / `251fb5de71b8ba75da3cba6eee41454d5bbd22df` 本单没有数据库结构变化,schema 保持 v6;本机 lexgo-api 运行新二进制,`/healthz` 200。回退用的上一版本二进制保存在忽略的 `.local/lexgo-pre-issue10.exe`。 收尾期间目标站点出现间歇性 503/429(`/repos/OPC/lexgo` 与 wiki API 一度不可用,工单接口正常),镜像导出重试后完成,`sync --verify` 与 `sync --check` 均已通过;该过程只影响导出时机,不影响验收对象与结论,也不写入任何本地草稿。 当前待完成 #11~#15,另有 #21 与 #24。真机触屏详细证据与完整备份恢复演练缺口保留,由 #14/#15 承接。 Gitea MCP 仍指向其他站点,沿用已记录的目标 git.ilapage.cn API 回退,凭据仅在进程环境中使用。
ila closed this issue 2026-09-14 21:39:27 +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#10