Commit 700c1c461133b4d9952f082f1c41f4ec5d4dc8b1
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 | + 本期其余任务,但也不算完整完成邮件发送这件事本身) |