MembershipService.java 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  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.LevelPricingPlan;
  8. import com.xuekairui.user.dto.MembershipBenefitResponse;
  9. import com.xuekairui.user.dto.MembershipResponse;
  10. import com.xuekairui.user.dto.UserLevelResponse;
  11. import com.xuekairui.user.entity.MembershipBenefit;
  12. import com.xuekairui.user.entity.User;
  13. import com.xuekairui.user.entity.UserLevel;
  14. import com.xuekairui.user.entity.UserMembership;
  15. import com.xuekairui.user.enums.MembershipLevel;
  16. import com.xuekairui.user.enums.OperationType;
  17. import com.xuekairui.user.mapper.MembershipBenefitMapper;
  18. import com.xuekairui.user.mapper.UserMapper;
  19. import com.xuekairui.user.mapper.UserMembershipMapper;
  20. import lombok.RequiredArgsConstructor;
  21. import lombok.extern.slf4j.Slf4j;
  22. import org.springframework.scheduling.annotation.Scheduled;
  23. import org.springframework.stereotype.Service;
  24. import org.springframework.transaction.annotation.Transactional;
  25. import java.math.BigDecimal;
  26. import java.time.LocalDateTime;
  27. import java.time.temporal.ChronoUnit;
  28. import java.util.Collections;
  29. import java.util.Comparator;
  30. import java.util.HashMap;
  31. import java.util.HashSet;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Set;
  35. import java.util.stream.Collectors;
  36. /**
  37. * 会员体系服务
  38. */
  39. @Slf4j
  40. @Service
  41. @RequiredArgsConstructor
  42. public class MembershipService {
  43. private final UserMembershipMapper userMembershipMapper;
  44. private final MembershipBenefitMapper membershipBenefitMapper;
  45. private final UserMapper userMapper;
  46. private final OperationAuditLogService auditLogService;
  47. /**
  48. * 获取用户当前生效的会员信息
  49. * <p>无活跃会员记录时,回退到用户表的当前等级(默认PLUS普通会员)
  50. */
  51. public MembershipResponse getMyMembership(Long userId) {
  52. UserMembership membership = userMembershipMapper.selectActiveByUserId(userId);
  53. if (membership != null) {
  54. return toResponse(membership);
  55. }
  56. // 无活跃会员记录,默认为免费普通会员(PLUS)
  57. return MembershipResponse.builder()
  58. .userId(userId)
  59. .level(MembershipLevel.PLUS.getCode())
  60. .levelName(MembershipLevel.PLUS.getDescription())
  61. .source("SYSTEM")
  62. .effectiveFrom(null)
  63. .effectiveTo(null)
  64. .status(1)
  65. .remark("免费会员")
  66. .build();
  67. }
  68. /**
  69. * 查询指定会员等级的权益列表
  70. */
  71. public List<MembershipBenefitResponse> getBenefits(String levelCode) {
  72. MembershipLevel level = MembershipLevel.fromCode(levelCode);
  73. if (level == null) {
  74. throw new BusinessException(ErrorCode.PARAM_ERROR, "会员等级无效");
  75. }
  76. List<MembershipBenefit> benefits = membershipBenefitMapper.selectEnabledByLevel(level);
  77. return benefits.stream()
  78. .map(this::toBenefitResponse)
  79. .collect(Collectors.toList());
  80. }
  81. /**
  82. * 发放/调整用户会员等级(运营/超管)
  83. */
  84. @Transactional
  85. public MembershipResponse grantMembership(GrantMembershipRequest request, Long operatorId, String operatorRole) {
  86. User user = userMapper.selectById(request.getUserId());
  87. if (user == null) {
  88. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  89. }
  90. MembershipLevel level = MembershipLevel.fromCode(request.getLevel());
  91. if (level == null) {
  92. throw new BusinessException(ErrorCode.PARAM_ERROR, "会员等级无效");
  93. }
  94. // 停用该用户其他有效会员记录
  95. List<UserMembership> activeList = userMembershipMapper.selectList(
  96. new LambdaQueryWrapper<UserMembership>()
  97. .eq(UserMembership::getUserId, request.getUserId())
  98. .eq(UserMembership::getStatus, 1));
  99. for (UserMembership active : activeList) {
  100. active.setStatus(0);
  101. userMembershipMapper.updateById(active);
  102. }
  103. // 创建新会员记录
  104. UserMembership membership = UserMembership.builder()
  105. .userId(request.getUserId())
  106. .level(level)
  107. .source(request.getSource() != null ? request.getSource() : "ADMIN_GRANT")
  108. .effectiveFrom(request.getEffectiveFrom() != null ? request.getEffectiveFrom() : LocalDateTime.now())
  109. .effectiveTo(request.getEffectiveTo())
  110. .status(1)
  111. .remark(request.getRemark())
  112. .build();
  113. userMembershipMapper.insert(membership);
  114. // 同步到用户表当前会员等级与过期时间
  115. user.setMembershipLevel(level);
  116. user.setMembershipExpireAt(membership.getEffectiveTo());
  117. userMapper.updateById(user);
  118. // 审计日志
  119. auditLogService.logSuccess(operatorId, "管理员", operatorRole,
  120. OperationType.USER_UPDATE,
  121. "MEMBERSHIP:" + request.getUserId() + "->" + level.getCode(), request.getUserId());
  122. log.info("运营人员{}为用户{}发放会员等级{}", operatorId, request.getUserId(), level.getCode());
  123. return toResponse(membership);
  124. }
  125. /**
  126. * 统一发放或续期会员(互斥模式)
  127. * <p>
  128. * 如果用户已有同等级活跃会员,续期(叠加天数);
  129. * 否则停用所有旧会员,创建新记录。
  130. */
  131. @Transactional
  132. public void grantOrExtendMembership(Long userId, MembershipLevel level, int days,
  133. String source, String remark) {
  134. User user = userMapper.selectById(userId);
  135. if (user == null) {
  136. return;
  137. }
  138. LocalDateTime newExpireAt;
  139. MembershipLevel effectiveLevel = level;
  140. UserMembership existing = userMembershipMapper.selectActiveByUserId(userId);
  141. if (existing != null) {
  142. if (existing.getLevel() == level) {
  143. // 同等级续期:在原到期时间基础上追加
  144. LocalDateTime base = existing.getEffectiveTo() != null
  145. && existing.getEffectiveTo().isAfter(LocalDateTime.now())
  146. ? existing.getEffectiveTo()
  147. : LocalDateTime.now();
  148. existing.setEffectiveTo(base.plusDays(days));
  149. userMembershipMapper.updateById(existing);
  150. newExpireAt = existing.getEffectiveTo();
  151. } else if (existing.getLevel().getWeight() > level.getWeight()) {
  152. // 已有更高等级:不降级,直接续期更高等级(如ULTRA用户邀请奖励仍保持ULTRA)
  153. LocalDateTime base = existing.getEffectiveTo() != null
  154. && existing.getEffectiveTo().isAfter(LocalDateTime.now())
  155. ? existing.getEffectiveTo()
  156. : LocalDateTime.now();
  157. existing.setEffectiveTo(base.plusDays(days));
  158. userMembershipMapper.updateById(existing);
  159. newExpireAt = existing.getEffectiveTo();
  160. effectiveLevel = existing.getLevel();
  161. } else {
  162. // 升级:停用所有低等级旧会员,创建新记录
  163. deactivateAllActive(userId);
  164. LocalDateTime now = LocalDateTime.now();
  165. UserMembership membership = UserMembership.builder()
  166. .userId(userId)
  167. .level(level)
  168. .source(source)
  169. .effectiveFrom(now)
  170. .effectiveTo(now.plusDays(days))
  171. .status(1)
  172. .remark(remark)
  173. .build();
  174. userMembershipMapper.insert(membership);
  175. newExpireAt = membership.getEffectiveTo();
  176. }
  177. } else {
  178. // 无活跃会员:创建新记录
  179. deactivateAllActive(userId);
  180. LocalDateTime now = LocalDateTime.now();
  181. UserMembership membership = UserMembership.builder()
  182. .userId(userId)
  183. .level(level)
  184. .source(source)
  185. .effectiveFrom(now)
  186. .effectiveTo(now.plusDays(days))
  187. .status(1)
  188. .remark(remark)
  189. .build();
  190. userMembershipMapper.insert(membership);
  191. newExpireAt = membership.getEffectiveTo();
  192. }
  193. // 同步用户表:等级 + 过期时间(仅当等级变化或过期时间变化时更新)
  194. boolean needUpdate = false;
  195. if (user.getMembershipLevel() == null
  196. || user.getMembershipLevel().getWeight() < effectiveLevel.getWeight()) {
  197. user.setMembershipLevel(effectiveLevel);
  198. needUpdate = true;
  199. }
  200. if (user.getMembershipExpireAt() == null
  201. || !user.getMembershipExpireAt().equals(newExpireAt)) {
  202. user.setMembershipExpireAt(newExpireAt);
  203. needUpdate = true;
  204. }
  205. if (needUpdate) {
  206. userMapper.updateById(user);
  207. }
  208. }
  209. /**
  210. * 邀请人奖励:赠送1个月(30天)高级会员(PRO)
  211. * <p>
  212. * 无论邀请人当前是什么等级,统一赠送高级会员时长。
  213. * 同等级续期叠加,不同等级则停用旧会员并启用新的PRO会员。
  214. */
  215. @Transactional
  216. public void grantInviteRewardMembership(Long inviterId, Long inviteeId) {
  217. grantOrExtendMembership(inviterId, MembershipLevel.PRO, 30,
  218. "INVITE", "邀请奖励:邀请用户" + inviteeId + "注册");
  219. log.info("邀请奖励:用户{}获得30天高级会员(PRO)", inviterId);
  220. }
  221. /**
  222. * 支付购买激活会员(互斥模式:同等级续期,不同等级替换)
  223. * @param userId 用户ID
  224. * @param level 会员等级
  225. * @param durationDays 有效期天数(月卡30天,年卡365天)
  226. * @param orderNo 关联支付订单号
  227. * @return 创建或续期的会员记录
  228. */
  229. @Transactional
  230. public UserMembership activateFromPayment(Long userId, MembershipLevel level,
  231. int durationDays, String orderNo) {
  232. User user = userMapper.selectById(userId);
  233. if (user == null) {
  234. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  235. }
  236. grantOrExtendMembership(userId, level, durationDays, "RECHARGE", "支付购买: " + orderNo);
  237. // 审计日志
  238. auditLogService.logSuccess(userId, user.getNickname(), "USER",
  239. OperationType.USER_UPDATE,
  240. "PAY:" + orderNo + "->" + level.getCode(), userId);
  241. log.info("用户{}通过支付{}激活会员等级{}, 有效期{}天", userId, orderNo, level.getCode(), durationDays);
  242. // 返回当前活跃会员记录
  243. return userMembershipMapper.selectActiveByUserId(userId);
  244. }
  245. /**
  246. * 分页查询会员记录(支持按用户/等级/来源/手机号/昵称筛选)
  247. */
  248. public Page<MembershipResponse> listMemberships(Integer pageNum, Integer pageSize,
  249. Long userId, String level,
  250. String source, String phone, String nickname) {
  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. if (source != null && !source.isBlank()) {
  263. wrapper.eq(UserMembership::getSource, source);
  264. }
  265. // 昵称筛选:先按昵称模糊查用户ID列表,再过滤会员记录
  266. if (nickname != null && !nickname.isBlank()) {
  267. List<Long> matchedUserIds = userMapper.selectList(
  268. new LambdaQueryWrapper<User>()
  269. .select(User::getId)
  270. .like(User::getNickname, nickname))
  271. .stream().map(User::getId).collect(Collectors.toList());
  272. if (matchedUserIds.isEmpty()) {
  273. return new Page<>(pageNum, pageSize, 0);
  274. }
  275. wrapper.in(UserMembership::getUserId, matchedUserIds);
  276. }
  277. // 手机号筛选:先按手机号模糊查用户ID列表,再过滤会员记录
  278. if (phone != null && !phone.isBlank()) {
  279. List<Long> matchedUserIds = userMapper.selectList(
  280. new LambdaQueryWrapper<User>()
  281. .select(User::getId)
  282. .like(User::getPhone, phone))
  283. .stream().map(User::getId).collect(Collectors.toList());
  284. if (matchedUserIds.isEmpty()) {
  285. return new Page<>(pageNum, pageSize, 0);
  286. }
  287. wrapper.in(UserMembership::getUserId, matchedUserIds);
  288. }
  289. wrapper.orderByDesc(UserMembership::getCreateTime);
  290. Page<UserMembership> result = userMembershipMapper.selectPage(page, wrapper);
  291. Page<MembershipResponse> responsePage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
  292. // 批量查询用户,避免N+1
  293. List<Long> userIds = result.getRecords().stream()
  294. .map(UserMembership::getUserId)
  295. .distinct()
  296. .collect(Collectors.toList());
  297. Map<Long, User> userMap = userIds.isEmpty() ? new HashMap<>()
  298. : userMapper.selectByIds(userIds).stream()
  299. .collect(Collectors.toMap(User::getId, u -> u));
  300. responsePage.setRecords(result.getRecords().stream()
  301. .map(m -> toResponse(m, userMap.get(m.getUserId())))
  302. .collect(Collectors.toList()));
  303. return responsePage;
  304. }
  305. private MembershipResponse toResponse(UserMembership membership) {
  306. if (membership == null) {
  307. return null;
  308. }
  309. User user = membership.getUserId() != null ? userMapper.selectById(membership.getUserId()) : null;
  310. return toResponse(membership, user);
  311. }
  312. /**
  313. * 停用用户所有活跃会员记录
  314. */
  315. private void deactivateAllActive(Long userId) {
  316. List<UserMembership> activeList = userMembershipMapper.selectList(
  317. new LambdaQueryWrapper<UserMembership>()
  318. .eq(UserMembership::getUserId, userId)
  319. .eq(UserMembership::getStatus, 1));
  320. for (UserMembership active : activeList) {
  321. active.setStatus(0);
  322. userMembershipMapper.updateById(active);
  323. }
  324. }
  325. private MembershipResponse toResponse(UserMembership membership, User user) {
  326. if (membership == null) {
  327. return null;
  328. }
  329. MembershipLevel level = membership.getLevel();
  330. return MembershipResponse.builder()
  331. .id(membership.getId())
  332. .userId(membership.getUserId())
  333. .phone(user != null ? user.getPhone() : null)
  334. .nickname(user != null ? user.getNickname() : null)
  335. .level(level != null ? level.getCode() : null)
  336. .levelName(level != null ? level.getDescription() : null)
  337. .source(membership.getSource())
  338. .effectiveFrom(membership.getEffectiveFrom())
  339. .effectiveTo(membership.getEffectiveTo())
  340. .membershipExpireAt(membership.getEffectiveTo())
  341. .status(membership.getStatus())
  342. .remark(membership.getRemark())
  343. .createTime(membership.getCreateTime())
  344. .build();
  345. }
  346. private MembershipBenefitResponse toBenefitResponse(MembershipBenefit benefit) {
  347. return MembershipBenefitResponse.builder()
  348. .benefitType(benefit.getBenefitType())
  349. .benefitTypeName(toBenefitTypeName(benefit.getBenefitType()))
  350. .benefitValue(benefit.getBenefitValue())
  351. .description(benefit.getDescription())
  352. .build();
  353. }
  354. private String toBenefitTypeName(String type) {
  355. return switch (type) {
  356. case "DAILY_QUOTA" -> "每日查询配额";
  357. case "MONTHLY_QUOTA" -> "每月爬虫加成";
  358. case "MAX_CONCURRENT" -> "最大并发加成";
  359. default -> type;
  360. };
  361. }
  362. /**
  363. * 定时任务:每5分钟检查过期会员,自动降级
  364. * <p>
  365. * 高级会员全部时长到期后,账号降级为免费普通会员(PLUS)。
  366. */
  367. @Scheduled(cron = "0 */5 * * * ?")
  368. @Transactional
  369. public void expireMemberships() {
  370. // 查找所有已过期但仍标记为有效的会员记录
  371. List<UserMembership> expired = userMembershipMapper.selectList(
  372. new LambdaQueryWrapper<UserMembership>()
  373. .eq(UserMembership::getStatus, 1)
  374. .isNotNull(UserMembership::getEffectiveTo)
  375. .lt(UserMembership::getEffectiveTo, LocalDateTime.now()));
  376. if (expired.isEmpty()) {
  377. return;
  378. }
  379. Set<Long> affectedUserIds = new HashSet<>();
  380. for (UserMembership m : expired) {
  381. m.setStatus(0);
  382. userMembershipMapper.updateById(m);
  383. affectedUserIds.add(m.getUserId());
  384. }
  385. // 对每个受影响的用户,检查是否还有其他有效会员
  386. for (Long userId : affectedUserIds) {
  387. UserMembership active = userMembershipMapper.selectActiveByUserId(userId);
  388. if (active == null) {
  389. // 无有效会员,降级为普通会员
  390. User user = userMapper.selectById(userId);
  391. if (user != null && user.getMembershipLevel() != MembershipLevel.PLUS) {
  392. user.setMembershipLevel(MembershipLevel.PLUS);
  393. user.setMembershipExpireAt(null);
  394. userMapper.updateById(user);
  395. log.info("用户{}会员已到期,降级为普通会员", userId);
  396. }
  397. }
  398. }
  399. log.info("处理过期会员记录: {}条,涉及用户: {}个", expired.size(), affectedUserIds.size());
  400. }
  401. // ==================== 等级查询(原 UserLevelService,已合并) ====================
  402. /** 会员等级与兼容 UserLevel 对象的映射 */
  403. private static final Map<MembershipLevel, UserLevel> LEVEL_MAP = Map.of(
  404. MembershipLevel.PLUS, UserLevel.builder()
  405. .id(1L).levelName("普通会员").levelCode("PLUS").levelWeight(1)
  406. .crawlerQuota(10).monthlyQuota(300).yearlyQuota(3650).maxConcurrent(1)
  407. .description("普通会员:每日有效查询10次").status(1).build(),
  408. MembershipLevel.PRO, UserLevel.builder()
  409. .id(2L).levelName("高级会员").levelCode("PRO").levelWeight(2)
  410. .crawlerQuota(999).monthlyQuota(29970).yearlyQuota(364635).maxConcurrent(5)
  411. .description("高级会员:每日不限量").status(1).build(),
  412. MembershipLevel.ULTRA, UserLevel.builder()
  413. .id(3L).levelName("超级会员").levelCode("ULTRA").levelWeight(3)
  414. .crawlerQuota(999).monthlyQuota(29970).yearlyQuota(364635).maxConcurrent(10)
  415. .description("超级会员:连锁专用").status(1).build());
  416. public List<UserLevelResponse> listLevels() {
  417. return LEVEL_MAP.keySet().stream()
  418. .sorted(Comparator.comparingInt(l -> LEVEL_MAP.get(l).getLevelWeight()))
  419. .map(this::buildUserLevelWithBenefits)
  420. .map(this::toLevelResponse)
  421. .collect(Collectors.toList());
  422. }
  423. public UserLevelResponse getLevel(Long levelId) {
  424. return LEVEL_MAP.values().stream()
  425. .filter(l -> l.getId().equals(levelId)).findFirst()
  426. .map(l -> buildUserLevelWithBenefits(l.getLevelCode() != null
  427. ? MembershipLevel.fromCode(l.getLevelCode()) : MembershipLevel.PLUS))
  428. .map(this::toLevelResponse)
  429. .orElseThrow(() -> new BusinessException(ErrorCode.LEVEL_NOT_FOUND));
  430. }
  431. public UserLevelResponse getUserLevel(Long userId) {
  432. User user = userMapper.selectById(userId);
  433. if (user == null) {
  434. throw new BusinessException(ErrorCode.USER_NOT_FOUND);
  435. }
  436. UserLevelResponse resp = toLevelResponse(resolveUserLevel(user));
  437. fillMembershipExpiration(resp, user);
  438. return resp;
  439. }
  440. public UserLevel getUserLevelEntity(Long userId) {
  441. return resolveUserLevel(userMapper.selectById(userId));
  442. }
  443. private UserLevel resolveUserLevel(User user) {
  444. if (user == null) {
  445. return buildUserLevelWithBenefits(MembershipLevel.PLUS);
  446. }
  447. if (user.getMembershipExpireAt() != null
  448. && user.getMembershipExpireAt().isBefore(LocalDateTime.now())) {
  449. return buildUserLevelWithBenefits(MembershipLevel.PLUS);
  450. }
  451. UserMembership active = userMembershipMapper.selectActiveByUserId(user.getId());
  452. return buildUserLevelWithBenefits(active != null && active.getLevel() != null
  453. ? active.getLevel() : MembershipLevel.PLUS);
  454. }
  455. private UserLevel buildUserLevelWithBenefits(MembershipLevel level) {
  456. UserLevel base = LEVEL_MAP.getOrDefault(level,
  457. LEVEL_MAP.get(MembershipLevel.PLUS));
  458. var builder = UserLevel.builder()
  459. .id(base.getId()).levelName(base.getLevelName()).levelCode(base.getLevelCode())
  460. .levelWeight(base.getLevelWeight()).description(base.getDescription())
  461. .status(base.getStatus()).crawlerQuota(base.getCrawlerQuota())
  462. .monthlyQuota(base.getMonthlyQuota()).yearlyQuota(base.getYearlyQuota())
  463. .maxConcurrent(base.getMaxConcurrent());
  464. for (MembershipBenefit b : membershipBenefitMapper.selectEnabledByLevel(level)) {
  465. if (b.getBenefitValue() == null) {
  466. continue;
  467. }
  468. switch (b.getBenefitType()) {
  469. case "DAILY_QUOTA" -> builder.crawlerQuota(b.getBenefitValue());
  470. case "MONTHLY_QUOTA" -> builder.monthlyQuota(b.getBenefitValue());
  471. case "YEARLY_QUOTA" -> builder.yearlyQuota(b.getBenefitValue());
  472. case "MAX_CONCURRENT" -> builder.maxConcurrent(b.getBenefitValue());
  473. }
  474. }
  475. return builder.build();
  476. }
  477. private UserLevelResponse toLevelResponse(
  478. UserLevel level) {
  479. return UserLevelResponse.builder()
  480. .id(level.getId()).levelName(level.getLevelName())
  481. .levelCode(level.getLevelCode()).levelWeight(level.getLevelWeight())
  482. .crawlerQuota(level.getCrawlerQuota()).monthlyQuota(level.getMonthlyQuota())
  483. .yearlyQuota(level.getYearlyQuota()).maxConcurrent(level.getMaxConcurrent())
  484. .description(level.getDescription()).version(null)
  485. .pricingPlans(buildPricingPlans(level.getLevelCode())).build();
  486. }
  487. private void fillMembershipExpiration(UserLevelResponse resp, User user) {
  488. if (user == null || resp == null) {
  489. return;
  490. }
  491. boolean premium = resp.getLevelCode() != null && !"PLUS".equals(resp.getLevelCode());
  492. if (!premium) { resp.setMembershipExpireAt(null); resp.setMembershipDaysRemaining(-1); return; }
  493. LocalDateTime expire = user.getMembershipExpireAt();
  494. if (expire != null && expire.isAfter(LocalDateTime.now())) {
  495. resp.setMembershipExpireAt(expire);
  496. resp.setMembershipDaysRemaining((int) ChronoUnit.DAYS.between(LocalDateTime.now(), expire));
  497. } else if (expire != null) {
  498. resp.setMembershipExpireAt(expire); resp.setMembershipDaysRemaining(0);
  499. } else {
  500. log.warn("高级会员缺少过期时间: userId={}", user.getId());
  501. resp.setMembershipExpireAt(null); resp.setMembershipDaysRemaining(0);
  502. }
  503. }
  504. private List<LevelPricingPlan> buildPricingPlans(String levelCode) {
  505. return switch (levelCode) {
  506. case "PRO" -> List.of(
  507. LevelPricingPlan.builder()
  508. .planCode("MONTHLY_PRO").planName("高级会员·月卡")
  509. .price(new BigDecimal("29.90"))
  510. .originalPrice(new BigDecimal("59.90"))
  511. .durationDays(30).description("每天不限量· 1个月29.9").build(),
  512. LevelPricingPlan.builder()
  513. .planCode("YEARLY_PRO").planName("高级会员·年卡")
  514. .price(new BigDecimal("299.90"))
  515. .originalPrice(new BigDecimal("599.90"))
  516. .durationDays(365).description("每天不限量· 1年299.9").build());
  517. case "ULTRA" -> List.of(
  518. LevelPricingPlan.builder()
  519. .planCode("ULTRA").planName("超级会员·连锁专享")
  520. .price(BigDecimal.ZERO).originalPrice(null)
  521. .durationDays(0).description("连锁专用,多店铺管理,敬请期待").build());
  522. default -> Collections.emptyList();
  523. };
  524. }
  525. }