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.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.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();
}
/**
* 查询指定会员等级的权益列表
*/
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, String operatorRole) {
User user = userMapper.selectById(request.getUserId());
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.logSuccess(operatorId, "管理员", operatorRole,
OperationType.USER_UPDATE,
"MEMBERSHIP:" + request.getUserId() + "->" + level.getCode(), request.getUserId());
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);
}
}
/**
* 邀请人奖励:赠送1个月(30天)高级会员(PRO)
*
* 无论邀请人当前是什么等级,统一赠送高级会员时长。
* 同等级续期叠加,不同等级则停用旧会员并启用新的PRO会员。
*/
@Transactional
public void grantInviteRewardMembership(Long inviterId, Long inviteeId) {
grantOrExtendMembership(inviterId, MembershipLevel.PRO, 30,
"INVITE", "邀请奖励:邀请用户" + inviteeId + "注册");
log.info("邀请奖励:用户{}获得30天高级会员(PRO)", inviterId);
}
/**
* 支付购买激活会员(互斥模式:同等级续期,不同等级替换)
* @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(), "USER",
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;
}
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())
.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()) {
return;
}
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());
}
// ==================== 等级查询(原 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; }
LocalDateTime expire = 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();
};
}
}