docs: P8 batch-2 spec+plan — account deletion (tombstone migration 0011, DELETE /api/auth/account, user delete CLI) + catalog export CLI (static fallback feed) + P7 error-copy tail; P3 relanding recorded as history-replay (owner's 3rd roadmap directive: implement everything pending)

This commit is contained in:
2026-10-01 04:06:56 +08:00
parent 5d20189d46
commit f1d8d9858b
2 changed files with 183 additions and 0 deletions
@@ -0,0 +1,81 @@
# P8 第二批(③账号注销 + ④目录导出 + P7 文案尾)Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** crearte-server `feat/p8b2-account-catalog`(0.15.0:迁移 0011、AccountService 注销内核、`DELETE /api/auth/account`、CLI `user delete`、CLI `catalog export`)与 crearte `feat/p8b2-account-ui`(0.21.0:AccountView 危险区注销、auth client `deleteAccount`+错误码、AdminUsersView 错误中文化)。spec:`docs/specs/2026-10-01-p8b2-deletion-catalog-design.md`(§1–§5 全量为审查依据,不只任务书)。
**Architecture:** 注销=单事务墓碑内核(users 改写 + works 下架 + submissions 收口),对象删除 tx 外 best-effort;导出=复用 handler.Games 映射的薄 Render 方法,cmd 落盘;前端只加第四段与错误映射。P3 重落是**另一批**(§历史重放,见 spec §4),本计划不含。
**Tech Stack:** Go 1.24(gin/pgx/stdlib;**禁新增依赖**)、Vue3+vitest+playwright、Postgres 17(gated 集成)。
## Global Constraints
- 工作目录:`crearte-monorepo/crearte-server/src`、`crearte-monorepo/crearte/src`。两内层仓从 `master`(server `921443a` / crearte `70ba10c`)切上述分支;**master 禁直接提交**;合回 `--no-ff`。
- Go 命令一律容器化(AGENTS.md):`docker run --rm -v /root/.openclaw/workspace/coder/crearte-monorepo/crearte-server:/work -v crearte_gomod:/go/pkg/mod -v crearte_gocache:/gocache -e GOCACHE=/gocache -e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn -w /work/src golang:1.24-alpine sh -c "<cmd>"`。冷跑用 background+poll,勿掐。
- 集成测试 gated:`TEST_DATABASE_URL` 缺失即 `t.Skip`(既有惯例);gated 测试 TRUNCATE 共享表 → 全量集成必须 `-p 1 -count=1`。
- 迁移只增不改:`0011_user_deletion.sql`;`userColumns` 追加 `deleted_at`,pg+memory 双实现同步(memory parity 注释照旧风格)。
- CHANGELOG:server `docs/CHANGELOG.md` 顶版 0.14.0 之上追加 `## [0.15.0] - 2026-10-01`;crearte 0.20.0 之上追加 `## [0.21.0] - 2026-10-01`;英文行+中文行连续,条目间空行;沿用两仓既有文风(去模板腔)。
- 完成定义:Task S/F 各自容器四连+新测试绿;控制者验收(Task V)六腿 + 冒烟全中;双审 PASS 后合主。
---
### Task S: server — 注销链 + catalog export
**Files:**
- Create: `src/internal/repository/migrations/0011_user_deletion.sql`
- Modify: `src/internal/model/user.go`(+`DeletedAt *time.Time`)、`src/internal/repository/user.go`(userColumns/scanUser/`MarkDeleted`/`ListBySubmitter` 无关——users repo 加 `MarkDeleted(ctx,id,Email,Username,PasswordHash)`;`List`/`CountAdmins` 过滤 deleted;memory 实现同步)
- Modify: `src/internal/repository/work.go` + `memory_content.go`(+`DelistByOwnerTx`/计数;接口名 `DelistPublishedByOwner(ctx, ownerID string) (int, error)`)
- Modify: `src/internal/repository/submission.go`(+`ListAllBySubmitter`? 否——`ListBySubmitter` 已够;`MarkReviewed` 已够;无新法——仅确认签名可用)
- Create: `src/internal/service/account.go` + `account_test.go`
- Create: `src/internal/handler/account.go` + `account_test.go`
- Create: `src/internal/api/account_postgres_test.go`(gated 集成:全链断言)
- Modify: `src/internal/api/router.go`(`RouteDeleteAccount`+`Deps.Account`)、`src/cmd/serve.go`(接线 AccountService/handler)、`src/cmd/user.go`(`user delete`)、Create `src/cmd/catalog.go`(`catalog export`)、`src/cmd/main.go` 或 root 注册
- Modify: `src/internal/handler/games.go`(导出 `RenderIndex(ctx) (any, error)` / `RenderDetail(ctx, id string) (any, error)` 薄封装)
- Modify: `docs/CHANGELOG.md`(0.15.0)
**Interfaces(冻结,FE/冒烟依赖):**
- `DELETE /api/auth/account` body `{"password": string}` → 204 | 400 invalid_request | 401 invalid_credentials | 409 last_admin | 410 account_deleted;`Cache-Control: no-store`。
- 错误常量:`service.ErrAccountDeleted`;`Authenticate`/`Login` 对墓碑分别 `ErrUnauthorized`/`ErrInvalidCredentials`。
- `AccountService{users repository.UserStore; content repository.ContentStore; objects storage.ObjectStorage; now func() time.Time}`,方法 `DeleteSelf(ctx, userID, password) (deletionReport, error)`、`DeleteByEmail(ctx, email) (deletionReport, error)`、`deletionReport{Delisted, DraftsRemoved, PendingClosed int, UploadKeys []string}`。
- CLI:`user delete <email>` 打印 `%s: deleted (works delisted=%d, drafts removed=%d, pending closed=%d)`;`catalog export --out DIR [--pretty]` 打印 `exported %d game(s) to %s`。
- 导出产物:`DIR/index.json` = `{"schemaVersion":2,"generatedAt":RFC3339,"games":[GameSummary...]}`;`DIR/games/<user>__<slug>.json` = GameDetail(与 `/api/games/:user/:slug` 逐字段同形状——测试用同一 handler 输出对拍)。
**Steps:**
- [ ] S1 写失败测试:`account_test.go`(memory):DeleteSelf 成功→墓碑字段断言(email=`deleted-<hexid>@deleted.invalid`、username=`gone-<id前8>`、display_name=已注销用户、role=user、token_version+1、deleted_at!=nil);错密码 ErrInvalidCredentials;二次注销 ErrAccountDeleted;名下 published work 隐身(`ListPublished` 不再含);draft 删、pending→rejected;唯一 admin ErrLastAdmin;非唯一 admin 可销。favorites/ratings 行保留(memory 聚合仍计入)。
- [ ] S2 实现 migration 0011 + model/repo/memory 扩展(MarkDeleted 一条 UPDATE 完成全部改写;List/CountAdmins 过滤 `deleted_at IS NULL`)。
- [ ] S3 实现 service/account.go 内核(顺序照 spec §3.2:护栏→内容级联 tx→墓碑 tx 内最后→tx 外删对象 best-effort);S1 测试转绿。
- [ ] S4 Authenticate/Login 墓碑纵深(各加 DeletedAt 判断)+ handler/account.go + router Deps.Account + serve.go 接线 + `account` 组 api handler_test(httptest:204/401/400/409/410)。
- [ ] S5 `user delete` CLI + gated 集成 `account_postgres_test.go`:真库全链(注册→import 作品→投稿 draft+pending→注销→目录隐身/详情 404/me 401/原邮箱重注册成功/audit actor 保留可读)。
- [ ] S6 handler.Games Render 方法 + `catalog export` cmd + 单测(memory store 造 2 作品——1 virtual 1 external——导出后读回断言 schemaVersion/文件名/详情 bundle 对象/external 无 bundle;`--out` 建目录幂等)。
- [ ] S7 容器四连 `gofmt -l .; go vet ./...; go build ./...; go test ./...` 全绿;集成腿带 `TEST_DATABASE_URL`(p8 同款一次性 postgres:17-alpine 容器,密码放 `/tmp/p8b2dbpw`)`-p 1 -count=1` 零 FAIL 零 skip(skip 守卫 grep 必须 0)。
- [ ] S8 CHANGELOG 0.15.0(Added/Changed 双语)+ commit `feat(account): self-service account deletion (migration 0011, DELETE /api/auth/account, user delete CLI)` 与 `feat(catalog): catalog export CLI producing static-fallback JSON contract` 两提交(分支 feat/p8b2-account-catalog)。
### Task F: crearte — 注销 UI + 错误中文化
**Files:**
- Modify: `src/app/auth/client.ts`(`deleteAccount(password)` DELETE 204)、`src/app/auth/types.ts`(+`'account_deleted'` 码)、`src/app/auth/errors.ts`(`AUTH_ERROR_MESSAGES.account_deleted='该账号已注销'`)
- Modify: `src/app/views/AccountView.vue`(第四段危险区)+ `AccountView.test.ts`(新用例:按钮禁用双闸/成功 invalidate+跳首页/401 出文案)
- Modify: `src/app/views/AdminUsersView.vue`(`actionError` 按 code 映射表 validation/not_found/internal 中文化,last_admin/未知透传)+ `AdminUsersView.test.ts`(+3 断言)
- Modify: `src/e2e/auth.spec.ts`(注销流,按该文件既有 mock 风格)
- Modify: `docs/CHANGELOG.md`(0.21.0)
**Steps:**
- [ ] F1 client/errors 单测先行(`client.test.ts`:204 空体 resolve、401 body code 解析、410 映射 account_deleted)→ 实现转绿。
- [ ] F2 AccountView 危险区:密码 input + 勾选 checkbox(`data-testid="delete-confirm-check"`、`delete-confirm-password`、`delete-account-button`、`delete-account-error`);双闸才 enabled;成功链 `authClient.deleteAccount → session.invalidate() → router.push('/')`;失败 `toUserMessage(e)`。测试四例照 Interfaces 断言。
- [ ] F3 AdminUsersView 文案映射(小纯函数 `adminActionMessage(e)` 便于单测直打)+ 用例。
- [ ] F4 `npx vitest run` + `npx vue-tsc --noEmit` 全绿(基线 499 只增不减);`npx playwright test`(主套件 70+1skip 基线)与 `--config playwright.noauth.config.ts`(4 passed)不回归;commit 两提交(auth client+errors / UI+admin 文案)分支 feat/p8b2-account-ui。
### Task V: 控制者验收(两 Task 均 report DONE 后)
- [ ] V1 server 终态容器四连 + 空库 `-p 1 -count=1` 集成复跑(skip 守卫 0)。
- [ ] V2 curl 冒烟(分支真产物静态编译 + 一次性 p8b2 postgres 容器,p8 同款流程):register→login→作品 import CLI(复用测试夹具 1 virtual)→投稿 draft+pending→**错密码 401→对密码 204→旧 token me 401→原邮箱重注册 201→唯一 admin 409**;`catalog export --out /tmp/p8b2cat` 产物三断言(index schemaVersion2、fixture__x.json 存在含 bundle、jq 形状对)。跑完即焚容器/密码。
- [ ] V3 FE 四腿复跑:vitest/typecheck/admin-flow+主套件/noauth。
- [ ] V4 双路独立审查(对照 spec §1–§5 + Interfaces 冻结,两路各自 fresh 会话,一路 server 一路 FE+跨端契约)。
- [ ] V5 审查 note 分诊(可修则修后复跑受影响腿;产品级偏离回报 owner)→ 合主 `--no-ff` 双仓 + 删分支 + push。
- [ ] V6 wrapper:ROADMAP P8 行改「第二批完成·③④交付(P3 另批进行中)」+ CHANGELOG 0.2.10 双语 + 索引登记本 spec/plan(已执行)→ commit push。
## Self-Review 记录
- spec D-A…D-I → S1/S3(墓碑+级联+admin 护栏)、S4(API 映射)、S6/D-H(export 契约)、F2/F3(UI)、V2(边界矩阵)。D-G 的 username 正则满足性在 S1 断言(`gone-` + 8hex ≤39 且首尾合法——hex 小写含数字,`-` 不出头)。
- 不确定点回填:`MarkReviewed` reviewer_id=自己(墓碑前)合法——FK 在行保留方案下恒成立;`catalog export` 走 `handler.Games` 需 `ContentService` 已在 serve.go 构造——cmd 里自建同栈。
- 占位符扫描:无 TBD。