From f1d8d9858b2b5844f6ec47bbd1c5e6a35db8d85b Mon Sep 17 00:00:00 2001 From: XingfenD Date: Thu, 1 Oct 2026 04:06:56 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20P8=20batch-2=20spec+plan=20=E2=80=94=20?= =?UTF-8?q?account=20deletion=20(tombstone=20migration=200011,=20DELETE=20?= =?UTF-8?q?/api/auth/account,=20user=20delete=20CLI)=20+=20catalog=20expor?= =?UTF-8?q?t=20CLI=20(static=20fallback=20feed)=20+=20P7=20error-copy=20ta?= =?UTF-8?q?il;=20P3=20relanding=20recorded=20as=20history-replay=20(owner'?= =?UTF-8?q?s=203rd=20roadmap=20directive:=20implement=20everything=20pendi?= =?UTF-8?q?ng)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/2026-10-01-p8b2-account-catalog.md | 81 ++++++++++++++ ...2026-10-01-p8b2-deletion-catalog-design.md | 102 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 docs/plans/2026-10-01-p8b2-account-catalog.md create mode 100644 docs/specs/2026-10-01-p8b2-deletion-catalog-design.md diff --git a/docs/plans/2026-10-01-p8b2-account-catalog.md b/docs/plans/2026-10-01-p8b2-account-catalog.md new file mode 100644 index 0000000..cb7d825 --- /dev/null +++ b/docs/plans/2026-10-01-p8b2-account-catalog.md @@ -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 ""`。冷跑用 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 ` 打印 `%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/__.json` = GameDetail(与 `/api/games/:user/:slug` 逐字段同形状——测试用同一 handler 输出对拍)。 + +**Steps:** +- [ ] S1 写失败测试:`account_test.go`(memory):DeleteSelf 成功→墓碑字段断言(email=`deleted-@deleted.invalid`、username=`gone-`、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。 diff --git a/docs/specs/2026-10-01-p8b2-deletion-catalog-design.md b/docs/specs/2026-10-01-p8b2-deletion-catalog-design.md new file mode 100644 index 0000000..fc564eb --- /dev/null +++ b/docs/specs/2026-10-01-p8b2-deletion-catalog-design.md @@ -0,0 +1,102 @@ +# P8 长尾打包(第二批:③账号注销 + ④静态兜底目录导出)· 设计 spec + +日期:2026-10-01 | 决策:owner 指令「实现 roadmap 所有待办事项」(03:27),③④ 由「待产品决策/按需」转为**批准开工**;产品细节按本文默认拍板并记录,owner 可事后推翻。 +范围同时捎带 P7 遗留小尾(用户管理面板 400/404 中文文案分支,§5)。 + +## 0. 一句话 + +③ 给账号一个不可逆的「注销」出口:自助 API + CLI,墓碑匿名化 + 内容级联下架;④ 把线上目录导出成静态 JSON(与 `public/data` 同 schema),补齐 no-backend 部署的静态兜底数据通道;尾部:admin 用户面板错误文案中文化。 + +## 1. 现状勘查(grounded,2026-10-01) + +- FK 面:`submissions.submitter_id`/`uploads.owner_id`/`works.owner_id` 均**无 ON DELETE**(物理删用户必炸);`favorites`/`ratings` 已 CASCADE;`audit_log.actor_id` 0010 注释明写「不加 FK:用户注销后审计仍可读」——作者已为注销预留语义。 +- `works` 有 `published_at/delisted_at`;`ListVisibleWithCurrentVersion` 过滤 `published_at IS NOT NULL AND delisted_at IS NULL`——置 `delisted_at` 即全站隐身(目录 404、详情 404、游玩 404)。 +- 会话失效机件现成:`token_version` 每次 +1,`Authenticate` 比对 `claims.TokenVersion`。 +- submissions repo 已有 `SetStatus(id, from, to)`(pending→rejected 可走)、`Delete`(仅 draft/pending)、`ListBySubmitter`;uploads repo 已有 `ListBySubmission`;`DeleteSubmission` 的「先删行、后 best-effort 删对象」模式可仿。 +- 静态兜底通道现状:前端 `StaticContentRepository` 读 `/data/index.json` + `/data/games/__.json`(schemaVersion 2,`assertGamesIndex/assertGameDetail` 校验,virtual 详情必须带 `bundle` 对象);`MergeContentRepository` 已做「API 挂→降级纯静态」。**缺的只是把线上目录灌成这套静态文件**——后端 CLI 导出是最短路径(`handler.Games.summary/detail` 映射已存在)。 +- FE:`AccountView` 已有改密/我的反应/会话三段,注销放第四段「危险区」;`auth/client.ts` 有 send 基建;`AdminUsersView` 的 `actionError` 对非 `last_admin` 错误目前直出后端英文原文。 + +## 2. 决策记录 + +| # | 岔路 | 决定 | 理由 / 放弃项 | +|---|------|------|--------------| +| D-A | 注销=物理删行 or 墓碑 | **墓碑匿名化**(users 加 `deleted_at`,PII 洗掉,行保留) | FK 无级联,物理删必炸或需大迁移;0010 注释的预留语义即「人会消失、记录要在」;放弃硬删(连坐删作品=删作者创作物,更糟)。 | +| D-B | 注销后名下作品 | **全部自动下架**(delisted_at 置位),行保留 | 「注销=不再对外提供我的作品」的合理默认;目录/详情/游玩同时隐身;不物理删 works(历史 id/命名空间保留)。 | +| D-C | 在途投稿 | draft 删除(连 upload 行 + best-effort 删对象);pending 自动 rejected(note「账号注销自动关闭」) | 复用现成 repo 方法;审核队列不留死人名条目。 | +| D-D | admin 注销 | **唯一 admin 拒绝(409 last_admin,复用护栏)**;非唯一 admin 允许(墓碑时 role→user) | 与 P7 降级护栏同语义;杜绝把站管没了。 | +| D-E | 入口 | 自助 `DELETE /api/auth/account`(body 带 `password` 二次确认)+ CLI `user delete `(免密码,运维护栏) | API 路径要证明持有凭证;CLI 走既有 `config.LoadDatabase` 模式。 | +| D-F | 反应数据 | favorites/ratings **保留**(匿名聚合继续计入) | CASCADE 语义本就允许;删了会让作品评分凭空漂移。 | +| D-G | email/username 释放 | 墓碑改写为 `deleted-@deleted.invalid` / `gone-`,**真实邮箱/用户名即刻可重用**(注册新号) | GDPR 式「删后可再注册」;新值仍满足 username CHECK 正则;冲突概率可忽略。 | +| D-H | ④ 交付形态 | **server CLI `catalog export --out DIR`**:写 `index.json`(schemaVersion 2 + generatedAt)+ `games/__.json` 详情,与 `/api/games*` 响应同形状 | 复用 handler 映射零漂移;FE 零改动即被静态兜底链吃到;放弃「前端构建期拉 API」(构建期与部署态耦合,且私有 API 凭据问题)。 | +| D-I | ④ 边界 | 覆盖**已上架**作品全集;`STORAGE_S3_*` 未配时 cover/bundle.url 为空串/省略(文件仍合法,可浏览不可玩);docs.json 不生成(站内文档本就是构建产物) | 与目录 API 可见集一致;导出用途=只读兜底,可玩性依赖存储配置属运维前提,README 注明。 | + +## 3. 设计 + +### 3.1 迁移 0011(server) + +`internal/repository/migrations/0011_user_deletion.sql`: + +```sql +-- 账号注销(P8 ③):users 墓碑列。行永不物理删(FK 面无 CASCADE,审计预留语义见 0010)。 +ALTER TABLE users ADD COLUMN IF NOT EXISTS deleted_at timestamptz; +``` + +`model.User` 增 `DeletedAt *time.Time json:"-"`;`userColumns` 追加 `deleted_at`;`MemoryUserStore` 同步支持。 + +### 3.2 服务层(server,`internal/service/account.go` 新文件) + +`AccountService{users UserStore, content ContentStore, objects storage.ObjectStorage}`: + +- `DeleteAccountSelf(ctx, userID, password string) (AccountService 同包错误)`:`GetByIDForUpdate` → 不存在 ErrUnauthorized;`VerifyPassword` 失败 → `ErrInvalidCredentials`;`deleted_at != nil` → `ErrAccountDeleted`(幂等拒二次注销)。 +- `DeleteAccountByEmail(ctx, email string)`(CLI 路径,免密码;不存在 → `ErrUserNotFound`)。 +- 共享内核 `deleteAccountLocked`: + 1. D-D 护栏:`role==admin && CountAdmins<=1` → `ErrLastAdmin`。 + 2. 内容级联(`content.WithTx`):`DelistPublishedByOwnerTx`(新 WorkRepo 方法,返回计数);`ListBySubmitter` 遍历——draft:`ListBySubmission` 记 upload keys → `Uploads().Delete` → `Submissions().Delete`;pending:`SetStatus(id, "pending", "rejected")` + `MarkReviewed` 语义不可用(reviewer 是自己),改走 `UpdateDraft`? 否——直接用 `MarkReviewed(id, rejected, <自己id>, "账号注销自动关闭")`:签名允许任意 reviewer id,行将随墓碑保留,审核历史自洽。 + 3. 用户墓碑(users 事务内最后一步):`MarkDeleted(ctx, id, email, username, passwordHash)`——email/username 按 D-G 改写、`display_name='已注销用户'`、password_hash=新随机 64hex(物理上永不可登录)、`role='user'`、`deleted_at=now()`、`token_version+1`。 + 4. tx 提交后 best-effort 删对象(ErrObjectNotFound 容忍,log 掉失败)。 +- 认证护栏:`Authenticate` 与 `Login` 各加 `DeletedAt != nil` → `ErrUnauthorized`/`ErrInvalidCredentials`(token_version 已 bump,此为纵深防御);`ListAdminUsers` 过滤 `deleted_at IS NULL`(内存实现同步);`Register` 无需改(唯一约束天然处理)。 +- 错误新增:`ErrAccountDeleted`(api 映射 410 `account_deleted`)。 + +### 3.3 API(server) + +`RouteDeleteAccount = "/api/auth/account"`,注册 `engine.DELETE(RouteDeleteAccount, RequireUser(deps.AuthService), deps.Account.DeleteAccount)`(storageEnabled 与否都要注册——注销不依赖对象存储;`objects==nil` 时跳过对象删除)。新增 `Deps.Account *handler.Account`(`NewAccount(accountSvc)`,与 NewAuth 平行,不动既有构造函数签名免碎全部测试现场)。 + +- dto:`DeleteAccountRequest{Password string json:"password"}`。 +- handler `Account.DeleteAccount`:body 缺 password → 400 `invalid_request` "password is required";映射:`ErrInvalidCredentials`→401 `invalid_credentials`;`ErrLastAdmin`→409 `last_admin`;`ErrAccountDeleted`→410 `account_deleted`;成功 204 + `Cache-Control: no-store`。 +- 旧 token 之后打 `/api/auth/me` → 401(version 失配)。 + +### 3.4 CLI(server) + +- `user delete `:跑 `deleteAccountLocked`(免密码);成功打印 `email: deleted (works delisted=N, drafts removed=M, pending closed=K)`;错误经 cobra RunE 冒泡非零退出。 +- `catalog export --out DIR [--pretty]`:`config.LoadDatabase` → pool → Migrate(幂等,保 schema 新鲜)→ `ContentService.ListPublished` + 逐条 `GetPublishedDetail`;文件写出:`index.json` = `{"schemaVersion":2,"generatedAt":"","games":[...]}`(与 `/api/games` 响应同形状,含 rating 聚合三字段);`games/__.json` = `/api/games/:user/:slug` 详情同形状。计数打印 `exported N game(s) to DIR`。空目录先建;不删除 DIR 内既有无关文件(文档约定 DIR 专用)。 + - 实现取径:在 `handler.Games` 上导出 `RenderIndex(ctx)` / `RenderDetail(ctx, id)`(薄封装现有 `summary/detail` 私有映射 + service 调用,不做 ETag),cmd 侧只序列化落盘——映射零复制。 + +### 3.5 前端(crearte) + +- `auth/client.ts`:`deleteAccount(password: string): Promise`(204 空体);`AuthErrorCode` 并集补 `'account_deleted'`(410→「该账号已注销」),`AUTH_ERROR_MESSAGES` 补条目。 +- `AccountView.vue` 第四段「危险区:注销账号」:密码输入 + 「我已知晓作品将下架且不可恢复」勾选(两者齐备按钮才 enabled)+ 按钮「注销账号」;点击后 `busy` 锁;成功 → `session.invalidate()` → 跳首页 `router.push('/')`;失败 → `users-action-error` 风格 `role=alert` 行(文案走 `toUserMessage`/`AUTH_ERROR_MESSAGES`,last_admin 直出后端原文可接受)。noauth 下本页不可达(路由守卫),无需开关。 +- `AdminUsersView.vue`:`actionError` 改为按 `AdminApiError.code` 中文化:`validation`→「角色参数非法」、`not_found`→「用户不存在」、`internal`→「服务器内部错误,请稍后重试」、`last_admin`→保留后端原文透传(现有降级双要点文案已覆盖)、未知 code→后端 message 原样。 +- e2e:`auth.spec`(按现有 mock/桩模式)加注销流:勾选+密码齐才可点、错误密码出文案、成功清会话回首页;`account-view` 相关单测(vitest)覆盖按钮禁用态与 204 后行为。 + +### 3.6 deploy / P3 重落(另派,见 §4) + +## 4. P3 重做注记(本 spec 附带记录,不另立文档) + +- P3 于 2026-09-30 18:19 曾合入(server `7b4e5c8` / deploy `e07c9f8`),18:34 **owner 下令回滚**(`d6c384e`/`6f2e8b6`,「tree identical to P2 state」),spec/plan 文档保留、未删。本次 owner 指令「实现 roadmap 所有待办事项」= 重新落地。 +- 执行取径改为**历史重放 + 适配**而非重写 TDD:server 从 master 切 `feat/p3-observability`,`cherry-pick 6ffd1ee 93a3c70`(冲突机械解:P7/P8 后 games.go/admin.go/serve.go/router.go 已动;cherry-pick 后补迁新增 `log.Printf`——admin_console.go、service/audit.go 等,grep 清零);deploy `cherry-pick 7fcfe0e`(CHANGELOG 顶版对齐 0.6.0,`.gitignore` 与 0.5.2 的 `backups/` 行去重)。 +- 验收仍按原 spec §5 四条 + plan Task 4 全腿(含 prod 实栈 `/metrics` 冒烟、backup+restore-drill 真跑、full-loop e2e 不回归)。 + +## 5. 验收标准(2026-10-01 定稿) + +1. server:容器四连(gofmt/vet/build/test)绿;空库 `-count=1 -p 1` 集成全绿零 skip(含新 `account_postgres_test.go`);`-race` 不适用本批(无新并发面),P3 分支另行处理。 +2. server 冒烟(分支真产物 + 真 PG,复用 p8 流程):注册→投稿链简版→`DELETE /api/auth/account` 错密码 401 → 对密码 204 → 旧 token 打 me 401 → 墓碑断言(DB 行 email/username 已改写)→ 原邮箱可重新注册 → 唯一 admin 注销 409;`catalog export` 跑通且产物可被 FE `StaticContentRepository` 契约吃(node 断言 schemaVersion2、virtual 详情含 bundle)。 +3. FE:`npx vitest run` / `vue-tsc --noEmit` / `admin-flow` / 主套件 / noauth 四腿绿(基线 499/7/70+1skip/4 只增不减)。 +4. 双路独立审查(对照本文 §1–§5 全量,不只任务书)。 +5. deploy(P3):`backup.sh --dry-run` 无栈可跑;compose config 四 profile 绿;真 prod 栈 backup 三件套 + latest 软链 + `BACKUP_KEEP_DAYS=1` 修剪;`restore-drill.sh` PASS;server `/metrics`+JSON 日志实栈断言;full-loop 1 passed。 + +## 6. 非目标 + +- 注销冷静期/软删恢复流(v1 不做,墓碑已可人工 SQL 复活,文档不提 UI)。 +- 注销账号名下作品物理删除 / 对象全量清扫(cleanup CLI 的 orphan 扫描覆盖 pending/bundles/covers 前缀,够用)。 +- GDPR 数据导出接口(另立项)。 +- catalog export 的定时化/服务化(runbook 一句 crontab 示例即可,不进 compose)。