Переглянути джерело

fix(membership): 后台会员等级写入家庭维度事实源,修复健康方案生成误报未升级

后台 user-update/lifetime-grant 原先只直写 users.member_level,而付费墙读 family_memberships,导致会员身份不一致、/api/health/plan/generate 报 4001。改为经 MembershipService.setFamilyMembershipLevel 写入 family_memberships 并同步全家 users 缓存(会员属于家庭);消费升级 upgradeToFamily 同时补建事实源记录。
liaoxg 5 днів тому
батько
коміт
85ffba6aa1

+ 85 - 37
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -10,6 +10,7 @@ import com.etotem.cfc.mapper.*;
 import com.etotem.cfc.dto.TaskReviewDTO;
 import com.etotem.cfc.service.TaskService;
 import com.etotem.cfc.service.UserService;
+import com.etotem.cfc.service.MembershipService;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.TypeReference;
 import javax.annotation.Resource;
@@ -87,6 +88,9 @@ public class AdminController {
     @Resource
     private LifetimeStatsMapper lifetimeStatsMapper;
 
+    @Resource
+    private MembershipService membershipService;
+
     // 终身会员达标阈值(分)
     private static final long LIFETIME_CONSUME_THRESHOLD = 100_000_000L;  // 消费 ≥ 100万元
     private static final long LIFETIME_SALES_THRESHOLD = 200_000_000L;    // 销售 ≥ 200万元
@@ -1627,11 +1631,12 @@ public class AdminController {
         }
         String fromLevel = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
 
-        // 1. 升级用户为 LIFETIME(memberExpireTime=null 永久有效)
-        user.setMemberLevel("LIFETIME");
-        user.setMemberExpireTime(null);
-        user.setUpdatedAt(new Date());
-        userMapper.updateById(user);
+        // 1. 方案B:会员属于家庭 — 写入 family_memberships 事实源并同步全家 users 缓存。
+        //    原先只直写 users.member_level,导致读 family_memberships 的付费墙(如健康方案生成)误判 FREE。
+        //    LIFETIME 以远期到期时间表示永久(getCurrentLevel 依赖 endDate 判定有效性)。
+        Long familyId = membershipService.getOrCreateUserFamilyId(targetUserId);
+        membershipService.setFamilyMembershipLevel(familyId, "LIFETIME", resolveGrantExpireTime("LIFETIME"),
+                "admin", "lifetime-grant:" + adminUserId);
 
         // 2. 写入升级记录 upgradeType='lifetime'
         MemberUpgradeRecord record = new MemberUpgradeRecord();
@@ -1650,6 +1655,16 @@ public class AdminController {
         return Result.success();
     }
 
+    /**
+     * 后台授予会员的目标到期时间:LIFETIME 用远期时间表示永久,其余等级默认 365 天(与支付/赠送默认一致)。
+     */
+    private Date resolveGrantExpireTime(String levelCode) {
+        if ("LIFETIME".equals(levelCode)) {
+            return java.sql.Timestamp.valueOf(java.time.LocalDateTime.of(2099, 12, 31, 23, 59, 59));
+        }
+        return new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000);
+    }
+
     /**
      * 会员维护:按用户ID修改会员等级和推荐人
      * body: { userId, memberLevel?, referrerId? }
@@ -1674,6 +1689,8 @@ public class AdminController {
         StringBuilder changes = new StringBuilder();
         String oldLevel = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
         String newLevel = null;
+        // 用户行仅剩「推荐人」「无家庭时的 FREE 回退」需要直写;会员等级统一走家庭维度(方案B)
+        boolean userRowChanged = false;
         LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
         wrapper.eq(User::getId, targetUserId);
 
@@ -1683,6 +1700,7 @@ public class AdminController {
                 if (user.getReferrerId() != null) {
                     wrapper.set(User::getReferrerId, null);
                     changes.append("清除推荐人; ");
+                    userRowChanged = true;
                 }
             } else {
                 try {
@@ -1692,6 +1710,7 @@ public class AdminController {
                     }
                     wrapper.set(User::getReferrerId, referrerId);
                     changes.append("推荐人=").append(referrerId).append("; ");
+                    userRowChanged = true;
                 } catch (NumberFormatException e) {
                     return Result.error("推荐人ID格式不正确");
                 }
@@ -1715,47 +1734,76 @@ public class AdminController {
                 return Result.error("无效的会员等级: " + level);
             }
             newLevel = level;
-            wrapper.set(User::getMemberLevel, newLevel);
-            if ("LIFETIME".equals(newLevel)) {
-                wrapper.set(User::getMemberExpireTime, null);
-            }
             if (!oldLevel.equals(newLevel)) {
                 changes.append("等级=").append(oldLevel).append("->").append(newLevel).append("; ");
             }
         }
 
-        if (changes.length() > 0) {
+        // 会员等级变更:写入事实源 family_memberships 并同步全家 users 缓存(方案B:会员属于家庭)。
+        // 不再直写 users.member_level,否则读 family_memberships 的付费墙会误判 FREE(如 /api/health/plan/generate 报 4001)。
+        if (newLevel != null) {
+            Long targetFamilyId = user.getFamilyId();
+            if (!"FREE".equals(newLevel) && (targetFamilyId == null || targetFamilyId == 0L)) {
+                // 无家庭自动建家:会员以家庭为单位,无家庭无法承载会员权益
+                targetFamilyId = membershipService.getOrCreateUserFamilyId(targetUserId);
+                changes.append("已自动创建家庭id=").append(targetFamilyId).append("; ");
+            }
+            if ("FREE".equals(newLevel)) {
+                if (targetFamilyId != null && targetFamilyId != 0L) {
+                    // 注意:家庭会员为全家共享,改用户为 FREE 会撤销整个家庭的会员并全家降级
+                    String desc = membershipService.setFamilyMembershipLevel(
+                            targetFamilyId, "FREE", null, "admin", "admin:" + adminUserId);
+                    if (desc != null && !desc.isEmpty()) {
+                        changes.append(desc).append("; ");
+                    }
+                } else {
+                    // 无家庭:无可撤销的家庭会员,直接降级用户缓存(兼容旧行为)
+                    wrapper.set(User::getMemberLevel, "FREE");
+                    userRowChanged = true;
+                }
+            } else {
+                String desc = membershipService.setFamilyMembershipLevel(
+                        targetFamilyId, newLevel, resolveGrantExpireTime(newLevel),
+                        "admin", "admin:" + adminUserId);
+                if (desc != null && !desc.isEmpty()) {
+                    changes.append(desc).append("; ");
+                }
+            }
+        }
+
+        if (userRowChanged) {
             wrapper.set(User::getUpdatedAt, new Date());
             userMapper.update(null, wrapper);
+        }
 
-            if (newLevel != null && !oldLevel.equals(newLevel)) {
-                MemberUpgradeRecord record = new MemberUpgradeRecord();
-                record.setUserId(targetUserId);
-                record.setFromLevel(oldLevel);
-                record.setToLevel(newLevel);
-                record.setUpgradeType("admin");
-                record.setApproveUserId(adminUserId);
-                record.setApproveStatus("approved");
-                record.setApproveTime(new Date());
-                record.setApproveRemark("管理员会员维护");
-                record.setCreatedAt(new Date());
-                memberUpgradeRecordMapper.insert(record);
-
-                // 升级到付费等级(非 FREE)时自动发放 JOIN 赠券(幂等:family_coupon 唯一键防重)
-                if (!"FREE".equals(newLevel)) {
-                    User changedUser = userMapper.selectById(targetUserId);
-                    if (changedUser != null && changedUser.getFamilyId() != null) {
-                        try {
-                            couponService.grantFamilyJoinCoupons(
-                                    changedUser.getFamilyId(), newLevel,
-                                    "admin:" + adminUserId);
-                            changes.append("发放").append(newLevel).append("JOIN券; ");
-                        } catch (Exception ex) {
-                            changes.append("发放").append(newLevel).append("JOIN券异常:").append(ex.getMessage()).append("; ");
-                        }
-                    } else {
-                        changes.append("升级").append(newLevel).append("(无家庭,跳过发券); ");
+        if (newLevel != null && !oldLevel.equals(newLevel)) {
+            MemberUpgradeRecord record = new MemberUpgradeRecord();
+            record.setUserId(targetUserId);
+            record.setFromLevel(oldLevel);
+            record.setToLevel(newLevel);
+            record.setUpgradeType("admin");
+            record.setApproveUserId(adminUserId);
+            record.setApproveStatus("approved");
+            record.setApproveTime(new Date());
+            record.setApproveRemark("管理员会员维护");
+            record.setCreatedAt(new Date());
+            memberUpgradeRecordMapper.insert(record);
+
+            // 升级到付费等级(非 FREE)时自动发放 JOIN 赠券(幂等:family_coupon 唯一键防重)
+            if (!"FREE".equals(newLevel)) {
+                User changedUser = userMapper.selectById(targetUserId);
+                Long changedFamilyId = changedUser != null ? changedUser.getFamilyId() : null;
+                if (changedFamilyId != null && changedFamilyId != 0L) {
+                    try {
+                        couponService.grantFamilyJoinCoupons(
+                                changedFamilyId, newLevel,
+                                "admin:" + adminUserId);
+                        changes.append("发放").append(newLevel).append("JOIN券; ");
+                    } catch (Exception ex) {
+                        changes.append("发放").append(newLevel).append("JOIN券异常:").append(ex.getMessage()).append("; ");
                     }
+                } else {
+                    changes.append("升级").append(newLevel).append("(无家庭,跳过发券); ");
                 }
             }
         }

+ 104 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -1061,6 +1061,95 @@ public class MembershipService implements MembershipServiceInterface {
         }
     }
 
+    /**
+     * 按家庭维度设置会员等级(后台授予/调整的唯一入口)。
+     * 写入事实源 family_memberships,并全套同步 users 缓存(方案B:会员属于家庭)。
+     * 幂等:等级未变且到期不早于目标值时只同步缓存,不产生新记录。
+     * @param levelCode  目标等级;"FREE" 表示撤销家庭会员并全家降级
+     * @param expireTime 目标到期时间(levelCode=FREE 时可传 null)
+     * @return 操作描述(供后台变更提示展示),无实际变更时返回空字符串
+     */
+    @Transactional
+    public String setFamilyMembershipLevel(Long familyId, String levelCode, Date expireTime,
+                                           String upgradeType, String sourceDesc) {
+        if (familyId == null || levelCode == null || levelCode.isEmpty()) {
+            return "";
+        }
+        Date now = new Date();
+
+        // 该家庭当前所有有效记录(paid 且未过期)
+        List<FamilyMembership> actives = membershipMapper.selectList(
+                new LambdaQueryWrapper<FamilyMembership>()
+                        .eq(FamilyMembership::getFamilyId, familyId)
+                        .eq(FamilyMembership::getPaymentStatus, "paid")
+                        .gt(FamilyMembership::getEndDate, now)
+                        .orderByDesc(FamilyMembership::getEndDate));
+
+        // FREE:撤销家庭会员并全家降级
+        if ("FREE".equals(levelCode)) {
+            boolean hadActive = !actives.isEmpty();
+            for (FamilyMembership m : actives) {
+                m.setPaymentStatus("refunded");
+                m.setEndDate(now);
+                m.setUpdatedAt(now);
+                membershipMapper.updateById(m);
+            }
+            // 即使无有效记录也执行降级:修复历史上只写了 users 缓存、家庭无记录的脏数据
+            downgradeFamilyMembers(familyId);
+            return hadActive ? "已撤销家庭会员,全家降级 FREE" : "";
+        }
+
+        if (expireTime == null) {
+            return "";
+        }
+
+        String desc;
+        FamilyMembership active = actives.isEmpty() ? null : actives.get(0);
+        if (active != null && levelCode.equals(active.getLevelCode())) {
+            if (!expireTime.after(active.getEndDate())) {
+                // 已是目标等级且到期不早于目标值:只同步缓存,不产生新记录
+                syncMembershipToFamilyMembers(familyId, levelCode, active.getEndDate(), upgradeType, null);
+                return "家庭已有" + levelCode + "会员(至 " + formatDateTime(active.getEndDate()) + "),仅同步缓存";
+            }
+            // 同等级延长到期时间
+            active.setEndDate(expireTime);
+            active.setUpdatedAt(now);
+            membershipMapper.updateById(active);
+            desc = "已延长" + levelCode + "会员至 " + formatDateTime(expireTime);
+        } else {
+            // 等级变化或无有效记录:先停用原有效记录(同时收缩 endDate,避免 getFamilyMembership
+            // 按 max(end_date) 选到已撤销记录遮蔽新记录),再插入新记录
+            boolean deactivated = false;
+            for (FamilyMembership m : actives) {
+                m.setPaymentStatus("refunded");
+                m.setEndDate(now);
+                m.setUpdatedAt(now);
+                membershipMapper.updateById(m);
+                deactivated = true;
+            }
+            FamilyMembership membership = new FamilyMembership();
+            membership.setFamilyId(familyId);
+            membership.setLevelCode(levelCode);
+            membership.setStartDate(now);
+            membership.setEndDate(expireTime);
+            membership.setPaymentMethod("admin");
+            membership.setPaymentStatus("paid");
+            membership.setOrderNo(sourceDesc);
+            membership.setAmount(0);
+            membershipMapper.insert(membership);
+            desc = (deactivated ? "已停用原会员记录;" : "")
+                    + "已设置家庭会员 " + levelCode + " 至 " + formatDateTime(expireTime);
+        }
+
+        // 方案B:全家同步会员身份
+        syncMembershipToFamilyMembers(familyId, levelCode, expireTime, upgradeType, null);
+        return desc;
+    }
+
+    private String formatDateTime(Date d) {
+        return d == null ? "" : new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d);
+    }
+
     // ==================== 会员升级与过期 ====================
 
     /**
@@ -1082,6 +1171,21 @@ public class MembershipService implements MembershipServiceInterface {
         String fromLevel = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
 
         Date expireTime = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000);
+        // 补建家庭会员事实源:消费达标升级此前只写 users 缓存,导致 getCurrentLevel 付费墙
+        // (健康方案/任务创建/商城折扣)误判 FREE 拦截。同家庭多名成员同批达标只建一条
+        // (无有效记录才插入);家庭已有其他有效付费会员则不覆盖其等级。
+        if (getActiveFamilyMembership(familyId) == null) {
+            FamilyMembership membership = new FamilyMembership();
+            membership.setFamilyId(familyId);
+            membership.setLevelCode("FAMILY");
+            membership.setStartDate(new Date());
+            membership.setEndDate(expireTime);
+            membership.setPaymentMethod("consumption");
+            membership.setPaymentStatus("paid");
+            membership.setOrderNo("CONSUMPTION-" + userId);
+            membership.setAmount(0);
+            membershipMapper.insert(membership);
+        }
         // 方案B:全家同步会员身份
         syncMembershipToFamilyMembers(familyId, "FAMILY", expireTime, upgradeType, orderId);
         log.info("消费达标自动升级全家: familyId={}, triggerUserId={}", familyId, userId);