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();
};
}
}