docs: 家庭管理设计文档(邀请码注册第二位家长)
新表 family_invite+注册页 /register+家长端家庭管理卡片; 新家长与现有家长权限一致,本次不做成员移除。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
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 后线上复验。
|
||||||
Reference in New Issue
Block a user