2026-05-25-invite-card-sharing.md 31 KB

邀请分享卡片 + 自动登录 实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 实现三种邀请分享卡片(邀请孩子/邀请家人/邀请规划师),接收方通过微信分享卡片进入小程序,可自动完成注册/登录/绑定。

架构: 后端新增 invite_card 表 + InviteCardController/InviteCardService,负责邀请码生成和接收验证;前端在三个入口页面添加分享按钮,登录页处理邀请参数自动登录。

Tech Stack: Spring Boot 2.7.18 + MyBatis-Plus, uni-app Vue 2, 微信小程序原生分享


文件结构

文件 操作 职责
zxyj-backend/.../entity/InviteCard.java 新建 邀请码实体
zxyj-backend/.../mapper/InviteCardMapper.java 新建 MyBatis Mapper
zxyj-backend/.../service/InviteCardService.java 新建 生成/验证/使用邀请码
zxyj-backend/.../controller/InviteCardController.java 新建 3个 REST 端点
zxyj-backend/.../config/DatabaseInitializer.java 修改 建 invite_card 表
zxyj-frontend/utils/api.js 修改 新增 3 个 API 函数
zxyj-frontend/pages/profile/children.vue 修改 每个孩子加「邀请」按钮 + share
zxyj-frontend/pages/profile/profile.vue 修改 加「邀请家人」按钮 + share
zxyj-frontend/pages/growth/index.vue 修改 加「邀请规划师」按钮 + share
zxyj-frontend/pages/login/login.vue 修改 处理 invite_code 自动登录

Task 1: 后端 — invitate_card 表 + 实体 + Mapper

前提: 了解项目 MyBatis-Plus 实体/Mapper 写法:见 entity/User.javamapper/UserMapper.java

Files:

  • Create: zxyj-backend/src/main/java/com/zxyj/entity/InviteCard.java
  • Create: zxyj-backend/src/main/java/com/zxyj/mapper/InviteCardMapper.java
  • Modify: zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java(加建表语句)
  • Test: 启动后端验证建表成功

  • [ ] Step 1: 创建 InviteCard 实体

    package com.zxyj.entity;
    
    import com.baomidou.mybatisplus.annotation.*;
    import java.time.LocalDateTime;
    
    @TableName("invite_card")
    public class InviteCard {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String type;      // child/family/guide
    private String code;      // 唯一邀请码
    private Long refId;       // 关联ID (childId/familyId)
    private Long creatorId;   // 创建人
    private LocalDateTime expiresAt;
    private Integer used;     // 0-未使用 1-已使用
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // getters/setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
    public String getCode() { return code; }
    public void setCode(String code) { this.code = code; }
    public Long getRefId() { return refId; }
    public void setRefId(Long refId) { this.refId = refId; }
    public Long getCreatorId() { return creatorId; }
    public void setCreatorId(Long creatorId) { this.creatorId = creatorId; }
    public LocalDateTime getExpiresAt() { return expiresAt; }
    public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
    public Integer getUsed() { return used; }
    public void setUsed(Integer used) { this.used = used; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    public LocalDateTime getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
    }
    
  • [ ] Step 2: 创建 InviteCardMapper

    package com.zxyj.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.zxyj.entity.InviteCard;
    
    public interface InviteCardMapper extends BaseMapper<InviteCard> {
    }
    
  • [ ] Step 3: DatabaseInitializer 中添加建表语句

找到 DatabaseInitializer.java 中的 SQL 执行部分,在最后加一条 CREATE TABLE IF NOT EXISTS:

CREATE TABLE IF NOT EXISTS invite_card (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  type VARCHAR(20) NOT NULL COMMENT 'child/family/guide',
  code VARCHAR(64) NOT NULL UNIQUE COMMENT '邀请码',
  ref_id BIGINT NOT NULL COMMENT '关联ID(childId/familyId)',
  creator_id BIGINT NOT NULL COMMENT '创建人',
  expires_at DATETIME NOT NULL,
  used TINYINT DEFAULT 0,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_code (code),
  INDEX idx_ref_type (ref_id, type)
) COMMENT='邀请卡片';
  • [ ] Step 4: 编译并启动验证

    cd zxyj-backend && mvn clean compile spring-boot:run
    

确认启动日志无报错,表自动创建。

  • [ ] Step 5: Commit

    git add zxyj-backend/src/main/java/com/zxyj/entity/InviteCard.java
    git add zxyj-backend/src/main/java/com/zxyj/mapper/InviteCardMapper.java
    git add zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java
    git commit -m "feat: add invite_card table and entity"
    

Task 2: 后端 — InviteCardService 生成 + 验证 + 使用

Files:

  • Create: zxyj-backend/src/main/java/com/zxyj/service/InviteCardService.java
  • Test: 编译验证

  • [ ] Step 1: 创建 InviteCardService

    package com.zxyj.service;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.zxyj.common.Result;
    import com.zxyj.entity.*;
    import com.zxyj.mapper.*;
    import com.zxyj.service.UserService;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.time.LocalDateTime;
    import java.util.*;
    
    @Service
    public class InviteCardService {
    
    @Resource
    private InviteCardMapper inviteCardMapper;
    @Resource
    private UserMapper userMapper;
    @Resource
    private UserService userService;
    @Resource
    private ChildMapper childMapper;
    @Resource
    private FamilyMapper familyMapper;
    
    /**
     * 生成邀请码
     * @param type child/family/guide
     * @param refId 关联ID
     * @param creatorId 创建人ID
     */
    public InviteCard generate(String type, Long refId, Long creatorId) {
        // 使旧的未使用邀请码失效
        invalidateOldCodes(type, refId);
    
        InviteCard card = new InviteCard();
        card.setType(type);
        card.setCode(generateCode(type));
        card.setRefId(refId);
        card.setCreatorId(creatorId);
        card.setExpiresAt(LocalDateTime.now().plusHours(24));
        card.setUsed(0);
        card.setCreatedAt(LocalDateTime.now());
        card.setUpdatedAt(LocalDateTime.now());
        inviteCardMapper.insert(card);
        return card;
    }
    
    private void invalidateOldCodes(String type, Long refId) {
        List<InviteCard> oldCards = inviteCardMapper.selectList(
            new LambdaQueryWrapper<InviteCard>()
                .eq(InviteCard::getType, type)
                .eq(InviteCard::getRefId, refId)
                .eq(InviteCard::getUsed, 0)
        );
        for (InviteCard card : oldCards) {
            card.setUsed(1);
            card.setUpdatedAt(LocalDateTime.now());
            inviteCardMapper.updateById(card);
        }
    }
    
    private String generateCode(String type) {
        String prefix;
        switch (type) {
            case "child":  prefix = "CHILD_"; break;
            case "family": prefix = "FAMILY_"; break;
            case "guide":  prefix = "GUIDE_"; break;
            default:       prefix = "INV_"; break;
        }
        String random = UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
        return prefix + random;
    }
    
    /**
     * 验证邀请码并返回关联信息
     */
    public Result<Map<String, Object>> verify(String code) {
        InviteCard card = inviteCardMapper.selectOne(
            new LambdaQueryWrapper<InviteCard>().eq(InviteCard::getCode, code)
        );
        if (card == null) {
            return Result.error("邀请码无效");
        }
        if (card.getUsed() == 1) {
            return Result.error("邀请码已使用");
        }
        if (card.getExpiresAt().isBefore(LocalDateTime.now())) {
            return Result.error("邀请码已过期");
        }
    
        Map<String, Object> result = new HashMap<>();
        result.put("type", card.getType());
        result.put("refId", card.getRefId());
    
        if ("child".equals(card.getType())) {
            Child child = childMapper.selectById(card.getRefId());
            if (child != null) {
                result.put("nickname", child.getNickname());
                Family family = familyMapper.selectById(child.getFamilyId());
                if (family != null) {
                    result.put("familyName", family.getName());
                }
            }
        } else if ("family".equals(card.getType())) {
            Family family = familyMapper.selectById(card.getRefId());
            if (family != null) {
                result.put("familyName", family.getName());
            }
        }
    
        return Result.success(result);
    }
    
    /**
     * 接受邀请 - 核心逻辑
     * @param code 邀请码
     * @param phone 手机号
     * @return 登录结果(token, userId, role, familyId)
     */
    public Result<Map<String, Object>> accept(String code, String phone) {
        InviteCard card = inviteCardMapper.selectOne(
            new LambdaQueryWrapper<InviteCard>().eq(InviteCard::getCode, code)
        );
        if (card == null) return Result.error("邀请码无效");
        if (card.getUsed() == 1) return Result.error("邀请码已使用");
        if (card.getExpiresAt().isBefore(LocalDateTime.now())) return Result.error("邀请码已过期");
    
        if ("child".equals(card.getType())) {
            return acceptChild(card, phone);
        } else if ("family".equals(card.getType())) {
            return acceptFamily(card, phone);
        } else if ("guide".equals(card.getType())) {
            return acceptGuide(card, phone);
        }
        return Result.error("未知邀请类型");
    }
    
    private Result<Map<String, Object>> acceptChild(InviteCard card, String phone) {
        Child child = childMapper.selectById(card.getRefId());
        if (child == null) return Result.error("孩子信息不存在");
        if (child.getPhone() == null || !child.getPhone().equals(phone)) {
            return Result.error("手机号不匹配,请联系家长确认");
        }
    
        // 检查孩子是否已有账号
        User existingUser = userMapper.selectOne(
            new LambdaQueryWrapper<User>().eq(User::getPhone, phone)
        );
    
        User user;
        if (existingUser != null) {
            user = existingUser;
        } else {
            // 自动注册
            user = new User();
            user.setPhone(phone);
            user.setNickname(child.getNickname() != null ? child.getNickname() : "孩子");
            user.setRole("child");
            user.setFamilyId(child.getFamilyId());
            user.setAvatar(child.getAvatar());
            userMapper.insert(user);
        }
    
        // 标记邀请码已使用
        card.setUsed(1);
        card.setUpdatedAt(LocalDateTime.now());
        inviteCardMapper.updateById(card);
    
        Map<String, Object> result = new HashMap<>();
        result.put("token", userService.generateToken(user));
        result.put("userId", user.getId());
        result.put("role", "child");
        result.put("familyId", user.getFamilyId());
        return Result.success(result);
    }
    
    private Result<Map<String, Object>> acceptFamily(InviteCard card, String phone) {
        Family family = familyMapper.selectById(card.getRefId());
        if (family == null) return Result.error("家庭不存在");
    
        // 检查手机号是否已注册
        User existingUser = userMapper.selectOne(
            new LambdaQueryWrapper<User>().eq(User::getPhone, phone)
        );
    
        User user;
        if (existingUser != null) {
            user = existingUser;
            // 更新家庭ID
            user.setFamilyId(card.getRefId());
            userMapper.updateById(user);
        } else {
            user = new User();
            user.setPhone(phone);
            user.setNickname("家人");
            user.setRole("parent");
            user.setFamilyId(card.getRefId());
            userMapper.insert(user);
        }
    
        card.setUsed(1);
        card.setUpdatedAt(LocalDateTime.now());
        inviteCardMapper.updateById(card);
    
        Map<String, Object> result = new HashMap<>();
        result.put("token", userService.generateToken(user));
        result.put("userId", user.getId());
        result.put("role", user.getRole());
        result.put("familyId", user.getFamilyId());
        return Result.success(result);
    }
    
    private Result<Map<String, Object>> acceptGuide(InviteCard card, String phone) {
        // 规划师邀请只返回家庭信息,让用户自己去规划师注册页走完整流程
        Family family = familyMapper.selectById(card.getRefId());
        if (family == null) return Result.error("家庭不存在");
    
        card.setUsed(1);
        card.setUpdatedAt(LocalDateTime.now());
        inviteCardMapper.updateById(card);
    
        Map<String, Object> result = new HashMap<>();
        result.put("familyId", card.getRefId());
        result.put("familyName", family.getName());
        result.put("type", "guide");
        return Result.success(result);
    }
    }
    
  • [ ] Step 2: 编译验证

    cd zxyj-backend && mvn clean compile
    

确认无编译错误。

  • [ ] Step 3: Commit

    git add zxyj-backend/src/main/java/com/zxyj/service/InviteCardService.java
    git commit -m "feat: add InviteCardService with generate/verify/accept logic"
    

Task 3: 后端 — InviteCardController 三个端点

Files:

  • Create: zxyj-backend/src/main/java/com/zxyj/controller/guide/InviteCardController.java
  • Test: 启动后端 + curl 测试

  • [ ] Step 1: 创建 InviteCardController

    package com.zxyj.controller.guide;
    
    import com.zxyj.common.Result;
    import com.zxyj.entity.*;
    import com.zxyj.mapper.*;
    import com.zxyj.service.InviteCardService;
    import io.swagger.v3.oas.annotations.Operation;
    import io.swagger.v3.oas.annotations.tags.Tag;
    import org.springframework.web.bind.annotation.*;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import java.util.HashMap;
    import java.util.Map;
    
    @Tag(name = "邀请卡片", description = "家庭成员/规划师邀请分享")
    @RestController
    @RequestMapping("/api/invite-card")
    public class InviteCardController {
    
    @Resource
    private InviteCardService inviteCardService;
    @Resource
    private ChildMapper childMapper;
    @Resource
    private FamilyMapper familyMapper;
    @Resource
    private UserMapper userMapper;
    
    @PostMapping("/generate")
    @Operation(summary = "生成邀请码")
    public Result<Map<String, Object>> generate(@RequestBody Map<String, Object> params,
                                                HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        String type = (String) params.get("type");
        Object refIdObj = params.get("refId");
    
        if (type == null || refIdObj == null) {
            return Result.error("缺少必要参数: type/refId");
        }
        Long refId = Long.valueOf(refIdObj.toString());
    
        // 验证用户对 refId 的归属权
        if ("child".equals(type)) {
            Child child = childMapper.selectById(refId);
            if (child == null) return Result.error("孩子不存在");
            User user = userMapper.selectById(userId);
            if (user == null || !user.getFamilyId().equals(child.getFamilyId())) {
                return Result.error("无权生成该孩子的邀请");
            }
        } else if ("family".equals(type) || "guide".equals(type)) {
            Family family = familyMapper.selectById(refId);
            if (family == null) return Result.error("家庭不存在");
            User user = userMapper.selectById(userId);
            if (user == null || !user.getFamilyId().equals(family.getId())) {
                return Result.error("无权生成该家庭的邀请");
            }
        }
    
        InviteCard card = inviteCardService.generate(type, refId, userId);
        Map<String, Object> result = new HashMap<>();
        result.put("code", card.getCode());
        result.put("expiresAt", card.getExpiresAt().toString());
        return Result.success(result);
    }
    
    @PostMapping("/verify")
    @Operation(summary = "验证邀请码")
    public Result<Map<String, Object>> verify(@RequestBody Map<String, Object> params) {
        String code = (String) params.get("code");
        if (code == null) return Result.error("缺少邀请码");
        return inviteCardService.verify(code);
    }
    
    @PostMapping("/accept")
    @Operation(summary = "接受邀请(自动注册/登录)")
    public Result<Map<String, Object>> accept(@RequestBody Map<String, Object> params) {
        String code = (String) params.get("code");
        String phone = (String) params.get("phone");
        if (code == null || phone == null) {
            return Result.error("缺少必要参数: code/phone");
        }
        return inviteCardService.accept(code, phone);
    }
    }
    
  • [ ] Step 2: 检查 UserService.generateToken 方法是否存在

搜索 userService.generateTokenUserService 中是否有生成 token 的方法。如果不存在,需要实现一个简单的 JWT token 生成(复用项目中已有的 JwtUtil):

// 在 InviteCardService 中注入 JwtUtil
@Resource
private com.zxyj.config.JwtUtil jwtUtil;

// 然后 generateToken 方法:
private String generateToken(User user) {
    return jwtUtil.generateToken(user.getId(), user.getPhone(), user.getRole());
}

如果项目中已有 JwtUtil,确认其 generateToken 的方法签名一致。

  • [ ] Step 3: 启动后端测试

    cd zxyj-backend && mvn spring-boot:run
    

在另一个终端测试:

# 生成邀请码(需要先获取 token)
# 测试 child 类型
curl -X POST http://localhost:8080/api/invite-card/generate \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"type":"child","refId":1}'

# 验证邀请码
curl -X POST http://localhost:8080/api/invite-card/verify \
  -H "Content-Type: application/json" \
  -d '{"code":"CHILD_XXXXXX"}'

确认返回正常。

  • [ ] Step 4: Commit

    git add zxyj-backend/src/main/java/com/zxyj/controller/guide/InviteCardController.java
    git commit -m "feat: add InviteCardController with generate/verify/accept endpoints"
    

Task 4: 前端 — api.js 新增三个 API 函数

Files:

  • Modify: zxyj-frontend/utils/api.js

  • [ ] Step 1: 新增 API 函数

utils/api.js 末尾添加:

// 邀请卡片
export const generateInviteCard = (type, refId) => {
  return request('/api/invite-card/generate', 'POST', { type, refId })
}

export const verifyInviteCard = (code) => {
  return request('/api/invite-card/verify', 'POST', { code })
}

export const acceptInviteCard = (code, phone) => {
  return request('/api/invite-card/accept', 'POST', { code, phone })
}
  • [ ] Step 2: Commit

    git add zxyj-frontend/utils/api.js
    git commit -m "feat: add invite card API functions"
    

Task 5: 前端 — 孩子管理页添加「邀请」按钮

Files:

  • Modify: zxyj-frontend/pages/profile/children.vue

  • [ ] Step 1: 在模板每个孩子项加「邀请」按钮

找到 <view class="child-actions">,在编辑后加邀请按钮:

<view class="child-actions">
  <text class="btn-invite" @click="inviteChild(child)">邀请</text>
  <text class="btn-edit" @click="editChild(child)">编辑</text>
</view>
  • Step 2: 加分享配置

data() 中添加:

data() {
  return {
    children: [],
    shareData: null  // 用于 onShareAppMessage
  }
}

onLoad 中定义 onShareAppMessage

onLoad() {
  // 必须在页面级别定义 onShareAppMessage,不能在 methods 里
},
onShareAppMessage() {
  if (this.shareData) {
    return {
      title: this.shareData.title,
      path: this.shareData.path,
      imageUrl: '/static/invite-card.png'
    }
  }
  return { title: '知行益家', path: '/pages/login/login' }
}
  • Step 3: 实现 inviteChild 方法

methods 中添加:

async inviteChild(child) {
  try {
    const { generateInviteCard } = require('../../utils/api.js')
    const res = await generateInviteCard('child', child.id)
    const code = res.data.code
    // 保存分享数据
    this.shareData = {
      title: `邀请 ${child.nickname || '孩子'} 加入家庭`,
      path: `/pages/login/login?invite_code=${code}`
    }
    // 触发微信原生分享
    // 注意:需要 button open-type="share" 才能触发
    uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })
  } catch (e) {
    uni.showToast({ title: e.message || '生成邀请失败', icon: 'none' })
  }
}
  • Step 4: 改「邀请」按钮为 button open-type="share"

由于微信分享必须通过 button open-type="share" 触发(不能通过 @click 手动调),需要改模板:

<view class="child-actions">
  <button class="btn-invite-btn" open-type="share" @click="prepareInvite(child)">邀请</button>
  <text class="btn-edit" @click="editChild(child)">编辑</text>
</view>

方法分离:

async prepareInvite(child) {
  try {
    const { generateInviteCard } = require('../../utils/api.js')
    const res = await generateInviteCard('child', child.id)
    const code = res.data.code
    this.shareData = {
      title: `邀请 ${child.nickname || '孩子'} 加入家庭`,
      path: `/pages/login/login?invite_code=${code}`
    }
    this._waitingShare = true
  } catch (e) {
    uni.showToast({ title: e.message || '生成邀请失败', icon: 'none' })
  }
}
  • Step 5: 添加邀请按钮样式

<style> 末尾添加:

.btn-invite-btn {
  background: #3B82F6;
  color: #fff;
  font-size: 24rpx;
  padding: 4rpx 16rpx;
  border-radius: 8rpx;
  margin-right: 10rpx;
  line-height: 1.8;
}
  • [ ] Step 6: Commit

    git add zxyj-frontend/pages/profile/children.vue
    git commit -m "feat: add invite button for each child in children management"
    

Task 6: 前端 — 我的页面加「邀请家人」按钮

Files:

  • Modify: zxyj-frontend/pages/profile/profile.vue

  • [ ] Step 1: 添加 invite 相关数据和 onShareAppMessage

data() 中加:

data() {
  return {
    // ... existing data
    shareData: null
  }
}

在页面添加 onShareAppMessage(如果已存在则追加逻辑):

onShareAppMessage() {
  if (this.shareData) {
    return {
      title: this.shareData.title,
      path: this.shareData.path,
      imageUrl: '/static/invite-card.png'
    }
  }
}
  • Step 2: 菜单列表添加「邀请家人」项

在菜单列表中找到合适位置,比如「积分记录」之后,添加:

<view class="menu-item" @click="prepareFamilyInvite" v-if="role === 'parent'">
  <text>👨‍👩‍👧‍👦 邀请家人</text>
  <text class="arrow">›</text>
</view>
  • Step 3: 改用 button open-type="share"

由于分享需要 button 触发,改为:

<button class="menu-item-btn" open-type="share" @click="prepareFamilyInvite" v-if="role === 'parent'">
  <text>👨‍👩‍👧‍👦 邀请家人</text>
  <text class="arrow">›</text>
</button>
  • Step 4: 实现 prepareFamilyInvite

methods 中添加:

async prepareFamilyInvite() {
  try {
    const userInfo = uni.getStorageSync('userInfo')
    const familyId = uni.getStorageSync('familyId')
    if (!familyId) {
      uni.showToast({ title: '您暂未加入家庭', icon: 'none' })
      return
    }
    const { generateInviteCard } = require('../../utils/api.js')
    const res = await generateInviteCard('family', familyId)
    const code = res.data.code
    this.shareData = {
      title: `${userInfo.nickname || '家人'} 邀请你加入家庭`,
      path: `/pages/login/login?invite_code=${code}`
    }
    uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })
  } catch (e) {
    uni.showToast({ title: e.message || '生成邀请失败', icon: 'none' })
  }
}
  • [ ] Step 5: 添加按钮样式

    .menu-item-btn {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    padding: 28rpx 24rpx;
    border: none;
    background: transparent;
    font-size: 28rpx;
    color: #333;
    border-bottom: 1rpx solid #f0f0f0;
    }
    .menu-item-btn::after { border: none; }
    
  • [ ] Step 6: Commit

    git add zxyj-frontend/pages/profile/profile.vue
    git commit -m "feat: add invite family button in profile page"
    

Task 7: 前端 — 成长档案页加「邀请规划师」按钮

Files:

  • Modify: zxyj-frontend/pages/growth/index.vue

  • [ ] Step 1: 添加分享数据和 onShareAppMessage

data() 中添加:

data() {
  return {
    // ... existing data
    shareData: null
  }
}

添加 onShareAppMessage 生命周期:

onShareAppMessage() {
  if (this.shareData) {
    return {
      title: this.shareData.title,
      path: this.shareData.path,
      imageUrl: '/static/invite-card.png'
    }
  }
}
  • Step 2: 在页面顶部或底部添加「邀请规划师」按钮

<view class="footer-btn"> 区域添加(或在 children-list 区域):

<view class="footer-btn" v-if="!selectedChild">
  <button class="btn-invite-guide" open-type="share" @click="prepareGuideInvite">
    邀请成长规划师
  </button>
</view>
  • Step 3: 实现 prepareGuideInvite

methods 中添加:

async prepareGuideInvite() {
  try {
    const familyId = uni.getStorageSync('familyId')
    if (!familyId) {
      uni.showToast({ title: '您暂未加入家庭', icon: 'none' })
      return
    }
    const userInfo = uni.getStorageSync('userInfo')
    const { generateInviteCard } = require('../../utils/api.js')
    const res = await generateInviteCard('guide', familyId)
    const code = res.data.code
    this.shareData = {
      title: `${userInfo.nickname || '家长'} 邀请你成为成长规划师`,
      path: `/pages/login/login?invite_code=${code}`
    }
    uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })
  } catch (e) {
    uni.showToast({ title: e.message || '生成邀请失败', icon: 'none' })
  }
}
  • [ ] Step 4: 添加样式

    .btn-invite-guide {
    background: linear-gradient(135deg, #667eea, #764ba2);
    color: #fff;
    border-radius: 50rpx;
    padding: 20rpx 0;
    font-size: 28rpx;
    margin-top: 20rpx;
    width: 100%;
    }
    
  • [ ] Step 5: Commit

    git add zxyj-frontend/pages/growth/index.vue
    git commit -m "feat: add invite guide button in growth page"
    

Task 8: 前端 — 登录页处理邀请码自动登录

Files:

  • Modify: zxyj-frontend/pages/login/login.vue

  • [ ] Step 1: onLoad 增加 invite_code 捕获

找到 onLoad 方法,修改为支持 invite_code 参数:

onLoad(options) {
  // 处理邀请码
  if (options.invite_code) {
    uni.setStorageSync('inviteCode', options.invite_code)
    console.log('检测到邀请码:', options.invite_code)
  }
  // 兼容旧版参数名
  if (options.inviteCode && !options.invite_code) {
    uni.setStorageSync('inviteCode', options.inviteCode)
  }
}
  • Step 2: 微信登录成功时检测邀请码并进行自动登录

找到 handleWechatPhoneLogin 方法,在 wechatPhoneLogin 调用之前,检测是否有 inviteCode

查看 handleWechatPhoneLogin 的逻辑。在当前流程中,微信登录拿到手机号后直接调后端 wechatPhoneLogin。需要在手机号获取成功后,如果存在邀请码,先调 acceptInviteCard 而不是普通登录。

修改 handleWechatPhoneLogin 方法:

async handleWechatPhoneLogin(e) {
  if (e.detail.errMsg === 'getPhoneNumber:ok') {
    // ... 现有手机号验证逻辑 ...

    this.loading = true
    try {
      const inputPhone = this.phone

      // 检查是否有邀请码
      const inviteCode = uni.getStorageSync('inviteCode')
      if (inviteCode) {
        // 走邀请码自动注册/登录流程
        const { acceptInviteCard } = require('../../utils/api.js')
        const res = await acceptInviteCard(inviteCode, inputPhone)
        
        // 保存登录信息
        uni.setStorageSync('token', res.data.token)
        uni.setStorageSync('userId', res.data.userId)
        uni.setStorageSync('role', res.data.role)
        uni.setStorageSync('currentRole', res.data.role)
        if (res.data.familyId) {
          uni.setStorageSync('familyId', res.data.familyId)
        }

        // 清除邀请码
        uni.removeStorageSync('inviteCode')

        uni.showToast({ title: '加入成功', icon: 'success' })
        setTimeout(() => {
          this.navigateToHome(res.data.role)
        }, 1000)
        return
      }

      // 没有邀请码,走普通登录逻辑
      // ... 现有登录逻辑 ...

注意:需要判断微信登录返回时 phone 字段的获取方式。当前微信登录的测试环境中,用户输入的 inputPhone 直接作为手机号传递。

  • Step 3: 处理手机号登录(验证码)的邀请码场景

phoneLogin 方法中,检测邀请码:

async phoneLogin() {
  // ... 现有验证逻辑 ...

  this.loading = true
  try {
    // 检查是否有邀请码
    const inviteCode = uni.getStorageSync('inviteCode')
    if (inviteCode) {
      const { acceptInviteCard } = require('../../utils/api.js')
      const res = await acceptInviteCard(inviteCode, this.phone)
      
      // 保存登录信息
      uni.setStorageSync('token', res.data.token)
      uni.setStorageSync('userId', res.data.userId)
      uni.setStorageSync('role', res.data.role)
      uni.setStorageSync('currentRole', res.data.role)
      if (res.data.familyId) {
        uni.setStorageSync('familyId', res.data.familyId)
      }
      uni.removeStorageSync('inviteCode')

      uni.showToast({ title: '加入成功', icon: 'success' })
      setTimeout(() => {
        this.navigateToHome(res.data.role)
      }, 1000)
      return
    }

    // 没有邀请码,走普通登录
    const res = await phoneLoginApi({...})
    // ... 现有逻辑 ...
  • [ ] Step 4: Commit

    git add zxyj-frontend/pages/login/login.vue
    git commit -m "feat: handle invite_code auto-login in login page"
    

自检清单

  1. 需求覆盖: 三种邀请卡片(child/family/guide)各自有生成端和接收端逻辑,所有场景覆盖
  2. 占位符检查: 无 TBD/TODO 遗留,所有步骤含完整代码
  3. 类型一致性: InviteCard 字段名(type/refId/code/used)在所有任务中保持一致
  4. 微信限制: 分享需 button open-type="share" 触发,已在 children.vue 和 profile.vue 中正确处理
  5. 测试环境: 微信 getPhoneNumber 解密在测试环境走 inputPhone 明文传递,已在 login.vue 中兼容