liuchengsen 1 ماه پیش
والد
کامیت
748b7e100f

+ 0 - 4
zhijiayun-user/pom.xml

@@ -104,10 +104,6 @@
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk18on</artifactId>
         </dependency>
-        <dependency>
-            <groupId>com.xuekairui</groupId>
-            <artifactId>zhijiayun-common</artifactId>
-        </dependency>
     </dependencies>
 
 </project>

+ 5 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/enums/MembershipLevel.java

@@ -1,5 +1,7 @@
 package com.xuekairui.user.enums;
 
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
 import lombok.Getter;
 
 /**
@@ -28,6 +30,8 @@ public enum MembershipLevel {
      */
     ULTRA("ULTRA", "超级会员", 3);
 
+    @EnumValue
+    @JsonValue
     private final String code;
     private final String description;
     private final int weight;
@@ -52,4 +56,4 @@ public enum MembershipLevel {
         }
         return null;
     }
-}
+}

+ 13 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/UserMembershipMapper.java

@@ -27,4 +27,16 @@ public interface UserMembershipMapper extends BaseMapper<UserMembership> {
      */
     @Select("SELECT * FROM t_user_membership WHERE user_id = #{userId} ORDER BY create_time DESC")
     List<UserMembership> selectListByUserId(@Param("userId") Long userId);
-}
+
+    /**
+      * 批量查询多个用户的当前有效会员记录
+     */
+    @Select("<script>" +
+            "SELECT * FROM t_user_membership WHERE user_id IN " +
+            "<foreach collection='userIds' item='id' open='(' separator=',' close=')'>" +
+            "  #{id}" +
+            "</foreach>" +
+            " AND status = 1 AND (effective_to IS NULL OR effective_to > NOW())" +
+            "</script>")
+    List<UserMembership> selectActiveByUserIds(@Param("userIds") List<Long> userIds);
+}

+ 5 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -1039,8 +1039,13 @@ public class AuthService {
     /**
      * 首次入驻审核通过:赠送30天高级会员 + 推送Redis事件供活动模块消费
      * <p>与 BusinessLicenseService.approve() 的奖励逻辑保持一致。
+     * <p>幂等性检查:若用户已有其他审核通过的入驻记录,不再重复赠送,避免高级会员过期时间累加。
      */
     private void grantLicenseApprovedReward(Long userId, Long licenseId) {
+        if (businessLicenseService.hasOtherApprovedLicensePublic(userId, licenseId)) {
+            log.info("用户{}已有其他审核通过的入驻记录,跳过重复赠送高级会员: licenseId={}", userId, licenseId);
+            return;
+        }
         membershipService.grantOrExtendMembership(
                 userId,
                 MembershipLevel.PRO,

+ 8 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java

@@ -125,6 +125,14 @@ public class BusinessLicenseService {
         return count != null && count > 0;
     }
 
+    /**
+     * 检查用户是否已有审核通过的入驻信息(排除指定记录)
+     * <p>供 AuthService 在第三方同步时做幂等性检查,避免重复赠送高级会员导致时间累加。
+     */
+    public boolean hasOtherApprovedLicensePublic(Long userId, Long excludeLicenseId) {
+        return hasOtherApprovedLicense(userId, excludeLicenseId);
+    }
+
     // ======================== 用户侧:提交入驻信息 ========================
 
     /**

+ 247 - 67
zhijiayun-user/src/main/java/com/xuekairui/user/service/MembershipService.java

@@ -352,69 +352,184 @@ public class MembershipService {
 
     /**
      * 会员用户快照列表(用户维度,查 t_user + 当前会员)
+     * <p>当前会员等级以 t_user_membership 表的活跃记录为准(取最高等级),
+     * 若无活跃记录则回退到 t_user.membership_level,再回退到 PLUS。
+     * 这样可兼容历史数据(t_user.membership_level 未同步更新的情况)。
      */
     public Page<MembershipUserResponse> listMembershipUsers(Integer pageNum, Integer pageSize,
                                                             String phone, String level) {
+        MembershipLevel filterLevel = null;
+        if (level != null && !level.isBlank()) {
+            filterLevel = MembershipLevel.fromCode(level);
+        }
+
+        // 如果按等级筛选,需要先从会员记录表找出符合条件的用户ID,再查用户表
+        if (filterLevel != null) {
+            Page<MembershipUserResponse> result = listMembershipUsersByLevel(pageNum, pageSize, phone, filterLevel);
+            return result;
+        }
+
         Page<User> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
         if (phone != null && !phone.isBlank()) {
             wrapper.like(User::getPhone, phone);
         }
-        if (level != null && !level.isBlank()) {
-            MembershipLevel ml = MembershipLevel.fromCode(level);
-            if (ml != null) {
-                wrapper.eq(User::getMembershipLevel, ml);
-            }
-        }
         wrapper.orderByDesc(User::getCreateTime);
         Page<User> result = userMapper.selectPage(page, wrapper);
 
-        // 批量查活跃会员记录,取 source
+        // 批量查活跃会员记录,取最高等级
         List<Long> userIds = result.getRecords().stream().map(User::getId).toList();
-        Map<Long, String> sourceMap = userIds.isEmpty() ? Map.of()
-                : userMembershipMapper.selectList(
-                        new LambdaQueryWrapper<UserMembership>()
-                                .in(UserMembership::getUserId, userIds)
-                                .eq(UserMembership::getStatus, 1))
-                        .stream().collect(Collectors.toMap(
-                                UserMembership::getUserId,
-                                UserMembership::getSource,
-                                (a, b) -> a));
-
-        List<MembershipUserResponse> list = result.getRecords().stream().map(u -> {
-            MembershipLevel currentLevel = u.getMembershipLevel() != null
-                    ? u.getMembershipLevel() : MembershipLevel.PLUS;
-            String levelCode = currentLevel.getCode();
-            boolean isPlus = currentLevel == MembershipLevel.PLUS;
-            LocalDateTime expireAt = isPlus ? null : u.getMembershipExpireAt();
-            int daysRemaining;
-            if (isPlus) {
-                daysRemaining = -1;
-            } else if (expireAt != null && expireAt.isAfter(LocalDateTime.now())) {
-                daysRemaining = (int) ChronoUnit.DAYS.between(LocalDateTime.now(), expireAt);
-            } else {
-                daysRemaining = 0;
-            }
+        Map<Long, UserMembership> activeMap = loadActiveMemberships(userIds);
 
-            return MembershipUserResponse.builder()
-                    .userId(u.getId())
-                    .phone(u.getPhone())
-                    .nickname(u.getNickname())
-                    .wechatNickname(u.getWechatNickname())
-                    .level(levelCode)
-                    .levelName(currentLevel.getDescription())
-                    .source(isPlus ? "SYSTEM" : sourceMap.getOrDefault(u.getId(), ""))
-                    .expireAt(expireAt)
-                    .daysRemaining(daysRemaining)
-                    .createTime(u.getCreateTime())
-                    .build();
-        }).collect(Collectors.toList());
+        List<MembershipUserResponse> list = result.getRecords().stream()
+                .map(u -> toMembershipUserResponse(u, activeMap.get(u.getId())))
+                .collect(Collectors.toList());
 
         Page<MembershipUserResponse> responsePage = new Page<>(pageNum, pageSize, result.getTotal());
         responsePage.setRecords(list);
         return responsePage;
     }
 
+    /**
+     * 按等级筛选会员用户列表
+     * <p>筛选逻辑:用户当前最高等级活跃记录等于筛选等级时才命中。
+     * 例如筛选 PRO 时,若用户同时有活跃 ULTRA 记录,则该用户归为 ULTRA 而非 PRO。
+     */
+    private Page<MembershipUserResponse> listMembershipUsersByLevel(Integer pageNum, Integer pageSize,
+                                                                     String phone, MembershipLevel filterLevel) {
+        // 查出所有活跃会员记录
+        LambdaQueryWrapper<UserMembership> mWrapper = new LambdaQueryWrapper<>();
+        mWrapper.eq(UserMembership::getStatus, 1)
+                .and(w -> w.isNull(UserMembership::getEffectiveTo)
+                        .or().gt(UserMembership::getEffectiveTo, LocalDateTime.now()));
+        List<UserMembership> memberships = userMembershipMapper.selectList(mWrapper);
+        if (memberships.isEmpty()) {
+            Page<MembershipUserResponse> emptyPage = new Page<>(pageNum, pageSize, 0);
+            emptyPage.setRecords(Collections.emptyList());
+            return emptyPage;
+        }
+
+        // 按 userId 分组,取每个用户最高等级的活跃记录
+        Map<Long, UserMembership> topMap = new HashMap<>();
+        for (UserMembership m : memberships) {
+            UserMembership existing = topMap.get(m.getUserId());
+            if (existing == null || m.getLevel().getWeight() > existing.getLevel().getWeight()) {
+                topMap.put(m.getUserId(), m);
+            }
+        }
+
+        // 只保留最高等级等于筛选等级的用户
+        Map<Long, UserMembership> matchedMap = new HashMap<>();
+        for (Map.Entry<Long, UserMembership> entry : topMap.entrySet()) {
+            if (entry.getValue().getLevel() == filterLevel) {
+                matchedMap.put(entry.getKey(), entry.getValue());
+            }
+        }
+        if (matchedMap.isEmpty()) {
+            Page<MembershipUserResponse> emptyPage = new Page<>(pageNum, pageSize, 0);
+            emptyPage.setRecords(Collections.emptyList());
+            return emptyPage;
+        }
+
+        List<Long> userIds = new java.util.ArrayList<>(matchedMap.keySet());
+
+        // 按手机号筛选用户
+        LambdaQueryWrapper<User> uWrapper = new LambdaQueryWrapper<>();
+        uWrapper.in(User::getId, userIds);
+        if (phone != null && !phone.isBlank()) {
+            uWrapper.like(User::getPhone, phone);
+        }
+        uWrapper.orderByDesc(User::getCreateTime);
+
+        Page<User> page = new Page<>(pageNum, pageSize);
+        Page<User> userPage = userMapper.selectPage(page, uWrapper);
+
+        List<MembershipUserResponse> list = userPage.getRecords().stream()
+                .map(u -> toMembershipUserResponse(u, matchedMap.get(u.getId())))
+                .collect(Collectors.toList());
+
+        Page<MembershipUserResponse> responsePage = new Page<>(pageNum, pageSize, userPage.getTotal());
+        responsePage.setRecords(list);
+        return responsePage;
+    }
+
+    /**
+     * 批量加载用户的活跃会员记录,返回 userId -> 最高等级会员记录 的映射
+     */
+    private Map<Long, UserMembership> loadActiveMemberships(List<Long> userIds) {
+        if (userIds == null || userIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<UserMembership> actives = userMembershipMapper.selectActiveByUserIds(userIds);
+        Map<Long, UserMembership> map = new HashMap<>();
+        for (UserMembership m : actives) {
+            UserMembership existing = map.get(m.getUserId());
+            if (existing == null || m.getLevel().getWeight() > existing.getLevel().getWeight()) {
+                map.put(m.getUserId(), m);
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 将用户和活跃会员记录组装为响应对象
+     * <p>优先使用会员记录表的等级和过期时间;若无活跃记录,回退到用户表字段。
+     * 高级会员过期后(effective_to 早于当前时间)降级显示为普通会员。
+     */
+    private MembershipUserResponse toMembershipUserResponse(User u, UserMembership active) {
+        MembershipLevel currentLevel;
+        LocalDateTime expireAt;
+        String source;
+
+        LocalDateTime now = LocalDateTime.now();
+        if (active != null && active.getLevel() != MembershipLevel.PLUS
+                && active.getEffectiveTo() != null && !active.getEffectiveTo().isAfter(now)) {
+            // 高级会员已过期:降级为普通会员
+            currentLevel = MembershipLevel.PLUS;
+            expireAt = null;
+            source = "SYSTEM";
+        } else if (active != null) {
+            currentLevel = active.getLevel();
+            expireAt = active.getEffectiveTo();
+            source = active.getSource();
+        } else {
+            currentLevel = u.getMembershipLevel() != null ? u.getMembershipLevel() : MembershipLevel.PLUS;
+            // 用户表等级为高级但无活跃记录,说明已过期,降级为普通会员
+            if (currentLevel != MembershipLevel.PLUS) {
+                currentLevel = MembershipLevel.PLUS;
+            }
+            expireAt = null;
+            source = "SYSTEM";
+        }
+
+        String levelCode = currentLevel.getCode();
+        boolean isPlus = currentLevel == MembershipLevel.PLUS;
+        if (isPlus) {
+            expireAt = null;
+        }
+        int daysRemaining;
+        if (isPlus) {
+            daysRemaining = -1;
+        } else if (expireAt != null && expireAt.isAfter(now)) {
+            daysRemaining = (int) ChronoUnit.DAYS.between(now, expireAt);
+        } else {
+            daysRemaining = 0;
+        }
+
+        return MembershipUserResponse.builder()
+                .userId(u.getId())
+                .phone(u.getPhone())
+                .nickname(u.getNickname())
+                .wechatNickname(u.getWechatNickname())
+                .level(levelCode)
+                .levelName(currentLevel.getDescription())
+                .source(source)
+                .expireAt(expireAt)
+                .daysRemaining(daysRemaining)
+                .createTime(u.getCreateTime())
+                .build();
+    }
+
     private MembershipResponse toResponse(UserMembership membership) {
         if (membership == null) {
             return null;
@@ -493,33 +608,96 @@ public class MembershipService {
                         .isNotNull(UserMembership::getEffectiveTo)
                         .lt(UserMembership::getEffectiveTo, LocalDateTime.now()));
 
-        if (expired.isEmpty()) {
-            return;
-        }
+        if (!expired.isEmpty()) {
+            Set<Long> affectedUserIds = new HashSet<>();
+            for (UserMembership m : expired) {
+                m.setStatus(0);
+                userMembershipMapper.updateById(m);
+                affectedUserIds.add(m.getUserId());
+            }
 
-        Set<Long> affectedUserIds = new HashSet<>();
-        for (UserMembership m : expired) {
-            m.setStatus(0);
-            userMembershipMapper.updateById(m);
-            affectedUserIds.add(m.getUserId());
-        }
-
-        // 对每个受影响的用户,检查是否还有其他有效会员
-        for (Long userId : affectedUserIds) {
-            UserMembership active = userMembershipMapper.selectActiveByUserId(userId);
-            if (active == null) {
-                // 无有效会员,降级为普通会员
-                User user = userMapper.selectById(userId);
-                if (user != null && user.getMembershipLevel() != MembershipLevel.PLUS) {
-                    user.setMembershipLevel(MembershipLevel.PLUS);
-                    user.setMembershipExpireAt(null);
-                    userMapper.updateById(user);
-                    log.info("用户{}会员已到期,降级为普通会员", userId);
+            // 对每个受影响的用户,检查是否还有其他有效会员
+            for (Long userId : affectedUserIds) {
+                UserMembership active = userMembershipMapper.selectActiveByUserId(userId);
+                if (active == null) {
+                    // 无有效会员,降级为普通会员
+                    User user = userMapper.selectById(userId);
+                    if (user != null && user.getMembershipLevel() != MembershipLevel.PLUS) {
+                        user.setMembershipLevel(MembershipLevel.PLUS);
+                        user.setMembershipExpireAt(null);
+                        userMapper.updateById(user);
+                        log.info("用户{}会员已到期,降级为普通会员", userId);
+                    }
                 }
             }
+            log.info("处理过期会员记录: {}条,涉及用户: {}个", expired.size(), affectedUserIds.size());
         }
 
-        log.info("处理过期会员记录: {}条,涉及用户: {}个", expired.size(), affectedUserIds.size());
+        // 修复历史数据:t_user_membership 有活跃记录但 t_user.membership_level 不一致
+        fixInconsistentUserMembershipLevel();
+    }
+
+    /**
+     * 修复历史数据:当 t_user_membership 有活跃记录但 t_user.membership_level 不一致时,
+     * 将 t_user.membership_level 同步为活跃记录中的最高等级。
+     * 若无活跃的高级会员记录,则降级为普通会员。
+     */
+    private void fixInconsistentUserMembershipLevel() {
+        // 查出所有有活跃会员记录的用户ID
+        List<UserMembership> actives = userMembershipMapper.selectList(
+                new LambdaQueryWrapper<UserMembership>()
+                        .eq(UserMembership::getStatus, 1)
+                        .and(w -> w.isNull(UserMembership::getEffectiveTo)
+                                .or().gt(UserMembership::getEffectiveTo, LocalDateTime.now())));
+
+        // 按 userId 分组,取每个用户最高等级
+        Map<Long, UserMembership> topMap = new HashMap<>();
+        for (UserMembership m : actives) {
+            UserMembership existing = topMap.get(m.getUserId());
+            if (existing == null || m.getLevel().getWeight() > existing.getLevel().getWeight()) {
+                topMap.put(m.getUserId(), m);
+            }
+        }
+
+        // 查出 t_user.membership_level 为高级会员的所有用户,检查是否需要降级
+        List<User> highLevelUsers = userMapper.selectList(
+                new LambdaQueryWrapper<User>()
+                        .isNotNull(User::getMembershipLevel)
+                        .ne(User::getMembershipLevel, MembershipLevel.PLUS));
+        int fixed = 0;
+        for (User user : highLevelUsers) {
+            UserMembership top = topMap.get(user.getId());
+            boolean needUpdate = false;
+            if (top == null) {
+                // 无活跃高级会员记录,降级为普通会员
+                user.setMembershipLevel(MembershipLevel.PLUS);
+                user.setMembershipExpireAt(null);
+                needUpdate = true;
+            } else {
+                // 等级不一致(无论升降级)都需要同步
+                if (!java.util.Objects.equals(user.getMembershipLevel(), top.getLevel())) {
+                    user.setMembershipLevel(top.getLevel());
+                    needUpdate = true;
+                }
+                LocalDateTime expectedExpire = top.getEffectiveTo();
+                if (top.getLevel() == MembershipLevel.PLUS) {
+                    expectedExpire = null;
+                }
+                if (!java.util.Objects.equals(user.getMembershipExpireAt(), expectedExpire)) {
+                    user.setMembershipExpireAt(expectedExpire);
+                    needUpdate = true;
+                }
+            }
+            if (needUpdate) {
+                userMapper.updateById(user);
+                fixed++;
+                log.info("修复用户{}会员等级不一致: -> {}",
+                        user.getId(), user.getMembershipLevel());
+            }
+        }
+        if (fixed > 0) {
+            log.info("修复历史会员等级不一致数据: {}个用户", fixed);
+        }
     }
 
     // ==================== 等级查询(原 UserLevelService,已合并) ====================
@@ -623,7 +801,9 @@ public class MembershipService {
         }
         boolean premium = resp.getLevelCode() != null && !"PLUS".equals(resp.getLevelCode());
         if (!premium) { resp.setMembershipExpireAt(null); resp.setMembershipDaysRemaining(-1); return; }
-        LocalDateTime expire = user.getMembershipExpireAt();
+        // 以会员记录表的活跃记录为准获取过期时间
+        UserMembership active = userMembershipMapper.selectActiveByUserId(user.getId());
+        LocalDateTime expire = active != null ? active.getEffectiveTo() : user.getMembershipExpireAt();
         if (expire != null && expire.isAfter(LocalDateTime.now())) {
             resp.setMembershipExpireAt(expire);
             resp.setMembershipDaysRemaining((int) ChronoUnit.DAYS.between(LocalDateTime.now(), expire));