# 换装跳舞直播间 — 第三期开发任务规划 需求见 `docs/todo/dressup-dance-room.md`(下称"需求文档")第 7.1 节、 第 9 节第 3 条;设计见 `docs/todo/dressup-dance-room-mvcp.md`(下称 "MVCP")M.1/M.10;架构见 `docs/structure/architecture.md`(下称"架构 文档")1.4/2.7 节。第一期(`dev-plan.md`,T1-T10)、第二期 (`dev-plan-2.md`,T11-T17)均已完成并提交。任务编号接着往下编。 工时是**粗估**,单位人天,用来排先后不用来考核。 --- ## 0. 第三期范围(需求文档 9 节第 3 条) **做**:用户注册体系上线(邮箱 + 密码登录),把现有匿名 `viewer_uid`/`viewer_nickname` cookie 身份**绑定**到账号——账号是第四期 付费功能的前提,必须先落地,但**第三期本身不做任何付费/积分/上传功能**。 **明确不做**(留到后续分期): - 积分体系(余额/流水/扣费/退还,`M.2 credit_ledger`)——第四期。 - Stripe 充值——第四期。 - 模式二/三(付费传衣服/传人)——第四期依赖账号,但功能本身也是第四期。 - Google/Apple/GitHub 三方登录——`M.1 oauthProviders` 字段预留,第三期 留空数组,不接入任何 OAuth provider。 - 密码找回邮件的完整产品化 UI(`M.10 password_reset` 令牌类型的表结构 留出来,但"忘记密码"这个前端入口不是本期出口条件,有余力可顺手做)。 - 把现有 `/api/admin/*` 的单管理员口令(T6,`ADMIN_SESSION_COOKIE`) 迁移成 `users.role: admin`——**T6 现状继续保留不动**,避免为了"架构 更统一"牵动已经跑通的后台鉴权;等第四期 `POST /api/admin/credit/adjust` 这类真正需要按管理员身份审计的接口出现时,再一并把 admin 鉴权切到 `users`/`sessions` 表(MVCP M.1 `role` 字段本来就是为那时候准备的)。 - `admin-users.html`(用户/积分管理后台页面,MVCP V.表)——挂在积分 管理上,第四期跟积分体系一起做,第三期没有积分可管。 --- ## 1. 已确认的技术决策 - **存储:`users`/`sessions` 两张表落 SQLite,不用 JSON 文件**——架构 文档 1.4 节明确把"账号 + 密码/凭证存储"列为 JSON 明文文件不适合的 触发条件之一(哪怕 hash 过也建议上 SQLite),现在正是这个触发点。 `characters`/`garments`/`dance-refs`/`library` 等既有 JSON 存储**不 受影响**,不做全站数据库迁移。 - **零新增 npm 依赖**:Node 24(当前运行版本已确认)自带 `node:sqlite`(`DatabaseSync`,实验性但可直接用)和 `node:crypto` 的 `scryptSync`——密码哈希不需要 `bcrypt`/`argon2` 这类第三方包, SQLite 不需要 `better-sqlite3`。这个项目至今没有 `package.json`, 保持这个状态。 - **两套 session 互不干扰**:现有 `admin_session` cookie(T6,纯内存 Map,重启即丢)继续只管后台;本期新增的用户登录态用独立的 `user_session` cookie + `sessions` 表(M.10),语义、有效期 (30 天)、校验逻辑都是新的一套,不会互相污染。 - **邮箱验证发信渠道未定**:需求文档第 8 节没有把这条列进"待验证 事项",是本文档新发现的缺口——发验证邮件需要一个真实的发信渠道 (SMTP 或 Resend/SendGrid 这类走 HTTP API 的服务,后者不需要额外 依赖,`fetch` 直接调),**这个供应商选型需要产品方拍板**,不阻塞 T18-T19 开工(下面 T20 会说明怎么绕开这个阻塞点先把其余部分做完)。 --- ## 2. 依赖关系 ```mermaid flowchart TD T18[T18 SQLite users/sessions 建表] --> T19[T19 注册/登录/退出后端] T19 --> T20[T20 邮箱验证] T19 --> T21[T21 匿名身份绑定] T19 --> T22[T22 登录/注册前端 UI] T20 --> T22 classDef ready fill:#d4edda,stroke:#28a745 class T18 ready ``` T18 是唯一的地基任务,其余都要等它先建好表。T19 完成后,T20(邮箱 验证)、T21(绑定匿名身份)、T22(前端 UI)可以并行——T22 前期能先 拿假数据/mock 接口联调(同第二期 T16 的做法),不用死等 T20 邮件通道 真正打通。 --- ## 3. 任务表 | # | 任务 | 依赖 | 验收标准 | 工时 | |---|---|---|---|---| | 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 | | 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 | | 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 | | 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 | | T22 | **登录/注册前端 UI**:复用 `live.html`/`dance.html` 现有昵称弹窗的交互套路(同一套按钮/输入框视觉风格),加一个登录/注册的模态框和一个"已登录"状态展示(右上角显示邮箱前缀或 nickname + 退出按钮);未登录时的行为跟现在完全一样(免登录用 `viewer_uid` 逛/投票),**这个模态框本期唯一触发方式是页面上新增的一个"登录"入口按钮**,不强制弹出、不阻塞免费模式一的任何现有流程 | T19(可先用 mock 数据联调), T20 | 未登录状态下页面行为和现在(第二期验收时)完全一致,没有引入任何强制登录点;点"登录"能完成注册/登录/退出的完整闭环;刷新页面登录态保持(cookie 生效) | 1 | **总工时约 5 人天**。T20/T21/T22 在 T19 完成后可以并行,实际墙钟时间 取决于并行度,关键路径是 T18→T19→T22,约 3.5 人天。 --- ## 4. 第三期出口 - 能用邮箱+密码完成注册、登录、退出,`users`/`sessions` 落 SQLite; - 登录态用独立的 `user_session` cookie,跟 T6 的 `admin_session` 互不 干扰,30 天有效期,重启进程/重新登录都不丢; - 邮箱验证链路走通(开发态不依赖真实发信渠道也能跑完整闭环); - 已有 `viewer_uid` cookie 的浏览器注册/登录后自动绑定,不丢失现有 的匿名身份延续性; - `live.html`/`dance.html` 免费模式一的现有体验(第一、二期验收过的 部分)不受任何影响,登录始终是可选项,不是前置门槛; - `characters`/`garments`/`dance-refs`/`library` 等既有 JSON 存储不受 影响,没有做不必要的全站数据库迁移。 达成后才进第四期(积分体系 + Stripe 充值 + 模式二付费传衣服)。 --- ## 5. 明确不在第三期范围 避免执行时范围蔓延: - 积分/充值/退款(`M.2 credit_ledger`,第四期) - 模式二/三的任何后端或前端部分(第四/五期) - Google/Apple/GitHub 三方登录的实际接入(字段预留,不实现) - 密码找回的前端产品化流程(表结构留出来,UI 可选顺手做) - 把 `/api/admin/*` 迁移到 `users.role='admin'`(T6 现状保留,第四期 真正需要按管理员身份审计时再切) - `admin-users.html`(第四期,跟积分管理一起做) - 真实邮件发信渠道的选型/接入(T20 标注为"待供应商选型解锁",不阻塞 本期其余任务,但也不算完整完成邮件发送这件事本身)