2026-05-26-child-invite-design.md 7.0 KB

孩子邀请功能设计方案

日期: 2026-05-26 状态: 待审批

问题

家长手动创建的孩子不能登录小程序,只能通过家长端切换角色来操作。但有些场景下孩子需要自主登录(如独立完成任务、查看积分)。需要:

  1. 家长可以发起邀请卡片分享,被邀请者通过微信卡片链接加入,直接以孩子身份登录
  2. 手动创建的孩子不能登录(因为没有关联 User 账号),只能家长切换使用
  3. 如果手动创建的孩子填了手机号,被邀请者用同一手机号注册时,应匹配到该孩子记录,不重复创建

现有系统分析

现有 InviteCard 表结构

invite_card (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  type VARCHAR(20)    -- 'family' | 'guide' | 'child'
  ref_id BIGINT        -- childId / familyId
  code VARCHAR(10)     -- 6位随机码
  creator_id BIGINT    -- 创建人
  expires_at DATETIME  -- 24h过期
  used TINYINT(1)      -- 是否已使用
  ...
)

现有相关接口

接口 说明
POST /api/invite-card/generate 生成邀请码,已有 type/ref_id 参数
POST /api/invite-card/verify 验证邀请码,返回 type/refId/creatorPhone/childInfo
POST /api/invite-card/accept 接受邀请,已有 child 类型处理逻辑
POST /api/auth/phone-login 手机验证码登录,返回 token + 用户信息

数据关系

  • Child.userId — 如果为 null,表示该孩子是手动创建,无关联 User 账号
  • Child.userId — 如果非 null,表示关联到 User 表,孩子可以自主登录
  • Child.phone — 可选,手动创建时可填写,用于后续邀请匹配
  • Child.familyId — 所属家庭

方案

流程概览

家长端孩子管理页
  ├── 「添加孩子」按钮 → 手动创建 Child(无 User 关联)
  └── 「邀请孩子」卡片入口 → 生成邀请卡 → 微信分享
                                          ↓
                                  被邀请者打开小程序卡片
                                          ↓
                                  login.vue 检测 invite_code
                                          ↓
                                  手机号 + 验证码 登录
                                          ↓
                                  登录成功后,检测 invite_type=child
                                          ↓
                                  调 /api/invite-card/accept
                                  手机号匹配 Child.phone
                                          ↓
                               ┌─ 匹配到 → 绑定 Child 到当前 User
                               └─ 未匹配 → 新建 Child + 绑定 User
                                          ↓
                                  设置 currentRole = child
                                  跳转到 child-index

核心规则

  1. 手动创建的孩子不能登录Child.userId = null,无 User 关联,JWT 登录拦截器会拦截
  2. 匹配逻辑accept 时用手机号查该家庭下的 Child:SELECT * FROM child WHERE family_id = ? AND phone = ?
  3. 已绑定 User 的孩子不允许重复邀请 — 生成邀请码时校验 child.userId != null 则提示「该孩子已绑定账号」
  4. 被邀请者注册 — 通过手机验证码登录后,后端自动完成孩子绑定,返回 role = child 的 JWT

后端改动

1. InviteCardService.acceptChild() — 增强匹配逻辑

当前逻辑:

// 现有:仅通过 refId 查找 Child,然后匹配手机号
Child child = childService.getById(refId);
if (!child.getPhone().equals(phone)) throw error;

改为:

// 增强:先尝试通过 phone + familyId 匹配已存在的 Child
Child existingChild = childService.lambdaQuery()
    .eq(Child::getPhone, phone)
    .eq(Child::getFamilyId, familyId)
    .one();
if (existingChild != null) {
    // 匹配到手动创建的孩子 → 绑定到当前 User
    existingChild.setUserId(currentUserId);
    childService.updateById(existingChild);
    return existingChild;
}
// 未匹配 → 新建 Child(用 User 信息创建)→ 绑定

2. InviteCardService.generate() — 生成前校验

生成 child 类型邀请卡时:

  • 校验该孩子属于当前用户所属家庭
  • 校验 child.userId == null(未绑定账号的孩子才可邀请)
  • 失效旧的未使用邀请码

3. AuthController / login flow — 登录后自动接受邀请

phoneLogin() 成功后,检测 session 中是否有 invite_code + type=child

  • 有 → 自动调用 accept → 返回 JWT 的 role=child
  • 无 → 正常返回(role = parent)

前端改动

1. children.vue — 邀请孩子入口

重构孩子管理页顶部按钮区域:

┌─────────────────────────────┐
│  ┌──────────┐  ┌──────────┐ │
│  │  + 添加孩子 │  │  📤 邀请孩子 │ │
│  └──────────┘  └──────────┘ │
└─────────────────────────────┘

「邀请孩子」按钮:

1. 点击 → 调 /api/invite-card/generate { type: "child", refId: childId }
2. 成功 → 存储 inviteCode 到本地
3. 调用 uni.shareAppMessage() 弹出微信分享菜单
4. 分享卡片 path = /pages/login/login?invite_code=xxx&invite_type=child

2. login.vue — 处理孩子邀请参数

onLoad(options) {
  if (options.invite_code) {
    uni.setStorageSync('inviteCode', options.invite_code);
    uni.setStorageSync('inviteType', options.invite_type || 'family');
  }
}

登录成功后:

const inviteType = uni.getStorageSync('inviteType');
if (inviteType === 'child') {
  const inviteCode = uni.getStorageSync('inviteCode');
  // 调 accept 接口完成孩子绑定
  await acceptInvite({ code: inviteCode, phone: this.phone });
  // 返回的 role 应为 child
  uni.setStorageSync('currentRole', 'child');
  uni.setStorageSync('isSwitchedChild', false);
  uni.reLaunch({ url: '/pages/index/index' });
}

3. api.js — 增加邀请相关 API

export const generateInviteCard = (data) => request('/api/invite-card/generate', data);
export const acceptInvite = (data) => request('/api/invite-card/accept', data);
export const verifyInviteCode = (data) => request('/api/invite-card/verify', data);

数据库变更

无。复用现有 invite_card 表 + child 表。仅需确保 child.phone 字段可用于手机号匹配。

约束

  • 每个孩子同时只能有一个未使用的邀请码
  • 已绑定 User 的孩子不能再次生成邀请码
  • 手动创建的孩子填了手机号 → 被邀请者用同手机号注册 → 匹配并绑定,不重复创建
  • 手动创建的孩子没有填手机号 → 被邀请者用任意手机号注册 → 创建新 Child 记录
  • 被邀请者登录后直接以孩子身份使用小程序