Files
SuperJump/docs/superpowers/specs/2026-08-19-family-manage-design.md
T
jacobxu666andClaude Haiku 4.5 4ac4e2091a docs: 家庭管理设计文档(邀请码注册第二位家长)
新表 family_invite+注册页 /register+家长端家庭管理卡片;
新家长与现有家长权限一致,本次不做成员移除。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-19 11:55:13 +08:00

4.7 KiB

家庭管理:邀请第二位家长加入 - 设计文档

日期:2026-08-19 状态:已批准

背景

家长端目前只有 seed 脚本创建的首位家长(手机号+密码登录),家庭内无法自助添加其他家长(如配偶)。 用户希望:能添加家庭其他家长,第二位家长也能查看孩子的学习情况。

关键现状:

  • users.familyId 已支持一家庭多家长;/parent 所有查询按 familyId 隔离, 第二位家长只要拿到同一 familyId 即自动看到全部孩子数据,无需权限改造。
  • 首位家长账号由 scripts/seed.ts 创建,生产无注册页。
  • 孩子加入:家长生成 6 位激活码(activation_code 表,一次性+15分钟) → 孩子设备输入 → 2 年设备会话。
  • families 表已有 name 字段(seed 写入),尚未展示。

需求(已与用户确认)

  1. 家长端「家庭管理」可生成邀请码,第二位家长凭码注册加入家庭。
  2. 新加入家长权限与现有家长完全一样(概览/课程/积分/设备/孩子管理全可见)。
  3. 本次不做成员移除(用户选定)。
  4. 邀请码:6 位数字、一次性、15 分钟有效(与孩子激活码一致)。

方案选择

  • 方案 A(采用):独立邀请码表 family_invite + 新注册页 /register
    • 仿 activation_code 既有模式,注册页表单(称呼+手机号+密码+邀请码),对方自设密码。
  • 方案 B(弃):家长代建账号直接填手机号+初始密码 → 对方无法自选密码,需线下传递。
  • 方案 C(弃):复用 activation_code 表 → 该表按 childUserId 绑定,语义冲突。

设计

1. 数据模型(schema.ts + 迁移 0009_family-invite.sql)

export const familyInvites = mysqlTable("family_invite", {
  id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
  familyId: bigint("family_id", { mode: "number" })
    .notNull()
    .references(() => families.id, { onDelete: "cascade" }),
  codeHash: varchar("code_hash", { length: 64 }).notNull(),
  createdBy: bigint("created_by", { mode: "number" })
    .notNull()
    .references(() => users.id),
  createdAt: datetime("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
  expiresAt: datetime("expires_at").notNull(),
  usedAt: datetime("used_at"),
});

2. 注册页:src/app/register/page.tsx + actions.ts(仿 join,server action + useActionState)

  • 表单:称呼、手机号(11 位)、密码(≥6 位)、邀请码(6 位数字)。
  • registerWithInvite(prev, formData) 逻辑:
    1. zod 校验表单;邀请码 sha256 查询 family_invite(未用且未过期),失败统一报「邀请码无效或已过期,请让家长重新生成」;
    2. 校验手机号未被占用(users.phone 唯一),占用报「该手机号已注册」;
    3. 事务:标记邀请码已用 → 插入 user(role=parent, familyId=邀请码家庭, phone, passwordHash) → createSession(30 天) → redirect("/parent")。
  • 密码 bcrypt 哈希(与 login 一致)。

3. 登录页入口

  • src/app/login/page.tsx 底部「去输入激活码」旁加「没有账号?用邀请码加入家庭」链接 → /register。

4. 家长端「家庭管理」卡片

  • src/components 或 src/app/parent/family-manage.tsx(client 组件,仿 child-manage):
    • 展示:家庭名 + 成员列表(家长显示称呼+「家长」徽标,孩子显示名字+「孩子」徽标);
    • 「生成邀请码」按钮 → 调 createFamilyInvite action → 内联显示 6 位码 +「15 分钟有效,一次性」;
    • 允许同时存在多个未用邀请码,不做互斥。
  • dashboard.tsx「功能」tab 中与孩子管理并列插入该卡片;/parent/page.tsx 查询家庭成员数据传入:
    • parents: users.role=parent AND familyId=parent.familyId
    • kids: 已有查询复用
    • familyName: families 表按 familyId 查询
  • action createFamilyInvite(parent/actions.ts):取会话家长 → 生成 6 位随机数字码(与 createActivationCode 同法,15 分钟过期) → 存 hash → 返回码明文给调用方展示。

5. 不改动

  • 孩子激活流程、login 逻辑、activation_code 表、devices-tab。

测试清单(手工,项目无自动化测试框架)

  1. 家长端功能 tab → 家庭管理卡片:显示家庭名与全部成员。
  2. 生成邀请码 → 显示 6 位码;15 分钟后(或码用过后)注册报「邀请码无效或已过期」。
  3. /register 成功注册 → 自动登录进入 /parent,看到全部孩子与数据(概览/错题/周测)。
  4. 第二家长登录后:设备管理能看到两家长会话;课程/积分改动互相可见。
  5. 手机号已注册(任意家庭) → 报「该手机号已注册」。
  6. 孩子设备激活流程不受影响;登录页新链接可达 /register。
  7. 部署 jump.hn12301.net 后线上复验。