编辑正文 → 新 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 行,保证并发删除不产生重号。
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。阶段 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 回退;凭据仅进入进程。
#10 方案确认与实施启动(2026-09-11)
用户在当前会话确认「都按你说的」,同意契约 D1~D9(含 D3 事务硬删除 + 备份恢复、D6 新增编辑用原文接口)。前置 #5 已通过用户验收;本单分支
feat/10-edit-delete-books从 main65b50d5创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。只读诊断发现(本单必须同时修掉的缺陷)
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文案保留用于历史行,但不再由新流程产生。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 接口与幂等
{title};200 返回 book;字段白名单,未知字段 400{title?, text?};正文变化才建新任务,返回{chapter, job?, versionChanged}{deleted:{bookId, chapters}}{deleted:{chapterId}, bookId, remaining}重复删除 404;重复保存同一正文不新建任务(sha 相同即无版本变化);他人资源一律 404;未登录 401。
D8 前端:沿用原型文案与弹窗。
BookView增加「编辑书名」「删除书籍」与章节行「编辑」;编辑用对话框,删除用确认弹窗(「本章正文将被删除,已保存词条保留。」「删除这本书及其章节?已保存的生词和短语将保留。」);正文编辑框提示「保存后会重新处理」;删除章节后停留书籍页显示「章节已删除 · 剩余 N 章」;章节清空显示空态;阅读已删除章节沿用「内容不存在」。D9 schema:无变化(复用
content_sha256与现有error_reason),无迁移,回退只需换二进制。测试计划
superseded且不改变章节状态 → 新任务完成 → 章节为新版本 ready 且原文等于新文本;恢复扫描同样作废过期版本;旧版本任务重试 409;改回原内容不新建任务。非目标:封面/音频附件(#21)、回收站与撤销、批量操作、章节跨书移动、语言变更。
回退说明:本会话 pi 无可用 Gitea MCP 工具,沿用本单既定回退,使用目标
git.ilapage.cnAPI;凭据仅从既有安全配置读入进程。#10 实施完成,待用户验收(2026-09-11)
用户确认的契约 D1~D9 见评论 7837。分支
feat/10-edit-delete-books从 main65b50d5创建,实现提交35ed692,已推送。PR #29 未合并,工单不关闭,停在待用户验收。只读诊断发现的缺陷与修复
诊断时确认:
content_sha256已是版本键,unprocessableReason()也会比较,但它把章节本身标成content_changed失败;恢复扫描按章节批量写状态;RetryIngestJob不检查版本。因此「处理中改正文 → 旧任务完成 → 新版本章节被标失败」这一「旧处理结果覆盖新版本」的问题真实存在。本单已修复:job.content_sha256 == chapter.content_sha256时能影响章节;不匹配时只把任务标为failed/superseded(复用现有error_reason,不新增状态、不改 schema),完全不触碰章节;unprocessableReason保留「存储文本重算 SHA 必须等于存储 SHA」的一致性检查,继续兜住绕过 API 的直接写入(content_changed)。实现与差异
server/app/lexgo/edit.go:改名、编辑、删除与编辑用原文接口。只有正文变化才重新处理(不新建任务、不改变状态);任务request_key由「章节 + 版本」确定性派生。UPDATE ... ORDER BY ordinal ASC重排序号,并在事务开始锁 book 行序列化并发删除。个人词条、复习排期与作答记录按 owner 归属,不随删除清理。BookView增加书名与章节编辑对话框、章节行「编辑」入口、两处删除确认弹窗(文案照原型);LibraryView在书库被删后显示「书籍已删除 · 已保存的生词和短语仍保留在生词本。」;store 增加五个编辑/删除方法。实际验证
go vet ./...LEXGO_TEST_DB_NAME=lexgo_test_issue9 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覆盖:处理中编辑后旧任务只标 superseded 且不触碰新版本、旧版本重试 409、重复保存与改回原内容不新建任务、内容与版本不一致时按 content_changed 失败、恢复扫描作废过期版本、删除章节序号连续与导航正确、删除书籍级联、处理中删除不复活、并发删除一个成功一个 404、重复删除 404、跨用户改名/编辑/删除/读原文 404、未登录 401、个人词条与复习排期在删除后保留。
本单不改数据库结构,无迁移;本机 lexgo-api 已用新二进制重启(schema 仍为 v6),
/healthz200,回退用二进制保存在.local/lexgo-pre-issue10.exe。未验证与边界
文档
ce95236e4c8de27c1063448571cfc528c895f93796eec89f74db744d8807baa24b1d1d611ff6bf8f251fb5de71b8ba75da3cba6eee41454d5bbd22df82d714b949d38f20586fbe42797b2dec6b0f19f1ea264da6747bd141ec2d67011830320452563415b85c7f4650aae95b1429c0b4f7d1412c440731ce回退
本单无 schema 变化:停止 lexgo-api,恢复
.local/lexgo-pre-issue10.exe,重新启动即可;已改名、编辑或删除的数据按数据库现状保留。Gitea MCP 仍指向其他站点,沿用目标站点 API 回退;凭据仅从既有安全配置读入进程。
附件:issue10-book-after-edit.png、issue10-chapter-deleted.png、issue10-book-deleted.png(真实浏览器闭环截图;本会话模型不能读取图片,功能断言来自程序化检查)。
补充提交
docs: 记录 #10 已确认的编辑与删除口径:按既有惯例把用户确认的口径写入仓库根AGENTS.md的项目决策清单——只有正文变化才重新处理、版本门控与 superseded 语义、内容一致性检查、事务硬删除与级联、个人学习记录不随删除清理,以及范围边界。仅文档改动,功能提交35ed692不变,治理严格检查通过。#10 代码审核:达标(2026-09-14,Claude Code)
审核对象:提交
35ed692、1d19b2b(PR #29)。只读审阅代码与测试差异,没有重跑测试。契约 D1~D9(评论 7837)逐条核对如下。逐条核对
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(超时恢复扫描不误伤新版本)两个用例直接复现了诊断阶段发现的问题并验证了修复。达标,且诊断和修复过程本身有对应的回归测试,质量高于一般实现。DeleteBook/DeleteChapter都在protect提供的单个数据库事务内执行(router.go第 86 行db.Transaction),硬删除靠外键级联(lexgo_chapters.book_id ON DELETE CASCADE、lexgo_ingest_jobs同理)。TestMySQLDeleteDuringProcessing验证了处理中删除后,在途任务的FinishIngestJob找不到章节直接返回nil(不报错、不复活内容)。达标。DeleteChapter先锁章节再锁书籍行,删除后按ORDER BY ordinal ASC依次递减,保证唯一键(book_id, ordinal)全程满足;TestMySQLConcurrentChapterDelete用两个 goroutine 并发删除同一章节,验证一个 200 一个 404,且剩余章节序号正确归一。达标。DeleteBook/DeleteChapter均不涉及lexgo_terms/lexgo_term_reviews/lexgo_review_answers任何操作;TestMySQLDeleteBookKeepsPersonalRecords删除整本书后验证个人词条、复习排期行数不变,且复习队列里仍能查到这个词。达标。GET /chapters/:id/source不看状态直接返回chapter.OriginalText,与 #5 已验收的「就绪才返回阅读原文」互不冲突(阅读接口另有实现);TestMySQLChapterSourceAnyStatus覆盖了 pending/ready 两态、跨账号 404、查询参数拒绝。达标。BookView.vue的编辑对话框、两处删除确认弹窗文案与原型一致;edit.spec.ts覆盖了对话框流程、取消、保存失败保留输入、删除后的书库/书籍页更新。达标。content_sha256与error_reason枚举值。达标。复核中确认的细节
UpdateChapter里标题更新发生在正文校验之前,validatePaste(chapter.Title, text)用的是(可能刚更新的)新标题做校验,这是既有校验函数的复用方式,不产生问题。结论
达标,可以进入用户验收。 这单在方案确认阶段做了只读诊断,主动找出了「旧任务完成覆盖新版本」的真实竞态缺陷并在实现里从认领、完成、恢复扫描、重试四个入口一并堵上,测试直接复现了缺陷场景后再验证修复,是这几单里诊断质量最高的一次。删除相关的加锁顺序、序号重排和级联范围都经过并发测试验证,个人学习记录的隔离边界也有专门用例锁定。
本次审核只读代码,没有重跑
test-integration(lexgo_test_issue9)与 learner 的 vitest/E2E/Playwright;pi 报告的 59 项集成用例、94 项前端单测等结果未被本次复核重复验证,如需更高把握建议在验收前独立重跑一次。Gitea MCP 仍指向其他站点,本次沿用已记录的目标站点 API 回退;凭据只从
~/.claude/gitea.env安全配置读入进程。用户验收与合并收尾
验收时间: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:
08d992aee15b7fd5cea0cdbb4eb0b90583068449a29e5eb9c8ff474ffdcb1278fc6b51e3b9167eb8559f97b2f00b7fc274bda52bdaad62d10f7926c840f720f23d608f2dd875ba0ba838297c07b7a8fa96eec89f74db744d8807baa24b1d1d611ff6bf8f/251fb5de71b8ba75da3cba6eee41454d5bbd22df本单没有数据库结构变化,schema 保持 v6;本机 lexgo-api 运行新二进制,
/healthz200。回退用的上一版本二进制保存在忽略的.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 回退,凭据仅在进程环境中使用。