MembershipService.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. package com.xuekairui.user.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  4. import com.xuekairui.common.BusinessException;
  5. import com.xuekairui.common.ErrorCode;
  6. import com.xuekairui.user.dto.GrantMembershipRequest;
  7. import com.xuekairui.user.dto.MembershipBenefitResponse;
  8. import com.xuekairui.user.dto.MembershipResponse;
  9. import com.xuekairui.user.entity.MembershipBenefit;
  10. import com.xuekairui.user.entity.User;
  11. import com.xuekairui.user.entity.UserMembership;
  12. import com.xuekairui.user.enums.MembershipLevel;
  13. import com.xuekairui.user.enums.OperationType;
  14. import com.xuekairui.user.mapper.MembershipBenefitMapper;
  15. import com.xuekairui.user.mapper.UserMapper;
  16. import com.xuekairui.user.mapper.UserMembershipMapper;
  17. import lombok.RequiredArgsConstructor;
  18. import lombok.extern.slf4j.Slf4j;
  19. import org.springframework.scheduling.annotation.Scheduled;
  20. import org.springframework.stereotype.Service;
  21. import org.springframework.transaction.annotation.Transactional;
  22. import java.time.LocalDateTime;
  23. import java.util.HashMap;
  24. import java.util.HashSet;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Set;
  28. import java.util.stream.Collectors;
  29. /**
  30. * 会员体系服务
  31. */
  32. @Slf4j
  33. @Service
  34. @RequiredArgsConstructor
  35. public class MembershipService {
  36. private final UserMembershipMapper userMembershipMapper;
  37. private final MembershipBenefitMapper membershipBenefitMapper;
  38. private final UserMapper userMapper;
  39. private final OperationAuditLogService auditLogService;
  40. /**
  41. * 获取用户当前生效的会员信息
  42. * <p>无活跃会员记录时,回退到用户表的当前等级(默认PLUS普通会员)
  43. */
  44. public MembershipResponse getMyMembership(Long userId) {
  45. UserMembership membership = userMembershipMapper.selectActiveByUserId(userId);
  46. if (membership != null) {
  47. return toResponse(membership);
  48. }
  49. // 无活跃会员记录,默认为免费普通会员(PLUS)
  50. return MembershipResponse.builder()
  51. .userId(userId)
  52. .level(MembershipLevel.PLUS.getCode())
  53. .levelName(MembershipLevel.PLUS.getDescription())
  54. .source("SYSTEM")
  55. .effectiveFrom(null)
  56. .effectiveTo(null)
  57. .status(1)
  58. .remark("免费会员")
  59. .build();
  60. }
  61. /**
  62. * 查询用户会员历史记录
  63. */
  64. public List<MembershipResponse> getUserMembershipHistory(Long userId) {
  65. List<UserMembership> list = userMembershipMapper.selectListByUserId(userId);
  66. return list.stream()
  67. .map(this::toResponse)
  68. .collect(Collectors.toList());
  69. }
  70. /**
  71. * 查询指定会员等级的权益列表
  72. */
  73. public List<MembershipBenefitResponse> getBenefits(String levelCode) {
  74. MembershipLevel level = MembershipLevel.fromCode(levelCode);
  75. if (level == null) {
  76. throw new BusinessException(ErrorCode.PARAM_ERROR, "会员等级无效");
  77. }
  78. List<MembershipBenefit> benefits = membershipBenefitMapper.selectEnabledByLevel(level);
  79. return benefits.stream()
  80. .map(this::toBenefitResponse)
  81. .collect(Collectors.toList());
  82. }
  83. /**
  84. * 发放/调整用户会员等级(运营/超管)
  85. */
  86. @Transactional
  87. public MembershipResponse grantMembership(GrantMembershipRequest request, Long operatorId, String operatorRole) {
  88. User user = userMapper.selectById(request.getUserId());
  89. if (user == null) {
  90. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  91. }
  92. MembershipLevel level = MembershipLevel.fromCode(request.getLevel());
  93. if (level == null) {
  94. throw new BusinessException(ErrorCode.PARAM_ERROR, "会员等级无效");
  95. }
  96. // 停用该用户其他有效会员记录
  97. List<UserMembership> activeList = userMembershipMapper.selectList(
  98. new LambdaQueryWrapper<UserMembership>()
  99. .eq(UserMembership::getUserId, request.getUserId())
  100. .eq(UserMembership::getStatus, 1));
  101. for (UserMembership active : activeList) {
  102. active.setStatus(0);
  103. userMembershipMapper.updateById(active);
  104. }
  105. // 创建新会员记录
  106. UserMembership membership = UserMembership.builder()
  107. .userId(request.getUserId())
  108. .level(level)
  109. .source(request.getSource() != null ? request.getSource() : "ADMIN_GRANT")
  110. .effectiveFrom(request.getEffectiveFrom() != null ? request.getEffectiveFrom() : LocalDateTime.now())
  111. .effectiveTo(request.getEffectiveTo())
  112. .status(1)
  113. .remark(request.getRemark())
  114. .build();
  115. userMembershipMapper.insert(membership);
  116. // 同步到用户表当前会员等级与过期时间
  117. user.setMembershipLevel(level);
  118. user.setMembershipExpireAt(membership.getEffectiveTo());
  119. userMapper.updateById(user);
  120. // 审计日志
  121. auditLogService.logSuccess(operatorId, "管理员", operatorRole,
  122. OperationType.USER_UPDATE,
  123. "MEMBERSHIP:" + request.getUserId() + "->" + level.getCode(), request.getUserId());
  124. log.info("运营人员{}为用户{}发放会员等级{}", operatorId, request.getUserId(), level.getCode());
  125. return toResponse(membership);
  126. }
  127. /**
  128. * 统一发放或续期会员(互斥模式)
  129. * <p>
  130. * 如果用户已有同等级活跃会员,续期(叠加天数);
  131. * 否则停用所有旧会员,创建新记录。
  132. */
  133. @Transactional
  134. public void grantOrExtendMembership(Long userId, MembershipLevel level, int days,
  135. String source, String remark) {
  136. User user = userMapper.selectById(userId);
  137. if (user == null) {
  138. return;
  139. }
  140. LocalDateTime newExpireAt;
  141. MembershipLevel effectiveLevel = level;
  142. UserMembership existing = userMembershipMapper.selectActiveByUserId(userId);
  143. if (existing != null) {
  144. if (existing.getLevel() == level) {
  145. // 同等级续期:在原到期时间基础上追加
  146. LocalDateTime base = existing.getEffectiveTo() != null
  147. && existing.getEffectiveTo().isAfter(LocalDateTime.now())
  148. ? existing.getEffectiveTo()
  149. : LocalDateTime.now();
  150. existing.setEffectiveTo(base.plusDays(days));
  151. userMembershipMapper.updateById(existing);
  152. newExpireAt = existing.getEffectiveTo();
  153. } else if (existing.getLevel().getWeight() > level.getWeight()) {
  154. // 已有更高等级:不降级,直接续期更高等级(如ULTRA用户邀请奖励仍保持ULTRA)
  155. LocalDateTime base = existing.getEffectiveTo() != null
  156. && existing.getEffectiveTo().isAfter(LocalDateTime.now())
  157. ? existing.getEffectiveTo()
  158. : LocalDateTime.now();
  159. existing.setEffectiveTo(base.plusDays(days));
  160. userMembershipMapper.updateById(existing);
  161. newExpireAt = existing.getEffectiveTo();
  162. effectiveLevel = existing.getLevel();
  163. } else {
  164. // 升级:停用所有低等级旧会员,创建新记录
  165. deactivateAllActive(userId);
  166. LocalDateTime now = LocalDateTime.now();
  167. UserMembership membership = UserMembership.builder()
  168. .userId(userId)
  169. .level(level)
  170. .source(source)
  171. .effectiveFrom(now)
  172. .effectiveTo(now.plusDays(days))
  173. .status(1)
  174. .remark(remark)
  175. .build();
  176. userMembershipMapper.insert(membership);
  177. newExpireAt = membership.getEffectiveTo();
  178. }
  179. } else {
  180. // 无活跃会员:创建新记录
  181. deactivateAllActive(userId);
  182. LocalDateTime now = LocalDateTime.now();
  183. UserMembership membership = UserMembership.builder()
  184. .userId(userId)
  185. .level(level)
  186. .source(source)
  187. .effectiveFrom(now)
  188. .effectiveTo(now.plusDays(days))
  189. .status(1)
  190. .remark(remark)
  191. .build();
  192. userMembershipMapper.insert(membership);
  193. newExpireAt = membership.getEffectiveTo();
  194. }
  195. // 同步用户表:等级 + 过期时间(仅当等级变化或过期时间变化时更新)
  196. boolean needUpdate = false;
  197. if (user.getMembershipLevel() == null
  198. || user.getMembershipLevel().getWeight() < effectiveLevel.getWeight()) {
  199. user.setMembershipLevel(effectiveLevel);
  200. needUpdate = true;
  201. }
  202. if (user.getMembershipExpireAt() == null
  203. || !user.getMembershipExpireAt().equals(newExpireAt)) {
  204. user.setMembershipExpireAt(newExpireAt);
  205. needUpdate = true;
  206. }
  207. if (needUpdate) {
  208. userMapper.updateById(user);
  209. }
  210. }
  211. /**
  212. * 邀请人奖励:赠送1个月(30天)高级会员(PRO)
  213. * <p>
  214. * 无论邀请人当前是什么等级,统一赠送高级会员时长。
  215. * 同等级续期叠加,不同等级则停用旧会员并启用新的PRO会员。
  216. */
  217. @Transactional
  218. public void grantInviteRewardMembership(Long inviterId, Long inviteeId) {
  219. grantOrExtendMembership(inviterId, MembershipLevel.PRO, 30,
  220. "INVITE", "邀请奖励:邀请用户" + inviteeId + "注册");
  221. log.info("邀请奖励:用户{}获得30天高级会员(PRO)", inviterId);
  222. }
  223. /**
  224. * 支付购买激活会员(互斥模式:同等级续期,不同等级替换)
  225. * @param userId 用户ID
  226. * @param level 会员等级
  227. * @param durationDays 有效期天数(月卡30天,年卡365天)
  228. * @param orderNo 关联支付订单号
  229. * @return 创建或续期的会员记录
  230. */
  231. @Transactional
  232. public UserMembership activateFromPayment(Long userId, MembershipLevel level,
  233. int durationDays, String orderNo) {
  234. User user = userMapper.selectById(userId);
  235. if (user == null) {
  236. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  237. }
  238. grantOrExtendMembership(userId, level, durationDays, "RECHARGE", "支付购买: " + orderNo);
  239. // 审计日志
  240. auditLogService.logSuccess(userId, user.getNickname(), "USER",
  241. OperationType.USER_UPDATE,
  242. "PAY:" + orderNo + "->" + level.getCode(), userId);
  243. log.info("用户{}通过支付{}激活会员等级{}, 有效期{}天", userId, orderNo, level.getCode(), durationDays);
  244. // 返回当前活跃会员记录
  245. return userMembershipMapper.selectActiveByUserId(userId);
  246. }
  247. /**
  248. * 分页查询会员记录
  249. */
  250. public Page<MembershipResponse> listMemberships(Integer pageNum, Integer pageSize, Long userId, String level) {
  251. Page<UserMembership> page = new Page<>(pageNum, pageSize);
  252. LambdaQueryWrapper<UserMembership> wrapper = new LambdaQueryWrapper<>();
  253. if (userId != null) {
  254. wrapper.eq(UserMembership::getUserId, userId);
  255. }
  256. if (level != null && !level.isBlank()) {
  257. MembershipLevel membershipLevel = MembershipLevel.fromCode(level);
  258. if (membershipLevel != null) {
  259. wrapper.eq(UserMembership::getLevel, membershipLevel);
  260. }
  261. }
  262. wrapper.orderByDesc(UserMembership::getCreateTime);
  263. Page<UserMembership> result = userMembershipMapper.selectPage(page, wrapper);
  264. Page<MembershipResponse> responsePage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
  265. // 批量查询用户,避免N+1
  266. List<Long> userIds = result.getRecords().stream()
  267. .map(UserMembership::getUserId)
  268. .distinct()
  269. .collect(Collectors.toList());
  270. Map<Long, User> userMap = userIds.isEmpty() ? new HashMap<>()
  271. : userMapper.selectBatchIds(userIds).stream()
  272. .collect(Collectors.toMap(User::getId, u -> u));
  273. responsePage.setRecords(result.getRecords().stream()
  274. .map(m -> toResponse(m, userMap.get(m.getUserId())))
  275. .collect(Collectors.toList()));
  276. return responsePage;
  277. }
  278. private MembershipResponse toResponse(UserMembership membership) {
  279. if (membership == null) {
  280. return null;
  281. }
  282. User user = membership.getUserId() != null ? userMapper.selectById(membership.getUserId()) : null;
  283. return toResponse(membership, user);
  284. }
  285. /**
  286. * 停用用户所有活跃会员记录
  287. */
  288. private void deactivateAllActive(Long userId) {
  289. List<UserMembership> activeList = userMembershipMapper.selectList(
  290. new LambdaQueryWrapper<UserMembership>()
  291. .eq(UserMembership::getUserId, userId)
  292. .eq(UserMembership::getStatus, 1));
  293. for (UserMembership active : activeList) {
  294. active.setStatus(0);
  295. userMembershipMapper.updateById(active);
  296. }
  297. }
  298. private MembershipResponse toResponse(UserMembership membership, User user) {
  299. if (membership == null) {
  300. return null;
  301. }
  302. MembershipLevel level = membership.getLevel();
  303. return MembershipResponse.builder()
  304. .id(membership.getId())
  305. .userId(membership.getUserId())
  306. .phone(user != null ? user.getPhone() : null)
  307. .nickname(user != null ? user.getNickname() : null)
  308. .level(level != null ? level.getCode() : null)
  309. .levelName(level != null ? level.getDescription() : null)
  310. .source(membership.getSource())
  311. .effectiveFrom(membership.getEffectiveFrom())
  312. .effectiveTo(membership.getEffectiveTo())
  313. .membershipExpireAt(membership.getEffectiveTo())
  314. .status(membership.getStatus())
  315. .remark(membership.getRemark())
  316. .createTime(membership.getCreateTime())
  317. .build();
  318. }
  319. private MembershipBenefitResponse toBenefitResponse(MembershipBenefit benefit) {
  320. return MembershipBenefitResponse.builder()
  321. .benefitType(benefit.getBenefitType())
  322. .benefitTypeName(toBenefitTypeName(benefit.getBenefitType()))
  323. .benefitValue(benefit.getBenefitValue())
  324. .description(benefit.getDescription())
  325. .build();
  326. }
  327. private String toBenefitTypeName(String type) {
  328. return switch (type) {
  329. case "DAILY_QUOTA" -> "每日查询配额";
  330. case "MONTHLY_QUOTA" -> "每月爬虫加成";
  331. case "MAX_CONCURRENT" -> "最大并发加成";
  332. default -> type;
  333. };
  334. }
  335. /**
  336. * 定时任务:每5分钟检查过期会员,自动降级
  337. * <p>
  338. * 高级会员全部时长到期后,账号降级为免费普通会员(PLUS)。
  339. */
  340. @Scheduled(cron = "0 */5 * * * ?")
  341. @Transactional
  342. public void expireMemberships() {
  343. // 查找所有已过期但仍标记为有效的会员记录
  344. List<UserMembership> expired = userMembershipMapper.selectList(
  345. new LambdaQueryWrapper<UserMembership>()
  346. .eq(UserMembership::getStatus, 1)
  347. .isNotNull(UserMembership::getEffectiveTo)
  348. .lt(UserMembership::getEffectiveTo, LocalDateTime.now()));
  349. if (expired.isEmpty()) {
  350. return;
  351. }
  352. Set<Long> affectedUserIds = new HashSet<>();
  353. for (UserMembership m : expired) {
  354. m.setStatus(0);
  355. userMembershipMapper.updateById(m);
  356. affectedUserIds.add(m.getUserId());
  357. }
  358. // 对每个受影响的用户,检查是否还有其他有效会员
  359. for (Long userId : affectedUserIds) {
  360. UserMembership active = userMembershipMapper.selectActiveByUserId(userId);
  361. if (active == null) {
  362. // 无有效会员,降级为普通会员
  363. User user = userMapper.selectById(userId);
  364. if (user != null && user.getMembershipLevel() != MembershipLevel.PLUS) {
  365. user.setMembershipLevel(MembershipLevel.PLUS);
  366. user.setMembershipExpireAt(null);
  367. userMapper.updateById(user);
  368. log.info("用户{}会员已到期,降级为普通会员", userId);
  369. }
  370. }
  371. }
  372. log.info("处理过期会员记录: {}条,涉及用户: {}个", expired.size(), affectedUserIds.size());
  373. }
  374. }