package com.xuekairui.user.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.xuekairui.common.BusinessException; import com.xuekairui.common.ErrorCode; import com.xuekairui.user.dto.GrantMembershipRequest; import com.xuekairui.user.dto.LevelPricingPlan; import com.xuekairui.user.dto.MembershipBenefitResponse; import com.xuekairui.user.dto.MembershipResponse; import com.xuekairui.user.dto.MembershipUserResponse; import com.xuekairui.user.dto.UserLevelResponse; import com.xuekairui.user.entity.MembershipBenefit; import com.xuekairui.user.entity.User; import com.xuekairui.user.entity.UserLevel; import com.xuekairui.user.entity.UserMembership; import com.xuekairui.user.enums.MembershipLevel; import com.xuekairui.user.enums.OperationType; import com.xuekairui.user.enums.OperatorRole; import com.xuekairui.user.mapper.MembershipBenefitMapper; import com.xuekairui.user.mapper.UserMapper; import com.xuekairui.user.mapper.UserMembershipMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 会员体系服务 */ @Slf4j @Service @RequiredArgsConstructor public class MembershipService { private final UserMembershipMapper userMembershipMapper; private final MembershipBenefitMapper membershipBenefitMapper; private final UserMapper userMapper; private final OperationAuditLogService auditLogService; /** * 获取用户当前生效的会员信息 *

无活跃会员记录时,回退到用户表的当前等级(默认PLUS普通会员) */ public MembershipResponse getMyMembership(Long userId) { UserMembership membership = userMembershipMapper.selectActiveByUserId(userId); if (membership != null) { return toResponse(membership); } // 无活跃会员记录,默认为免费普通会员(PLUS) return MembershipResponse.builder() .userId(userId) .level(MembershipLevel.PLUS.getCode()) .levelName(MembershipLevel.PLUS.getDescription()) .source("SYSTEM") .effectiveFrom(null) .effectiveTo(null) .status(1) .remark("免费会员") .build(); } /** * 用户注册时写入一条 PLUS(免费)会员记录,确保会员历史可追溯 */ public void initPlusMembership(Long userId) { UserMembership exists = userMembershipMapper.selectOne( new LambdaQueryWrapper() .eq(UserMembership::getUserId, userId) .eq(UserMembership::getLevel, MembershipLevel.PLUS) .last("LIMIT 1")); if (exists != null) { return; // 已有 PLUS 记录,不重复写入 } UserMembership membership = UserMembership.builder() .userId(userId) .level(MembershipLevel.PLUS) .source("SYSTEM") .effectiveFrom(LocalDateTime.now()) .effectiveTo(null) // 永久有效 .status(1) .remark("注册即得免费会员") .build(); userMembershipMapper.insert(membership); } /** * 查询指定会员等级的权益列表 */ public List getBenefits(String levelCode) { MembershipLevel level = MembershipLevel.fromCode(levelCode); if (level == null) { throw new BusinessException(ErrorCode.PARAM_ERROR, "会员等级无效"); } List benefits = membershipBenefitMapper.selectEnabledByLevel(level); return benefits.stream() .map(this::toBenefitResponse) .collect(Collectors.toList()); } /** * 发放/调整用户会员等级(运营/超管) */ @Transactional public MembershipResponse grantMembership(GrantMembershipRequest request, Long operatorId) { User user; if (request.getUserId() != null) { user = userMapper.selectById(request.getUserId()); } else if (request.getPhone() != null && !request.getPhone().isBlank()) { user = userMapper.selectOne( new LambdaQueryWrapper().eq(User::getPhone, request.getPhone())); } else { throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID和手机号至少填写一个"); } if (user == null) { throw new BusinessException(ErrorCode.USER_NOT_FOUND); } MembershipLevel level = MembershipLevel.fromCode(request.getLevel()); if (level == null) { throw new BusinessException(ErrorCode.PARAM_ERROR, "会员等级无效"); } // 停用该用户其他有效会员记录 List activeList = userMembershipMapper.selectList( new LambdaQueryWrapper() .eq(UserMembership::getUserId, request.getUserId()) .eq(UserMembership::getStatus, 1)); for (UserMembership active : activeList) { active.setStatus(0); userMembershipMapper.updateById(active); } // 创建新会员记录 UserMembership membership = UserMembership.builder() .userId(request.getUserId()) .level(level) .source(request.getSource() != null ? request.getSource() : "ADMIN_GRANT") .effectiveFrom(request.getEffectiveFrom() != null ? request.getEffectiveFrom() : LocalDateTime.now()) .effectiveTo(request.getEffectiveTo()) .status(1) .remark(request.getRemark()) .build(); userMembershipMapper.insert(membership); // 同步到用户表当前会员等级与过期时间 user.setMembershipLevel(level); user.setMembershipExpireAt(membership.getEffectiveTo()); userMapper.updateById(user); // 审计日志 auditLogService.logAdmin(operatorId, OperationType.USER_UPDATE, "MEMBERSHIP:" + user.getId() + "->" + level.getCode(), user.getId()); log.info("运营人员{}为用户{}发放会员等级{}", operatorId, request.getUserId(), level.getCode()); return toResponse(membership); } /** * 统一发放或续期会员(互斥模式) *

* 如果用户已有同等级活跃会员,续期(叠加天数); * 否则停用所有旧会员,创建新记录。 */ @Transactional public void grantOrExtendMembership(Long userId, MembershipLevel level, int days, String source, String remark) { User user = userMapper.selectById(userId); if (user == null) { return; } LocalDateTime newExpireAt; MembershipLevel effectiveLevel = level; UserMembership existing = userMembershipMapper.selectActiveByUserId(userId); if (existing != null) { if (existing.getLevel() == level) { // 同等级续期:在原到期时间基础上追加 LocalDateTime base = existing.getEffectiveTo() != null && existing.getEffectiveTo().isAfter(LocalDateTime.now()) ? existing.getEffectiveTo() : LocalDateTime.now(); existing.setEffectiveTo(base.plusDays(days)); userMembershipMapper.updateById(existing); newExpireAt = existing.getEffectiveTo(); } else if (existing.getLevel().getWeight() > level.getWeight()) { // 已有更高等级:不降级,直接续期更高等级(如ULTRA用户邀请奖励仍保持ULTRA) LocalDateTime base = existing.getEffectiveTo() != null && existing.getEffectiveTo().isAfter(LocalDateTime.now()) ? existing.getEffectiveTo() : LocalDateTime.now(); existing.setEffectiveTo(base.plusDays(days)); userMembershipMapper.updateById(existing); newExpireAt = existing.getEffectiveTo(); effectiveLevel = existing.getLevel(); } else { // 升级:停用所有低等级旧会员,创建新记录 deactivateAllActive(userId); LocalDateTime now = LocalDateTime.now(); UserMembership membership = UserMembership.builder() .userId(userId) .level(level) .source(source) .effectiveFrom(now) .effectiveTo(now.plusDays(days)) .status(1) .remark(remark) .build(); userMembershipMapper.insert(membership); newExpireAt = membership.getEffectiveTo(); } } else { // 无活跃会员:创建新记录 deactivateAllActive(userId); LocalDateTime now = LocalDateTime.now(); UserMembership membership = UserMembership.builder() .userId(userId) .level(level) .source(source) .effectiveFrom(now) .effectiveTo(now.plusDays(days)) .status(1) .remark(remark) .build(); userMembershipMapper.insert(membership); newExpireAt = membership.getEffectiveTo(); } // 同步用户表:等级 + 过期时间(仅当等级变化或过期时间变化时更新) boolean needUpdate = false; if (user.getMembershipLevel() == null || user.getMembershipLevel().getWeight() < effectiveLevel.getWeight()) { user.setMembershipLevel(effectiveLevel); needUpdate = true; } if (user.getMembershipExpireAt() == null || !user.getMembershipExpireAt().equals(newExpireAt)) { user.setMembershipExpireAt(newExpireAt); needUpdate = true; } if (needUpdate) { userMapper.updateById(user); } } /** * 支付购买激活会员(互斥模式:同等级续期,不同等级替换) * @param userId 用户ID * @param level 会员等级 * @param durationDays 有效期天数(月卡30天,年卡365天) * @param orderNo 关联支付订单号 * @return 创建或续期的会员记录 */ @Transactional public UserMembership activateFromPayment(Long userId, MembershipLevel level, int durationDays, String orderNo) { User user = userMapper.selectById(userId); if (user == null) { throw new BusinessException(ErrorCode.USER_NOT_FOUND); } grantOrExtendMembership(userId, level, durationDays, "RECHARGE", "支付购买: " + orderNo); // 审计日志 auditLogService.logSuccess(userId, user.getNickname(), OperatorRole.USER.getCode(), OperationType.USER_UPDATE, "PAY:" + orderNo + "->" + level.getCode(), userId); log.info("用户{}通过支付{}激活会员等级{}, 有效期{}天", userId, orderNo, level.getCode(), durationDays); // 返回当前活跃会员记录 return userMembershipMapper.selectActiveByUserId(userId); } /** * 分页查询会员记录(支持按用户/等级/来源/手机号/昵称筛选) */ public Page listMemberships(Integer pageNum, Integer pageSize, Long userId, String level, String source, String phone, String nickname) { Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (userId != null) { wrapper.eq(UserMembership::getUserId, userId); } if (level != null && !level.isBlank()) { MembershipLevel membershipLevel = MembershipLevel.fromCode(level); if (membershipLevel != null) { wrapper.eq(UserMembership::getLevel, membershipLevel); } } if (source != null && !source.isBlank()) { wrapper.eq(UserMembership::getSource, source); } // 昵称筛选:先按昵称模糊查用户ID列表,再过滤会员记录 if (nickname != null && !nickname.isBlank()) { List matchedUserIds = userMapper.selectList( new LambdaQueryWrapper() .select(User::getId) .like(User::getNickname, nickname)) .stream().map(User::getId).collect(Collectors.toList()); if (matchedUserIds.isEmpty()) { return new Page<>(pageNum, pageSize, 0); } wrapper.in(UserMembership::getUserId, matchedUserIds); } // 手机号筛选:先按手机号模糊查用户ID列表,再过滤会员记录 if (phone != null && !phone.isBlank()) { List matchedUserIds = userMapper.selectList( new LambdaQueryWrapper() .select(User::getId) .like(User::getPhone, phone)) .stream().map(User::getId).collect(Collectors.toList()); if (matchedUserIds.isEmpty()) { return new Page<>(pageNum, pageSize, 0); } wrapper.in(UserMembership::getUserId, matchedUserIds); } wrapper.orderByDesc(UserMembership::getCreateTime); Page result = userMembershipMapper.selectPage(page, wrapper); Page responsePage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); // 批量查询用户,避免N+1 List userIds = result.getRecords().stream() .map(UserMembership::getUserId) .distinct() .collect(Collectors.toList()); Map userMap = userIds.isEmpty() ? new HashMap<>() : userMapper.selectByIds(userIds).stream() .collect(Collectors.toMap(User::getId, u -> u)); responsePage.setRecords(result.getRecords().stream() .map(m -> toResponse(m, userMap.get(m.getUserId()))) .collect(Collectors.toList())); return responsePage; } /** * 会员用户快照列表(用户维度,查 t_user + 当前会员) *

当前会员等级以 t_user_membership 表的活跃记录为准(取最高等级), * 若无活跃记录则回退到 t_user.membership_level,再回退到 PLUS。 * 这样可兼容历史数据(t_user.membership_level 未同步更新的情况)。 */ public Page 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 result = listMembershipUsersByLevel(pageNum, pageSize, phone, filterLevel); return result; } Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (phone != null && !phone.isBlank()) { wrapper.like(User::getPhone, phone); } wrapper.orderByDesc(User::getCreateTime); Page result = userMapper.selectPage(page, wrapper); // 批量查活跃会员记录,取最高等级 List userIds = result.getRecords().stream().map(User::getId).toList(); Map activeMap = loadActiveMemberships(userIds); List list = result.getRecords().stream() .map(u -> toMembershipUserResponse(u, activeMap.get(u.getId()))) .collect(Collectors.toList()); Page responsePage = new Page<>(pageNum, pageSize, result.getTotal()); responsePage.setRecords(list); return responsePage; } /** * 按等级筛选会员用户列表 *

筛选逻辑:用户当前最高等级活跃记录等于筛选等级时才命中。 * 例如筛选 PRO 时,若用户同时有活跃 ULTRA 记录,则该用户归为 ULTRA 而非 PRO。 */ private Page listMembershipUsersByLevel(Integer pageNum, Integer pageSize, String phone, MembershipLevel filterLevel) { // 查出所有活跃会员记录 LambdaQueryWrapper mWrapper = new LambdaQueryWrapper<>(); mWrapper.eq(UserMembership::getStatus, 1) .and(w -> w.isNull(UserMembership::getEffectiveTo) .or().gt(UserMembership::getEffectiveTo, LocalDateTime.now())); List memberships = userMembershipMapper.selectList(mWrapper); if (memberships.isEmpty()) { Page emptyPage = new Page<>(pageNum, pageSize, 0); emptyPage.setRecords(Collections.emptyList()); return emptyPage; } // 按 userId 分组,取每个用户最高等级的活跃记录 Map 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 matchedMap = new HashMap<>(); for (Map.Entry entry : topMap.entrySet()) { if (entry.getValue().getLevel() == filterLevel) { matchedMap.put(entry.getKey(), entry.getValue()); } } if (matchedMap.isEmpty()) { Page emptyPage = new Page<>(pageNum, pageSize, 0); emptyPage.setRecords(Collections.emptyList()); return emptyPage; } List userIds = new java.util.ArrayList<>(matchedMap.keySet()); // 按手机号筛选用户 LambdaQueryWrapper uWrapper = new LambdaQueryWrapper<>(); uWrapper.in(User::getId, userIds); if (phone != null && !phone.isBlank()) { uWrapper.like(User::getPhone, phone); } uWrapper.orderByDesc(User::getCreateTime); Page page = new Page<>(pageNum, pageSize); Page userPage = userMapper.selectPage(page, uWrapper); List list = userPage.getRecords().stream() .map(u -> toMembershipUserResponse(u, matchedMap.get(u.getId()))) .collect(Collectors.toList()); Page responsePage = new Page<>(pageNum, pageSize, userPage.getTotal()); responsePage.setRecords(list); return responsePage; } /** * 批量加载用户的活跃会员记录,返回 userId -> 最高等级会员记录 的映射 */ private Map loadActiveMemberships(List userIds) { if (userIds == null || userIds.isEmpty()) { return Collections.emptyMap(); } List actives = userMembershipMapper.selectActiveByUserIds(userIds); Map 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; } /** * 将用户和活跃会员记录组装为响应对象 *

优先使用会员记录表的等级和过期时间;若无活跃记录,回退到用户表字段。 * 高级会员过期后(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; } User user = membership.getUserId() != null ? userMapper.selectById(membership.getUserId()) : null; return toResponse(membership, user); } /** * 停用用户所有活跃会员记录 */ private void deactivateAllActive(Long userId) { List activeList = userMembershipMapper.selectList( new LambdaQueryWrapper() .eq(UserMembership::getUserId, userId) .eq(UserMembership::getStatus, 1)); for (UserMembership active : activeList) { active.setStatus(0); userMembershipMapper.updateById(active); } } private MembershipResponse toResponse(UserMembership membership, User user) { if (membership == null) { return null; } MembershipLevel level = membership.getLevel(); return MembershipResponse.builder() .id(membership.getId()) .userId(membership.getUserId()) .phone(user != null ? user.getPhone() : null) .nickname(user != null ? user.getNickname() : null) .level(level != null ? level.getCode() : null) .levelName(level != null ? level.getDescription() : null) .source(membership.getSource()) .effectiveFrom(membership.getEffectiveFrom()) .effectiveTo(membership.getEffectiveTo()) .membershipExpireAt(membership.getEffectiveTo()) .status(membership.getStatus()) .remark(membership.getRemark()) .wechatNickname(user != null ? user.getWechatNickname() : null) .createTime(membership.getCreateTime()) .build(); } private MembershipBenefitResponse toBenefitResponse(MembershipBenefit benefit) { return MembershipBenefitResponse.builder() .benefitType(benefit.getBenefitType()) .benefitTypeName(toBenefitTypeName(benefit.getBenefitType())) .benefitValue(benefit.getBenefitValue()) .description(benefit.getDescription()) .build(); } private String toBenefitTypeName(String type) { return switch (type) { case "DAILY_QUOTA" -> "每日查询配额"; case "MONTHLY_QUOTA" -> "每月爬虫加成"; case "MAX_CONCURRENT" -> "最大并发加成"; default -> type; }; } /** * 定时任务:每5分钟检查过期会员,自动降级 *

* 高级会员全部时长到期后,账号降级为免费普通会员(PLUS)。 */ @Scheduled(cron = "0 */5 * * * ?") @Transactional public void expireMemberships() { // 查找所有已过期但仍标记为有效的会员记录 List expired = userMembershipMapper.selectList( new LambdaQueryWrapper() .eq(UserMembership::getStatus, 1) .isNotNull(UserMembership::getEffectiveTo) .lt(UserMembership::getEffectiveTo, LocalDateTime.now())); if (!expired.isEmpty()) { Set 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); } } } 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 actives = userMembershipMapper.selectList( new LambdaQueryWrapper() .eq(UserMembership::getStatus, 1) .and(w -> w.isNull(UserMembership::getEffectiveTo) .or().gt(UserMembership::getEffectiveTo, LocalDateTime.now()))); // 按 userId 分组,取每个用户最高等级 Map 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 highLevelUsers = userMapper.selectList( new LambdaQueryWrapper() .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,已合并) ==================== /** 会员等级与兼容 UserLevel 对象的映射 */ private static final Map LEVEL_MAP = Map.of( MembershipLevel.PLUS, UserLevel.builder() .id(1L).levelName("普通会员").levelCode("PLUS").levelWeight(1) .crawlerQuota(10).monthlyQuota(300).yearlyQuota(3650).maxConcurrent(1) .description("普通会员:每日有效查询10次").status(1).build(), MembershipLevel.PRO, UserLevel.builder() .id(2L).levelName("高级会员").levelCode("PRO").levelWeight(2) .crawlerQuota(999).monthlyQuota(29970).yearlyQuota(364635).maxConcurrent(5) .description("高级会员:每日不限量").status(1).build(), MembershipLevel.ULTRA, UserLevel.builder() .id(3L).levelName("超级会员").levelCode("ULTRA").levelWeight(3) .crawlerQuota(999).monthlyQuota(29970).yearlyQuota(364635).maxConcurrent(10) .description("超级会员:连锁专用").status(1).build()); public List listLevels() { return LEVEL_MAP.keySet().stream() .sorted(Comparator.comparingInt(l -> LEVEL_MAP.get(l).getLevelWeight())) .map(this::buildUserLevelWithBenefits) .map(this::toLevelResponse) .collect(Collectors.toList()); } public UserLevelResponse getLevel(Long levelId) { return LEVEL_MAP.values().stream() .filter(l -> l.getId().equals(levelId)).findFirst() .map(l -> buildUserLevelWithBenefits(l.getLevelCode() != null ? MembershipLevel.fromCode(l.getLevelCode()) : MembershipLevel.PLUS)) .map(this::toLevelResponse) .orElseThrow(() -> new BusinessException(ErrorCode.LEVEL_NOT_FOUND)); } public UserLevelResponse getUserLevel(Long userId) { User user = userMapper.selectById(userId); if (user == null) { throw new BusinessException(ErrorCode.USER_NOT_FOUND); } UserLevelResponse resp = toLevelResponse(resolveUserLevel(user)); fillMembershipExpiration(resp, user); return resp; } public UserLevel getUserLevelEntity(Long userId) { return resolveUserLevel(userMapper.selectById(userId)); } private UserLevel resolveUserLevel(User user) { if (user == null) { return buildUserLevelWithBenefits(MembershipLevel.PLUS); } if (user.getMembershipExpireAt() != null && user.getMembershipExpireAt().isBefore(LocalDateTime.now())) { return buildUserLevelWithBenefits(MembershipLevel.PLUS); } UserMembership active = userMembershipMapper.selectActiveByUserId(user.getId()); return buildUserLevelWithBenefits(active != null && active.getLevel() != null ? active.getLevel() : MembershipLevel.PLUS); } private UserLevel buildUserLevelWithBenefits(MembershipLevel level) { UserLevel base = LEVEL_MAP.getOrDefault(level, LEVEL_MAP.get(MembershipLevel.PLUS)); var builder = UserLevel.builder() .id(base.getId()).levelName(base.getLevelName()).levelCode(base.getLevelCode()) .levelWeight(base.getLevelWeight()).description(base.getDescription()) .status(base.getStatus()).crawlerQuota(base.getCrawlerQuota()) .monthlyQuota(base.getMonthlyQuota()).yearlyQuota(base.getYearlyQuota()) .maxConcurrent(base.getMaxConcurrent()); for (MembershipBenefit b : membershipBenefitMapper.selectEnabledByLevel(level)) { if (b.getBenefitValue() == null) { continue; } switch (b.getBenefitType()) { case "DAILY_QUOTA" -> builder.crawlerQuota(b.getBenefitValue()); case "MONTHLY_QUOTA" -> builder.monthlyQuota(b.getBenefitValue()); case "YEARLY_QUOTA" -> builder.yearlyQuota(b.getBenefitValue()); case "MAX_CONCURRENT" -> builder.maxConcurrent(b.getBenefitValue()); } } return builder.build(); } private UserLevelResponse toLevelResponse( UserLevel level) { return UserLevelResponse.builder() .id(level.getId()).levelName(level.getLevelName()) .levelCode(level.getLevelCode()).levelWeight(level.getLevelWeight()) .crawlerQuota(level.getCrawlerQuota()).monthlyQuota(level.getMonthlyQuota()) .yearlyQuota(level.getYearlyQuota()).maxConcurrent(level.getMaxConcurrent()) .description(level.getDescription()).version(null) .pricingPlans(buildPricingPlans(level.getLevelCode())).build(); } private void fillMembershipExpiration(UserLevelResponse resp, User user) { if (user == null || resp == null) { return; } boolean premium = resp.getLevelCode() != null && !"PLUS".equals(resp.getLevelCode()); if (!premium) { resp.setMembershipExpireAt(null); resp.setMembershipDaysRemaining(-1); return; } // 以会员记录表的活跃记录为准获取过期时间 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)); } else if (expire != null) { resp.setMembershipExpireAt(expire); resp.setMembershipDaysRemaining(0); } else { log.warn("高级会员缺少过期时间: userId={}", user.getId()); resp.setMembershipExpireAt(null); resp.setMembershipDaysRemaining(0); } } private List buildPricingPlans(String levelCode) { return switch (levelCode) { case "PRO" -> List.of( LevelPricingPlan.builder() .planCode("MONTHLY_PRO").planName("高级会员·月卡") .price(new BigDecimal("29.90")) .originalPrice(new BigDecimal("59.90")) .durationDays(30).description("每天不限量· 1个月29.9").build(), LevelPricingPlan.builder() .planCode("YEARLY_PRO").planName("高级会员·年卡") .price(new BigDecimal("299.90")) .originalPrice(new BigDecimal("599.90")) .durationDays(365).description("每天不限量· 1年299.9").build()); case "ULTRA" -> List.of( LevelPricingPlan.builder() .planCode("ULTRA").planName("超级会员·连锁专享") .price(BigDecimal.ZERO).originalPrice(null) .durationDays(0).description("连锁专用,多店铺管理,敬请期待").build()); default -> Collections.emptyList(); }; } }