Преглед на файлове

feat: 家庭邀请流程改为申请审批制

- 后端: InviteValidateDTO 加 inviterAvatar; FamilyInvitationService 返回头像
- 后端: requestJoinByCode 兼容 token/inviteCode/familyId 三种输入
- 后端: FamilyJoinRequestService.createJoinRequest 支持 UUID token 识别
- 前端: invite/join.vue 展示邀请人头像, 改为提交申请+轮询审批流程
- 前端: FamilyMemberStrip.vue 加 isAdmin prop、红点徽章、审批弹窗(通过/拒绝)
- 前端: api.js 补充 pending-requests/approve-request/reject-request/my-request API
Xiaogang Liao преди 1 месец
родител
ревизия
bdedf86b46

+ 15 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java

@@ -99,7 +99,7 @@ public class FamilyInviteController {
     }
 
     /**
-     * 通过邀请码提交加入家庭请求(需管理员审批)
+     * 通过邀请码/令牌提交加入家庭请求(需管理员审批)
      */
     @Operation(summary = "通过邀请码申请加入家庭")
     @PostMapping("/request-join-by-code")
@@ -110,6 +110,20 @@ public class FamilyInviteController {
         }
 
         String inviteCode = body.get("inviteCode");
+
+        // 兼容邀请令牌(token):通过 token 解析到 familyId
+        if (inviteCode == null || inviteCode.trim().isEmpty()) {
+            String token = body.get("token");
+            if (token != null && !token.trim().isEmpty()) {
+                try {
+                    InviteValidateDTO dto = familyInvitationService.validateInvitation(token);
+                    inviteCode = String.valueOf(dto.getFamilyId());
+                } catch (Exception e) {
+                    return Result.error("邀请令牌无效");
+                }
+            }
+        }
+
         if (inviteCode == null || inviteCode.trim().isEmpty()) {
             return Result.error("邀请码不能为空");
         }

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/InviteValidateDTO.java

@@ -8,5 +8,6 @@ public class InviteValidateDTO {
     private Long familyId;
     private String familyName;
     private String inviterName;     // 邀请人昵称
+    private String inviterAvatar;   // 邀请人头像
     private Integer memberCount;
 }

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyInvitationService.java

@@ -72,6 +72,7 @@ public class FamilyInvitationService {
         dto.setFamilyId(invitation.getFamilyId());
         dto.setFamilyName(family.getName());
         dto.setInviterName(inviter != null ? inviter.getNickname() : null);
+        dto.setInviterAvatar(inviter != null ? inviter.getAvatar() : null);
         dto.setMemberCount(memberCount.intValue());
         return dto;
     }

+ 41 - 7
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java

@@ -2,9 +2,11 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.FamilyJoinRequestDTO;
+import com.etotem.cfc.entity.FamilyInvitation;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.FamilyJoinRequest;
 import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyInvitationMapper;
 import com.etotem.cfc.mapper.FamilyJoinRequestMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.UserMapper;
@@ -20,6 +22,9 @@ import java.util.stream.Collectors;
 @Service
 public class FamilyJoinRequestService {
 
+    @Resource
+    private FamilyInvitationMapper familyInvitationMapper;
+
     @Resource
     private FamilyJoinRequestMapper familyJoinRequestMapper;
 
@@ -33,13 +38,42 @@ public class FamilyJoinRequestService {
      * 创建家庭加入请求(待审批)
      */
     public FamilyJoinRequest createJoinRequest(Long userId, String inviteCode) {
-        // 查找家庭
-        Family family = familyMapper.selectOne(
-                new LambdaQueryWrapper<Family>()
-                        .eq(Family::getInviteCode, inviteCode.trim())
-        );
-        if (family == null) {
-            throw new RuntimeException("邀请码无效");
+        Family family;
+
+        // 支持传入 familyId(数字)作为 inviteCode
+        try {
+            Long familyId = Long.valueOf(inviteCode.trim());
+            family = familyMapper.selectById(familyId);
+            if (family == null) {
+                throw new RuntimeException("邀请码无效");
+            }
+        } catch (NumberFormatException e) {
+            // 支持邀请令牌(32位UUID hex字符串)
+            String trimmed = inviteCode.trim();
+            if (trimmed.length() == 32 && trimmed.matches("[0-9a-fA-F]+")) {
+                FamilyInvitation invitation = familyInvitationMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyInvitation>()
+                                .eq(FamilyInvitation::getToken, trimmed)
+                                .eq(FamilyInvitation::getStatus, "active")
+                                .and(w -> w.isNull(FamilyInvitation::getExpiresAt)
+                                        .or().gt(FamilyInvitation::getExpiresAt, new Date()))
+                );
+                if (invitation == null) {
+                    throw new RuntimeException("邀请令牌无效或已过期");
+                }
+                family = familyMapper.selectById(invitation.getFamilyId());
+                if (family == null) {
+                    throw new RuntimeException("邀请令牌无效");
+                }
+            } else {
+                family = familyMapper.selectOne(
+                        new LambdaQueryWrapper<Family>()
+                                .eq(Family::getInviteCode, trimmed)
+                );
+                if (family == null) {
+                    throw new RuntimeException("邀请码无效");
+                }
+            }
         }
 
         User currentUser = userMapper.selectById(userId);

+ 245 - 12
cfc-frontend/components/FamilyMemberStrip.vue

@@ -1,5 +1,12 @@
 <template>
-  <view class="family-member-strip" v-if="members && members.length > 0">
+  <view class="family-member-strip" v-if="(members && members.length > 0) || isAdmin">
+    <!-- 待审批加入请求徽章 -->
+    <view class="strip-toolbar" v-if="isAdmin">
+      <view class="pending-badge" v-if="pendingCount > 0" @tap="openRequests">
+        <text class="badge-icon">🔔</text>
+        <text class="badge-count">{{ pendingCount }}</text>
+      </view>
+    </view>
     <scroll-view
       class="strip-scroll"
       scroll-x
@@ -33,46 +40,69 @@
     </view>
 
     <!-- 长按操作菜单 -->
-    <view class="action-overlay" v-if="showActionMenu" @click="closeMenu"></view>
+    <view class="action-overlay" v-if="showActionMenu" @tap="closeMenu"></view>
     <view class="action-sheet" v-if="showActionMenu">
       <view class="action-header">
         <text class="action-title">{{ actionMember ? actionMember.nickname : '' }}</text>
         <text class="action-relation">{{ actionMember ? (actionMember.relationship || actionMember.relativeLabel || '') : '' }}</text>
       </view>
-      <view class="action-item" @click="switchToMember" v-if="canSwitch">
+      <view class="action-item" @tap="switchToMember" v-if="canSwitch">
         <text class="action-item-icon">🔄</text>
         <text class="action-item-text">切换到该成员视角</text>
       </view>
-      <view class="action-item" @click="editMember" v-if="canEdit">
+      <view class="action-item" @tap="editMember" v-if="canEdit">
         <text class="action-item-icon">✏️</text>
         <text class="action-item-text">编辑资料</text>
       </view>
-      <view class="action-item" @click="questionnaireMember">
+      <view class="action-item" @tap="questionnaireMember">
         <text class="action-item-icon">📋</text>
         <text class="action-item-text">关系问卷</text>
       </view>
-      <view class="action-item" @click="interactionMember">
+      <view class="action-item" @tap="interactionMember">
         <text class="action-item-icon">💬</text>
         <text class="action-item-text">记录互动</text>
       </view>
-      <view class="action-item" @click="callMember" v-if="actionMember && actionMember.phone">
+      <view class="action-item" @tap="callMember" v-if="actionMember && actionMember.phone">
         <text class="action-item-icon">📞</text>
         <text class="action-item-text">拨打电话</text>
       </view>
-      <view class="action-item action-danger" @click="removeMember" v-if="canRemove">
+      <view class="action-item action-danger" @tap="removeMember" v-if="canRemove">
         <text class="action-item-icon">🗑️</text>
         <text class="action-item-text">移除成员</text>
       </view>
-      <view class="action-item action-cancel" @click="closeMenu">
+      <view class="action-item action-cancel" @tap="closeMenu">
         <text class="action-item-text">取消</text>
       </view>
     </view>
+
+    <!-- 待审批请求弹窗 -->
+    <view class="request-overlay" v-if="showRequests" @tap="closeRequests"></view>
+    <view class="request-sheet" v-if="showRequests">
+      <view class="sheet-header">
+        <text class="sheet-title">待处理加入申请({{ pendingRequests.length }})</text>
+        <text class="sheet-close" @tap="closeRequests">×</text>
+      </view>
+      <view v-if="pendingRequests.length === 0" class="empty-requests">
+        <text>暂无待处理的请求</text>
+      </view>
+      <view class="request-item" v-for="req in pendingRequests" :key="req.id">
+        <image class="req-avatar" src="/static/default-avatar.png" mode="aspectFill"></image>
+        <view class="req-info">
+          <text class="req-name">{{ req.requesterNickname }}</text>
+          <text class="req-time">{{ req.createdAt }}</text>
+        </view>
+        <view class="req-actions">
+          <button class="req-btn btn-approve" @tap="approveRequest(req.id)">通过</button>
+          <button class="req-btn btn-reject" @tap="rejectRequest(req.id)">拒绝</button>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
 <script>
 import FamilyMemberCard from '@/components/FamilyMemberCard'
-import { kickFamilyMemberById } from '@/utils/api'
+import { kickFamilyMemberById, getPendingFamilyRequests, approveFamilyJoinRequest, rejectFamilyJoinRequest } from '@/utils/api'
 
 export default {
   components: { FamilyMemberCard },
@@ -80,14 +110,21 @@ export default {
     members: { type: Array, default: function() { return [] } },
     selectedMemberId: { type: Number, default: null },
     energyMap: { type: Object, default: function() { return {} } },
-    isParent: { type: Boolean, default: false }
+    isParent: { type: Boolean, default: false },
+    isAdmin: { type: Boolean, default: false }
   },
   data: function() {
     return {
       showActionMenu: false,
-      actionMember: null
+      actionMember: null,
+      pendingCount: 0,
+      pendingRequests: [],
+      showRequests: false
     }
   },
+  mounted: function() {
+    this.loadPendingRequests()
+  },
   computed: {
     canSwitch: function() {
       // 仅家庭成员记录(source=family_member)且已关联账号时才能切换视角;
@@ -183,6 +220,67 @@ export default {
     getMemberEnergy: function(member) {
       if (!member || !this.energyMap) return null
       return this.energyMap[member.id] || this.energyMap[member.memberId] || null
+    },
+    loadPendingRequests: function() {
+      var self = this
+      if (!this.isAdmin) return
+      getPendingFamilyRequests()
+        .then(function(res) {
+          if (res.code === 200) {
+            var data = res.data || []
+            self.pendingCount = data.length
+            self.pendingRequests = data
+          }
+        })
+        .catch(function() {
+          // 静默失败
+        })
+    },
+    openRequests: function() {
+      this.showRequests = true
+    },
+    closeRequests: function() {
+      this.showRequests = false
+    },
+    approveRequest: function(requestId) {
+      var self = this
+      uni.showModal({
+        title: '确认通过',
+        content: '确认通过该成员的加入申请?',
+        success: function(modalRes) {
+          if (modalRes.confirm) {
+            approveFamilyJoinRequest(requestId)
+              .then(function() {
+                uni.showToast({ title: '已通过', icon: 'success' })
+                self.loadPendingRequests()
+                self.closeRequests()
+              })
+              .catch(function(err) {
+                uni.showToast({ title: err.message || '操作失败', icon: 'none' })
+              })
+          }
+        }
+      })
+    },
+    rejectRequest: function(requestId) {
+      var self = this
+      uni.showModal({
+        title: '确认拒绝',
+        content: '确认拒绝该成员的加入申请?',
+        success: function(modalRes) {
+          if (modalRes.confirm) {
+            rejectFamilyJoinRequest(requestId)
+              .then(function() {
+                uni.showToast({ title: '已拒绝', icon: 'success' })
+                self.loadPendingRequests()
+                self.closeRequests()
+              })
+              .catch(function(err) {
+                uni.showToast({ title: err.message || '操作失败', icon: 'none' })
+              })
+          }
+        }
+      })
     }
   }
 }
@@ -275,4 +373,139 @@ export default {
 .action-cancel .action-item-text {
   color: #6B7280;
 }
+
+/* 待审批徽章 */
+.strip-toolbar {
+  display: flex;
+  justify-content: flex-end;
+  padding: 8rpx 30rpx 4rpx;
+}
+
+.pending-badge {
+  display: flex;
+  align-items: center;
+  padding: 8rpx 16rpx;
+  background: #FFFFFF;
+  border: 2rpx solid #EF4444;
+  border-radius: 32rpx;
+  box-shadow: 0 2rpx 8rpx rgba(239, 68, 68, 0.15);
+}
+
+.badge-icon {
+  font-size: 28rpx;
+  margin-right: 6rpx;
+}
+
+.badge-count {
+  font-size: 26rpx;
+  font-weight: 700;
+  color: #EF4444;
+}
+
+/* 待审批请求弹窗 */
+.request-overlay {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.4);
+  z-index: 1500;
+}
+
+.request-sheet {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: #FFFFFF;
+  border-radius: 24rpx 24rpx 0 0;
+  z-index: 1501;
+  max-height: 70vh;
+  overflow-y: auto;
+  padding: 20rpx 0;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+}
+
+.sheet-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 30rpx 16rpx;
+  border-bottom: 1rpx solid #F3F4F6;
+}
+
+.sheet-title {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #1F2937;
+}
+
+.sheet-close {
+  font-size: 40rpx;
+  color: #9CA3AF;
+  padding: 8rpx;
+  line-height: 1;
+}
+
+.empty-requests {
+  text-align: center;
+  padding: 60rpx 0;
+  font-size: 28rpx;
+  color: #9CA3AF;
+}
+
+.request-item {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 30rpx;
+  border-bottom: 1rpx solid #F9FAFB;
+}
+
+.req-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+
+.req-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.req-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1F2937;
+  display: block;
+}
+
+.req-time {
+  font-size: 24rpx;
+  color: #9CA3AF;
+  display: block;
+  margin-top: 4rpx;
+}
+
+.req-actions {
+  display: flex;
+  gap: 12rpx;
+}
+
+.req-btn {
+  padding: 12rpx 24rpx;
+  border-radius: 8rpx;
+  font-size: 24rpx;
+  font-weight: 600;
+  border: none;
+}
+
+.btn-approve {
+  background: #16A34A;
+  color: #FFFFFF;
+}
+
+.btn-reject {
+  background: #EF4444;
+  color: #FFFFFF;
+}
 </style>

+ 148 - 13
cfc-frontend/pages/invite/join.vue

@@ -14,6 +14,11 @@
       </view>
 
       <view class="invite-body">
+        <image
+          class="inviter-avatar"
+          :src="(inviteInfo.inviterAvatar || '/static/default-avatar.png')"
+          mode="aspectFill"
+        ></image>
         <text class="family-name">{{ inviteInfo.familyName }}</text>
         <text class="inviter" v-if="inviteInfo.inviterName">
           邀请人:{{ inviteInfo.inviterName }}
@@ -33,7 +38,7 @@
       </view>
 
       <!-- 已登录:显示关系选择 -->
-      <view class="action-area" v-if="isLoggedIn && !accepted">
+      <view class="action-area" v-if="isLoggedIn && !accepted && !requestSubmitted">
         <!-- 已有家庭时显示合并选项 -->
         <view class="family-warning" v-if="familyStatus && familyStatus.inFamily">
           <text class="warning-icon">⚠️</text>
@@ -63,12 +68,28 @@
           </label>
         </view>
 
-        <button class="btn btn-primary" @tap="handleAccept" :disabled="acceptLoading">
-          <text v-if="!acceptLoading">加入家庭</text>
-          <text v-else>加入中...</text>
+        <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>
+      </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>
@@ -123,7 +144,7 @@
 </template>
 
 <script>
-import { validateInvitation, acceptInvitation, checkUserFamily, bindReferral } from '@/utils/api.js'
+import { validateInvitation, checkUserFamily, bindReferral, requestJoinByCode, getMyPendingRequest } from '@/utils/api.js'
 
 export default {
   data() {
@@ -141,6 +162,10 @@ export default {
       familyRoleIndex: 0,
       mergeFamily: false,
       accepted: false,
+      requestSubmitted: false,
+      requestStatus: '',
+      requestTimer: null,
+      rejectMessage: '',
       error: '',
       familyRoleOptions: ['爸爸', '妈妈', '爷爷', '奶奶', '姥爷', '姥姥', '其他']
     }
@@ -191,6 +216,12 @@ export default {
       }
     }
   },
+  onUnload() {
+    if (this.requestTimer) {
+      clearInterval(this.requestTimer)
+      this.requestTimer = null
+    }
+  },
   methods: {
     checkLoginStatus() {
       var token = uni.getStorageSync('token')
@@ -219,7 +250,7 @@ export default {
         if (res && res.data) {
           this.familyStatus = res.data
           // 如果已经在目标家庭,直接显示成功
-          if (this.familyStatus.inFamily && this.familyStatus.familyId === this.inviteInfo.familyId) {
+          if (this.inviteInfo && this.familyStatus.inFamily && this.familyStatus.familyId === this.inviteInfo.familyId) {
             this.accepted = true
           }
         }
@@ -285,7 +316,7 @@ export default {
     toggleMerge() {
       this.mergeFamily = !this.mergeFamily
     },
-    async handleAccept() {
+    async handleSubmitRequest() {
       var familyRole = this.familyRoleOptions[this.familyRoleIndex] || ''
       if (!familyRole) {
         uni.showToast({ title: '请选择您在家庭中的身份', icon: 'none' })
@@ -294,17 +325,59 @@ export default {
 
       this.acceptLoading = true
       try {
-        var res = await acceptInvitation(this.token, familyRole, this.mergeFamily)
+        var res = await requestJoinByCode(this.token)
         if (res && res.code === 200) {
-          this.accepted = true
-          uni.showToast({ title: '加入家庭成功', icon: 'success' })
+          this.requestSubmitted = true
+          this.requestStatus = 'pending'
+          this.acceptLoading = false
+          uni.showToast({ title: '申请已提交,等待审批', icon: 'success' })
+          this.startPolling()
         } else {
-          uni.showToast({ title: res.message || '加入失败', icon: 'none' })
+          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.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)
+            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) {
-        uni.showToast({ title: e.message || '加入失败', icon: 'none' })
+        console.log('轮询请求状态失败', e)
       }
-      this.acceptLoading = false
     },
     goHome() {
       uni.reLaunch({ url: '/pages/index-home/index' })
@@ -411,6 +484,14 @@ export default {
   margin-bottom: 8rpx;
 }
 
+.inviter-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  margin-bottom: 12rpx;
+  background: #F1F5F9;
+}
+
 .member-count {
   font-size: 24rpx;
   color: #94A3B8;
@@ -574,4 +655,58 @@ 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>

+ 16 - 2
cfc-frontend/utils/api.js

@@ -1239,8 +1239,22 @@ export const cancelMyJoinRequest = (requestId) => {
   return request('/api/family/invite/cancel-request', 'POST', { requestId: requestId })
 }
 
-// ===== AI 鑱婂ぉ =====
-export const aiSendMessage = (data) => {
+// ===== 家庭加入请求 API 别名(供组件统一引用)=====
+export const getPendingFamilyRequests = () => {
+  return request('/api/family/invite/pending-requests', 'POST', {})
+}
+export const approveFamilyJoinRequest = (requestId) => {
+  return request('/api/family/invite/approve-request', 'POST', { requestId: requestId })
+}
+export const rejectFamilyJoinRequest = (requestId) => {
+  return request('/api/family/invite/reject-request', 'POST', { requestId: requestId })
+}
+export const getMyPendingRequest = () => {
+  return request('/api/family/invite/my-request', 'POST', {})
+}
+
+// ===== AI 聊天 =====
+export const aiSendMessage = (data) => {
   var query = data && data.query ? data.query : ''
   var conversationId = data && data.conversationId ? data.conversationId : ''
   var userId = uni.getStorageSync('userId') || ''