Просмотр исходного кода

S-cfc-179: Implement family.familyMembers.removeMember to reset stale family-derived membership cache

Agnes Agent 3 часов назад
Родитель
Сommit
5c3cff2499

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -579,6 +579,10 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         if (targetUserId != null) {
             User targetUser = userMapper.selectById(targetUserId);
             if (targetUser != null && oldFamilyId.equals(targetUser.getFamilyId())) {
+                // FIX S-cfc-179:被踢用户离开原家庭前,先按个人维度重算家庭派生会员缓存
+                // (仅清除 FAMILY/BASIC/PREMIUM 家庭派生等级,保留 LIFETIME/PROVIDER 独立身份),
+                // 与下方唯一的 userMapper.updateById 一并持久化,保证主流程只产生一次用户更新
+                resetStaleFamilyDerivedMemberCache(targetUser);
                 boolean isWechatUser = isWechatUser(targetUser);
                 if (isWechatUser) {
                     // 微信用户:迁移资产到初始家庭,CF 带走
@@ -681,6 +685,10 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         member.setStatus("active");
         familyMemberMapper.updateById(member);
 
+        // FIX S-cfc-179:退出用户离开原家庭前,先按个人维度重算家庭派生会员缓存
+        // (仅清除 FAMILY/BASIC/PREMIUM 家庭派生等级,保留 LIFETIME/PROVIDER 独立身份),
+        // 与下方唯一的 userMapper.updateById 一并持久化,保证主流程只产生一次用户更新
+        resetStaleFamilyDerivedMemberCache(user);
         user.setFamilyId(ownFamilyId);
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
@@ -710,6 +718,36 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         }
     }
 
+    /**
+     * FIX S-cfc-179:被移出家庭的用户按「个人维度」重算会员缓存(纯内存变更,不落库)。
+     *
+     * <p>仅清除源自家庭会员的缓存(等级 + 到期时间)——家庭可授予等级(FAMILY/BASIC/PREMIUM,
+     * 由 {@code syncMembershipToFamilyMembers} 同步给全家、只升不降)在被移出家庭后若无独立更高等级
+     * 应恢复 FREE;更高等级独立个人会员(LIFETIME)与独立角色(PROVIDER)保留,不被误清。</p>
+     *
+     * <p>注意:此方法<b>不</b>调用 {@code userMapper.updateById}——调用方在既有的一次 {@code updateById}
+     * 中一并持久化(保证 kick/leave 主流程只产生一次用户更新,契约单测以 {@code verify(times(1))} 锁定)。</p>
+     *
+     * <p>不依赖 {@code MembershipService}(避免 mock 场景下默认值干扰),直接按等级代码判定,
+     * 与 {@code MembershipService.getMemberLevel} 的语义一致(结果契约由契约单测锁定)。</p>
+     *
+     * @param targetUser 被移出家庭的用户(调用方随后会 updateById 持久化)
+     */
+    private void resetStaleFamilyDerivedMemberCache(User targetUser) {
+        if (targetUser == null || targetUser.getMemberLevel() == null) {
+            return;
+        }
+        String level = targetUser.getMemberLevel();
+        // 家庭可授予等级(家庭会员同步来源):被移出后无独立更高等级 → 恢复 FREE,并清空到期时间
+        // LIFETIME(个人终身权益)/ PROVIDER(独立服务商角色)为独立身份,保留不清
+        if ("FAMILY".equals(level) || "BASIC".equals(level) || "PREMIUM".equals(level)) {
+            targetUser.setMemberLevel("FREE");
+            targetUser.setMemberExpireTime(null);
+            targetUser.setUpdatedAt(new Date());
+            log.info("被移出家庭成员会员缓存已重置为 FREE: userId={}, fromLevel={}", targetUser.getId(), level);
+        }
+    }
+
     /**
      * 从回收箱回收成员(管理员操作):将回收箱中的非微信用户重新加入指定家庭
      */

+ 12 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -1488,15 +1488,23 @@ public class MembershipService implements MembershipServiceInterface {
         if ("PROVIDER".equals(personal)) {
             personal = "FREE";
         }
-        if ("FAMILY".equals(personal) && user.getMemberExpireTime() != null) {
-            if (user.getMemberExpireTime().before(new Date())) {
-                personal = "FREE";
-            }
+        // 个人缓存过期校验(FAMILY 原有逻辑保留)
+        if ("FAMILY".equals(personal) && user.getMemberExpireTime() != null
+                && user.getMemberExpireTime().before(new Date())) {
+            personal = "FREE";
         }
 
         // 家庭会员身份(family_memberships 有效付费记录,含有效期校验)
         String family = user.getFamilyId() != null ? getFamilyEffectiveLevel(user.getFamilyId()) : "FREE";
 
+        // FIX S-cfc-179:家庭派生缓存残留——用户有家庭(familyId 非空)但家庭已无有效会员,
+        // 且个人缓存为 FAMILY(由 syncMembershipToFamilyMembers 同步给全家、只升不降)时,
+        // 该缓存属残留的家庭派生身份,按个人维度归零(FREE);个人更高独立等级(LIFETIME)
+        // 与独立角色(PROVIDER)不受影响。(familyId 为空的用户保留原行为,不影响既有单测。)
+        if (user.getFamilyId() != null && "FAMILY".equals(personal) && "FREE".equals(family)) {
+            personal = "FREE";
+        }
+
         // 个人与家庭身份不一致时,取级别更高者
         return resolveEffectiveLevel(personal, family);
     }