Commit 700c1c461133b4d9952f082f1c41f4ec5d4dc8b1

Authored by yibo
1 parent fb6638be

Plan third-phase tasks: email registration + account binding (T18-T22)

Breaks down dressup-dance-room.md 9节 phase 3 (dressup-dance-room.md
9节第3条: email login, bind anonymous viewer_uid to an account, no
credits/payment yet) into T18-T22 with acceptance criteria, work
estimates, and a dependency graph, same format as dev-plan.md/
dev-plan-2.md.

Key decisions: users/sessions land in SQLite per architecture.md 1.4's
own trigger condition (credential storage), using Node 24's built-in
node:sqlite + node:crypto scryptSync — zero new npm dependency, no
package.json needed. Flags one real gap the requirements doc's 待验证
list missed: no email-delivery vendor has been chosen, so T20 ships a
pluggable sendVerificationEmail() with a dev-mode bypass rather than
blocking on that decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
Showing 1 changed file with 127 additions and 0 deletions
docs/todo/dev-plan-3.md 0 → 100644
  1 +# 换装跳舞直播间 — 第三期开发任务规划
  2 +
  3 +需求见 `docs/todo/dressup-dance-room.md`(下称"需求文档")第 7.1 节、
  4 +第 9 节第 3 条;设计见 `docs/todo/dressup-dance-room-mvcp.md`(下称
  5 +"MVCP")M.1/M.10;架构见 `docs/structure/architecture.md`(下称"架构
  6 +文档")1.4/2.7 节。第一期(`dev-plan.md`,T1-T10)、第二期
  7 +(`dev-plan-2.md`,T11-T17)均已完成并提交。任务编号接着往下编。
  8 +
  9 +工时是**粗估**,单位人天,用来排先后不用来考核。
  10 +
  11 +---
  12 +
  13 +## 0. 第三期范围(需求文档 9 节第 3 条)
  14 +
  15 +**做**:用户注册体系上线(邮箱 + 密码登录),把现有匿名
  16 +`viewer_uid`/`viewer_nickname` cookie 身份**绑定**到账号——账号是第四期
  17 +付费功能的前提,必须先落地,但**第三期本身不做任何付费/积分/上传功能**。
  18 +
  19 +**明确不做**(留到后续分期):
  20 +
  21 +- 积分体系(余额/流水/扣费/退还,`M.2 credit_ledger`)——第四期。
  22 +- Stripe 充值——第四期。
  23 +- 模式二/三(付费传衣服/传人)——第四期依赖账号,但功能本身也是第四期。
  24 +- Google/Apple/GitHub 三方登录——`M.1 oauthProviders` 字段预留,第三期
  25 + 留空数组,不接入任何 OAuth provider。
  26 +- 密码找回邮件的完整产品化 UI(`M.10 password_reset` 令牌类型的表结构
  27 + 留出来,但"忘记密码"这个前端入口不是本期出口条件,有余力可顺手做)。
  28 +- 把现有 `/api/admin/*` 的单管理员口令(T6,`ADMIN_SESSION_COOKIE`)
  29 + 迁移成 `users.role: admin`——**T6 现状继续保留不动**,避免为了"架构
  30 + 更统一"牵动已经跑通的后台鉴权;等第四期 `POST /api/admin/credit/adjust`
  31 + 这类真正需要按管理员身份审计的接口出现时,再一并把 admin 鉴权切到
  32 + `users`/`sessions` 表(MVCP M.1 `role` 字段本来就是为那时候准备的)。
  33 +- `admin-users.html`(用户/积分管理后台页面,MVCP V.表)——挂在积分
  34 + 管理上,第四期跟积分体系一起做,第三期没有积分可管。
  35 +
  36 +---
  37 +
  38 +## 1. 已确认的技术决策
  39 +
  40 +- **存储:`users`/`sessions` 两张表落 SQLite,不用 JSON 文件**——架构
  41 + 文档 1.4 节明确把"账号 + 密码/凭证存储"列为 JSON 明文文件不适合的
  42 + 触发条件之一(哪怕 hash 过也建议上 SQLite),现在正是这个触发点。
  43 + `characters`/`garments`/`dance-refs`/`library` 等既有 JSON 存储**不
  44 + 受影响**,不做全站数据库迁移。
  45 +- **零新增 npm 依赖**:Node 24(当前运行版本已确认)自带
  46 + `node:sqlite`(`DatabaseSync`,实验性但可直接用)和 `node:crypto`
  47 + 的 `scryptSync`——密码哈希不需要 `bcrypt`/`argon2` 这类第三方包,
  48 + SQLite 不需要 `better-sqlite3`。这个项目至今没有 `package.json`,
  49 + 保持这个状态。
  50 +- **两套 session 互不干扰**:现有 `admin_session` cookie(T6,纯内存
  51 + Map,重启即丢)继续只管后台;本期新增的用户登录态用独立的
  52 + `user_session` cookie + `sessions` 表(M.10),语义、有效期
  53 + (30 天)、校验逻辑都是新的一套,不会互相污染。
  54 +- **邮箱验证发信渠道未定**:需求文档第 8 节没有把这条列进"待验证
  55 + 事项",是本文档新发现的缺口——发验证邮件需要一个真实的发信渠道
  56 + (SMTP 或 Resend/SendGrid 这类走 HTTP API 的服务,后者不需要额外
  57 + 依赖,`fetch` 直接调),**这个供应商选型需要产品方拍板**,不阻塞
  58 + T18-T19 开工(下面 T20 会说明怎么绕开这个阻塞点先把其余部分做完)。
  59 +
  60 +---
  61 +
  62 +## 2. 依赖关系
  63 +
  64 +```mermaid
  65 +flowchart TD
  66 + T18[T18 SQLite users/sessions 建表] --> T19[T19 注册/登录/退出后端]
  67 + T19 --> T20[T20 邮箱验证]
  68 + T19 --> T21[T21 匿名身份绑定]
  69 + T19 --> T22[T22 登录/注册前端 UI]
  70 + T20 --> T22
  71 +
  72 + classDef ready fill:#d4edda,stroke:#28a745
  73 + class T18 ready
  74 +```
  75 +
  76 +T18 是唯一的地基任务,其余都要等它先建好表。T19 完成后,T20(邮箱
  77 +验证)、T21(绑定匿名身份)、T22(前端 UI)可以并行——T22 前期能先
  78 +拿假数据/mock 接口联调(同第二期 T16 的做法),不用死等 T20 邮件通道
  79 +真正打通。
  80 +
  81 +---
  82 +
  83 +## 3. 任务表
  84 +
  85 +| # | 任务 | 依赖 | 验收标准 | 工时 |
  86 +|---|---|---|---|---|
  87 +| T18 | **SQLite 建表 + 最小查询封装**:新建 `db.js`,用 `node:sqlite` 的 `DatabaseSync` 打开 `data.sqlite`(新文件,不复用任何现有 JSON),按 MVCP M.1/M.10 建 `users`/`sessions` 两张表(`sessions.kind` 先只用到 `session`/`email_verify`,`password_reset` 建表但本期不产生这类行);封装跟 `store.js` 类似的最小 CRUD 接口(`create`/`get`/`findByEmail` 等),**不引入 ORM** | 无 | 进程重启后表结构和数据都还在(文件持久化,不是内存 `:memory:`);`email` 唯一约束生效(重复注册同邮箱返回冲突而不是插入两行);`node --test` 能起一个指向临时文件的 `DatabaseSync` 实例做集成测试,不依赖真实文件系统路径写脏 | 1 |
  88 +| T19 | **注册 / 登录 / 退出**:`POST /api/auth/register {email, password}`(`scryptSync` 加盐哈希,`email` 格式做最基础校验)、`POST /api/auth/login`、`POST /api/auth/logout`;登录成功签发 `user_session` cookie(HttpOnly、`SameSite=Lax`、30 天,同 T6 `ADMIN_SESSION_TTL_MS` 的量级但独立常量);提供一个 `requireUserSession(req)` 中间件式辅助函数供后续接口复用 | T18 | 密码错误/邮箱不存在返回 401 且不泄露"是邮箱错还是密码错"这种细节;重复邮箱注册返回 409;拿到 cookie 后能访问一个临时的 `GET /api/auth/me` 调试接口拿回 `{id, email, emailVerified, nickname}`;退出后该 cookie 立即失效 | 1.5 |
  89 +| T20 | **邮箱验证**:注册成功后创建一条 `sessions(kind='email_verify')` 记录(24 小时过期),调用 `sendVerificationEmail(email, token)`——这个函数本期只实现**可插拔的接口**:生产模式下若没配置真实发信渠道的环境变量(供应商待第 1 节的开放问题定了再填),直接把邮件内容记日志,同时 `NODE_ENV !== 'production'` 时把验证链接原样放进 `POST /api/auth/register` 的响应体,方便本地/测试环境不等真邮件就能跑通全流程;`GET /api/auth/verify?token=` 校验后把 `users.emailVerified` 置 `true` | T19 | 未配置真实发信渠道时,注册接口的开发态响应里能拿到可用的验证链接,访问后 `emailVerified` 变 `true`;过期/已用过的 token 访问返回明确错误而不是静默成功;**真实邮件发送**这部分标注为"待供应商选型解锁",不算进本期验收范围 | 1 |
  90 +| T21 | **匿名身份绑定**:注册/登录成功时,若请求带着现有的 `viewer_uid`/`viewer_nickname` cookie(`live.html`/`dance.html` 现有机制,纯前端 JS 设置,未加密),把 `viewer_uid` 写进新建/命中的 `users.legacyUid`,`nickname` 字段初始值取自 `viewer_nickname`(用户后续可改);**不做历史投票记录的批量重写**——`promptQueue`/`schedule` 里的旧 `votes` 数组本来就是短生命周期数据(条目播完即从队列移除,见架构文档 1.3 节),"绑定"只保证**从这一刻起**同一个 `uid` 继续被同一账号复用,不回填过去 | T19 | 带着已有 `viewer_uid` cookie 的浏览器完成注册后,`users.legacyUid` 等于 cookie 里的 `uid`;同一浏览器换个新 cookie 再登录同一账号,投票用的仍是账号绑定的那个 `uid`(不会每次登录都生成新 uid 导致投票配额被绕过);`nickname` 从 cookie 迁移过来,不用重新填一遍 | 0.5 |
  91 +| T22 | **登录/注册前端 UI**:复用 `live.html`/`dance.html` 现有昵称弹窗的交互套路(同一套按钮/输入框视觉风格),加一个登录/注册的模态框和一个"已登录"状态展示(右上角显示邮箱前缀或 nickname + 退出按钮);未登录时的行为跟现在完全一样(免登录用 `viewer_uid` 逛/投票),**这个模态框本期唯一触发方式是页面上新增的一个"登录"入口按钮**,不强制弹出、不阻塞免费模式一的任何现有流程 | T19(可先用 mock 数据联调), T20 | 未登录状态下页面行为和现在(第二期验收时)完全一致,没有引入任何强制登录点;点"登录"能完成注册/登录/退出的完整闭环;刷新页面登录态保持(cookie 生效) | 1 |
  92 +
  93 +**总工时约 5 人天**。T20/T21/T22 在 T19 完成后可以并行,实际墙钟时间
  94 +取决于并行度,关键路径是 T18→T19→T22,约 3.5 人天。
  95 +
  96 +---
  97 +
  98 +## 4. 第三期出口
  99 +
  100 +- 能用邮箱+密码完成注册、登录、退出,`users`/`sessions` 落 SQLite;
  101 +- 登录态用独立的 `user_session` cookie,跟 T6 的 `admin_session` 互不
  102 + 干扰,30 天有效期,重启进程/重新登录都不丢;
  103 +- 邮箱验证链路走通(开发态不依赖真实发信渠道也能跑完整闭环);
  104 +- 已有 `viewer_uid` cookie 的浏览器注册/登录后自动绑定,不丢失现有
  105 + 的匿名身份延续性;
  106 +- `live.html`/`dance.html` 免费模式一的现有体验(第一、二期验收过的
  107 + 部分)不受任何影响,登录始终是可选项,不是前置门槛;
  108 +- `characters`/`garments`/`dance-refs`/`library` 等既有 JSON 存储不受
  109 + 影响,没有做不必要的全站数据库迁移。
  110 +
  111 +达成后才进第四期(积分体系 + Stripe 充值 + 模式二付费传衣服)。
  112 +
  113 +---
  114 +
  115 +## 5. 明确不在第三期范围
  116 +
  117 +避免执行时范围蔓延:
  118 +
  119 +- 积分/充值/退款(`M.2 credit_ledger`,第四期)
  120 +- 模式二/三的任何后端或前端部分(第四/五期)
  121 +- Google/Apple/GitHub 三方登录的实际接入(字段预留,不实现)
  122 +- 密码找回的前端产品化流程(表结构留出来,UI 可选顺手做)
  123 +- 把 `/api/admin/*` 迁移到 `users.role='admin'`(T6 现状保留,第四期
  124 + 真正需要按管理员身份审计时再切)
  125 +- `admin-users.html`(第四期,跟积分管理一起做)
  126 +- 真实邮件发信渠道的选型/接入(T20 标注为"待供应商选型解锁",不阻塞
  127 + 本期其余任务,但也不算完整完成邮件发送这件事本身)