package com.example.demo.service; import java.time.LocalDateTime; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.example.demo.config.CacheConfig; import com.example.demo.model.MembershipLevel; import com.example.demo.model.PointsFreezeRecord; import com.example.demo.model.User; import com.example.demo.model.UserMembership; import com.example.demo.repository.MembershipLevelRepository; import com.example.demo.repository.PointsFreezeRecordRepository; import com.example.demo.repository.UserMembershipRepository; import com.example.demo.repository.UserRepository; import com.example.demo.util.UserIdGenerator; @Service public class UserService { private static final Logger logger = LoggerFactory.getLogger(UserService.class); private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final PointsFreezeRecordRepository pointsFreezeRecordRepository; private final com.example.demo.repository.OrderRepository orderRepository; private final com.example.demo.repository.PaymentRepository paymentRepository; private final CacheManager cacheManager; private final MembershipLevelRepository membershipLevelRepository; private final UserMembershipRepository userMembershipRepository; public UserService(UserRepository userRepository, @Lazy PasswordEncoder passwordEncoder, PointsFreezeRecordRepository pointsFreezeRecordRepository, com.example.demo.repository.OrderRepository orderRepository, com.example.demo.repository.PaymentRepository paymentRepository, CacheManager cacheManager, MembershipLevelRepository membershipLevelRepository, UserMembershipRepository userMembershipRepository) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.pointsFreezeRecordRepository = pointsFreezeRecordRepository; this.orderRepository = orderRepository; this.paymentRepository = paymentRepository; this.cacheManager = cacheManager; this.membershipLevelRepository = membershipLevelRepository; this.userMembershipRepository = userMembershipRepository; } @Transactional public User register(String username, String email, String rawPassword) { if (userRepository.existsByUsername(username)) { throw new IllegalArgumentException("用户名已存在"); } if (userRepository.existsByEmail(email)) { throw new IllegalArgumentException("邮箱已被使用"); } User user = new User(); // 生成唯一用户ID,重试最多10次确保唯一性 String userId = null; for (int i = 0; i < 10; i++) { userId = UserIdGenerator.generate(); if (!userRepository.existsByUserId(userId)) { break; } logger.warn("用户ID冲突,重新生成: userId={}", userId); } user.setUserId(userId); user.setUsername(username); user.setEmail(email); user.setPasswordHash(passwordEncoder.encode(rawPassword)); // 注册时默认为普通用户 user.setRole("ROLE_USER"); User savedUser = userRepository.save(user); // 自动创建默认会员记录(标准会员,到期时间为1年后) createDefaultMembership(savedUser); return savedUser; } /** * 为新用户创建默认会员记录 */ private void createDefaultMembership(User user) { try { // 查找入门版会员等级(后端标记仍为 free) Optional freeLevel = membershipLevelRepository.findByName("free"); if (freeLevel.isEmpty()) { logger.warn("未找到入门版会员等级(free),跳过创建会员记录"); return; } UserMembership membership = new UserMembership(); membership.setUserId(user.getId()); membership.setMembershipLevelId(freeLevel.get().getId()); membership.setStartDate(LocalDateTime.now()); membership.setEndDate(LocalDateTime.of(2099, 12, 31, 23, 59, 59)); // 入门版会员永久有效 membership.setStatus("ACTIVE"); membership.setAutoRenew(false); membership.setCreatedAt(LocalDateTime.now()); userMembershipRepository.save(membership); logger.info("✅ 为新用户创建默认会员记录: userId={}, level=入门版会员(永久有效)", user.getId()); } catch (Exception e) { logger.error("创建默认会员记录失败: userId={}", user.getId(), e); // 不抛出异常,允许用户注册成功 } } @Transactional(readOnly = true) public java.util.List findAll() { return userRepository.findAll(); } @Transactional(readOnly = true) public User findById(Long id) { return userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("用户不存在")); } @Cacheable(value = CacheConfig.USER_CACHE, key = "#username", unless = "#result == null") @Transactional(readOnly = true) public User findByUsername(String username) { return userRepository.findByUsername(username).orElseThrow(() -> new IllegalArgumentException("用户不存在")); } @Cacheable(value = CacheConfig.USER_CACHE, key = "#username", unless = "#result == null") @Transactional(readOnly = true) public User findByUsernameOrNull(String username) { return userRepository.findByUsername(username).orElse(null); } @Transactional(readOnly = true) public boolean existsByUserId(String userId) { return userRepository.existsByUserId(userId); } @Transactional public User create(String username, String email, String rawPassword) { return register(username, email, rawPassword); } @Transactional public User update(Long id, String username, String email, String rawPasswordNullable) { return update(id, username, email, rawPasswordNullable, null); } @Transactional public User update(Long id, String username, String email, String rawPasswordNullable, String role) { User user = findById(id); String oldUsername = user.getUsername(); if (!oldUsername.equals(username) && userRepository.existsByUsername(username)) { throw new IllegalArgumentException("用户名已存在"); } if (!user.getEmail().equals(email) && userRepository.existsByEmail(email)) { throw new IllegalArgumentException("邮箱已被使用"); } user.setUsername(username); user.setEmail(email); if (role != null && !role.isBlank()) { user.setRole(role); } if (rawPasswordNullable != null && !rawPasswordNullable.isBlank()) { user.setPasswordHash(rawPasswordNullable); } User savedUser = userRepository.save(user); // 手动清除旧用户名和新用户名的缓存 evictUserCache(oldUsername); if (!oldUsername.equals(username)) { evictUserCache(username); } return savedUser; } /** * 手动清除用户缓存 * 注意:使用 CacheManager 直接操作,避免同类内部调用 @CacheEvict 不生效的问题 */ public void evictUserCache(String username) { Cache cache = cacheManager.getCache(CacheConfig.USER_CACHE); if (cache != null && username != null) { cache.evict(username); logger.debug("清除用户缓存: {}", username); } } @Transactional public void delete(Long id) { // 先获取用户名用于清除缓存 userRepository.findById(id).ifPresent(user -> { evictUserCache(user.getUsername()); }); userRepository.deleteById(id); } /** * 检查密码是否匹配(加密比较) */ public boolean checkPassword(String rawPassword, String storedPassword) { return passwordEncoder.matches(rawPassword, storedPassword); } /** * 修改指定用户的密码 * * 原密码为可选: * - 如果提供了原密码,则验证原密码是否正确 * - 如果未提供原密码,则直接设置新密码 */ @Transactional public void changePassword(Long userId, String oldPassword, String newPassword) { User user = findById(userId); if (newPassword == null || newPassword.isBlank()) { throw new IllegalArgumentException("新密码不能为空"); } if (newPassword.length() < 8) { throw new IllegalArgumentException("新密码长度不能少于8位"); } // 验证密码必须包含英文字母和数字 if (!newPassword.matches(".*[a-zA-Z].*")) { throw new IllegalArgumentException("新密码必须包含英文字母"); } if (!newPassword.matches(".*[0-9].*")) { throw new IllegalArgumentException("新密码必须包含数字"); } String currentPasswordHash = user.getPasswordHash(); // 如果提供了原密码,则需要验证 if (oldPassword != null && !oldPassword.isBlank()) { if (currentPasswordHash != null && !currentPasswordHash.isBlank()) { if (!checkPassword(oldPassword, currentPasswordHash)) { throw new IllegalArgumentException("原密码不正确"); } } } // 如果未提供原密码,直接设置新密码 user.setPasswordHash(passwordEncoder.encode(newPassword)); userRepository.save(user); evictUserCache(user.getUsername()); // 清除缓存 } /** * 根据邮箱查找用户 */ @Transactional(readOnly = true) public User findByEmail(String email) { return userRepository.findByEmail(email).orElse(null); } @Transactional(readOnly = true) public User findByEmailOrNull(String email) { return userRepository.findByEmail(email).orElse(null); } /** * 根据手机号查找用户 */ @Transactional(readOnly = true) public User findByPhone(String phone) { return userRepository.findByPhone(phone).orElse(null); } /** * 保存用户 */ @Transactional public User save(User user) { User savedUser = userRepository.save(user); evictUserCache(user.getUsername()); // 清除缓存 return savedUser; } /** * 更新用户活跃时间(不清除缓存,避免频繁清除影响性能) * 此方法专门用于 UserActivityInterceptor,仅更新 lastActiveTime 字段 */ @Transactional public void updateLastActiveTime(String username) { userRepository.findByUsername(username).ifPresent(user -> { user.setLastActiveTime(java.time.LocalDateTime.now()); userRepository.save(user); // 注意:不清除缓存,因为 lastActiveTime 不是缓存的关键数据 }); } /** * 增加用户积分 */ @Transactional public User addPoints(Long userId, Integer points) { java.util.Objects.requireNonNull(userId, "用户ID不能为空"); User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("用户不存在")); int newPoints = user.getPoints() + points; if (newPoints < 0) { throw new RuntimeException("积分不能为负数"); } user.setPoints(newPoints); User savedUser = userRepository.save(user); evictUserCache(user.getUsername()); // 清除缓存 return savedUser; } /** * 减少用户积分 */ @Transactional public User deductPoints(Long userId, Integer points) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("用户不存在")); int newPoints = user.getPoints() - points; if (newPoints < 0) { throw new RuntimeException("积分不足"); } user.setPoints(newPoints); User savedUser = userRepository.save(user); evictUserCache(user.getUsername()); // 清除缓存 return savedUser; } /** * 冻结用户积分 * 修复:重试失败任务时需要重新冻结积分 */ @Transactional public PointsFreezeRecord freezePoints(String username, String taskId, PointsFreezeRecord.TaskType taskType, Integer points, String reason) { // 检查是否已经存在相同 taskId 的冻结记录 Optional existingRecord = pointsFreezeRecordRepository.findByTaskId(taskId); if (existingRecord.isPresent()) { PointsFreezeRecord record = existingRecord.get(); // 只有状态为 FROZEN 时才跳过(真正的重复冻结) if (record.getStatus() == PointsFreezeRecord.FreezeStatus.FROZEN) { logger.info("积分已冻结,跳过重复冻结: taskId={}, status={}", taskId, record.getStatus()); return record; } // 如果状态是 DEDUCTED,说明任务已完成,不应该重试 if (record.getStatus() == PointsFreezeRecord.FreezeStatus.DEDUCTED) { logger.warn("任务已完成并扣除积分,不允许重复冻结: taskId={}", taskId); throw new RuntimeException("任务已完成,不允许重复提交: " + taskId); } // 如果状态是 RETURNED/EXPIRED,说明是重试任务,需要重新冻结 logger.info("检测到重试任务,旧记录状态: {}, 将创建新的冻结记录: taskId={}", record.getStatus(), taskId); // 继续执行下面的冻结逻辑 } User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); // 检查可用积分是否足够 if (user.getAvailablePoints() < points) { throw new RuntimeException("可用积分不足,当前可用积分: " + user.getAvailablePoints() + ",需要积分: " + points); } // 检查总积分是否足够(防止冻结积分过多导致总积分为负) if (user.getPoints() < points) { throw new RuntimeException("总积分不足,当前总积分: " + user.getPoints() + ",需要积分: " + points); } // 增加冻结积分 user.setFrozenPoints(user.getFrozenPoints() + points); userRepository.save(user); evictUserCache(username); // 清除缓存 // 创建新的冻结记录 PointsFreezeRecord record = new PointsFreezeRecord(username, taskId, taskType, points, reason); record = pointsFreezeRecordRepository.save(record); logger.info("积分冻结成功: taskId={}, username={}, points={}, reason={}", taskId, username, points, reason); return record; } /** * 扣除冻结的积分(任务完成) * 使用悲观锁防止并发重复扣除 * 使用 REQUIRES_NEW 传播行为,确保在一个独立的事务中执行, * 这样即使抛出异常回滚,也不会影响调用者的事务(只要调用者捕获了异常) */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void deductFrozenPoints(String taskId) { // 使用悲观写锁查询,防止并发重复扣除 PointsFreezeRecord record = pointsFreezeRecordRepository.findByTaskIdWithLock(taskId) .orElseThrow(() -> new RuntimeException("找不到冻结记录: " + taskId)); // 如果已经扣除过,直接返回,避免重复扣除(双重检查,悲观锁已经保证了并发安全) if (record.getStatus() == PointsFreezeRecord.FreezeStatus.DEDUCTED) { return; } if (record.getStatus() != PointsFreezeRecord.FreezeStatus.FROZEN) { throw new RuntimeException("冻结记录状态不正确: " + record.getStatus() + ",期望状态: FROZEN"); } User user = userRepository.findByUsername(record.getUsername()) .orElseThrow(() -> new RuntimeException("用户不存在")); // 减少总积分和冻结积分 user.setPoints(user.getPoints() - record.getFreezePoints()); user.setFrozenPoints(user.getFrozenPoints() - record.getFreezePoints()); userRepository.save(user); evictUserCache(record.getUsername()); // 清除缓存 // 更新冻结记录状态 record.updateStatus(PointsFreezeRecord.FreezeStatus.DEDUCTED); pointsFreezeRecordRepository.save(record); } /** * 返还冻结的积分(任务失败) * 使用悲观锁防止并发重复返还 * 使用 REQUIRES_NEW 传播行为,防止异常导致外部事务回滚 * 如果积分已经返还或扣除,则静默返回(幂等操作) */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void returnFrozenPoints(String taskId) { // 使用悲观写锁查询,防止并发重复返还 // 注意:只查询 FROZEN 状态的记录,如果找不到说明已经处理过 Optional recordOpt = pointsFreezeRecordRepository.findByTaskIdWithLock(taskId); if (recordOpt.isEmpty()) { // 找不到 FROZEN 状态的记录,说明已经返还或扣除过,静默返回(幂等操作) logger.debug("找不到 FROZEN 状态的冻结记录,可能已处理过: taskId={}", taskId); return; } PointsFreezeRecord record = recordOpt.get(); // 双重检查:如果状态不是 FROZEN,说明已经处理过,静默返回(幂等操作) if (record.getStatus() != PointsFreezeRecord.FreezeStatus.FROZEN) { logger.debug("积分记录状态为 {},跳过返还: taskId={}", record.getStatus(), taskId); return; } User user = userRepository.findByUsername(record.getUsername()) .orElseThrow(() -> new RuntimeException("用户不存在")); // 减少冻结积分(总积分不变),确保不会为负数 user.setFrozenPoints(Math.max(0, user.getFrozenPoints() - record.getFreezePoints())); userRepository.save(user); evictUserCache(record.getUsername()); // 清除缓存 // 更新冻结记录状态 record.updateStatus(PointsFreezeRecord.FreezeStatus.RETURNED); pointsFreezeRecordRepository.save(record); } /** * 给用户增加积分 */ @Transactional public void addPoints(String username, Integer points) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在: " + username)); user.setPoints(user.getPoints() + points); userRepository.save(user); evictUserCache(username); // 清除缓存 } /** * 设置用户积分 */ @Transactional public void setPoints(String username, Integer points) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在: " + username)); user.setPoints(points); userRepository.save(user); evictUserCache(username); // 清除缓存 } /** * 获取用户可用积分 */ @Transactional(readOnly = true) public Integer getAvailablePoints(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); return user.getAvailablePoints(); } /** * 获取用户冻结积分 */ @Transactional(readOnly = true) public Integer getFrozenPoints(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); return user.getFrozenPoints(); } /** * 获取用户积分冻结记录 */ @Transactional(readOnly = true) public java.util.List getPointsFreezeRecords(String username) { return pointsFreezeRecordRepository.findByUsernameOrderByCreatedAtDesc(username); } /** * 处理过期的冻结记录 */ @Transactional public int processExpiredFrozenRecords() { java.time.LocalDateTime expiredTime = java.time.LocalDateTime.now().minusHours(24); java.util.List expiredRecords = pointsFreezeRecordRepository.findExpiredFrozenRecords(expiredTime); int processedCount = 0; for (PointsFreezeRecord record : expiredRecords) { try { // 返还过期冻结的积分 returnFrozenPoints(record.getTaskId()); processedCount++; } catch (Exception e) { logger.error("处理过期冻结记录失败: {}", record.getTaskId(), e); } } return processedCount; } /** * 修复所有用户的frozen_points字段,使其与实际FROZEN记录的积分总和一致 * 防止因为并发问题或异常导致frozen_points字段与实际冻结积分不匹配 */ @Transactional public int repairAllUsersFrozenPoints() { int fixedCount = 0; java.util.List allUsers = userRepository.findAll(); for (User user : allUsers) { try { Integer actualFrozen = pointsFreezeRecordRepository.sumFrozenPointsByUsername(user.getUsername()); if (actualFrozen == null) actualFrozen = 0; if (!actualFrozen.equals(user.getFrozenPoints())) { logger.warn("修复用户冻结积分不一致: username={}, 记录值={}, 实际值={}", user.getUsername(), user.getFrozenPoints(), actualFrozen); user.setFrozenPoints(actualFrozen); userRepository.save(user); evictUserCache(user.getUsername()); fixedCount++; } } catch (Exception e) { logger.error("修复用户冻结积分失败: username={}", user.getUsername(), e); } } return fixedCount; } /** * 获取用户积分 */ @Transactional(readOnly = true) public Integer getUserPoints(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("用户不存在")); return user.getPoints(); } /** * 获取积分使用历史(充值和使用记录) * 包括:订单充值记录和积分消耗记录 */ @Transactional(readOnly = true) public java.util.List> getPointsHistory(String username, int page, int size) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); java.util.List> history = new java.util.ArrayList<>(); // 1. 获取成功支付的记录(充值记录) // 注意:积分是在支付成功时通过 PaymentService.addPointsForPayment 添加的 // 所以应该从支付记录中获取充值记录,而不是从订单中获取 java.util.List allPayments = paymentRepository .findByUserIdOrderByCreatedAtDesc(user.getId()); java.util.List successfulPayments = allPayments .stream() .filter(payment -> payment.getStatus() == com.example.demo.model.PaymentStatus.SUCCESS) .collect(java.util.stream.Collectors.toList()); for (com.example.demo.model.Payment payment : successfulPayments) { // 从支付记录中提取积分数量(使用与 PaymentService.addPointsForPayment 相同的逻辑) Integer points = extractPointsFromPayment(payment); // 即使没有提取到积分,也显示充值记录(可能金额不在套餐范围内,但用户确实支付了) // 如果提取到积分,使用提取的积分;否则显示0积分(表示支付成功但未获得积分) Integer displayPoints = (points != null && points > 0) ? points : 0; java.util.Map record = new java.util.HashMap<>(); record.put("type", "充值"); String description = payment.getDescription() != null ? payment.getDescription() : "支付充值"; if (displayPoints == 0 && points == null) { // 如果未提取到积分,在描述中说明 description = description + "(金额不在套餐范围内,未获得积分)"; } record.put("description", description); record.put("points", displayPoints); record.put("time", payment.getPaidAt() != null ? payment.getPaidAt() : payment.getCreatedAt()); record.put("orderId", payment.getOrderId()); record.put("paymentId", payment.getId()); history.add(record); } // 2. 也检查已支付和已完成订单(作为补充,以防有订单但没有支付记录的情况) java.util.List paidOrders = new java.util.ArrayList<>(); // 包含PAID状态的订单 paidOrders.addAll(orderRepository.findByUserIdAndStatus( user.getId(), com.example.demo.model.OrderStatus.PAID )); // 包含COMPLETED状态的订单 paidOrders.addAll(orderRepository.findByUserIdAndStatus( user.getId(), com.example.demo.model.OrderStatus.COMPLETED )); for (com.example.demo.model.Order order : paidOrders) { // 检查是否已经在支付记录中处理过(避免重复) // 通过Payment关联的Order ID来匹配,而不是通过orderId字符串 boolean alreadyProcessed = successfulPayments.stream() .anyMatch(p -> p.getOrder() != null && p.getOrder().getId().equals(order.getId())); if (!alreadyProcessed) { // 从订单描述或订单项中提取积分数量 Integer points = extractPointsFromOrder(order); if (points != null && points > 0) { java.util.Map record = new java.util.HashMap<>(); record.put("type", "充值"); record.put("description", "订单充值 - " + (order.getDescription() != null ? order.getDescription() : "")); record.put("points", points); record.put("time", order.getPaidAt() != null ? order.getPaidAt() : order.getCreatedAt()); record.put("orderNumber", order.getOrderNumber()); record.put("orderType", order.getOrderType() != null ? order.getOrderType().name() : ""); history.add(record); } } } // 3. 获取积分冻结记录(使用记录)- 只获取已扣除的记录 // 直接在数据库层面过滤DEDUCTED状态,避免加载所有记录到内存 java.util.List deductedRecords = pointsFreezeRecordRepository .findByUsernameAndStatusOrderByCompletedAtDesc(username, PointsFreezeRecord.FreezeStatus.DEDUCTED); for (PointsFreezeRecord record : deductedRecords) { java.util.Map historyRecord = new java.util.HashMap<>(); historyRecord.put("type", "消耗"); historyRecord.put("description", record.getTaskType().getDescription() + " - " + (record.getFreezeReason() != null ? record.getFreezeReason() : "任务消耗")); historyRecord.put("points", -record.getFreezePoints()); // 负数表示消耗 historyRecord.put("time", record.getCompletedAt() != null ? record.getCompletedAt() : record.getCreatedAt()); historyRecord.put("taskId", record.getTaskId()); historyRecord.put("taskType", record.getTaskType().name()); history.add(historyRecord); } // 4. 按时间倒序排序 history.sort((a, b) -> { java.time.LocalDateTime timeA = (java.time.LocalDateTime) a.get("time"); java.time.LocalDateTime timeB = (java.time.LocalDateTime) b.get("time"); return timeB.compareTo(timeA); // 倒序 }); // 5. 分页处理 int start = page * size; int end = Math.min(start + size, history.size()); if (start >= history.size()) { return new java.util.ArrayList<>(); } return history.subList(start, end); } /** * 从订单中提取积分数量 * 这里需要根据实际业务逻辑调整 * 假设订单描述或订单项中包含积分信息 */ private Integer extractPointsFromOrder(com.example.demo.model.Order order) { // 方法1:从订单描述中提取(如果描述包含积分信息) if (order.getDescription() != null) { java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)积分"); java.util.regex.Matcher matcher = pattern.matcher(order.getDescription()); if (matcher.find()) { return Integer.valueOf(matcher.group(1)); } } // 方法2:从订单项中提取(如果订单项名称包含积分信息) if (order.getOrderItems() != null && !order.getOrderItems().isEmpty()) { for (com.example.demo.model.OrderItem item : order.getOrderItems()) { if (item.getProductName() != null) { java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)积分"); java.util.regex.Matcher matcher = pattern.matcher(item.getProductName()); if (matcher.find()) { return Integer.valueOf(matcher.group(1)); } // 如果是会员订阅,从数据库读取积分配置 if (item.getProductName().contains("标准版") || item.getProductName().contains("专业版")) { // 从membership_levels表读取积分配置(禁止硬编码) MembershipLevel standardLevel = membershipLevelRepository.findByName("standard") .orElse(null); MembershipLevel proLevel = membershipLevelRepository.findByName("professional") .orElse(null); if (item.getProductName().contains("标准版") && standardLevel != null) { return standardLevel.getPointsBonus(); } else if (item.getProductName().contains("专业版") && proLevel != null) { return proLevel.getPointsBonus(); } } } } } // 方法3:根据订单类型和金额计算积分(从数据库读取配置) if (order.getOrderType() != null) { if (order.getOrderType() == com.example.demo.model.OrderType.SUBSCRIPTION) { // 订阅订单:根据金额计算积分(从数据库读取,禁止硬编码) if (order.getTotalAmount() != null) { // 使用OrderService的逻辑从数据库读取积分配置 try { MembershipLevel standardLevel = membershipLevelRepository.findByName("standard") .orElseThrow(() -> new IllegalStateException("数据库中缺少standard会员等级配置")); MembershipLevel proLevel = membershipLevelRepository.findByName("professional") .orElseThrow(() -> new IllegalStateException("数据库中缺少professional会员等级配置")); int standardPrice = standardLevel.getPrice().intValue(); int standardPoints = standardLevel.getPointsBonus(); int proPrice = proLevel.getPrice().intValue(); int proPoints = proLevel.getPointsBonus(); int amountInt = order.getTotalAmount().intValue(); // 判断套餐类型(允许10%的价格浮动范围) if (amountInt >= proPrice * 0.9 && amountInt <= proPrice * 1.1) { return proPoints; // 专业版积分 } else if (amountInt >= standardPrice * 0.9 && amountInt <= standardPrice * 1.1) { return standardPoints; // 标准版积分 } else if (amountInt >= proPrice) { return proPoints; } else if (amountInt >= standardPrice) { return standardPoints; } } catch (Exception e) { logger.error("从数据库读取会员等级配置失败: {}", e.getMessage(), e); } } } } return null; } /** * 从支付记录中提取积分数量 * 使用与 PaymentService.addPointsForPayment 相同的逻辑 */ private Integer extractPointsFromPayment(com.example.demo.model.Payment payment) { if (payment == null) { return null; } java.math.BigDecimal amount = payment.getAmount(); if (amount == null) { logger.warn("支付记录 ID: {} 的金额为空", payment.getId()); return null; } String description = payment.getDescription() != null ? payment.getDescription() : ""; Integer pointsToAdd = 0; // 从membership_levels表读取价格和积分配置(必须从数据库获取,禁止硬编码) MembershipLevel standardLevel = membershipLevelRepository.findByName("standard") .orElseThrow(() -> new IllegalStateException("数据库中缺少standard会员等级配置")); MembershipLevel proLevel = membershipLevelRepository.findByName("professional") .orElseThrow(() -> new IllegalStateException("数据库中缺少professional会员等级配置")); int standardPrice = standardLevel.getPrice().intValue(); int standardPoints = standardLevel.getPointsBonus(); int proPrice = proLevel.getPrice().intValue(); int proPoints = proLevel.getPointsBonus(); // 优先从描述中识别套餐类型 if (description.contains("标准版") || description.contains("standard") || description.contains("Standard") || description.contains("STANDARD")) { pointsToAdd = standardPoints; } else if (description.contains("专业版") || description.contains("premium") || description.contains("Premium") || description.contains("PREMIUM") || description.contains("professional") || description.contains("Professional")) { pointsToAdd = proPoints; } else { // 如果描述中没有套餐信息,根据金额判断 int amountInt = amount.intValue(); if (amountInt >= proPrice * 0.9 && amountInt <= proPrice * 1.1) { pointsToAdd = proPoints; } else if (amountInt >= standardPrice * 0.9 && amountInt <= standardPrice * 1.1) { pointsToAdd = standardPoints; } else if (amountInt >= proPrice) { pointsToAdd = proPoints; } else if (amountInt >= standardPrice) { pointsToAdd = standardPoints; } } return pointsToAdd > 0 ? pointsToAdd : null; } /** * 统计在线用户数(最近N分钟内有活动的用户) * * @param minutes 活跃时间范围(分钟),默认10分钟 * @return 在线用户数量 */ @Transactional(readOnly = true) public long countOnlineUsers(int minutes) { LocalDateTime activeAfter = LocalDateTime.now().minusMinutes(minutes); return userRepository.countByLastActiveTimeAfter(activeAfter); } /** * 统计在线用户数(默认10分钟内活跃) * * @return 在线用户数量 */ @Transactional(readOnly = true) public long countOnlineUsers() { return countOnlineUsers(10); } /** * 处理过期会员:付费会员过期同时清零积分,但保留会员等级 * 免费/入门会员不会过期(到期时间设为2099年) * @return 处理的过期会员数量 */ @Transactional public int processExpiredMemberships(com.example.demo.repository.UserMembershipRepository userMembershipRepository) { LocalDateTime now = LocalDateTime.now(); java.util.List expiredMemberships = userMembershipRepository.findByStatusAndEndDateBefore("ACTIVE", now); // 查找免费/入门版会员等级(后端标记为 free) Optional freeLevelOpt = membershipLevelRepository.findByName("free"); if (freeLevelOpt.isEmpty()) { logger.error("未找到免费/入门版会员等级(free),无法处理过期会员"); return 0; } Long freeLevelId = freeLevelOpt.get().getId(); // 永久有效的到期时间(针对免费/入门会员) LocalDateTime permanentEndDate = LocalDateTime.of(2099, 12, 31, 23, 59, 59); int processedCount = 0; for (com.example.demo.model.UserMembership membership : expiredMemberships) { try { // 跳过免费/入门版会员(他们不应该过期,但以防万一) if (membership.getMembershipLevelId().equals(freeLevelId)) { // 免费/入门版会员如果意外过期,直接延长到2099年 membership.setEndDate(permanentEndDate); membership.setUpdatedAt(now); userMembershipRepository.save(membership); logger.info("✅ 免费/入门版会员到期时间已延长: userId={}", membership.getUserId()); continue; } // 🔥 付费会员过期:保留等级,只清零积分,不降级 // 不修改 membershipLevelId(保留原等级) // 不修改 endDate(保留过期时间) // 不修改 status(保持ACTIVE,前端根据endDate判断是否过期) membership.setUpdatedAt(now); userMembershipRepository.save(membership); // 清零用户积分 java.util.Optional userOpt = userRepository.findById(membership.getUserId()); if (userOpt.isPresent()) { User user = userOpt.get(); int oldPoints = user.getPoints(); if (oldPoints > 0) { user.setPoints(0); user.setFrozenPoints(0); // 同时清零冻结积分 userRepository.save(user); logger.info("✅ 付费会员过期积分清零: userId={}, 原积分={}", membership.getUserId(), oldPoints); } } processedCount++; logger.info("✅ 付费会员过期已处理(保留等级,积分清零): userId={}, 过期时间={}", membership.getUserId(), membership.getEndDate()); } catch (Exception e) { logger.error("处理过期会员失败: userId={}", membership.getUserId(), e); } } if (processedCount > 0) { logger.info("✅ 共处理 {} 个过期付费会员,已清零积分但保留会员等级", processedCount); } return processedCount; } }