# 家庭管理:邀请第二位家长加入 - 设计文档 日期: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) ```ts 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 后线上复验。