Files
crearte-monorepo/docs/plans/2026-10-01-p8b2-account-catalog.md
T

82 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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。