docs: 家庭管理设计文档(邀请码注册第二位家长)

新表 family_invite+注册页 /register+家长端家庭管理卡片;
新家长与现有家长权限一致,本次不做成员移除。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-19 11:55:13 +08:00
co-authored by Claude Haiku 4.5
parent f6d5aa8680
commit 4ac4e2091a
@@ -0,0 +1,90 @@
# 家庭管理:邀请第二位家长加入 - 设计文档
日期: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 后线上复验。