| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839 |
- 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;
- /**
- * 获取用户当前生效的会员信息
- * <p>无活跃会员记录时,回退到用户表的当前等级(默认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<UserMembership>()
- .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<MembershipBenefitResponse> getBenefits(String levelCode) {
- MembershipLevel level = MembershipLevel.fromCode(levelCode);
- if (level == null) {
- throw new BusinessException(ErrorCode.PARAM_ERROR, "会员等级无效");
- }
- List<MembershipBenefit> 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<User>().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<UserMembership> activeList = userMembershipMapper.selectList(
- new LambdaQueryWrapper<UserMembership>()
- .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);
- }
- /**
- * 统一发放或续期会员(互斥模式)
- * <p>
- * 如果用户已有同等级活跃会员,续期(叠加天数);
- * 否则停用所有旧会员,创建新记录。
- */
- @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<MembershipResponse> listMemberships(Integer pageNum, Integer pageSize,
- Long userId, String level,
- String source, String phone, String nickname) {
- Page<UserMembership> page = new Page<>(pageNum, pageSize);
- LambdaQueryWrapper<UserMembership> 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<Long> matchedUserIds = userMapper.selectList(
- new LambdaQueryWrapper<User>()
- .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<Long> matchedUserIds = userMapper.selectList(
- new LambdaQueryWrapper<User>()
- .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<UserMembership> result = userMembershipMapper.selectPage(page, wrapper);
- Page<MembershipResponse> responsePage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
- // 批量查询用户,避免N+1
- List<Long> userIds = result.getRecords().stream()
- .map(UserMembership::getUserId)
- .distinct()
- .collect(Collectors.toList());
- Map<Long, User> 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 + 当前会员)
- * <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);
- }
- wrapper.orderByDesc(User::getCreateTime);
- Page<User> result = userMapper.selectPage(page, wrapper);
- // 批量查活跃会员记录,取最高等级
- List<Long> userIds = result.getRecords().stream().map(User::getId).toList();
- Map<Long, UserMembership> activeMap = loadActiveMemberships(userIds);
- 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;
- }
- User user = membership.getUserId() != null ? userMapper.selectById(membership.getUserId()) : null;
- return toResponse(membership, user);
- }
- /**
- * 停用用户所有活跃会员记录
- */
- private void deactivateAllActive(Long userId) {
- List<UserMembership> activeList = userMembershipMapper.selectList(
- new LambdaQueryWrapper<UserMembership>()
- .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分钟检查过期会员,自动降级
- * <p>
- * 高级会员全部时长到期后,账号降级为免费普通会员(PLUS)。
- */
- @Scheduled(cron = "0 */5 * * * ?")
- @Transactional
- public void expireMemberships() {
- // 查找所有已过期但仍标记为有效的会员记录
- List<UserMembership> expired = userMembershipMapper.selectList(
- new LambdaQueryWrapper<UserMembership>()
- .eq(UserMembership::getStatus, 1)
- .isNotNull(UserMembership::getEffectiveTo)
- .lt(UserMembership::getEffectiveTo, LocalDateTime.now()));
- if (!expired.isEmpty()) {
- 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);
- }
- }
- }
- 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,已合并) ====================
- /** 会员等级与兼容 UserLevel 对象的映射 */
- private static final Map<MembershipLevel, UserLevel> 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<UserLevelResponse> 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<LevelPricingPlan> 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();
- };
- }
- }
|