Prechádzať zdrojové kódy

feat: 家庭邀请新流程(短邀请码落地页 + 首登绑定邀请人)+ 分享补邀请码/朋友圈

liaoxg 2 týždňov pred
rodič
commit
d6773ab43c

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -9500,8 +9500,7 @@ public class DatabaseInitializer implements CommandLineRunner {
         migratePlanCenterMenu();
         // 迁移268: coupon 表添加 status 列(优惠券停用/启用)
         migrateCouponStatus();
-
-        // 迁移273: five_dimension_self_checks 表添加 question_ids_json 列(自检轮换用)
+// 迁移273: five_dimension_self_checks 表添加 question_ids_json 列(自检轮换用)
         ensureColumn("five_dimension_self_checks", "question_ids_json", "VARCHAR(255) COMMENT '本次自检使用的题号JSON: [1,5,9,...]'");
 
         // 迁移274: 创建 self_check_ignores 表(记录用户忽略自检提醒的动作,作为 15 天周期重置依据)
@@ -9517,6 +9516,9 @@ public class DatabaseInitializer implements CommandLineRunner {
         } catch (Exception e) {
             log.warn("创建 self_check_ignores 表失败: {}", e.getMessage());
         }
+
+        // 迁移275: users 表添加 family_inviter_id(家庭邀请追踪)
+        ensureColumn("users", "family_inviter_id", "BIGINT DEFAULT NULL COMMENT '家庭邀请人ID(首次注册时绑定,用于追踪邀请来源)'");
     }
 
     /**

+ 69 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java

@@ -295,6 +295,75 @@ public class FamilyInviteController {
         }
     }
 
+    /**
+     * 验证家庭短邀请码(如 fam_abc123)
+     * 供落地页展示家庭信息用,不创建加入请求记录
+     */
+    @Operation(summary = "验证家庭短邀请码")
+    @PostMapping("/validate-family-code")
+    public Result<InviteValidateDTO> validateFamilyCode(@RequestBody Map<String, String> body) {
+        String inviteCode = body.get("inviteCode");
+        if (inviteCode == null || inviteCode.trim().isEmpty()) {
+            return Result.error("邀请码不能为空");
+        }
+
+        Family family = familyMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Family>()
+                        .eq(Family::getInviteCode, inviteCode.trim()));
+        if (family == null) {
+            return Result.error("邀请码无效或不存在");
+        }
+
+        User creator = userMapper.selectById(family.getCreatorId());
+        Long memberCount = userMapper.selectCount(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
+                        .eq(User::getFamilyId, family.getId()));
+
+        InviteValidateDTO dto = new InviteValidateDTO();
+        dto.setFamilyId(family.getId());
+        dto.setFamilyName(family.getName());
+        dto.setInviterName(creator != null ? creator.getNickname() : null);
+        dto.setInviterAvatar(creator != null ? creator.getAvatar() : null);
+        dto.setMemberCount(memberCount.intValue());
+        return Result.success(dto);
+    }
+
+    /**
+     * 首次注册绑定家庭邀请人(幂等,已有绑定则忽略)
+     */
+    @Operation(summary = "首次注册绑定家庭邀请人")
+    @PostMapping("/bind-inviter")
+    public Result<Void> bindFamilyInviter(@RequestBody Map<String, String> body,
+        @RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        String inviteCode = body.get("familyInviteCode");
+        if (inviteCode == null || inviteCode.trim().isEmpty()) {
+            return Result.error("邀请码不能为空");
+        }
+
+        Family family = familyMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Family>()
+                        .eq(Family::getInviteCode, inviteCode.trim()));
+        if (family == null) {
+            return Result.error("邀请码无效");
+        }
+
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        // 幂等:已绑定过则直接返回成功
+        if (user.getFamilyInviterId() != null && user.getFamilyInviterId().equals(family.getCreatorId())) {
+            return Result.success();
+        }
+        user.setFamilyInviterId(family.getCreatorId());
+        user.setUpdatedAt(new java.util.Date());
+        userMapper.updateById(user);
+        return Result.success();
+    }
+
     /**
      * 接受家庭邀请
      */

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java

@@ -126,6 +126,9 @@ public class User implements Serializable {
     // 佣金系统 - 推荐人ID
     private Long referrerId;
 
+    // 家庭邀请 - 首次注册时绑定的邀请人ID(用于追踪邀请来源)
+    private Long familyInviterId;
+
     // 佣金系统 - 个人邀请码(6位字母数字)
     private String referralCode;
 

+ 1 - 0
cfc-backend/src/main/resources/schema.sql

@@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS users (
     birth_hour VARCHAR(10) COMMENT '出生时辰: 子丑寅卯辰巳午未申酉戌亥',
     mascot VARCHAR(20) DEFAULT NULL COMMENT 'AI助手形象: xibao(浠宝)/fubao(福宝)',
     portrait_prompt TEXT COMMENT '用户自定义画像 prompt(为空时自动从 profile_snapshot 渲染)',
+    family_inviter_id BIGINT DEFAULT NULL COMMENT '家庭邀请人ID(首次注册时绑定,用于追踪邀请来源)',
     province VARCHAR(50) DEFAULT NULL COMMENT '省份',
     city VARCHAR(50) DEFAULT NULL COMMENT '城市',
     district VARCHAR(50) DEFAULT NULL COMMENT '区县',

+ 21 - 4
cfc-frontend/components/ProfileMenu.vue

@@ -110,7 +110,7 @@
 </template>
 
 <script>
-import { setPassword, verifyPassword, updateMemberVisibility, getFamilyMemberList, vendorStatus, ensureFamily } from '../utils/api.js'
+import { setPassword, verifyPassword, updateMemberVisibility, getFamilyMemberList, vendorStatus, ensureFamily, getReferralCode } from '../utils/api.js'
 
 export default {
   name: 'ProfileMenu',
@@ -133,7 +133,8 @@ export default {
       confirmPassword: '',
       showToFamily: 1,
       isVendor: false,
-      
+      referralCode: '',
+      referralCodeLoaded: false
     }
   },
   onShow() {
@@ -141,8 +142,20 @@ export default {
     this.hasPassword = !!uni.getStorageSync('passwordSet')
     this.loadShowToFamily()
     this.checkVendorStatus()
-    },
+    this.loadReferralCode()
+  },
   methods: {
+    /** 获取自己的邀请码用于分享 */
+    async loadReferralCode() {
+      if (this.referralCodeLoaded || !uni.getStorageSync('token')) return
+      try {
+        var res = await getReferralCode()
+        if (res && res.code === 200 && res.data) {
+          this.referralCode = res.data.referralCode || res.data.code || ''
+        }
+      } catch (e) {}
+      this.referralCodeLoaded = true
+    },
     async loadShowToFamily() {
       try {
         const uid = this.$store.state.userId || uni.getStorageSync('userId')
@@ -238,9 +251,13 @@ export default {
         }
       }
       const config = inviteTypes[type] || inviteTypes.family
+      var path = config.path
+      if (this.referralCode) {
+        path = path + '&inviteCode=' + this.referralCode
+      }
       this.$emit('invite-generate', {
         title: config.title,
-        path: config.path + '&invite_code=',
+        path: path,
         imageUrl: '/static/invite-card.png'
       })
       uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })

+ 33 - 9
cfc-frontend/pages/index-home/index.vue

@@ -397,7 +397,7 @@ import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 import DailyTaskCard from '../../components/daily-task-card.vue'
 import ReportUploadCard from '../../components/report-upload-card.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus, getSelfCheckStatus } from '../../utils/api.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus, getSelfCheckStatus, getReferralCode } from '../../utils/api.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
 import { parseDate } from '../../utils/format.js'
@@ -485,9 +485,13 @@ export default {
       // 会员状态(沙盘下方会员入口使用)
       isMember: false,
       isLifetime: false,
-      membershipDays: 0,
+membershipDays: 0,
+
+      // 分享邀请码
+      referralCode: '',
+      referralCodeLoaded: false
+
 
-      
     }
   },
   computed: {
@@ -623,6 +627,7 @@ export default {
     loadLoggedInData: function() {
       this.loadFamilyMembers()
       this.loadChildren()
+      this.loadReferralCode()
     },
     loadChildren: function() {
       var self = this
@@ -1233,26 +1238,45 @@ export default {
                 uni.showToast({ title: e.message || '加入失败', icon: 'none' })
               }
             }
-          }
-        })
-      }, 500)
+}
+      })
+    }, 500)
     },
-getImageUrl: function(path) {
+    /** 获取自己的邀请码用于分享 */
+    loadReferralCode: function() {
+      if (this.referralCodeLoaded || !this.isLoggedIn()) return
+      var self = this
+      getReferralCode().then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          self.referralCode = res.data.referralCode || res.data.code || ''
+        }
+      }).catch(function() {})
+      self.referralCodeLoaded = true
+    },
+    getImageUrl: function(path) {
       return config.API_BASE_URL + path
     },
     /** 转发给朋友/群 */
     onShareAppMessage: function() {
+      var path = '/pages/index-home/index'
+      if (this.referralCode) {
+        path = path + '?inviteCode=' + this.referralCode
+      }
       return {
         title: '浠艾福 · 家庭五维幸福成长',
-        path: '/pages/index-home/index',
+        path: path,
         imageUrl: '/static/share-cover.png'
       }
     },
     /** 分享到朋友圈 */
     onShareTimeline: function() {
+      var query = ''
+      if (this.referralCode) {
+        query = 'inviteCode=' + this.referralCode
+      }
       return {
         title: '浠艾福 · 家庭五维幸福成长',
-        query: '',
+        query: query,
         imageUrl: '/static/share-cover.png'
       }
     }

+ 130 - 466
cfc-frontend/pages/invite/join.vue

@@ -6,7 +6,7 @@
       <text class="loading-text">加载中...</text>
     </view>
 
-    <!-- 邀请信息卡片 -->
+    <!-- 邀请信息卡片(仅用于展示,不做交互) -->
     <view class="invite-card" v-if="!loading && inviteInfo">
       <view class="invite-header">
         <view class="invite-icon">🏠</view>
@@ -28,79 +28,16 @@
         </text>
       </view>
 
-      <!-- 未登录:显示登录按钮 -->
-      <view class="action-area" v-if="!isLoggedIn">
-        <button class="btn btn-primary" @tap="handleWechatLogin" :disabled="loginLoading">
-          <text v-if="!loginLoading">微信一键登录并加入家庭</text>
-          <text v-else>登录中...</text>
+      <view class="action-area">
+        <text class="tip-text">{{ tipText }}</text>
+        <button class="btn btn-primary" @tap="goNext" :disabled="redirecting">
+          <text v-if="!redirecting">{{ btnText }}</text>
+          <text v-else>跳转中...</text>
         </button>
-        <text class="tip-text">登录后即可申请加入此家庭</text>
-      </view>
-
-      <!-- 已登录:显示关系选择 -->
-      <view class="action-area" v-if="isLoggedIn && !accepted && !requestSubmitted">
-        <!-- 已有家庭时显示合并选项 -->
-        <view class="family-warning" v-if="familyStatus && familyStatus.inFamily">
-          <text class="warning-icon">⚠️</text>
-          <text class="warning-text">
-            您当前已加入「{{ familyStatus.familyName }}」家庭,加入新家庭可选择合并(成员将一起迁移)或拒绝
-          </text>
-        </view>
-
-        <!-- 关系选择 -->
-        <view class="form-group">
-          <text class="form-label">您在家庭中的身份</text>
-          <picker @change="onFamilyRoleChange" :value="familyRoleIndex" :range="familyRoleOptions">
-            <view class="picker-select">
-              <text>{{ familyRoleOptions[familyRoleIndex] || '请选择您的身份' }}</text>
-              <text class="picker-arrow">›</text>
-            </view>
-          </picker>
-        </view>
-
-        <!-- 已有家庭时显示合并勾选框 -->
-        <view class="form-group" v-if="familyStatus && familyStatus.inFamily">
-          <label class="checkbox-label" @tap="toggleMerge">
-            <view class="checkbox" :class="{ checked: mergeFamily }">
-              <text v-if="mergeFamily">✓</text>
-            </view>
-            <text>合并家庭(我的家庭成员将一起迁入新家庭)</text>
-          </label>
-        </view>
-
-        <button class="btn btn-primary" @tap="handleSubmitRequest" :disabled="acceptLoading">
-          <text v-if="!acceptLoading">提交申请</text>
-          <text v-else>提交中...</text>
-        </button>
-      </view>
-
-      <!-- 已提交申请,等待审批 -->
-      <view class="pending-card" v-if="requestSubmitted && !accepted">
-        <view class="pending-icon">🕐</view>
-        <text class="pending-title">申请已提交</text>
-        <text class="pending-text">等待邀请人确认</text>
-        <text class="pending-hint">通常几分钟内会有回复</text>
-        <button class="btn btn-outline btn-cancel-request" @tap="onCancelRequest">取消申请</button>
-      </view>
-
-      <!-- 申请被拒绝 -->
-      <view class="error-area" v-if="requestSubmitted && requestStatus === 'rejected'">
-        <view class="error-icon">❌</view>
-        <text class="error-text-main">申请已被拒绝</text>
-        <text class="error-text-sub" v-if="rejectMessage">{{ rejectMessage }}</text>
-        <button class="btn btn-outline" @tap="goHome">返回首页</button>
-      </view>
-
-      <!-- 加入成功 -->
-      <view class="success-area" v-if="accepted">
-        <view class="success-icon">✅</view>
-        <text class="success-text">加入成功!</text>
-        <text class="success-sub">欢迎加入「{{ inviteInfo.familyName }}」</text>
-        <button class="btn btn-primary" @tap="goHome">进入首页</button>
       </view>
     </view>
 
-    <!-- 推广码绑定卡片 -->
+    <!-- 推广码绑定卡片(兼容旧链接) -->
     <view class="invite-card" v-if="!loading && refCode && !bindDone">
       <view class="invite-header">
         <view class="invite-icon">🤝</view>
@@ -117,7 +54,7 @@
         <text class="tip-text">绑定后该推荐人后续消费时记入其家庭公共账户</text>
       </view>
       <view class="action-area" v-else>
-        <button class="btn btn-primary" @tap="handleWechatLogin" :disabled="loginLoading">
+        <button class="btn btn-primary" @tap="goLoginWithRef" :disabled="loginLoading">
           <text v-if="!loginLoading">微信一键登录</text>
           <text v-else>登录中...</text>
         </button>
@@ -145,341 +82,197 @@
 </template>
 
 <script>
-import { validateInvitation, checkUserFamily, bindReferral, requestJoinByCode, getMyPendingRequest, cancelMyJoinRequest } from '@/utils/api.js'
+import { validateInvitation, validateFamilyCode, checkUserFamily, bindReferral } from '@/utils/api.js'
 
 export default {
   data() {
     return {
-      token: '',
+      // 家庭邀请码(单一参数)
+      familyInviteCode: '',
+      // 推广码(兼容)
       refCode: '',
       bindLoading: false,
       bindDone: false,
       loading: true,
       loginLoading: false,
-      acceptLoading: false,
+      redirecting: false,
       isLoggedIn: false,
       inviteInfo: null,
       familyStatus: null,
-      familyRoleIndex: 0,
-      mergeFamily: false,
-      accepted: false,
-      requestSubmitted: false,
-      requestStatus: '',
-      requestId: null,
-      requestTimer: null,
-      rejectMessage: '',
       error: '',
-      familyRoleOptions: ['爸爸', '妈妈', '爷爷', '奶奶', '姥爷', '姥姥', '其他']
+      // 动态文案
+      tipText: '',
+      btnText: ''
     }
   },
   onLoad(query) {
-    // 邀请码(推荐人)统一解析:query.inviteCode / query.refCode / 场景值 ref=xxx
-    // 分享链接用 inviteCode,落地页内部用 refCode,二维码场景值用 ref= 前缀
-    var refCode = query.refCode || query.inviteCode || ''
-    if (!refCode && query.scene) {
-      var refScene = decodeURIComponent(query.scene)
-      if (refScene.indexOf('ref=') === 0) {
-        refCode = refScene.substring(4)
+    query = query || {}
+    
+    // 1. 解析家庭邀请码(单一参数,兼容 inviteCode/familyInviteCode)
+    var familyInviteCode = query.familyInviteCode || query.inviteCode || ''
+    if (!familyInviteCode && query.scene) {
+      var sceneDecoded = decodeURIComponent(query.scene)
+      // 支持 scene=fam_xxx 或 ref=xxx 格式
+      if (sceneDecoded.indexOf('fam_') === 0) {
+        familyInviteCode = sceneDecoded
+      } else if (sceneDecoded.indexOf('ref=') === 0) {
+        // ref=xxx 归为推广码
+      } else if (sceneDecoded.length >= 6) {
+        // 兜底:长度>=6 视为家庭邀请码
+        familyInviteCode = familyInviteCode || sceneDecoded
       }
     }
 
-    // token(家庭邀请):优先从 query 参数,其次从场景值
-    var token = query.token || query.inviteToken || ''
-    if (!token) {
-      var scene = query.scene
-      if (scene) {
-        var sceneDecoded = decodeURIComponent(scene)
-        // 新格式:JSON 场景值 {"t":"TOKEN"}
-        if (sceneDecoded.indexOf('{') === 0) {
-          try {
-            var sceneObj = JSON.parse(sceneDecoded)
-            token = sceneObj.t || sceneObj.token || ''
-          } catch (e) {
-            token = ''
-          }
-        } else if (sceneDecoded.indexOf('invite=') === 0) {
-          // 旧格式兼容
-          token = sceneDecoded.substring(7)
-        } else if (sceneDecoded.indexOf('ref=') === 0) {
-          // 推广码场景值(ref=xxx),已在上方解析为 refCode,不作为 token
-          token = ''
-        } else if (sceneDecoded.length === 6) {
-          // 裸邀请码场景值(InviteController 生成的二维码 scene=referralCode,6位)
-          // 归为推广码,不作为家庭 token
-          refCode = refCode || sceneDecoded
-          token = ''
-        } else {
-          token = sceneDecoded
-        }
+    // 2. 解析推广码(兼容旧链接)
+    var refCode = query.refCode || ''
+    if (!refCode && query.scene) {
+      var sceneDecoded = decodeURIComponent(query.scene)
+      if (sceneDecoded.indexOf('ref=') === 0) {
+        refCode = sceneDecoded.substring(4)
       }
     }
 
-    if (!token && !refCode) {
-      // 兼容旧分享链接(promotion/index.vue 旧版 onShareAppMessage 不带参数)
-      // 无有效邀请信息时跳转首页,不显示"邀请链接无效"死页面
+    // 3. 无任何有效参数 -> 首页
+    if (!familyInviteCode && !refCode) {
       uni.reLaunch({ url: '/pages/index-home/index' })
       return
     }
-    if (refCode) {
-      // 推广码模式:仅存储 refCode,无需校验邀请
-      this.refCode = refCode
-      this.loading = false
-      this.checkLoginStatus()
+
+    // 4. 有家庭邀请码 -> 存储并校验
+    if (familyInviteCode) {
+      this.familyInviteCode = familyInviteCode
+      // 存入本地,供登录页/个人信息页使用
+      uni.setStorageSync('pendingFamilyInviteCode', familyInviteCode)
+      this.validateFamilyInvite(familyInviteCode)
       return
     }
-    this.token = token
+
+    // 5. 仅有推广码 -> 走原有推广绑定流程
+    this.refCode = refCode
+    this.loading = false
     this.checkLoginStatus()
-    this.validateToken()
   },
   onShow() {
-    // 从登录页返回时重新检查登录状态
-    if (this.token) {
+    // 从登录页返回,重新检查
+    if (this.familyInviteCode) {
       this.checkLoginStatus()
       if (!this.inviteInfo) {
-        this.validateToken()
+        this.validateFamilyInvite(this.familyInviteCode)
       }
-    }
-  },
-  onUnload() {
-    if (this.requestTimer) {
-      clearInterval(this.requestTimer)
-      this.requestTimer = null
+    } else if (this.refCode) {
+      this.checkLoginStatus()
     }
   },
   methods: {
     checkLoginStatus() {
       var token = uni.getStorageSync('token')
       this.isLoggedIn = !!token
-      if (this.isLoggedIn) {
-        this.checkFamily()
-      }
     },
-    async validateToken() {
+
+    // 校验家庭邀请码有效性(仅获取家庭信息用于展示)
+    async validateFamilyInvite(code) {
       this.loading = true
       try {
-        var res = await validateInvitation(this.token)
-        if (res && res.data) {
+        var res = await validateFamilyCode(code)
+        if (res && res.code === 200) {
           this.inviteInfo = res.data
-          // 已登录用户进入落地页:回显已有的待确认申请(避免重复显示提交表单)
+          // 已登录用户:检查是否已在目标家庭
           if (this.isLoggedIn) {
-            this.checkExistingRequest()
+            await this.checkFamilyAndRedirect()
+          } else {
+            // 未登录:设置文案引导去登录
+            this.setRedirectInfo(false)
+            this.loading = false
           }
         } else {
           this.error = '邀请已失效'
+          this.loading = false
         }
       } catch (e) {
         this.error = e.message || '邀请验证失败'
+        this.loading = false
       }
-      this.loading = false
     },
-    async checkFamily() {
+
+    // 检查家庭状态并决定跳转
+    async checkFamilyAndRedirect() {
       try {
         var res = await checkUserFamily()
         if (res && res.data) {
           this.familyStatus = res.data
-          // 已在家庭内:同步 familyId 到 store(hasFamily getter 依赖)
+          // 同步 familyId
           if (res.data.inFamily && res.data.familyId) {
             this.$store && this.$store.commit('setFamilyId', res.data.familyId)
             uni.setStorageSync('familyId', res.data.familyId)
           }
-          // 如果已经在目标家庭,直接显示成功
-          if (this.inviteInfo && this.familyStatus.inFamily && this.familyStatus.familyId === this.inviteInfo.familyId) {
-            this.accepted = true
-          } else if (!this.familyStatus.inFamily && this.inviteInfo) {
-            // 已登录且不在目标家庭:回显已有申请状态,避免重复显示提交表单
-            this.checkExistingRequest()
+          // 已在目标家庭 -> 直接进首页
+          if (this.inviteInfo && this.familyStatus.inFamily && 
+              this.familyStatus.familyId === this.inviteInfo.familyId) {
+            this.setRedirectInfo(true)
+            this.loading = false
+            // 延迟跳转,让用户看到成功提示
+            setTimeout(() => this.goHome(), 1000)
+            return
           }
+          // 不在目标家庭 -> 去完善个人信息页提交申请
+          this.setRedirectInfo(false)
+          this.loading = false
+        } else {
+          this.setRedirectInfo(false)
+          this.loading = false
         }
       } catch (e) {
         console.log('检查家庭状态失败', e)
+        this.setRedirectInfo(false)
+        this.loading = false
       }
     },
-    async handleWechatLogin() {
-      this.loginLoading = true
-      try {
-        var that = this
-        // 微信登录
-        var loginRes = await new Promise(function(resolve, reject) {
-          uni.login({
-            provider: 'weixin',
-            success: function(res) {
-              resolve(res)
-            },
-            fail: function(err) {
-              reject(err)
-            }
-          })
-        })
-
-        var code = loginRes.code
-        if (!code) {
-          uni.showToast({ title: '登录失败', icon: 'none' })
-          this.loginLoading = false
-          return
-        }
 
-        // 调起微信登录
-        var { wechatLogin } = require('@/utils/api.js')
-        var res = await wechatLogin(code, {})
-        if (res && res.data) {
-          // 保存登录信息
-          var data = res.data
-          uni.setStorageSync('token', data.token)
-          uni.setStorageSync('userId', data.userId)
-          uni.setStorageSync('role', data.role)
-          uni.setStorageSync('currentRole', data.role)
-          uni.setStorageSync('isSwitchedChild', false)
-          uni.setStorageSync('isSwitchedTeacher', false)
-          if (data.openid) {
-            uni.setStorageSync('openid', data.openid)
-          }
-          that.$store && that.$store.commit('setToken', data.token)
-          that.$store && that.$store.commit('setFamilyId', data.familyId)
-
-          this.isLoggedIn = true
-          uni.showToast({ title: '登录成功', icon: 'success' })
-          // 兼容旧分享链接:登录成功后自动绑定推广码(与 login.vue 流程一致)
-          if (this.refCode && !this.bindDone) {
-            await this.bindRef()
-          }
-          // 重新检查家庭状态
-          await this.checkFamily()
-        }
-      } catch (e) {
-        uni.showToast({ title: '登录失败', icon: 'none' })
-        console.log('微信登录失败', e)
+    setRedirectInfo(isInTargetFamily) {
+      if (isInTargetFamily) {
+        this.tipText = '您已是该家庭成员'
+        this.btnText = '进入首页'
+      } else {
+        this.tipText = '完善个人信息后将自动提交加入申请'
+        this.btnText = '去完善信息'
       }
-      this.loginLoading = false
-    },
-    onFamilyRoleChange(e) {
-      this.familyRoleIndex = e.detail.value
-    },
-    toggleMerge() {
-      this.mergeFamily = !this.mergeFamily
     },
-    async handleSubmitRequest() {
-      var familyRole = this.familyRoleOptions[this.familyRoleIndex] || ''
-      if (!familyRole) {
-        uni.showToast({ title: '请选择您在家庭中的身份', icon: 'none' })
-        return
-      }
 
-      this.acceptLoading = true
-      try {
-        var res = await requestJoinByCode(this.token, familyRole)
-        if (res && res.code === 200) {
-          this.requestSubmitted = true
-          this.requestStatus = 'pending'
-          this.requestId = (res.data && res.data.id) || null
-          this.acceptLoading = false
-          uni.showToast({ title: '申请已提交,等待邀请人确认', icon: 'success' })
-          this.startPolling()
-        } else {
-          this.acceptLoading = false
-          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
-        }
-      } catch (e) {
-        this.acceptLoading = false
-        uni.showToast({ title: e.message || '提交失败', icon: 'none' })
-      }
-    },
-    startPolling() {
-      var self = this
-      if (this.requestTimer) {
-        clearInterval(this.requestTimer)
-      }
-      this.requestTimer = setInterval(function() {
-        if (self.accepted || self.requestStatus === 'rejected') {
-          clearInterval(self.requestTimer)
-          self.requestTimer = null
-          return
-        }
-        self.checkRequestStatus()
-      }, 5000)
-    },
-    async checkRequestStatus() {
-      try {
-        var res = await getMyPendingRequest()
-        if (res && res.code === 200 && res.data) {
-          var requestData = res.data
-          this.requestId = requestData.id || this.requestId
-          this.requestStatus = requestData.status || 'pending'
-          if (this.requestStatus === 'approved') {
-            clearInterval(this.requestTimer)
-            this.requestTimer = null
-            this.accepted = true
-            uni.setStorageSync('familyId', this.inviteInfo.familyId)
-            uni.setStorageSync('currentFamilyId', this.inviteInfo.familyId)
-            // 审批通过后同步 familyId 到 store(hasFamily getter 依赖)
-            this.$store && this.$store.commit('setFamilyId', this.inviteInfo.familyId)
-            uni.showToast({ title: '加入家庭成功', icon: 'success' })
-          } else if (this.requestStatus === 'rejected') {
-            clearInterval(this.requestTimer)
-            this.requestTimer = null
-            this.rejectMessage = requestData.adminComment || ''
-            uni.showToast({ title: '申请已被拒绝', icon: 'none' })
-          }
-        }
-      } catch (e) {
-        console.log('轮询请求状态失败', e)
+    // 统一跳转逻辑
+    goNext() {
+      if (this.redirecting) return
+      this.redirecting = true
+      
+      if (this.inviteInfo && this.familyStatus && this.familyStatus.inFamily && 
+          this.familyStatus.familyId === this.inviteInfo.familyId) {
+        this.goHome()
+      } else {
+        // 跳转个人信息完善页,带上邀请码参数
+        uni.navigateTo({ 
+          url: '/pages/user-edit/user-edit?familyInviteCode=' + this.familyInviteCode 
+        })
       }
     },
-    async checkExistingRequest() {
-      // 回显守卫:仅在已登录、未提交、未成功且不在目标家庭时查询
-      if (!this.inviteInfo || this.requestSubmitted || this.accepted) return
-      if (this.familyStatus && this.familyStatus.inFamily && this.familyStatus.familyId === this.inviteInfo.familyId) return
-      try {
-        var res = await getMyPendingRequest()
-        if (res && res.code === 200 && res.data && res.data.status === 'pending') {
-          var requestData = res.data
-          if (requestData.targetFamilyId && requestData.targetFamilyId !== this.inviteInfo.familyId) return
-          this.requestId = requestData.id || null
-          this.requestSubmitted = true
-          this.requestStatus = 'pending'
-          this.startPolling()
-        }
-      } catch (e) {
-        console.log('回显加入申请状态失败', e)
-      }
+
+    goHome() {
+      uni.reLaunch({ url: '/pages/index-home/index' })
     },
-    onCancelRequest() {
-      var self = this
-      uni.showModal({
-        title: '取消申请',
-        content: '确定取消本次加入申请吗?',
-        success: function(mr) {
-          if (mr.confirm) {
-            self.doCancelRequest()
-          }
-        }
+
+    // 登录页跳转(带上邀请码)
+    goLogin() {
+      uni.navigateTo({ 
+        url: '/pages/login/login?familyInviteCode=' + this.familyInviteCode 
       })
     },
-    async doCancelRequest() {
-      if (!this.requestId) {
-        uni.showToast({ title: '未找到申请记录', icon: 'none' })
-        return
-      }
-      try {
-        var res = await cancelMyJoinRequest(this.requestId)
-        if (res && res.code === 200) {
-          if (this.requestTimer) {
-            clearInterval(this.requestTimer)
-            this.requestTimer = null
-          }
-          this.requestSubmitted = false
-          this.requestStatus = ''
-          this.requestId = null
-          uni.showToast({ title: '已取消申请', icon: 'success' })
-        } else {
-          uni.showToast({ title: (res && res.message) || '取消失败', icon: 'none' })
-        }
-      } catch (e) {
-        uni.showToast({ title: e.message || '取消失败', icon: 'none' })
-      }
-    },
-    goHome() {
-      uni.reLaunch({ url: '/pages/index-home/index' })
+
+    // 推广码场景:去登录
+    goLoginWithRef() {
+      uni.navigateTo({ 
+        url: '/pages/login/login?refCode=' + this.refCode 
+      })
     },
+
+    // 推广码绑定
     async bindRef() {
       if (!this.refCode) {
         uni.showToast({ title: '推广码为空', icon: 'none' })
@@ -537,7 +330,7 @@ export default {
   background: #FFFFFF;
   border-radius: 24rpx;
   padding: 40rpx;
-  box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
+  box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 008);
   margin-top: 10vh;
 }
 
@@ -599,82 +392,13 @@ export default {
   display: flex;
   flex-direction: column;
   gap: 16rpx;
+  width: 100%;
 }
 
-.family-warning {
-  display: flex;
-  align-items: flex-start;
-  padding: 20rpx;
-  background: #FEF3C7;
-  border-radius: 12rpx;
-  gap: 12rpx;
-}
-
-.warning-icon {
-  font-size: 32rpx;
-  flex-shrink: 0;
-}
-
-.warning-text {
-  font-size: 24rpx;
-  color: #92400E;
-  line-height: 1.5;
-}
-
-.form-group {
-  margin-bottom: 20rpx;
-}
-
-.form-label {
-  display: block;
-  font-size: 28rpx;
-  color: #374151;
-  margin-bottom: 12rpx;
-  font-weight: 500;
-}
-
-.picker-select {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 24rpx 28rpx;
-  background: #F9FAFB;
-  border: 2rpx solid #E5E7EB;
-  border-radius: 16rpx;
-  font-size: 28rpx;
-  color: #1E293B;
-}
-
-.picker-arrow {
-  font-size: 36rpx;
-  color: #9CA3AF;
-}
-
-.checkbox-label {
-  display: flex;
-  align-items: center;
-  gap: 12rpx;
-  font-size: 26rpx;
-  color: #4B5563;
-}
-
-.checkbox {
-  width: 36rpx;
-  height: 36rpx;
-  border: 2rpx solid #D1D5DB;
-  border-radius: 8rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
-}
-
-.checkbox.checked {
-  background: #F97316;
-  border-color: #F97316;
-  color: #FFFFFF;
+.tip-text {
   font-size: 24rpx;
-  font-weight: 700;
+  color: #94A3B8;
+  text-align: center;
 }
 
 .btn {
@@ -693,7 +417,7 @@ export default {
 }
 
 .btn-primary:disabled {
-  opacity: 0.6;
+  opacity: 06;
 }
 
 .btn-outline {
@@ -702,12 +426,6 @@ export default {
   border: 2rpx solid #F97316;
 }
 
-.tip-text {
-  font-size: 24rpx;
-  color: #94A3B8;
-  text-align: center;
-}
-
 .success-area {
   display: flex;
   flex-direction: column;
@@ -753,58 +471,4 @@ export default {
   color: #6B7280;
   text-align: center;
 }
-
-.pending-card {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 12rpx;
-  padding: 40rpx;
-  background: #FEF3C7;
-  border-radius: 20rpx;
-  margin-top: 20rpx;
-}
-
-.pending-icon {
-  font-size: 72rpx;
-}
-
-.pending-title {
-  font-size: 32rpx;
-  font-weight: 700;
-  color: #92400E;
-}
-
-.pending-text {
-  font-size: 28rpx;
-  color: #78350F;
-}
-
-.pending-hint {
-  font-size: 24rpx;
-  color: #A16207;
-}
-
-.error-area {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 12rpx;
-  padding: 40rpx;
-  background: #FEF2F2;
-  border-radius: 20rpx;
-  margin-top: 20rpx;
-}
-
-.error-text-main {
-  font-size: 32rpx;
-  font-weight: 700;
-  color: #DC2626;
-}
-
-.error-text-sub {
-  font-size: 26rpx;
-  color: #991B1B;
-  margin-bottom: 20rpx;
-}
-</style>
+</style>

+ 49 - 2
cfc-frontend/pages/login/login.vue

@@ -87,7 +87,7 @@
 </template>
 
 <script>
-import { wechatPhoneLogin, checkTeamInviteStatus, joinTeamByInvite, bindReferral } from '../../utils/api.js'
+import { wechatPhoneLogin, checkTeamInviteStatus, joinTeamByInvite, bindReferral, bindFamilyInviter } from '../../utils/api.js'
 
 export default {
   data() {
@@ -100,11 +100,20 @@ export default {
     }
   },
   onLoad(options) {
-      // 处理邀请码
+      options = options || {}
+      // 处理家庭邀请码(新流程:仅首次注册时绑定邀请人)
+      if (options.familyInviteCode) {
+        uni.setStorageSync('pendingFamilyInviteCode', options.familyInviteCode)
+      }
+      // 兼容旧邀请码参数
       if (options.inviteCode) {
         uni.setStorageSync('inviteCode', options.inviteCode)
         uni.setStorageSync('inviteType', options.inviteType || 'family')
       }
+      // 推广码
+      if (options.refCode) {
+        uni.setStorageSync('pendingRefCode', options.refCode)
+      }
       // 存储登录后跳转地址
       if (options.redirect) {
         this.redirectUrl = decodeURIComponent(options.redirect)
@@ -184,12 +193,50 @@ export default {
       self.$store.commit('setFamilyId', data.familyId)
       uni.showToast({ title: '登录成功', icon: 'success' })
       self.loading = false
+
+      // 判断是否首次注册(后端返回 needsRoleSelect 或无 familyId)
+      var isNewUser = data.needsRoleSelect === true || !data.familyId
+
+      // 处理家庭邀请码(仅首次注册时绑定邀请人)
+      var familyInviteCode = uni.getStorageSync('pendingFamilyInviteCode')
+      if (familyInviteCode && isNewUser) {
+        // 新用户注册:绑定家庭邀请人
+        self.bindFamilyInviter(familyInviteCode).then(function() {
+          uni.removeStorageSync('pendingFamilyInviteCode')
+          self.handlePostLoginRouting()
+        }).catch(function() {
+          uni.removeStorageSync('pendingFamilyInviteCode')
+          self.handlePostLoginRouting()
+        })
+        return
+      }
+
+      // 非新用户或无家庭邀请码,清理存储并走原有流程
+      if (familyInviteCode) {
+        uni.removeStorageSync('pendingFamilyInviteCode')
+      }
+      
       if (data.needsRoleSelect) {
         uni.redirectTo({ url: '/pages/user-edit/user-edit?token=' + data.token + '&userId=' + data.userId + '&autoParent=true' })
       } else {
         self.handlePostLoginRouting()
       }
     },
+
+    // 绑定家庭邀请人(首次注册调用)
+    bindFamilyInviter: function(code) {
+      var self = this
+      return bindFamilyInviter(code).then(function(res) {
+        if (res && res.code === 200) {
+          console.log('首次注册绑定家庭邀请人成功', code)
+          uni.showToast({ title: '已关联邀请家庭', icon: 'success' })
+        } else if (res && res.code === 400) {
+          console.log('已有邀请人,跳过绑定')
+        }
+      }).catch(function(e) {
+        console.log('绑定家庭邀请人失败', e)
+      })
+    },
     handlePostLoginRouting() {
       const inviteCode = uni.getStorageSync('inviteCode')
       const inviteType = uni.getStorageSync('inviteType')

+ 25 - 0
cfc-frontend/pages/profile-extra/family-members.vue

@@ -203,6 +203,31 @@ export default {
       })
     }
   },
+  onShareTimeline() {
+    var self = this
+    var title = '邀请你加入「' + (this.familyName || '我的家庭') + '」'
+    var buildResult = function(token) {
+      return {
+        title: title,
+        query: 'token=' + (token || ''),
+        imageUrl: ''
+      }
+    }
+    if (this.inviteToken) {
+      return buildResult(this.inviteToken)
+    }
+    // 令牌未就绪时异步获取
+    return {
+      title: title,
+      query: '',
+      imageUrl: '',
+      promise: new Promise(function(resolve) {
+        self.prepareInviteToken(function(token) {
+          resolve(token ? buildResult(token) : { query: '', imageUrl: '' })
+        })
+      })
+    }
+  },
   methods: {
     async loadMembers() {
       this.loading = true

+ 41 - 2
cfc-frontend/pages/profile/index.vue

@@ -21,6 +21,7 @@
 <script>
 import ProfileHeader from '../../components/ProfileHeader.vue'
 import ProfileMenu from '../../components/ProfileMenu.vue'
+import { getReferralCode } from '../../utils/api.js'
 
 export default {
   components: {
@@ -30,7 +31,9 @@ export default {
   data() {
     return {
       shareData: null,
-      fromWealth: false
+      fromWealth: false,
+      referralCode: '',
+      referralCodeLoaded: false
     }
   },
   computed: {
@@ -47,16 +50,52 @@ export default {
         this.fromWealth = true
       }
     },
+  onShow() {
+    if (this.isLoggedIn) {
+      this.loadReferralCode()
+    }
+  },
     onShareAppMessage() {
     if (this.shareData) {
+      var path = this.shareData.path
+      if (this.referralCode && path.indexOf('inviteCode=') === -1) {
+        var sep = path.indexOf('?') === -1 ? '?' : '&'
+        path = path + sep + 'inviteCode=' + this.referralCode
+      }
       return {
         title: this.shareData.title,
-        path: this.shareData.path,
+        path: path,
         imageUrl: '/static/invite-card.png'
       }
     }
   },
+  onShareTimeline() {
+    var query = ''
+    if (this.shareData && this.shareData.path) {
+      var qi = this.shareData.path.indexOf('?')
+      if (qi !== -1) query = this.shareData.path.substring(qi + 1)
+    }
+    if (this.referralCode && query.indexOf('inviteCode=') === -1) {
+      query = query + (query ? '&' : '') + 'inviteCode=' + this.referralCode
+    }
+    return {
+      title: this.shareData ? this.shareData.title : '浠艾福 — 给全家的一站式幸福提案',
+      query: query,
+      imageUrl: '/static/invite-card.png'
+    }
+  },
   methods: {
+    /** 获取自己的邀请码用于分享 */
+    async loadReferralCode() {
+      if (this.referralCodeLoaded || !this.isLoggedIn) return
+      try {
+        var res = await getReferralCode()
+        if (res && res.code === 200 && res.data) {
+          this.referralCode = res.data.referralCode || res.data.code || ''
+        }
+      } catch (e) {}
+      this.referralCodeLoaded = true
+    },
     goToWealth() {
       uni.navigateTo({ url: '/pages/wealth/index' })
     },

+ 32 - 4
cfc-frontend/pages/profile/report-detail.vue

@@ -32,13 +32,15 @@
 </template>
 
 <script>
-import { getReportSummaryDetail, downloadReportSummary } from '../../utils/api.js'
+import { getReportSummaryDetail, downloadReportSummary, getReferralCode } from '../../utils/api.js'
 
 export default {
   data() {
     return {
       reportId: null,
-      detail: {}
+      detail: {},
+      referralCode: '',
+      referralCodeLoaded: false
     }
   },
   onLoad(options) {
@@ -46,14 +48,40 @@ export default {
       this.reportId = parseInt(options.id)
       this.loadDetail()
     }
+    this.loadReferralCode()
   },
   onShareAppMessage() {
+    var path = '/pages/profile/report-detail?id=' + this.reportId
+    if (this.referralCode) {
+      path = path + '&inviteCode=' + this.referralCode
+    }
     return {
-      title: '浠艾福报告',
-      path: '/pages/profile/report-detail?id=' + this.reportId
+      title: this.detail.title || '浠艾福报告',
+      path: path
+    }
+  },
+  onShareTimeline() {
+    var query = 'id=' + this.reportId
+    if (this.referralCode) {
+      query = query + '&inviteCode=' + this.referralCode
+    }
+    return {
+      title: this.detail.title || '浠艾福报告',
+      query: query
     }
   },
   methods: {
+    /** 获取自己的邀请码用于分享 */
+    async loadReferralCode() {
+      if (this.referralCodeLoaded || !uni.getStorageSync('token')) return
+      try {
+        var res = await getReferralCode()
+        if (res && res.code === 200 && res.data) {
+          this.referralCode = res.data.referralCode || res.data.code || ''
+        }
+      } catch (e) {}
+      this.referralCodeLoaded = true
+    },
     loadDetail() {
       var self = this
       getReportSummaryDetail(self.reportId).then(function(res) {

+ 11 - 0
cfc-frontend/pages/promotion/index.vue

@@ -282,6 +282,17 @@ export default {
       imageUrl: ''
     }
   },
+  onShareTimeline() {
+    var query = ''
+    if (this.shareReferralCode) {
+      query = 'inviteCode=' + this.shareReferralCode
+    }
+    return {
+      title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
+      query: query,
+      imageUrl: ''
+    }
+  },
   methods: {
     async ensureShareCode() {
       if (this.shareReferralCode) return

+ 12 - 0
cfc-frontend/pages/promotion/invite.vue

@@ -137,6 +137,18 @@ export default {
       imageUrl: ''
     }
   },
+  onShareTimeline() {
+    var query = ''
+    var code = this.referralCode || ''
+    if (code) {
+      query = 'inviteCode=' + code
+    }
+    return {
+      title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
+      query: query,
+      imageUrl: ''
+    }
+  },
   methods: {
     async loadCode() {
       try {

+ 52 - 6
cfc-frontend/pages/user-edit/user-edit.vue

@@ -223,6 +223,18 @@
 				</view>
 			</view>
 		</view>
+		<!-- 家庭角色选择(新用户流程、角色为家长、有家庭邀请码时显示,可选) -->
+		<view class="family-section" v-if="isEdit && form.role === 'parent' && isNewUserFlow && hasPendingFamilyInvite">
+			<view class="section-title">👨‍👩‍👧‍👦 家庭身份(可选)</view>
+			<view class="form-item">
+				<text class="label">您在家庭中的身份</text>
+				<picker mode="selector" :range="familyRoleOptions" :value="familyRoleIndex" @change="onFamilyRoleChange">
+					<view class="picker-input">{{ form.familyRole || '请选择(可跳过)' }}</view>
+				</picker>
+			</view>
+			<text class="hint-text">不选也可先保存,后续在家庭页面发起申请</text>
+		</view>
+
 		<!-- 免费用户提示 -->
 		<view class="family-section" v-if="isEdit && memberLevel === 'FREE'">
 			<view class="mascot-locked-hint" @click="goUpgrade">
@@ -232,7 +244,7 @@
 			</view>
 		</view>
 
-		<!-- 新用户流程:保存资料 / 跳过(不再创建或加入家庭) -->
+		<!-- 新用户流程:保存资料 / 跳过 -->
 		<view class="family-section" v-if="isEdit && form.role !== 'teacher' && isNewUserFlow">
 			<view class="family-actions">
 				<button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
@@ -323,6 +335,7 @@ export default {
 				birthday: '',
 				birthHour: '',
 				inviteCode: '',
+				familyRole: '', // 家庭角色:爸爸/妈妈/爷爷/奶奶/姥爷/姥姥/其他(可选)
 				mascot: '',
 				phone: '',
 				teacherNo: '',
@@ -356,6 +369,7 @@ export default {
 			birthHourValues: ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'],
 			educationOptions: ['小学','初中','高中','中专','大专','本科','硕士','博士'],
 			maritalOptions: ['未婚','已婚','离异','丧偶'],
+			familyRoleOptions: ['爸爸', '妈妈', '爷爷', '奶奶', '姥爷', '姥姥', '其他'],
 			loading: false,
 			userId: '',
 			token: ''
@@ -377,10 +391,22 @@ export default {
     maritalIndex() {
       return this.maritalOptions.indexOf(this.form.maritalStatus)
     },
+    familyRoleIndex: {
+      get() {
+        return this.familyRoleOptions.indexOf(this.form.familyRole)
+      },
+      set(val) {
+        this.form.familyRole = this.familyRoleOptions[val] || ''
+      }
+    },
     hasFamily: function() {
       if (this.token) return true
       return this.$store.getters.hasFamily
     },
+    // 是否有待处理的家庭邀请码
+    hasPendingFamilyInvite: function() {
+      return !!uni.getStorageSync('pendingFamilyInviteCode')
+    },
     regionBreadcrumb: function() {
       var names = []
       names.push(this.tempProvince ? this.getRegionName(this.tempProvince) : '省份')
@@ -725,6 +751,24 @@ export default {
 				district: this.form.district,
 				street: this.form.street
 			})
+			
+			// 新用户流程且有家庭邀请码:提交加入家庭申请
+			if (this.isNewUserFlow && this.hasPendingFamilyInvite) {
+				var inviteCode = uni.getStorageSync('pendingFamilyInviteCode')
+				var familyRole = this.form.familyRole || ''
+				if (inviteCode) {
+					try {
+						await requestJoinByCode(inviteCode, familyRole)
+						uni.showToast({ title: '加入家庭申请已提交,等待管理员审批', icon: 'success' })
+					} catch (e) {
+						uni.showToast({ title: e.message || '申请提交失败,可稍后在家庭页重试', icon: 'none' })
+					}
+					uni.removeStorageSync('pendingFamilyInviteCode')
+				}
+				setTimeout(() => {
+					this.navigateToHome()
+				}, 1500)
+			} else {
 				uni.showToast({ title: '保存成功', icon: 'success' })
 				setTimeout(() => {
 					if (this.isNewUserFlow) {
@@ -733,12 +777,13 @@ export default {
 						uni.navigateBack()
 					}
 				}, 1500)
-			} catch (e) {
-				uni.showToast({ title: '保存失败', icon: 'none' })
-			} finally {
-				this.loading = false
 			}
-		},
+		} catch (e) {
+			uni.showToast({ title: '保存失败', icon: 'none' })
+		} finally {
+			this.loading = false
+		}
+	},
 		async submitTeacherInfo() {
 			// 验证必填字段
 			if (!this.form.nickname) {
@@ -877,6 +922,7 @@ export default {
 			}
 		},
 		skip() {
+			// 跳过时不清理邀请码,用户可后续重新进入编辑页提交申请
 			this.navigateToHome()
 		},
 		navigateToHome() {

+ 11 - 1
cfc-frontend/utils/api.js

@@ -1211,6 +1211,11 @@ export const validateInvitation = (token) => {
   return request('/api/family/invite/validate', 'POST', { token })
 }
 
+// 验证家庭短邀请码(落地页展示用)
+export const validateFamilyCode = (inviteCode) => {
+  return request('/api/family/invite/validate-family-code', 'POST', { inviteCode })
+}
+
 // 接受邀请(加入家庭)
 export const acceptInvitation = (token, familyRole, mergeFamily) => {
   return request('/api/family/invite/accept', 'POST', { token, familyRole, mergeFamily: !!mergeFamily })
@@ -1528,11 +1533,16 @@ export const getShareQrCode = (page, scene) => {
   return request('/api/share/qrcode', 'POST', { page, scene })
 }
 
-// 绑定推荐人
+// 绑定推荐人(推广码/推荐人佣金绑定)
 export const bindReferral = (referralCode) => {
   return request('/api/invite/bind', 'POST', { referralCode })
 }
 
+// 绑定家庭邀请人(首次注册时关联邀请家庭,仅新用户调用)
+export const bindFamilyInviter = (familyInviteCode) => {
+  return request('/api/family/invite/bind-inviter', 'POST', { familyInviteCode })
+}
+
 // 我的邀请列表
 export const getReferralList = (page = 1, size = 10) => {
   return request('/api/invite/list', 'POST', { page, size })

+ 191 - 0
docs/superpowers/api/API_REFERENCE.md

@@ -176,6 +176,197 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/family/invite/check-family` | 检查家庭状态 | — |
 | `POST /api/family/invite/revoke` | 撤销邀请 | — |
 | `POST /api/family/invite/transfer-admin` | 转让管理员 | — |
+| `POST /api/family/invite/validate-family-code` | **验证家庭短邀请码(落地页展示用,不创建请求)** | `inviteCode` |
+| `POST /api/family/invite/bind-inviter` | **绑定家庭邀请人(仅首次注册调用)** | `familyInviteCode` |
+
+### 4.2 Family Invite 接口详细
+
+#### `POST /api/family/invite/request-join-by-code` — 申请加入家庭
+
+**说明**:用户通过邀请码/令牌提交加入家庭的申请,等待邀请人审批。
+
+**请求体**:
+```json
+{
+  "inviteCode": "fam_abc123",      // 必填,邀请码或令牌
+  "familyRole": "爸爸"              // 可选,用户在目标家庭中的身份
+}
+```
+
+**响应**:
+```json
+{
+  "code": 200,
+  "message": "申请已提交,等待邀请人确认",
+  "data": {
+    "requestId": 12345,
+    "familyName": "张三的家庭"
+  }
+}
+```
+
+---
+
+#### `POST /api/family/invite/bind-inviter` — 绑定家庭邀请人
+
+**说明**:新用户首次注册时调用,建立「被邀请人 → 邀请人」关系,用于追踪邀请来源。**仅首次注册调用**,已有家庭邀请人的用户调用会返回失败。
+
+**请求体**:
+```json
+{
+  "familyInviteCode": "fam_abc123"
+}
+```
+
+**响应**:
+```json
+{
+  "code": 200,
+  "message": "绑定成功",
+  "data": null
+}
+```
+
+**错误码**:
+| code | message | 处理建议 |
+|------|---------|----------|
+| 400 | 用户已有家庭邀请人 | 跳过,已在目标家庭中 |
+| 404 | 邀请码无效 | 提示用户链接无效 |
+
+---
+
+#### `POST /api/family/invite/validate` — 验证邀请令牌
+
+**说明**:邀请落地页使用,验证 token 有效性并返回家庭信息(仅供展示)。
+
+**请求体**:
+```json
+{
+  "token": "abc123def456"
+}
+```
+
+**响应**:
+```json
+{
+  "code": 200,
+  "data": {
+    "familyId": 100,
+    "familyName": "张三的家庭",
+    "inviterName": "张三",
+    "inviterAvatar": "/static/avatar.jpg",
+    "memberCount": 3
+  }
+}
+```
+
+---
+
+#### `POST /api/family/invite/check-family` — 检查用户家庭状态
+
+**请求体**:无额外参数(从 JWT 取 userId)
+
+**响应**:
+```json
+{
+  "code": 200,
+  "data": {
+    "inFamily": true,
+    "familyId": 100,
+    "familyName": "张三的家庭",
+    "isAdmin": true
+  }
+}
+```
+
+---
+
+#### `POST /api/family/invite/pending-requests` — 查询待我处理的申请
+
+**说明**:邀请人视角,查询他人提交给自己的加入申请。
+
+**请求体**:无额外参数
+
+**响应**:
+```json
+{
+  "code": 200,
+  "data": [
+    {
+      "id": 1001,
+      "requesterUserId": 200,
+      "requesterNickname": "李四",
+      "requesterAvatar": "/static/avatar.jpg",
+      "familyRole": "妈妈",
+      "status": "pending",
+      "createdAt": "2026-08-31T10:00:00"
+    }
+  ]
+}
+```
+
+---
+
+#### `POST /api/family/invite/approve-request` — 批准加入申请
+
+**请求体**:
+```json
+{
+  "requestId": 1001,
+  "comment": ""
+}
+```
+
+**响应**:
+```json
+{
+  "code": 200,
+  "message": "已同意"
+}
+```
+
+---
+
+#### `POST /api/family/invite/reject-request` — 拒绝加入申请
+
+**请求体**:
+```json
+{
+  "requestId": 1001,
+  "comment": "不符合条件"
+}
+```
+
+**响应**:
+```json
+{
+  "code": 200,
+  "message": "已拒绝"
+}
+```
+
+---
+
+#### `POST /api/family/invite/my-request` — 查询我的申请
+
+**请求体**:无额外参数
+
+**响应**:
+```json
+{
+  "code": 200,
+  "data": {
+    "id": 1001,
+    "targetFamilyId": 100,
+    "targetFamilyName": "张三的家庭",
+    "familyRole": "爸爸",
+    "status": "pending",
+    "createdAt": "2026-08-31T10:00:00"
+  }
+}
+```
+
+**status 枚举**:`pending` | `approved` | `rejected`
 
 ### 4.3 任务(`/api/tasks`)