Files
AIGC/src/main/java/com/example/demo/service/UserService.java
blandarebiter 90b5118e45 perf(backend+frontend): 列表API响应体积优化 3.1MB→145KB (↓95.4%)
- 后端: JPQL构造器投影排除LONGTEXT大字段(uploadedImages/videoReferenceImages)
- 后端: DTO层过滤非分镜图类型的base64内联resultUrl
- 前端: 列表缩略图从video改为img loading=lazy,消除172并发请求
- 前端: download函数增加resultUrl懒加载(详情接口兜底)
- 文档: 新增性能优化报告 docs/performance-optimization-report.md
2026-04-10 18:46:37 +08:00

924 lines
41 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<MembershipLevel> 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<User> 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<PointsFreezeRecord> 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<PointsFreezeRecord> 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<PointsFreezeRecord> getPointsFreezeRecords(String username) {
return pointsFreezeRecordRepository.findByUsernameOrderByCreatedAtDesc(username);
}
/**
* 处理过期的冻结记录
*/
@Transactional
public int processExpiredFrozenRecords() {
java.time.LocalDateTime expiredTime = java.time.LocalDateTime.now().minusHours(24);
java.util.List<PointsFreezeRecord> 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<User> 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<java.util.Map<String, Object>> getPointsHistory(String username, int page, int size) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
java.util.List<java.util.Map<String, Object>> history = new java.util.ArrayList<>();
// 1. 获取成功支付的记录(充值记录)
// 注意:积分是在支付成功时通过 PaymentService.addPointsForPayment 添加的
// 所以应该从支付记录中获取充值记录,而不是从订单中获取
java.util.List<com.example.demo.model.Payment> allPayments = paymentRepository
.findByUserIdOrderByCreatedAtDesc(user.getId());
java.util.List<com.example.demo.model.Payment> 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<String, Object> 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<com.example.demo.model.Order> 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<String, Object> 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<PointsFreezeRecord> deductedRecords = pointsFreezeRecordRepository
.findByUsernameAndStatusOrderByCompletedAtDesc(username, PointsFreezeRecord.FreezeStatus.DEDUCTED);
for (PointsFreezeRecord record : deductedRecords) {
java.util.Map<String, Object> 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<com.example.demo.model.UserMembership> expiredMemberships =
userMembershipRepository.findByStatusAndEndDateBefore("ACTIVE", now);
// 查找免费/入门版会员等级(后端标记为 free
Optional<MembershipLevel> 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<User> 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;
}
}