后台完成修复,初始化项目
This commit is contained in:
671
src/main/java/com/org/flashsalesystem/service/AdminService.java
Normal file
671
src/main/java/com/org/flashsalesystem/service/AdminService.java
Normal file
@@ -0,0 +1,671 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.entity.User;
|
||||
import com.org.flashsalesystem.repository.FlashSaleRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import com.org.flashsalesystem.repository.UserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 管理后台服务类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AdminService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private FlashSaleRepository flashSaleRepository;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
public Map<String, Object> getDashboardStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总用户数
|
||||
long totalUsers = userRepository.count();
|
||||
stats.put("totalUsers", totalUsers);
|
||||
|
||||
// 总商品数
|
||||
long totalProducts = productRepository.count();
|
||||
stats.put("totalProducts", totalProducts);
|
||||
|
||||
// 活跃秒杀数
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long activeFlashSales = flashSaleRepository.countByStartTimeLessThanEqualAndEndTimeGreaterThanEqual(now,
|
||||
now);
|
||||
stats.put("activeFlashSales", activeFlashSales);
|
||||
|
||||
// 今日订单数
|
||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
||||
long todayOrders = orderRepository.countByCreatedAtBetween(startOfDay, endOfDay);
|
||||
stats.put("todayOrders", todayOrders);
|
||||
|
||||
// 总订单数
|
||||
long totalOrders = orderRepository.count();
|
||||
stats.put("totalOrders", totalOrders);
|
||||
|
||||
// 已支付订单数
|
||||
Long paidOrdersCount = orderRepository.countByStatus(2); // 2-已支付
|
||||
long paidOrders = paidOrdersCount != null ? paidOrdersCount : 0L;
|
||||
stats.put("paidOrders", paidOrders);
|
||||
|
||||
// 待处理订单数
|
||||
Long pendingOrdersCount = orderRepository.countByStatus(1); // 1-待支付
|
||||
long pendingOrders = pendingOrdersCount != null ? pendingOrdersCount : 0L;
|
||||
stats.put("pendingOrders", pendingOrders);
|
||||
|
||||
// 总交易额
|
||||
BigDecimal totalAmount = orderRepository.sumTotalPriceByStatus(2); // 2-已支付
|
||||
if (totalAmount == null) {
|
||||
totalAmount = BigDecimal.ZERO;
|
||||
}
|
||||
stats.put("totalAmount", totalAmount);
|
||||
|
||||
log.info("获取仪表盘统计数据成功: {}", stats);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取仪表盘统计数据失败", e);
|
||||
// 返回默认值
|
||||
stats.put("totalUsers", 0L);
|
||||
stats.put("totalProducts", 0L);
|
||||
stats.put("activeFlashSales", 0L);
|
||||
stats.put("todayOrders", 0L);
|
||||
stats.put("totalOrders", 0L);
|
||||
stats.put("paidOrders", 0L);
|
||||
stats.put("pendingOrders", 0L);
|
||||
stats.put("totalAmount", BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
*/
|
||||
public Map<String, Object> getUserStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总用户数
|
||||
long totalUsers = userRepository.count();
|
||||
stats.put("totalUsers", totalUsers);
|
||||
|
||||
// 活跃用户数(最近7天登录)
|
||||
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
|
||||
long activeUsers = userRepository.countByLastLoginAfter(sevenDaysAgo);
|
||||
stats.put("activeUsers", activeUsers);
|
||||
|
||||
// 新用户数(今天注册)
|
||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
||||
long newUsers = userRepository.countByCreatedAtBetween(startOfDay, endOfDay);
|
||||
stats.put("newUsers", newUsers);
|
||||
|
||||
// 在线用户数(从Redis获取)
|
||||
String onlineUsersKey = "online_users";
|
||||
long onlineUsers = redisService.sCard(onlineUsersKey);
|
||||
stats.put("onlineUsers", onlineUsers);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户统计数据失败", e);
|
||||
stats.put("totalUsers", 0L);
|
||||
stats.put("activeUsers", 0L);
|
||||
stats.put("newUsers", 0L);
|
||||
stats.put("onlineUsers", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计数据
|
||||
*/
|
||||
public Map<String, Object> getOrderStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总订单数
|
||||
long totalOrders = orderRepository.count();
|
||||
stats.put("totalOrders", totalOrders);
|
||||
|
||||
// 已支付订单数
|
||||
Long paidOrdersCount = orderRepository.countByStatus(2); // 2-已支付
|
||||
long paidOrders = paidOrdersCount != null ? paidOrdersCount : 0L;
|
||||
stats.put("paidOrders", paidOrders);
|
||||
|
||||
// 待处理订单数
|
||||
Long pendingOrdersCount = orderRepository.countByStatus(1); // 1-待支付
|
||||
long pendingOrders = pendingOrdersCount != null ? pendingOrdersCount : 0L;
|
||||
stats.put("pendingOrders", pendingOrders);
|
||||
|
||||
// 已取消订单数
|
||||
Long cancelledOrdersCount = orderRepository.countByStatus(5); // 5-已取消
|
||||
long cancelledOrders = cancelledOrdersCount != null ? cancelledOrdersCount : 0L;
|
||||
stats.put("cancelledOrders", cancelledOrders);
|
||||
|
||||
// 总交易额
|
||||
BigDecimal totalAmount = orderRepository.sumTotalPriceByStatus(2); // 2-已支付
|
||||
if (totalAmount == null) {
|
||||
totalAmount = BigDecimal.ZERO;
|
||||
}
|
||||
stats.put("totalAmount", totalAmount);
|
||||
|
||||
// 今日订单数
|
||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
||||
long todayOrders = orderRepository.countByCreatedAtBetween(startOfDay, endOfDay);
|
||||
stats.put("todayOrders", todayOrders);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单统计数据失败", e);
|
||||
stats.put("totalOrders", 0L);
|
||||
stats.put("paidOrders", 0L);
|
||||
stats.put("pendingOrders", 0L);
|
||||
stats.put("cancelledOrders", 0L);
|
||||
stats.put("totalAmount", BigDecimal.ZERO);
|
||||
stats.put("todayOrders", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品统计数据
|
||||
*/
|
||||
public Map<String, Object> getProductStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总商品数
|
||||
long totalProducts = productRepository.count();
|
||||
stats.put("totalProducts", totalProducts);
|
||||
|
||||
// 上架商品数
|
||||
long activeProducts = productRepository.countByStatus(1);
|
||||
stats.put("activeProducts", activeProducts);
|
||||
|
||||
// 下架商品数
|
||||
long inactiveProducts = productRepository.countByStatus(0);
|
||||
stats.put("inactiveProducts", inactiveProducts);
|
||||
|
||||
// 库存不足商品数(库存小于10)
|
||||
long lowStockProducts = productRepository.countByStockLessThan(10);
|
||||
stats.put("lowStockProducts", lowStockProducts);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品统计数据失败", e);
|
||||
stats.put("totalProducts", 0L);
|
||||
stats.put("activeProducts", 0L);
|
||||
stats.put("inactiveProducts", 0L);
|
||||
stats.put("lowStockProducts", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀统计数据
|
||||
*/
|
||||
public Map<String, Object> getFlashSaleStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 总秒杀活动数
|
||||
long totalFlashSales = flashSaleRepository.count();
|
||||
stats.put("totalFlashSales", totalFlashSales);
|
||||
|
||||
// 活跃秒杀数
|
||||
long activeFlashSales = flashSaleRepository.countByStartTimeLessThanEqualAndEndTimeGreaterThanEqual(now,
|
||||
now);
|
||||
stats.put("activeFlashSales", activeFlashSales);
|
||||
|
||||
// 即将开始的秒杀数
|
||||
LocalDateTime oneHourLater = now.plusHours(1);
|
||||
long upcomingFlashSales = flashSaleRepository.countByStartTimeBetween(now, oneHourLater);
|
||||
stats.put("upcomingFlashSales", upcomingFlashSales);
|
||||
|
||||
// 已结束的秒杀数
|
||||
long endedFlashSales = flashSaleRepository.countByEndTimeLessThan(now);
|
||||
stats.put("endedFlashSales", endedFlashSales);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取秒杀统计数据失败", e);
|
||||
stats.put("totalFlashSales", 0L);
|
||||
stats.put("activeFlashSales", 0L);
|
||||
stats.put("upcomingFlashSales", 0L);
|
||||
stats.put("endedFlashSales", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近订单列表
|
||||
*/
|
||||
public List<Map<String, Object>> getRecentOrders(int limit) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Order> orders = orderRepository.findAll(pageable);
|
||||
|
||||
return orders.getContent().stream().map(order -> {
|
||||
Map<String, Object> orderMap = new HashMap<>();
|
||||
orderMap.put("id", order.getId());
|
||||
orderMap.put("username", order.getUser().getUsername());
|
||||
orderMap.put("productName", order.getProduct().getName());
|
||||
orderMap.put("quantity", order.getQuantity());
|
||||
orderMap.put("totalAmount", order.getTotalPrice());
|
||||
orderMap.put("status", order.getStatus());
|
||||
orderMap.put("createdAt", order.getCreatedAt());
|
||||
orderMap.put("isFlashSale", order.getOrderType() == 2); // 2表示秒杀订单
|
||||
return orderMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取最近订单失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门商品列表
|
||||
*/
|
||||
public List<Map<String, Object>> getHotProducts(int limit) {
|
||||
try {
|
||||
// 这里可以根据销量排序,暂时按创建时间排序
|
||||
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Product> products = productRepository.findAll(pageable);
|
||||
|
||||
return products.getContent().stream().map(product -> {
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("id", product.getId());
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("price", product.getPrice());
|
||||
productMap.put("stock", product.getStock());
|
||||
productMap.put("sales", 0); // 暂时设为0,后续可以添加销量统计
|
||||
return productMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取热门商品失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
public Map<String, Object> getUsers(int page, int size, String keyword, Integer status) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<User> userPage;
|
||||
|
||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
userPage = userRepository.findByUsernameContainingOrEmailContaining(keyword, keyword, pageable);
|
||||
} else if (status != null) {
|
||||
userPage = userRepository.findByStatus(status, pageable);
|
||||
} else {
|
||||
userPage = userRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
List<UserDTO> userDTOs = userPage.getContent().stream().map(user -> {
|
||||
UserDTO dto = new UserDTO();
|
||||
BeanUtils.copyProperties(user, dto);
|
||||
dto.setPassword(null); // 不返回密码
|
||||
dto.setIsOnline(redisService.sIsMember("online_users", user.getId().toString()));
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("users", userDTOs);
|
||||
result.put("total", userPage.getTotalElements());
|
||||
result.put("totalPages", userPage.getTotalPages());
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户列表失败", e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("users", new ArrayList<>());
|
||||
result.put("total", 0L);
|
||||
result.put("totalPages", 0);
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
public Map<String, Object> getOrders(int page, int size, String keyword, String status) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Order> orderPage;
|
||||
|
||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
orderPage = orderRepository.findByIdContainingOrUserUsernameContaining(keyword, keyword, pageable);
|
||||
} else if (status != null && !status.trim().isEmpty()) {
|
||||
Integer statusInt = Integer.parseInt(status);
|
||||
orderPage = orderRepository.findByStatus(statusInt, pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> orders = orderPage.getContent().stream().map(order -> {
|
||||
Map<String, Object> orderMap = new HashMap<>();
|
||||
orderMap.put("id", order.getId());
|
||||
orderMap.put("username", order.getUser().getUsername());
|
||||
orderMap.put("productName", order.getProduct().getName());
|
||||
orderMap.put("quantity", order.getQuantity());
|
||||
orderMap.put("totalAmount", order.getTotalPrice());
|
||||
orderMap.put("status", order.getStatus());
|
||||
orderMap.put("createdAt", order.getCreatedAt());
|
||||
orderMap.put("isFlashSale", order.getOrderType() == 2); // 2表示秒杀订单
|
||||
return orderMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("orders", orders);
|
||||
result.put("total", orderPage.getTotalElements());
|
||||
result.put("totalPages", orderPage.getTotalPages());
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单列表失败", e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("orders", new ArrayList<>());
|
||||
result.put("total", 0L);
|
||||
result.put("totalPages", 0);
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
*/
|
||||
public Object getProducts(int page, int size, String keyword, Integer status) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Product> productPage;
|
||||
|
||||
if (keyword != null && !keyword.trim().isEmpty() && status != null) {
|
||||
// 同时按关键词和状态筛选
|
||||
productPage = productRepository.findByNameContainingAndStatus(keyword, status, pageable);
|
||||
} else if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
productPage = productRepository.findByNameContaining(keyword, pageable);
|
||||
} else if (status != null) {
|
||||
productPage = productRepository.findByStatus(status, pageable);
|
||||
} else {
|
||||
productPage = productRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<Map<String, Object>> productList = productPage.getContent().stream().map(product -> {
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("id", product.getId());
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("price", product.getPrice());
|
||||
productMap.put("stock", product.getStock());
|
||||
productMap.put("status", product.getStatus());
|
||||
productMap.put("description", product.getDescription());
|
||||
productMap.put("imageUrl", product.getImageUrl());
|
||||
productMap.put("createdAt", product.getCreatedAt());
|
||||
return productMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("products", productList);
|
||||
result.put("total", productPage.getTotalElements());
|
||||
result.put("totalPages", productPage.getTotalPages());
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品列表失败", e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("products", new ArrayList<>());
|
||||
result.put("total", 0L);
|
||||
result.put("totalPages", 0);
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统状态
|
||||
*/
|
||||
public Object getSystemStatus() {
|
||||
try {
|
||||
Map<String, Object> systemStatus = new HashMap<>();
|
||||
|
||||
// 获取JVM内存使用情况
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long totalMemory = runtime.totalMemory();
|
||||
long freeMemory = runtime.freeMemory();
|
||||
long usedMemory = totalMemory - freeMemory;
|
||||
double memoryUsage = (double) usedMemory / totalMemory * 100;
|
||||
|
||||
// 获取可用处理器数量(模拟CPU使用率)
|
||||
int availableProcessors = runtime.availableProcessors();
|
||||
double cpuUsage = Math.random() * 30 + 20; // 模拟20-50%的CPU使用率
|
||||
|
||||
// 模拟磁盘使用率
|
||||
double diskUsage = Math.random() * 40 + 10; // 模拟10-50%的磁盘使用率
|
||||
|
||||
systemStatus.put("status", "正常");
|
||||
systemStatus.put("cpuUsage", Math.round(cpuUsage));
|
||||
systemStatus.put("memoryUsage", Math.round(memoryUsage));
|
||||
systemStatus.put("diskUsage", Math.round(diskUsage));
|
||||
systemStatus.put("availableProcessors", availableProcessors);
|
||||
systemStatus.put("totalMemory", totalMemory / 1024 / 1024 + "MB");
|
||||
systemStatus.put("usedMemory", usedMemory / 1024 / 1024 + "MB");
|
||||
|
||||
return systemStatus;
|
||||
} catch (Exception e) {
|
||||
log.error("获取系统状态失败", e);
|
||||
Map<String, Object> errorStatus = new HashMap<>();
|
||||
errorStatus.put("status", "异常");
|
||||
errorStatus.put("cpuUsage", 0);
|
||||
errorStatus.put("memoryUsage", 0);
|
||||
errorStatus.put("diskUsage", 0);
|
||||
return errorStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis状态
|
||||
*/
|
||||
public Object getRedisStatus() {
|
||||
try {
|
||||
List<Map<String, Object>> redisNodes = new ArrayList<>();
|
||||
|
||||
// 模拟Redis集群节点状态
|
||||
String[] nodes = {
|
||||
"42.192.62.91:7000", "42.192.62.91:7001", "42.192.62.91:7002",
|
||||
"42.192.62.91:7003", "42.192.62.91:7004", "42.192.62.91:7005"
|
||||
};
|
||||
|
||||
for (String node : nodes) {
|
||||
Map<String, Object> nodeStatus = new HashMap<>();
|
||||
nodeStatus.put("node", node);
|
||||
nodeStatus.put("status", "正常");
|
||||
nodeStatus.put("memory", (200 + (int) (Math.random() * 100)) + "MB");
|
||||
nodeStatus.put("connections", 30 + (int) (Math.random() * 30));
|
||||
redisNodes.add(nodeStatus);
|
||||
}
|
||||
|
||||
return redisNodes;
|
||||
} catch (Exception e) {
|
||||
log.error("获取Redis状态失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个商品详情
|
||||
*/
|
||||
public Object getProduct(Long id) {
|
||||
try {
|
||||
Optional<Product> productOpt = productRepository.findById(id);
|
||||
if (productOpt.isPresent()) {
|
||||
Product product = productOpt.get();
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("id", product.getId());
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("price", product.getPrice());
|
||||
productMap.put("stock", product.getStock());
|
||||
productMap.put("status", product.getStatus());
|
||||
productMap.put("description", product.getDescription());
|
||||
productMap.put("imageUrl", product.getImageUrl());
|
||||
productMap.put("createdAt", product.getCreatedAt());
|
||||
productMap.put("updatedAt", product.getUpdatedAt());
|
||||
|
||||
// 添加统计信息(模拟数据,实际应该从统计表获取)
|
||||
productMap.put("totalSales", 0); // 总销量
|
||||
productMap.put("totalRevenue", 0.0); // 总收入
|
||||
productMap.put("viewCount", 0); // 浏览次数
|
||||
productMap.put("rating", 0.0); // 平均评分
|
||||
|
||||
return productMap;
|
||||
} else {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品详情失败", e);
|
||||
throw new RuntimeException("获取商品详情失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品
|
||||
*/
|
||||
public void updateProduct(Long id, Map<String, Object> productData) {
|
||||
try {
|
||||
Optional<Product> productOpt = productRepository.findById(id);
|
||||
if (productOpt.isPresent()) {
|
||||
Product product = productOpt.get();
|
||||
|
||||
if (productData.containsKey("name")) {
|
||||
product.setName((String) productData.get("name"));
|
||||
}
|
||||
if (productData.containsKey("price")) {
|
||||
product.setPrice(new BigDecimal(productData.get("price").toString()));
|
||||
}
|
||||
if (productData.containsKey("stock")) {
|
||||
product.setStock(Integer.parseInt(productData.get("stock").toString()));
|
||||
}
|
||||
if (productData.containsKey("status")) {
|
||||
product.setStatus(Integer.parseInt(productData.get("status").toString()));
|
||||
}
|
||||
if (productData.containsKey("description")) {
|
||||
product.setDescription((String) productData.get("description"));
|
||||
}
|
||||
if (productData.containsKey("imageUrl")) {
|
||||
product.setImageUrl((String) productData.get("imageUrl"));
|
||||
}
|
||||
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
productRepository.save(product);
|
||||
} else {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新商品失败", e);
|
||||
throw new RuntimeException("更新商品失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*/
|
||||
public void deleteProduct(Long id) {
|
||||
try {
|
||||
if (productRepository.existsById(id)) {
|
||||
productRepository.deleteById(id);
|
||||
} else {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("删除商品失败", e);
|
||||
throw new RuntimeException("删除商品失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加商品
|
||||
*/
|
||||
public Object addProduct(Map<String, Object> productData) {
|
||||
try {
|
||||
Product product = new Product();
|
||||
product.setName((String) productData.get("name"));
|
||||
product.setPrice(new BigDecimal(productData.get("price").toString()));
|
||||
product.setStock(Integer.parseInt(productData.get("stock").toString()));
|
||||
product.setStatus(Integer.parseInt(productData.get("status").toString()));
|
||||
product.setDescription((String) productData.get("description"));
|
||||
product.setImageUrl((String) productData.get("imageUrl"));
|
||||
product.setCreatedAt(LocalDateTime.now());
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
Product savedProduct = productRepository.save(product);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("id", savedProduct.getId());
|
||||
result.put("name", savedProduct.getName());
|
||||
result.put("price", savedProduct.getPrice());
|
||||
result.put("stock", savedProduct.getStock());
|
||||
result.put("status", savedProduct.getStatus());
|
||||
result.put("description", savedProduct.getDescription());
|
||||
result.put("imageUrl", savedProduct.getImageUrl());
|
||||
result.put("createdAt", savedProduct.getCreatedAt());
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("添加商品失败", e);
|
||||
throw new RuntimeException("添加商品失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
314
src/main/java/com/org/flashsalesystem/service/CartService.java
Normal file
314
src/main/java/com/org/flashsalesystem/service/CartService.java
Normal file
@@ -0,0 +1,314 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.CartDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 购物车服务类
|
||||
* 使用Redis Hash结构存储购物车数据
|
||||
* 实现购物车的增删改查、持久化等功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class CartService {
|
||||
|
||||
private static final String CART_PREFIX = "user:";
|
||||
private static final String CART_SUFFIX = ":cart";
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
@Value("${flashsale.cart.expire-days:7}")
|
||||
private int cartExpireDays;
|
||||
@Value("${flashsale.cart.max-items:20}")
|
||||
private int maxCartItems;
|
||||
|
||||
/**
|
||||
* 添加商品到购物车
|
||||
*/
|
||||
public CartDTO addToCart(Long userId, CartDTO.AddItemDTO addItemDTO) {
|
||||
log.info("添加商品到购物车: 用户ID={}, 商品ID={}, 数量={}",
|
||||
userId, addItemDTO.getProductId(), addItemDTO.getQuantity());
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 检查商品是否存在
|
||||
ProductDTO product = productService.getProductById(addItemDTO.getProductId());
|
||||
if (product == null) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
// 检查商品是否上架
|
||||
if (product.getStatus() != 1) {
|
||||
throw new RuntimeException("商品已下架");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
Integer availableStock = productService.getProductStock(addItemDTO.getProductId());
|
||||
if (availableStock < addItemDTO.getQuantity()) {
|
||||
throw new RuntimeException("商品库存不足");
|
||||
}
|
||||
|
||||
// 检查购物车商品种类数量限制
|
||||
Long cartSize = redisService.hLen(cartKey);
|
||||
if (cartSize != null && cartSize >= maxCartItems &&
|
||||
!redisService.hExists(cartKey, addItemDTO.getProductId().toString())) {
|
||||
throw new RuntimeException("购物车商品种类已达上限");
|
||||
}
|
||||
|
||||
// 获取当前购物车中该商品的数量
|
||||
String currentQuantityStr = (String) redisService.hGet(cartKey, addItemDTO.getProductId().toString());
|
||||
int currentQuantity = currentQuantityStr != null ? Integer.parseInt(currentQuantityStr) : 0;
|
||||
|
||||
// 计算新的数量
|
||||
int newQuantity = currentQuantity + addItemDTO.getQuantity();
|
||||
|
||||
// 再次检查库存
|
||||
if (availableStock < newQuantity) {
|
||||
throw new RuntimeException("商品库存不足,当前库存:" + availableStock);
|
||||
}
|
||||
|
||||
// 更新购物车
|
||||
redisService.hSet(cartKey, addItemDTO.getProductId().toString(), String.valueOf(newQuantity));
|
||||
|
||||
// 设置过期时间
|
||||
redisService.expire(cartKey, cartExpireDays, TimeUnit.DAYS);
|
||||
|
||||
log.info("商品添加到购物车成功: 用户ID={}, 商品ID={}, 新数量={}",
|
||||
userId, addItemDTO.getProductId(), newQuantity);
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新购物车商品数量
|
||||
*/
|
||||
public CartDTO updateQuantity(Long userId, CartDTO.UpdateQuantityDTO updateDTO) {
|
||||
log.info("更新购物车商品数量: 用户ID={}, 商品ID={}, 数量={}",
|
||||
userId, updateDTO.getProductId(), updateDTO.getQuantity());
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 检查商品是否在购物车中
|
||||
if (!redisService.hExists(cartKey, updateDTO.getProductId().toString())) {
|
||||
throw new RuntimeException("商品不在购物车中");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
Integer availableStock = productService.getProductStock(updateDTO.getProductId());
|
||||
if (availableStock < updateDTO.getQuantity()) {
|
||||
throw new RuntimeException("商品库存不足,当前库存:" + availableStock);
|
||||
}
|
||||
|
||||
// 更新数量
|
||||
redisService.hSet(cartKey, updateDTO.getProductId().toString(), String.valueOf(updateDTO.getQuantity()));
|
||||
|
||||
// 刷新过期时间
|
||||
redisService.expire(cartKey, cartExpireDays, TimeUnit.DAYS);
|
||||
|
||||
log.info("购物车商品数量更新成功: 用户ID={}, 商品ID={}, 新数量={}",
|
||||
userId, updateDTO.getProductId(), updateDTO.getQuantity());
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从购物车移除商品
|
||||
*/
|
||||
public CartDTO removeFromCart(Long userId, CartDTO.RemoveItemDTO removeDTO) {
|
||||
log.info("从购物车移除商品: 用户ID={}, 商品ID={}", userId, removeDTO.getProductId());
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 移除商品
|
||||
redisService.hDel(cartKey, removeDTO.getProductId().toString());
|
||||
|
||||
log.info("商品从购物车移除成功: 用户ID={}, 商品ID={}", userId, removeDTO.getProductId());
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移除购物车商品
|
||||
*/
|
||||
public CartDTO batchRemove(Long userId, List<Long> productIds) {
|
||||
log.info("批量移除购物车商品: 用户ID={}, 商品IDs={}", userId, productIds);
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 批量移除
|
||||
String[] fields = productIds.stream()
|
||||
.map(String::valueOf)
|
||||
.toArray(String[]::new);
|
||||
redisService.hDel(cartKey, (Object[]) fields);
|
||||
|
||||
log.info("批量移除购物车商品成功: 用户ID={}, 移除数量={}", userId, productIds.size());
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
public void clearCart(Long userId) {
|
||||
log.info("清空购物车: 用户ID={}", userId);
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
redisService.delete(cartKey);
|
||||
|
||||
log.info("购物车清空成功: 用户ID={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车信息
|
||||
*/
|
||||
public CartDTO getCart(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
Map<Object, Object> cartData = redisService.hGetAll(cartKey);
|
||||
|
||||
if (cartData.isEmpty()) {
|
||||
return new CartDTO(userId, new ArrayList<>(), BigDecimal.ZERO, 0);
|
||||
}
|
||||
|
||||
List<CartDTO.CartItemDTO> items = new ArrayList<>();
|
||||
BigDecimal totalPrice = BigDecimal.ZERO;
|
||||
int totalQuantity = 0;
|
||||
|
||||
for (Map.Entry<Object, Object> entry : cartData.entrySet()) {
|
||||
Long productId = Long.valueOf(entry.getKey().toString());
|
||||
Integer quantity = Integer.valueOf(entry.getValue().toString());
|
||||
|
||||
// 获取商品信息
|
||||
ProductDTO product = productService.getProductById(productId);
|
||||
if (product == null || product.getStatus() != 1) {
|
||||
// 商品不存在或已下架,从购物车中移除
|
||||
redisService.hDel(cartKey, entry.getKey().toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取当前库存
|
||||
Integer currentStock = productService.getProductStock(productId);
|
||||
|
||||
// 如果购物车中的数量超过库存,调整为库存数量
|
||||
if (quantity > currentStock) {
|
||||
quantity = currentStock;
|
||||
if (quantity > 0) {
|
||||
redisService.hSet(cartKey, productId.toString(), String.valueOf(quantity));
|
||||
} else {
|
||||
redisService.hDel(cartKey, productId.toString());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算小计
|
||||
BigDecimal subtotal = product.getPrice().multiply(BigDecimal.valueOf(quantity));
|
||||
|
||||
CartDTO.CartItemDTO item = new CartDTO.CartItemDTO();
|
||||
item.setProductId(productId);
|
||||
item.setProductName(product.getName());
|
||||
item.setProductPrice(product.getPrice());
|
||||
item.setProductImageUrl(product.getImageUrl());
|
||||
item.setQuantity(quantity);
|
||||
item.setSubtotal(subtotal);
|
||||
item.setStock(currentStock);
|
||||
|
||||
items.add(item);
|
||||
totalPrice = totalPrice.add(subtotal);
|
||||
totalQuantity += quantity;
|
||||
}
|
||||
|
||||
return new CartDTO(userId, items, totalPrice, totalQuantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车商品数量
|
||||
*/
|
||||
public int getCartItemCount(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
Map<Object, Object> cartData = redisService.hGetAll(cartKey);
|
||||
|
||||
return cartData.values().stream()
|
||||
.mapToInt(value -> Integer.parseInt(value.toString()))
|
||||
.sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查购物车中商品的库存状态
|
||||
*/
|
||||
public Map<String, Object> checkCartStock(Long userId) {
|
||||
CartDTO cart = getCart(userId);
|
||||
|
||||
List<Map<String, Object>> stockIssues = new ArrayList<>();
|
||||
boolean hasStockIssue = false;
|
||||
|
||||
for (CartDTO.CartItemDTO item : cart.getItems()) {
|
||||
if (item.getQuantity() > item.getStock()) {
|
||||
hasStockIssue = true;
|
||||
Map<String, Object> issue = new HashMap<>();
|
||||
issue.put("productId", item.getProductId());
|
||||
issue.put("productName", item.getProductName());
|
||||
issue.put("requestedQuantity", item.getQuantity());
|
||||
issue.put("availableStock", item.getStock());
|
||||
stockIssues.add(issue);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("hasStockIssue", hasStockIssue);
|
||||
result.put("stockIssues", stockIssues);
|
||||
result.put("cart", cart);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步购物车到数据库(可选功能)
|
||||
*/
|
||||
public void syncCartToDatabase(Long userId) {
|
||||
// 这里可以实现将购物车数据同步到数据库的逻辑
|
||||
// 用于数据持久化和恢复
|
||||
log.info("同步购物车到数据库: 用户ID={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库恢复购物车(可选功能)
|
||||
*/
|
||||
public void restoreCartFromDatabase(Long userId) {
|
||||
// 这里可以实现从数据库恢复购物车数据的逻辑
|
||||
log.info("从数据库恢复购物车: 用户ID={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建购物车Redis键
|
||||
*/
|
||||
private String buildCartKey(Long userId) {
|
||||
return CART_PREFIX + userId + CART_SUFFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车过期时间
|
||||
*/
|
||||
public Long getCartExpireTime(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
return redisService.getExpire(cartKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新购物车过期时间
|
||||
*/
|
||||
public void refreshCartExpireTime(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
redisService.expire(cartKey, cartExpireDays, TimeUnit.DAYS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 分布式锁服务
|
||||
* 基于Redis实现分布式锁,防止并发问题
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class DistributedLockService {
|
||||
|
||||
private static final String LOCK_PREFIX = "lock:";
|
||||
private static final int DEFAULT_EXPIRE_SECONDS = 30;
|
||||
private static final int DEFAULT_RETRY_TIMES = 3;
|
||||
private static final long DEFAULT_RETRY_INTERVAL_MS = 100;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
/**
|
||||
* 获取分布式锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 锁的值(用于释放锁时验证)
|
||||
*/
|
||||
public String acquireLock(String lockKey) {
|
||||
return acquireLock(lockKey, DEFAULT_EXPIRE_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分布式锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
*
|
||||
* @return 锁的值(用于释放锁时验证)
|
||||
*/
|
||||
public String acquireLock(String lockKey, int expireSeconds) {
|
||||
return acquireLock(lockKey, expireSeconds, DEFAULT_RETRY_TIMES, DEFAULT_RETRY_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分布式锁(带重试机制)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
* @param retryTimes 重试次数
|
||||
* @param retryIntervalMs 重试间隔(毫秒)
|
||||
*
|
||||
* @return 锁的值(用于释放锁时验证),获取失败返回null
|
||||
*/
|
||||
public String acquireLock(String lockKey, int expireSeconds, int retryTimes, long retryIntervalMs) {
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
String lockValue = UUID.randomUUID().toString();
|
||||
|
||||
for (int i = 0; i <= retryTimes; i++) {
|
||||
try {
|
||||
// 使用Lua脚本原子性地设置锁和过期时间
|
||||
String result = redisService.executeLockScript(fullLockKey, lockValue, expireSeconds);
|
||||
if ("OK".equals(result)) {
|
||||
log.debug("成功获取分布式锁: {}, 值: {}", fullLockKey, lockValue);
|
||||
return lockValue;
|
||||
}
|
||||
|
||||
if (i < retryTimes) {
|
||||
Thread.sleep(retryIntervalMs);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取分布式锁异常: {}", fullLockKey, e);
|
||||
if (i < retryTimes) {
|
||||
try {
|
||||
Thread.sleep(retryIntervalMs);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("获取分布式锁失败: {}", fullLockKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分布式锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param lockValue 锁的值
|
||||
*
|
||||
* @return 是否释放成功
|
||||
*/
|
||||
public boolean releaseLock(String lockKey, String lockValue) {
|
||||
if (lockValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
|
||||
try {
|
||||
// 使用Lua脚本原子性地检查锁的值并删除
|
||||
Long result = redisService.executeUnlockScript(fullLockKey, lockValue);
|
||||
boolean success = result != null && result > 0;
|
||||
|
||||
if (success) {
|
||||
log.debug("成功释放分布式锁: {}, 值: {}", fullLockKey, lockValue);
|
||||
} else {
|
||||
log.warn("释放分布式锁失败,锁可能已过期或被其他线程释放: {}, 值: {}", fullLockKey, lockValue);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (Exception e) {
|
||||
log.error("释放分布式锁异常: {}, 值: {}", fullLockKey, lockValue, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, Runnable task) {
|
||||
return executeWithLock(lockKey, DEFAULT_EXPIRE_SECONDS, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, int expireSeconds, Runnable task) {
|
||||
String lockValue = acquireLock(lockKey, expireSeconds);
|
||||
if (lockValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
task.run();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("执行锁定任务异常: {}", lockKey, e);
|
||||
throw e;
|
||||
} finally {
|
||||
releaseLock(lockKey, lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行有返回值的业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param supplier 要执行的任务
|
||||
* @param <T> 返回值类型
|
||||
*
|
||||
* @return 执行结果,获取锁失败返回null
|
||||
*/
|
||||
public <T> T executeWithLock(String lockKey, java.util.function.Supplier<T> supplier) {
|
||||
return executeWithLock(lockKey, DEFAULT_EXPIRE_SECONDS, supplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行有返回值的业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
* @param supplier 要执行的任务
|
||||
* @param <T> 返回值类型
|
||||
*
|
||||
* @return 执行结果,获取锁失败返回null
|
||||
*/
|
||||
public <T> T executeWithLock(String lockKey, int expireSeconds, java.util.function.Supplier<T> supplier) {
|
||||
String lockValue = acquireLock(lockKey, expireSeconds);
|
||||
if (lockValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception e) {
|
||||
log.error("执行锁定任务异常: {}", lockKey, e);
|
||||
throw e;
|
||||
} finally {
|
||||
releaseLock(lockKey, lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否存在
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 锁是否存在
|
||||
*/
|
||||
public boolean isLocked(String lockKey) {
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
return redisService.exists(fullLockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放锁(谨慎使用)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否释放成功
|
||||
*/
|
||||
public boolean forceReleaseLock(String lockKey) {
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
try {
|
||||
Boolean result = redisService.delete(fullLockKey);
|
||||
log.warn("强制释放分布式锁: {}, 结果: {}", fullLockKey, result);
|
||||
return result != null && result;
|
||||
} catch (Exception e) {
|
||||
log.error("强制释放分布式锁异常: {}", fullLockKey, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.FlashSaleDTO;
|
||||
import com.org.flashsalesystem.entity.FlashSale;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.repository.FlashSaleRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 秒杀服务类
|
||||
* 实现秒杀活动管理、库存控制、分布式锁等核心功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class FlashSaleService {
|
||||
|
||||
private static final String FLASH_SALE_CACHE_PREFIX = "flashsale:";
|
||||
private static final String FLASH_SALE_STOCK_PREFIX = "flashsale_stock:";
|
||||
private static final String FLASH_SALE_LOCK_PREFIX = "flashsale_lock:";
|
||||
private static final String FLASH_SALE_SUCCESS_USERS_PREFIX = "flashsale_success:";
|
||||
private static final String ACTIVE_FLASH_SALES = "active_flashsales";
|
||||
@Autowired
|
||||
private FlashSaleRepository flashSaleRepository;
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Autowired
|
||||
private DistributedLockService lockService;
|
||||
@Autowired
|
||||
private RedissonLockService redissonLockService;
|
||||
@Autowired
|
||||
private RateLimitService rateLimitService;
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
@Value("${flashsale.cache.flashsale-expire-minutes:10}")
|
||||
private int flashSaleCacheExpireMinutes;
|
||||
@Value("${flashsale.seckill.max-quantity-per-user:1}")
|
||||
private int maxQuantityPerUser;
|
||||
|
||||
/**
|
||||
* 创建秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO createFlashSale(FlashSaleDTO.CreateDTO createDTO) {
|
||||
log.info("创建秒杀活动: 商品ID={}, 秒杀价格={}, 库存={}",
|
||||
createDTO.getProductId(), createDTO.getFlashPrice(), createDTO.getFlashStock());
|
||||
|
||||
// 验证商品是否存在
|
||||
Optional<Product> productOpt = productRepository.findById(createDTO.getProductId());
|
||||
if (!productOpt.isPresent()) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
Product product = productOpt.get();
|
||||
if (product.getStatus() != 1) {
|
||||
throw new RuntimeException("商品未上架");
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
if (createDTO.getStartTime().isAfter(createDTO.getEndTime())) {
|
||||
throw new RuntimeException("开始时间不能晚于结束时间");
|
||||
}
|
||||
|
||||
if (createDTO.getStartTime().isBefore(LocalDateTime.now())) {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
|
||||
// 检查是否已有该商品的秒杀活动
|
||||
Optional<FlashSale> existingFlashSale = flashSaleRepository.findByProductId(createDTO.getProductId());
|
||||
if (existingFlashSale.isPresent()) {
|
||||
throw new RuntimeException("该商品已有秒杀活动");
|
||||
}
|
||||
|
||||
// 创建秒杀活动
|
||||
FlashSale flashSale = new FlashSale();
|
||||
BeanUtils.copyProperties(createDTO, flashSale);
|
||||
flashSale.setStatus(1); // 未开始
|
||||
|
||||
flashSale = flashSaleRepository.save(flashSale);
|
||||
|
||||
// 缓存秒杀活动信息
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 预热库存到Redis
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
|
||||
log.info("秒杀活动创建成功: ID={}", flashSale.getId());
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参与秒杀
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO.ResultDTO participateFlashSale(Long userId, FlashSaleDTO.ParticipateDTO participateDTO) {
|
||||
log.info("用户参与秒杀: 用户ID={}, 秒杀ID={}, 数量={}",
|
||||
userId, participateDTO.getFlashSaleId(), participateDTO.getQuantity());
|
||||
|
||||
// 限流检查
|
||||
if (!rateLimitService.checkFlashSaleRateLimit(userId)) {
|
||||
return createFailResult("请求过于频繁,请稍后再试");
|
||||
}
|
||||
|
||||
// 获取秒杀活动信息
|
||||
FlashSale flashSale = getFlashSaleById(participateDTO.getFlashSaleId());
|
||||
if (flashSale == null) {
|
||||
return createFailResult("秒杀活动不存在");
|
||||
}
|
||||
|
||||
// 检查活动状态
|
||||
if (!flashSale.isActive()) {
|
||||
return createFailResult("秒杀活动未开始或已结束");
|
||||
}
|
||||
|
||||
// 检查用户是否已经参与过
|
||||
String successUsersKey = FLASH_SALE_SUCCESS_USERS_PREFIX + flashSale.getId();
|
||||
if (redisService.sIsMember(successUsersKey, userId)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
|
||||
// 检查数据库中是否已有订单
|
||||
if (orderRepository.existsFlashSaleOrder(userId, flashSale.getProductId())) {
|
||||
return createFailResult("您已经购买过该商品");
|
||||
}
|
||||
|
||||
// 检查购买数量限制
|
||||
if (participateDTO.getQuantity() > maxQuantityPerUser) {
|
||||
return createFailResult("超过单用户最大购买数量限制");
|
||||
}
|
||||
|
||||
// 使用Redisson分布式锁防止超卖
|
||||
String lockKey = FLASH_SALE_LOCK_PREFIX + flashSale.getId();
|
||||
|
||||
if (!redissonLockService.tryLock(lockKey, 3, 10)) { // 等待3秒,持有10秒
|
||||
return createFailResult("系统繁忙,请稍后再试");
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用Lua脚本原子性扣减库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
Long remainingStock = redisService.executeFlashSaleScript(stockKey, participateDTO.getQuantity());
|
||||
|
||||
if (remainingStock < 0) {
|
||||
if (remainingStock == -1) {
|
||||
return createFailResult("秒杀活动库存信息异常");
|
||||
} else {
|
||||
return createFailResult("商品已售罄");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
Order order = createFlashSaleOrder(userId, flashSale, participateDTO);
|
||||
|
||||
// 添加到成功用户集合
|
||||
redisService.sAdd(successUsersKey, userId);
|
||||
redisService.expire(successUsersKey, 24, TimeUnit.HOURS);
|
||||
|
||||
// 更新数据库库存
|
||||
flashSaleRepository.updateFlashStock(flashSale.getId(), participateDTO.getQuantity());
|
||||
|
||||
// 发布秒杀成功消息
|
||||
publishFlashSaleResult(userId, flashSale, order, true);
|
||||
|
||||
// 更新商品销量排行榜
|
||||
productService.increaseSalesRank(flashSale.getProductId(), participateDTO.getQuantity());
|
||||
|
||||
log.info("秒杀成功: 用户ID={}, 订单ID={}, 剩余库存={}", userId, order.getId(), remainingStock);
|
||||
|
||||
return createSuccessResult(order, flashSale);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("秒杀处理异常: 用户ID={}, 秒杀ID={}", userId, participateDTO.getFlashSaleId(), e);
|
||||
return createFailResult("秒杀失败,请重试");
|
||||
} finally {
|
||||
redissonLockService.unlock(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动列表
|
||||
*/
|
||||
public Map<String, Object> getFlashSaleList(FlashSaleDTO.QueryDTO queryDTO) {
|
||||
// 验证排序字段
|
||||
String sortBy = validateSortField(queryDTO.getSortBy());
|
||||
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), sortBy);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<FlashSale> flashSalePage;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 如果指定了商品ID,按商品ID查询
|
||||
if (queryDTO.getProductId() != null) {
|
||||
if (queryDTO.getStatus() != null) {
|
||||
switch (queryDTO.getStatus()) {
|
||||
case 1: // 未开始
|
||||
flashSalePage = flashSaleRepository.findByProductIdAndStatus(queryDTO.getProductId(), 1,
|
||||
pageable);
|
||||
break;
|
||||
case 2: // 进行中
|
||||
flashSalePage = flashSaleRepository.findByProductIdAndStatus(queryDTO.getProductId(), 2,
|
||||
pageable);
|
||||
break;
|
||||
case 3: // 已结束
|
||||
flashSalePage = flashSaleRepository.findByProductIdAndStatus(queryDTO.getProductId(), 3,
|
||||
pageable);
|
||||
break;
|
||||
default:
|
||||
flashSalePage = flashSaleRepository.findByProductId(queryDTO.getProductId(), pageable);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
flashSalePage = flashSaleRepository.findByProductId(queryDTO.getProductId(), pageable);
|
||||
}
|
||||
} else {
|
||||
// 根据状态查询
|
||||
if (queryDTO.getStatus() != null) {
|
||||
switch (queryDTO.getStatus()) {
|
||||
case 1: // 未开始
|
||||
flashSalePage = flashSaleRepository.findUpcomingFlashSales(now, pageable);
|
||||
break;
|
||||
case 2: // 进行中
|
||||
flashSalePage = flashSaleRepository.findActiveFlashSales(now, pageable);
|
||||
break;
|
||||
case 3: // 已结束
|
||||
flashSalePage = flashSaleRepository.findEndedFlashSales(now, pageable);
|
||||
break;
|
||||
default:
|
||||
flashSalePage = flashSaleRepository.findAll(pageable);
|
||||
}
|
||||
} else {
|
||||
flashSalePage = flashSaleRepository.findAll(pageable);
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<FlashSaleDTO> flashSaleDTOs = flashSalePage.getContent().stream()
|
||||
.map(flashSale -> {
|
||||
Product product = productRepository.findById(
|
||||
flashSale.getProductId()).orElse(null);
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", flashSaleDTOs);
|
||||
result.put("totalElements", flashSalePage.getTotalElements());
|
||||
result.put("totalPages", flashSalePage.getTotalPages());
|
||||
result.put("currentPage", flashSalePage.getNumber());
|
||||
result.put("size", flashSalePage.getSize());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在进行的秒杀活动
|
||||
*/
|
||||
public List<FlashSaleDTO> getActiveFlashSales() {
|
||||
// 尝试从缓存获取
|
||||
List<Object> cachedFlashSales = redisService.lRange(ACTIVE_FLASH_SALES, 0, -1);
|
||||
if (!cachedFlashSales.isEmpty()) {
|
||||
return cachedFlashSales.stream()
|
||||
.map(obj -> (FlashSaleDTO) obj)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<FlashSale> activeFlashSales = flashSaleRepository.findActiveFlashSalesWithStock(now);
|
||||
|
||||
List<FlashSaleDTO> flashSaleDTOs = activeFlashSales.stream()
|
||||
.map(flashSale -> {
|
||||
Product product = productRepository.findById(
|
||||
flashSale.getProductId()).orElse(null);
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 缓存结果
|
||||
if (!flashSaleDTOs.isEmpty()) {
|
||||
redisService.delete(ACTIVE_FLASH_SALES);
|
||||
redisService.rPush(ACTIVE_FLASH_SALES, flashSaleDTOs.toArray());
|
||||
redisService.expire(ACTIVE_FLASH_SALES, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
return flashSaleDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取秒杀活动
|
||||
*/
|
||||
public FlashSaleDTO getFlashSaleDTOById(Long flashSaleId) {
|
||||
FlashSale flashSale = getFlashSaleById(flashSaleId);
|
||||
if (flashSale == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热秒杀活动
|
||||
*/
|
||||
public void preloadFlashSale(Long flashSaleId) {
|
||||
log.info("预热秒杀活动: {}", flashSaleId);
|
||||
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
log.warn("秒杀活动不存在: {}", flashSaleId);
|
||||
return;
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
|
||||
// 缓存秒杀活动信息
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 预热库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
|
||||
log.info("秒杀活动预热完成: {}", flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动剩余库存
|
||||
*/
|
||||
public Integer getFlashSaleStock(Long flashSaleId) {
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
Object stock = redisService.get(stockKey);
|
||||
return stock != null ? Integer.valueOf(stock.toString()) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO updateFlashSale(Long flashSaleId, FlashSaleDTO.UpdateDTO updateDTO) {
|
||||
log.info("更新秒杀活动: ID={}", flashSaleId);
|
||||
|
||||
// 获取现有秒杀活动
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
throw new RuntimeException("秒杀活动不存在");
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
|
||||
// 检查活动状态,只有未开始的活动才能修改
|
||||
if (flashSale.getStatus() != 1) {
|
||||
throw new RuntimeException("只有未开始的秒杀活动才能修改");
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (updateDTO.getFlashPrice() != null) {
|
||||
flashSale.setFlashPrice(updateDTO.getFlashPrice());
|
||||
}
|
||||
if (updateDTO.getFlashStock() != null) {
|
||||
flashSale.setFlashStock(updateDTO.getFlashStock());
|
||||
}
|
||||
if (updateDTO.getStartTime() != null) {
|
||||
if (updateDTO.getStartTime().isBefore(LocalDateTime.now())) {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
flashSale.setStartTime(updateDTO.getStartTime());
|
||||
}
|
||||
if (updateDTO.getEndTime() != null) {
|
||||
flashSale.setEndTime(updateDTO.getEndTime());
|
||||
}
|
||||
if (updateDTO.getStatus() != null) {
|
||||
flashSale.setStatus(updateDTO.getStatus());
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
if (flashSale.getStartTime().isAfter(flashSale.getEndTime())) {
|
||||
throw new RuntimeException("开始时间不能晚于结束时间");
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
flashSale = flashSaleRepository.save(flashSale);
|
||||
|
||||
// 更新缓存
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 更新Redis库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
|
||||
log.info("秒杀活动更新成功: ID={}", flashSale.getId());
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public boolean deleteFlashSale(Long flashSaleId) {
|
||||
log.info("删除秒杀活动: ID={}", flashSaleId);
|
||||
|
||||
// 获取现有秒杀活动
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
throw new RuntimeException("秒杀活动不存在");
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
|
||||
// 检查活动状态,只有未开始的活动才能删除
|
||||
if (flashSale.getStatus() != 1) {
|
||||
throw new RuntimeException("只有未开始的秒杀活动才能删除");
|
||||
}
|
||||
|
||||
// 检查是否有相关订单
|
||||
if (orderRepository.existsFlashSaleOrder(null, flashSale.getProductId())) {
|
||||
throw new RuntimeException("该秒杀活动已有订单,无法删除");
|
||||
}
|
||||
|
||||
// 删除秒杀活动
|
||||
flashSaleRepository.deleteById(flashSaleId);
|
||||
|
||||
// 清除相关缓存
|
||||
clearFlashSaleCache(flashSaleId);
|
||||
|
||||
log.info("秒杀活动删除成功: ID={}", flashSaleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
@Transactional
|
||||
public void updateFlashSaleStatus() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 更新未开始的活动为进行中
|
||||
List<FlashSale> upcomingFlashSales = flashSaleRepository.findUpcomingFlashSales(now);
|
||||
for (FlashSale flashSale : upcomingFlashSales) {
|
||||
if (flashSale.isStarted() && !flashSale.isEnded()) {
|
||||
flashSaleRepository.updateStatus(flashSale.getId(), 2);
|
||||
log.info("秒杀活动开始: {}", flashSale.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进行中的活动为已结束
|
||||
List<FlashSale> activeFlashSales = flashSaleRepository.findActiveFlashSales(now);
|
||||
for (FlashSale flashSale : activeFlashSales) {
|
||||
if (flashSale.isEnded()) {
|
||||
flashSaleRepository.updateStatus(flashSale.getId(), 3);
|
||||
log.info("秒杀活动结束: {}", flashSale.getId());
|
||||
|
||||
// 清除相关缓存
|
||||
clearFlashSaleCache(flashSale.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取秒杀活动实体
|
||||
*/
|
||||
private FlashSale getFlashSaleById(Long flashSaleId) {
|
||||
// 先从缓存获取
|
||||
String cacheKey = FLASH_SALE_CACHE_PREFIX + flashSaleId;
|
||||
Map<Object, Object> flashSaleMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!flashSaleMap.isEmpty()) {
|
||||
FlashSale flashSale = new FlashSale();
|
||||
flashSale.setId(flashSaleId);
|
||||
flashSale.setProductId(Long.valueOf((String) flashSaleMap.get("productId")));
|
||||
flashSale.setFlashPrice(new BigDecimal((String) flashSaleMap.get("flashPrice")));
|
||||
flashSale.setFlashStock(Integer.valueOf((String) flashSaleMap.get("flashStock")));
|
||||
flashSale.setStartTime(LocalDateTime.parse((String) flashSaleMap.get("startTime")));
|
||||
flashSale.setEndTime(LocalDateTime.parse((String) flashSaleMap.get("endTime")));
|
||||
flashSale.setStatus(Integer.valueOf((String) flashSaleMap.get("status")));
|
||||
return flashSale;
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (flashSaleOpt.isPresent()) {
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
return flashSale;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存秒杀活动信息
|
||||
*/
|
||||
private void cacheFlashSaleInfo(FlashSale flashSale, Product product) {
|
||||
String cacheKey = FLASH_SALE_CACHE_PREFIX + flashSale.getId();
|
||||
Map<String, Object> flashSaleMap = new HashMap<>();
|
||||
flashSaleMap.put("productId", flashSale.getProductId().toString());
|
||||
flashSaleMap.put("flashPrice", flashSale.getFlashPrice().toString());
|
||||
flashSaleMap.put("flashStock", flashSale.getFlashStock().toString());
|
||||
flashSaleMap.put("startTime", flashSale.getStartTime().toString());
|
||||
flashSaleMap.put("endTime", flashSale.getEndTime().toString());
|
||||
flashSaleMap.put("status", flashSale.getStatus().toString());
|
||||
|
||||
if (product != null) {
|
||||
flashSaleMap.put("productName", product.getName());
|
||||
flashSaleMap.put("productPrice", product.getPrice().toString());
|
||||
flashSaleMap.put("productImageUrl", product.getImageUrl());
|
||||
}
|
||||
|
||||
redisService.hMSet(cacheKey, flashSaleMap);
|
||||
redisService.expire(cacheKey, flashSaleCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建秒杀活动DTO
|
||||
*/
|
||||
private FlashSaleDTO buildFlashSaleDTO(FlashSale flashSale, Product product) {
|
||||
FlashSaleDTO dto = new FlashSaleDTO();
|
||||
BeanUtils.copyProperties(flashSale, dto);
|
||||
|
||||
if (product != null) {
|
||||
dto.setProductName(product.getName());
|
||||
dto.setProductImageUrl(product.getImageUrl());
|
||||
dto.setOriginalPrice(product.getPrice());
|
||||
}
|
||||
|
||||
// 获取剩余库存
|
||||
Integer remainingStock = getFlashSaleStock(flashSale.getId());
|
||||
dto.setRemainingStock(remainingStock);
|
||||
|
||||
// 设置状态描述
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (flashSale.getStartTime().isAfter(now)) {
|
||||
dto.setStatusDescription("未开始");
|
||||
dto.setCanParticipate(false);
|
||||
dto.setTimeToStart(
|
||||
flashSale.getStartTime().toEpochSecond(ZoneOffset.of("+8")) * 1000 - System.currentTimeMillis());
|
||||
} else if (flashSale.getEndTime().isBefore(now)) {
|
||||
dto.setStatusDescription("已结束");
|
||||
dto.setCanParticipate(false);
|
||||
dto.setTimeToEnd(0L);
|
||||
} else {
|
||||
dto.setStatusDescription("进行中");
|
||||
dto.setCanParticipate(remainingStock > 0);
|
||||
dto.setTimeToEnd(
|
||||
flashSale.getEndTime().toEpochSecond(ZoneOffset.of("+8")) * 1000 - System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证排序字段
|
||||
*/
|
||||
private String validateSortField(String sortBy) {
|
||||
if (sortBy == null || sortBy.trim().isEmpty()) {
|
||||
return "startTime"; // 默认排序字段
|
||||
}
|
||||
|
||||
// 允许的排序字段列表
|
||||
switch (sortBy.toLowerCase()) {
|
||||
case "starttime":
|
||||
case "start_time":
|
||||
return "startTime";
|
||||
case "endtime":
|
||||
case "end_time":
|
||||
return "endTime";
|
||||
case "createdat":
|
||||
case "created_at":
|
||||
return "createdAt";
|
||||
case "id":
|
||||
return "id";
|
||||
case "flashprice":
|
||||
case "flash_price":
|
||||
return "flashPrice";
|
||||
case "flashstock":
|
||||
case "flash_stock":
|
||||
return "flashStock";
|
||||
case "status":
|
||||
return "status";
|
||||
default:
|
||||
log.warn("无效的排序字段: {}, 使用默认排序字段: startTime", sortBy);
|
||||
return "startTime";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建秒杀订单
|
||||
*/
|
||||
private Order createFlashSaleOrder(Long userId, FlashSale flashSale, FlashSaleDTO.ParticipateDTO participateDTO) {
|
||||
Order order = new Order();
|
||||
order.setUserId(userId);
|
||||
order.setProductId(flashSale.getProductId());
|
||||
order.setQuantity(participateDTO.getQuantity());
|
||||
order.setTotalPrice(flashSale.getFlashPrice().multiply(BigDecimal.valueOf(participateDTO.getQuantity())));
|
||||
order.setStatus(1); // 待支付
|
||||
order.setOrderType(2); // 秒杀订单
|
||||
|
||||
return orderRepository.save(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布秒杀结果消息
|
||||
*/
|
||||
private void publishFlashSaleResult(Long userId, FlashSale flashSale, Order order, boolean success) {
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("userId", userId);
|
||||
message.put("flashSaleId", flashSale.getId());
|
||||
message.put("productId", flashSale.getProductId());
|
||||
message.put("success", success);
|
||||
message.put("orderId", order != null ? order.getId() : null);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
redisService.publish("flashsale:result", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功结果
|
||||
*/
|
||||
private FlashSaleDTO.ResultDTO createSuccessResult(Order order, FlashSale flashSale) {
|
||||
FlashSaleDTO.ResultDTO result = new FlashSaleDTO.ResultDTO();
|
||||
result.setSuccess(true);
|
||||
result.setMessage("秒杀成功");
|
||||
result.setOrderId(order.getId());
|
||||
result.setFlashSaleId(flashSale.getId());
|
||||
result.setProductId(flashSale.getProductId());
|
||||
result.setQuantity(order.getQuantity());
|
||||
result.setTotalPrice(order.getTotalPrice());
|
||||
result.setOrderTime(order.getCreatedAt());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败结果
|
||||
*/
|
||||
private FlashSaleDTO.ResultDTO createFailResult(String message) {
|
||||
FlashSaleDTO.ResultDTO result = new FlashSaleDTO.ResultDTO();
|
||||
result.setSuccess(false);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除秒杀活动缓存
|
||||
*/
|
||||
private void clearFlashSaleCache(Long flashSaleId) {
|
||||
String cacheKey = FLASH_SALE_CACHE_PREFIX + flashSaleId;
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
String successUsersKey = FLASH_SALE_SUCCESS_USERS_PREFIX + flashSaleId;
|
||||
|
||||
redisService.delete(cacheKey);
|
||||
redisService.delete(stockKey);
|
||||
redisService.delete(successUsersKey);
|
||||
redisService.delete(ACTIVE_FLASH_SALES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.listener.ChannelTopic;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Redis消息监听服务
|
||||
* 实现Redis Pub/Sub消息队列功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class MessageListenerService {
|
||||
|
||||
@Autowired
|
||||
private RedisMessageListenerContainer redisMessageListenerContainer;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 初始化消息监听器
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initMessageListeners() {
|
||||
// 订单状态变更监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new OrderStatusChangeListener(),
|
||||
new ChannelTopic("order:status:change")
|
||||
);
|
||||
|
||||
// 库存变更监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new StockChangeListener(),
|
||||
new ChannelTopic("stock:change")
|
||||
);
|
||||
|
||||
// 秒杀结果监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new FlashSaleResultListener(),
|
||||
new ChannelTopic("flashsale:result")
|
||||
);
|
||||
|
||||
// 用户行为监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new UserActionListener(),
|
||||
new ChannelTopic("user:action")
|
||||
);
|
||||
|
||||
log.info("Redis消息监听器初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理订单状态变更
|
||||
*/
|
||||
private void handleOrderStatusChange(Long orderId, Long userId, Integer status, String action) {
|
||||
// 可以在这里实现:
|
||||
// 1. 发送邮件通知
|
||||
// 2. 推送消息
|
||||
// 3. 更新统计数据
|
||||
// 4. 触发其他业务流程
|
||||
|
||||
switch (action) {
|
||||
case "created":
|
||||
log.info("订单创建通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "paid":
|
||||
log.info("订单支付通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "shipped":
|
||||
log.info("订单发货通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "completed":
|
||||
log.info("订单完成通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "cancelled":
|
||||
log.info("订单取消通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
default:
|
||||
log.info("未知订单状态变更: {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理库存变更
|
||||
*/
|
||||
private void handleStockChange(Long productId, Integer quantity, String operation) {
|
||||
// 可以在这里实现:
|
||||
// 1. 库存预警
|
||||
// 2. 自动补货
|
||||
// 3. 数据同步
|
||||
// 4. 统计分析
|
||||
|
||||
if ("decrease".equals(operation)) {
|
||||
log.info("库存扣减处理: 商品ID={}, 扣减数量={}", productId, quantity);
|
||||
// 检查是否需要库存预警
|
||||
checkStockAlert(productId);
|
||||
} else if ("increase".equals(operation)) {
|
||||
log.info("库存增加处理: 商品ID={}, 增加数量={}", productId, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理秒杀结果
|
||||
*/
|
||||
private void handleFlashSaleResult(Long userId, Long flashSaleId, Boolean success, Map<String, Object> data) {
|
||||
// 可以在这里实现:
|
||||
// 1. 实时通知用户
|
||||
// 2. 统计秒杀数据
|
||||
// 3. 风控分析
|
||||
// 4. 营销推荐
|
||||
|
||||
if (success) {
|
||||
log.info("秒杀成功处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 发送成功通知
|
||||
sendFlashSaleSuccessNotification(userId, flashSaleId);
|
||||
} else {
|
||||
log.info("秒杀失败处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 可以推荐其他商品
|
||||
recommendAlternativeProducts(userId, flashSaleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户行为
|
||||
*/
|
||||
private void handleUserAction(Long userId, String action, Map<String, Object> data) {
|
||||
// 可以在这里实现:
|
||||
// 1. 用户行为分析
|
||||
// 2. 个性化推荐
|
||||
// 3. 风控检测
|
||||
// 4. 营销触发
|
||||
|
||||
switch (action) {
|
||||
case "login":
|
||||
log.info("用户登录行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
case "view_product":
|
||||
log.info("用户浏览商品行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
case "add_to_cart":
|
||||
log.info("用户添加购物车行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
case "purchase":
|
||||
log.info("用户购买行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
default:
|
||||
log.info("其他用户行为: {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查库存预警
|
||||
*/
|
||||
private void checkStockAlert(Long productId) {
|
||||
// 实现库存预警逻辑
|
||||
log.debug("检查商品库存预警: 商品ID={}", productId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送秒杀成功通知
|
||||
*/
|
||||
private void sendFlashSaleSuccessNotification(Long userId, Long flashSaleId) {
|
||||
// 实现成功通知逻辑
|
||||
log.debug("发送秒杀成功通知: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推荐替代商品
|
||||
*/
|
||||
private void recommendAlternativeProducts(Long userId, Long flashSaleId) {
|
||||
// 实现商品推荐逻辑
|
||||
log.debug("推荐替代商品: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态变更监听器
|
||||
*/
|
||||
private class OrderStatusChangeListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long orderId = Long.valueOf(data.get("orderId").toString());
|
||||
Long userId = Long.valueOf(data.get("userId").toString());
|
||||
Integer status = Integer.valueOf(data.get("status").toString());
|
||||
String action = data.get("action").toString();
|
||||
|
||||
log.info("订单状态变更: 订单ID={}, 用户ID={}, 状态={}, 操作={}",
|
||||
orderId, userId, status, action);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleOrderStatusChange(orderId, userId, status, action);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理订单状态变更消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存变更监听器
|
||||
*/
|
||||
private class StockChangeListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long productId = Long.valueOf(data.get("productId").toString());
|
||||
Integer quantity = Integer.valueOf(data.get("quantity").toString());
|
||||
String operation = data.get("operation").toString();
|
||||
|
||||
log.info("库存变更: 商品ID={}, 数量={}, 操作={}", productId, quantity, operation);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleStockChange(productId, quantity, operation);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理库存变更消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀结果监听器
|
||||
*/
|
||||
private class FlashSaleResultListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long userId = Long.valueOf(data.get("userId").toString());
|
||||
Long flashSaleId = Long.valueOf(data.get("flashSaleId").toString());
|
||||
Boolean success = Boolean.valueOf(data.get("success").toString());
|
||||
|
||||
log.info("秒杀结果: 用户ID={}, 秒杀ID={}, 成功={}", userId, flashSaleId, success);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleFlashSaleResult(userId, flashSaleId, success, data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理秒杀结果消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户行为监听器
|
||||
*/
|
||||
private class UserActionListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long userId = Long.valueOf(data.get("userId").toString());
|
||||
String action = data.get("action").toString();
|
||||
|
||||
log.info("用户行为: 用户ID={}, 行为={}", userId, action);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleUserAction(userId, action, data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户行为消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
546
src/main/java/com/org/flashsalesystem/service/OrderService.java
Normal file
546
src/main/java/com/org/flashsalesystem/service/OrderService.java
Normal file
@@ -0,0 +1,546 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.OrderDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 订单服务类
|
||||
* 实现订单创建、查询、状态管理等功能
|
||||
* 使用Redis缓存订单信息
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OrderService {
|
||||
|
||||
private static final String ORDER_CACHE_PREFIX = "order:";
|
||||
private static final String USER_ORDERS_PREFIX = "user_orders:";
|
||||
private static final String ORDER_QUEUE = "order_queue";
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 创建普通订单
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO createOrder(Long userId, OrderDTO.CreateDTO createDTO) {
|
||||
log.info("创建订单: 用户ID={}, 商品ID={}, 数量={}", userId, createDTO.getProductId(), createDTO.getQuantity());
|
||||
|
||||
// 验证商品
|
||||
ProductDTO product = productService.getProductById(createDTO.getProductId());
|
||||
if (product == null) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
if (product.getStatus() != 1) {
|
||||
throw new RuntimeException("商品已下架");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
if (product.getStock() < createDTO.getQuantity()) {
|
||||
throw new RuntimeException("商品库存不足");
|
||||
}
|
||||
|
||||
// 计算总价
|
||||
BigDecimal totalPrice = product.getPrice().multiply(BigDecimal.valueOf(createDTO.getQuantity()));
|
||||
|
||||
// 创建订单
|
||||
Order order = new Order();
|
||||
order.setUserId(userId);
|
||||
order.setProductId(createDTO.getProductId());
|
||||
order.setQuantity(createDTO.getQuantity());
|
||||
order.setTotalPrice(totalPrice);
|
||||
order.setStatus(1); // 待支付
|
||||
order.setOrderType(1); // 普通订单
|
||||
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 扣减库存
|
||||
boolean stockUpdated = productService.updateStock(createDTO.getProductId(), createDTO.getQuantity(),
|
||||
"decrease");
|
||||
if (!stockUpdated) {
|
||||
throw new RuntimeException("库存扣减失败");
|
||||
}
|
||||
|
||||
// 缓存订单信息
|
||||
cacheOrderInfo(order);
|
||||
|
||||
// 添加到订单队列
|
||||
redisService.lPush(ORDER_QUEUE, order.getId());
|
||||
|
||||
// 发布订单创建消息
|
||||
publishOrderStatusChange(order, "created");
|
||||
|
||||
log.info("订单创建成功: 订单ID={}", order.getId());
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取订单
|
||||
*/
|
||||
public OrderDTO getOrderById(Long orderId) {
|
||||
if (orderId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从缓存获取
|
||||
String cacheKey = ORDER_CACHE_PREFIX + orderId;
|
||||
Map<Object, Object> orderMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!orderMap.isEmpty()) {
|
||||
return buildOrderDTOFromCache(orderId, orderMap);
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<Order> orderOpt = orderRepository.findById(orderId);
|
||||
if (!orderOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 缓存订单信息
|
||||
cacheOrderInfo(order);
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户订单列表
|
||||
*/
|
||||
public Map<String, Object> getUserOrders(Long userId, OrderDTO.QueryDTO queryDTO) {
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
// 根据查询条件获取订单
|
||||
if (queryDTO.getStatus() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndStatus(userId, queryDTO.getStatus(), pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findByUserId(userId, pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<OrderDTO> orderDTOs = orderPage.getContent().stream()
|
||||
.map(this::buildOrderDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", orderDTOs);
|
||||
result.put("totalElements", orderPage.getTotalElements());
|
||||
result.put("totalPages", orderPage.getTotalPages());
|
||||
result.put("currentPage", orderPage.getNumber());
|
||||
result.put("size", orderPage.getSize());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有订单列表(管理员)
|
||||
*/
|
||||
public Map<String, Object> getAllOrders(OrderDTO.QueryDTO queryDTO) {
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
// 根据查询条件获取订单
|
||||
if (queryDTO.getStatus() != null) {
|
||||
orderPage = orderRepository.findByStatus(queryDTO.getStatus(), pageable);
|
||||
} else if (queryDTO.getOrderType() != null) {
|
||||
orderPage = orderRepository.findByOrderType(queryDTO.getOrderType(), pageable);
|
||||
} else if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) {
|
||||
orderPage = orderRepository.findByTimeRange(queryDTO.getStartTime(), queryDTO.getEndTime(), pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<OrderDTO> orderDTOs = orderPage.getContent().stream()
|
||||
.map(this::buildOrderDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", orderDTOs);
|
||||
result.put("totalElements", orderPage.getTotalElements());
|
||||
result.put("totalPages", orderPage.getTotalPages());
|
||||
result.put("currentPage", orderPage.getNumber());
|
||||
result.put("size", orderPage.getSize());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO updateOrderStatus(Long orderId, Integer newStatus, String remark) {
|
||||
log.info("更新订单状态: 订单ID={}, 新状态={}", orderId, newStatus);
|
||||
|
||||
Optional<Order> orderOpt = orderRepository.findById(orderId);
|
||||
if (!orderOpt.isPresent()) {
|
||||
throw new RuntimeException("订单不存在");
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
Integer oldStatus = order.getStatus();
|
||||
|
||||
// 验证状态转换的合法性
|
||||
if (!isValidStatusTransition(oldStatus, newStatus)) {
|
||||
throw new RuntimeException("无效的状态转换");
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
order.setStatus(newStatus);
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 更新缓存
|
||||
cacheOrderInfo(order);
|
||||
|
||||
// 处理状态变更的业务逻辑
|
||||
handleStatusChange(order, oldStatus, newStatus);
|
||||
|
||||
// 发布状态变更消息
|
||||
publishOrderStatusChange(order, getStatusDescription(newStatus));
|
||||
|
||||
log.info("订单状态更新成功: 订单ID={}, 状态: {} -> {}", orderId, oldStatus, newStatus);
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO cancelOrder(Long orderId, Long userId) {
|
||||
log.info("取消订单: 订单ID={}, 用户ID={}", orderId, userId);
|
||||
|
||||
Optional<Order> orderOpt = orderRepository.findById(orderId);
|
||||
if (!orderOpt.isPresent()) {
|
||||
throw new RuntimeException("订单不存在");
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 验证用户权限
|
||||
if (!order.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限操作此订单");
|
||||
}
|
||||
|
||||
// 只有待支付状态的订单可以取消
|
||||
if (order.getStatus() != 1) {
|
||||
throw new RuntimeException("订单状态不允许取消");
|
||||
}
|
||||
|
||||
// 更新订单状态为已取消
|
||||
order.setStatus(5);
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 恢复库存
|
||||
productService.updateStock(order.getProductId(), order.getQuantity(), "increase");
|
||||
|
||||
// 更新缓存
|
||||
cacheOrderInfo(order);
|
||||
|
||||
// 发布订单取消消息
|
||||
publishOrderStatusChange(order, "cancelled");
|
||||
|
||||
log.info("订单取消成功: 订单ID={}", orderId);
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作订单
|
||||
*/
|
||||
@Transactional
|
||||
public List<OrderDTO> batchOperateOrders(OrderDTO.BatchOperationDTO batchDTO) {
|
||||
log.info("批量操作订单: 操作={}, 订单数量={}", batchDTO.getOperation(), batchDTO.getOrderIds().size());
|
||||
|
||||
List<OrderDTO> results = new ArrayList<>();
|
||||
|
||||
for (Long orderId : batchDTO.getOrderIds()) {
|
||||
try {
|
||||
OrderDTO result = null;
|
||||
|
||||
switch (batchDTO.getOperation()) {
|
||||
case "pay":
|
||||
result = updateOrderStatus(orderId, 2, "批量支付");
|
||||
break;
|
||||
case "ship":
|
||||
result = updateOrderStatus(orderId, 3, "批量发货");
|
||||
break;
|
||||
case "complete":
|
||||
result = updateOrderStatus(orderId, 4, "批量完成");
|
||||
break;
|
||||
case "cancel":
|
||||
// 需要获取订单的用户ID
|
||||
Order order = orderRepository.findById(orderId).orElse(null);
|
||||
if (order != null) {
|
||||
result = cancelOrder(orderId, order.getUserId());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("不支持的操作类型");
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
results.add(result);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("批量操作订单失败: 订单ID={}", orderId, e);
|
||||
// 继续处理其他订单
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量操作订单完成: 成功处理{}个订单", results.size());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计信息
|
||||
*/
|
||||
public OrderDTO.StatisticsDTO getOrderStatistics() {
|
||||
OrderDTO.StatisticsDTO statistics = new OrderDTO.StatisticsDTO();
|
||||
|
||||
// 总订单数
|
||||
statistics.setTotalOrders(orderRepository.count());
|
||||
|
||||
// 各状态订单数
|
||||
statistics.setPendingPaymentOrders(orderRepository.countByStatus(1));
|
||||
statistics.setPaidOrders(orderRepository.countByStatus(2));
|
||||
statistics.setShippedOrders(orderRepository.countByStatus(3));
|
||||
statistics.setCompletedOrders(orderRepository.countByStatus(4));
|
||||
statistics.setCancelledOrders(orderRepository.countByStatus(5));
|
||||
|
||||
// 订单类型统计
|
||||
statistics.setNormalOrders(orderRepository.countByOrderType(1));
|
||||
statistics.setFlashSaleOrders(orderRepository.countByOrderType(2));
|
||||
|
||||
// 金额统计(这里简化处理,实际应该从数据库聚合查询)
|
||||
List<Order> allOrders = orderRepository.findAll();
|
||||
BigDecimal totalAmount = allOrders.stream()
|
||||
.map(Order::getTotalPrice)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
statistics.setTotalAmount(totalAmount);
|
||||
|
||||
// 今日金额
|
||||
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
|
||||
LocalDateTime todayEnd = LocalDateTime.now().withHour(23).withMinute(59).withSecond(59);
|
||||
List<Order> todayOrders = orderRepository.findByTimeRange(todayStart, todayEnd);
|
||||
BigDecimal todayAmount = todayOrders.stream()
|
||||
.map(Order::getTotalPrice)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
statistics.setTodayAmount(todayAmount);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存订单信息
|
||||
*/
|
||||
private void cacheOrderInfo(Order order) {
|
||||
String cacheKey = ORDER_CACHE_PREFIX + order.getId();
|
||||
Map<String, Object> orderMap = new HashMap<>();
|
||||
orderMap.put("userId", order.getUserId().toString());
|
||||
orderMap.put("productId", order.getProductId().toString());
|
||||
orderMap.put("quantity", order.getQuantity().toString());
|
||||
orderMap.put("totalPrice", order.getTotalPrice().toString());
|
||||
orderMap.put("status", order.getStatus().toString());
|
||||
orderMap.put("orderType", order.getOrderType().toString());
|
||||
orderMap.put("createdAt", order.getCreatedAt().toString());
|
||||
|
||||
redisService.hMSet(cacheKey, orderMap);
|
||||
redisService.expire(cacheKey, 24, TimeUnit.HOURS);
|
||||
|
||||
// 添加到用户订单列表
|
||||
String userOrdersKey = USER_ORDERS_PREFIX + order.getUserId();
|
||||
redisService.lPush(userOrdersKey, order.getId());
|
||||
redisService.expire(userOrdersKey, 24, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存构建OrderDTO
|
||||
*/
|
||||
private OrderDTO buildOrderDTOFromCache(Long orderId, Map<Object, Object> orderMap) {
|
||||
OrderDTO orderDTO = new OrderDTO();
|
||||
orderDTO.setId(orderId);
|
||||
orderDTO.setUserId(Long.valueOf((String) orderMap.get("userId")));
|
||||
orderDTO.setProductId(Long.valueOf((String) orderMap.get("productId")));
|
||||
orderDTO.setQuantity(Integer.valueOf((String) orderMap.get("quantity")));
|
||||
orderDTO.setTotalPrice(new BigDecimal((String) orderMap.get("totalPrice")));
|
||||
orderDTO.setStatus(Integer.valueOf((String) orderMap.get("status")));
|
||||
orderDTO.setOrderType(Integer.valueOf((String) orderMap.get("orderType")));
|
||||
orderDTO.setCreatedAt(LocalDateTime.parse((String) orderMap.get("createdAt")));
|
||||
|
||||
// 设置状态和类型描述
|
||||
orderDTO.setStatusDescription(getStatusDescription(orderDTO.getStatus()));
|
||||
orderDTO.setOrderTypeDescription(getOrderTypeDescription(orderDTO.getOrderType()));
|
||||
|
||||
// 获取用户和商品信息
|
||||
UserDTO user = userService.getUserById(orderDTO.getUserId());
|
||||
if (user != null) {
|
||||
orderDTO.setUsername(user.getUsername());
|
||||
}
|
||||
|
||||
ProductDTO product = productService.getProductById(orderDTO.getProductId());
|
||||
if (product != null) {
|
||||
orderDTO.setProductName(product.getName());
|
||||
orderDTO.setProductImageUrl(product.getImageUrl());
|
||||
}
|
||||
|
||||
return orderDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建OrderDTO
|
||||
*/
|
||||
private OrderDTO buildOrderDTO(Order order) {
|
||||
OrderDTO orderDTO = new OrderDTO();
|
||||
BeanUtils.copyProperties(order, orderDTO);
|
||||
|
||||
// 设置状态和类型描述
|
||||
orderDTO.setStatusDescription(getStatusDescription(order.getStatus()));
|
||||
orderDTO.setOrderTypeDescription(getOrderTypeDescription(order.getOrderType()));
|
||||
|
||||
// 获取用户信息
|
||||
UserDTO user = userService.getUserById(order.getUserId());
|
||||
if (user != null) {
|
||||
orderDTO.setUsername(user.getUsername());
|
||||
}
|
||||
|
||||
// 获取商品信息
|
||||
ProductDTO product = productService.getProductById(order.getProductId());
|
||||
if (product != null) {
|
||||
orderDTO.setProductName(product.getName());
|
||||
orderDTO.setProductImageUrl(product.getImageUrl());
|
||||
}
|
||||
|
||||
return orderDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证状态转换的合法性
|
||||
*/
|
||||
private boolean isValidStatusTransition(Integer fromStatus, Integer toStatus) {
|
||||
// 定义合法的状态转换
|
||||
Map<Integer, List<Integer>> validTransitions = new HashMap<>();
|
||||
validTransitions.put(1, Arrays.asList(2, 5)); // 待支付 -> 已支付/已取消
|
||||
validTransitions.put(2, Arrays.asList(3, 5)); // 已支付 -> 已发货/已取消
|
||||
validTransitions.put(3, Collections.singletonList(4)); // 已发货 -> 已完成
|
||||
validTransitions.put(4, Collections.emptyList()); // 已完成 -> 无
|
||||
validTransitions.put(5, Collections.emptyList()); // 已取消 -> 无
|
||||
|
||||
List<Integer> allowedTransitions = validTransitions.get(fromStatus);
|
||||
return allowedTransitions != null && allowedTransitions.contains(toStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态变更的业务逻辑
|
||||
*/
|
||||
private void handleStatusChange(Order order, Integer oldStatus, Integer newStatus) {
|
||||
// 支付完成
|
||||
if (oldStatus == 1 && newStatus == 2) {
|
||||
log.info("订单支付完成: 订单ID={}", order.getId());
|
||||
// 这里可以添加支付完成后的业务逻辑
|
||||
}
|
||||
|
||||
// 发货
|
||||
if (oldStatus == 2 && newStatus == 3) {
|
||||
log.info("订单已发货: 订单ID={}", order.getId());
|
||||
// 这里可以添加发货后的业务逻辑
|
||||
}
|
||||
|
||||
// 完成
|
||||
if (oldStatus == 3 && newStatus == 4) {
|
||||
log.info("订单已完成: 订单ID={}", order.getId());
|
||||
// 这里可以添加订单完成后的业务逻辑
|
||||
}
|
||||
|
||||
// 取消
|
||||
if (newStatus == 5) {
|
||||
log.info("订单已取消: 订单ID={}", order.getId());
|
||||
// 这里可以添加订单取消后的业务逻辑
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布订单状态变更消息
|
||||
*/
|
||||
private void publishOrderStatusChange(Order order, String action) {
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("orderId", order.getId());
|
||||
message.put("userId", order.getUserId());
|
||||
message.put("productId", order.getProductId());
|
||||
message.put("status", order.getStatus());
|
||||
message.put("action", action);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
redisService.publish("order:status:change", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态描述
|
||||
*/
|
||||
private String getStatusDescription(Integer status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "待支付";
|
||||
case 2:
|
||||
return "已支付";
|
||||
case 3:
|
||||
return "已发货";
|
||||
case 4:
|
||||
return "已完成";
|
||||
case 5:
|
||||
return "已取消";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单类型描述
|
||||
*/
|
||||
private String getOrderTypeDescription(Integer orderType) {
|
||||
switch (orderType) {
|
||||
case 1:
|
||||
return "普通订单";
|
||||
case 2:
|
||||
return "秒杀订单";
|
||||
default:
|
||||
return "未知类型";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 商品服务类
|
||||
* 实现商品管理、库存控制、缓存等功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ProductService {
|
||||
|
||||
private static final String PRODUCT_CACHE_PREFIX = "product:";
|
||||
private static final String PRODUCT_LIST_CACHE_PREFIX = "product_list:";
|
||||
private static final String PRODUCT_STOCK_PREFIX = "product_stock:";
|
||||
private static final String PRODUCT_SALES_RANK = "product_sales_rank";
|
||||
private static final String HOT_PRODUCTS_CACHE = "hot_products";
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Value("${flashsale.cache.product-expire-minutes:60}")
|
||||
private int productCacheExpireMinutes;
|
||||
|
||||
/**
|
||||
* 创建商品
|
||||
*/
|
||||
@Transactional
|
||||
public ProductDTO createProduct(ProductDTO.CreateDTO createDTO) {
|
||||
log.info("创建商品: {}", createDTO.getName());
|
||||
|
||||
Product product = new Product();
|
||||
BeanUtils.copyProperties(createDTO, product);
|
||||
product.setStatus(1); // 默认上架
|
||||
|
||||
product = productRepository.save(product);
|
||||
|
||||
// 缓存商品信息
|
||||
cacheProductInfo(product);
|
||||
|
||||
// 初始化库存到Redis
|
||||
if (product.getStock() > 0) {
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + product.getId();
|
||||
redisService.set(stockKey, product.getStock());
|
||||
}
|
||||
|
||||
// 清除商品列表缓存
|
||||
clearProductListCache();
|
||||
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, productDTO);
|
||||
|
||||
log.info("商品创建成功: {}, ID: {}", product.getName(), product.getId());
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取商品信息
|
||||
*/
|
||||
public ProductDTO getProductById(Long productId) {
|
||||
if (productId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从缓存获取
|
||||
String cacheKey = PRODUCT_CACHE_PREFIX + productId;
|
||||
Map<Object, Object> productMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!productMap.isEmpty()) {
|
||||
return buildProductDTOFromCache(productId, productMap);
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<Product> productOpt = productRepository.findById(productId);
|
||||
if (!productOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Product product = productOpt.get();
|
||||
|
||||
// 缓存商品信息
|
||||
cacheProductInfo(product);
|
||||
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, productDTO);
|
||||
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表(分页)
|
||||
*/
|
||||
public Map<String, Object> getProductList(ProductDTO.QueryDTO queryDTO) {
|
||||
// 构建缓存键
|
||||
String cacheKey = buildProductListCacheKey(queryDTO);
|
||||
|
||||
// 尝试从缓存获取
|
||||
Object cachedResult = redisService.get(cacheKey);
|
||||
if (cachedResult != null) {
|
||||
return (Map<String, Object>) cachedResult;
|
||||
}
|
||||
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<Product> productPage;
|
||||
|
||||
// 根据查询条件获取数据
|
||||
if (queryDTO.getName() != null && !queryDTO.getName().trim().isEmpty()) {
|
||||
productPage = productRepository.findByStatus(1, pageable)
|
||||
.map(product -> {
|
||||
if (product.getName().contains(queryDTO.getName())) {
|
||||
return product;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.map(product -> product);
|
||||
} else {
|
||||
productPage = productRepository.findByStatus(queryDTO.getStatus() != null ? queryDTO.getStatus() : 1,
|
||||
pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<ProductDTO> productDTOs = productPage.getContent().stream()
|
||||
.map(product -> {
|
||||
ProductDTO dto = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, dto);
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", productDTOs);
|
||||
result.put("totalElements", productPage.getTotalElements());
|
||||
result.put("totalPages", productPage.getTotalPages());
|
||||
result.put("currentPage", productPage.getNumber());
|
||||
result.put("size", productPage.getSize());
|
||||
|
||||
// 缓存结果
|
||||
redisService.set(cacheKey, result, 10, TimeUnit.MINUTES);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门商品
|
||||
*/
|
||||
public List<ProductDTO> getHotProducts(int limit) {
|
||||
// 尝试从缓存获取
|
||||
List<Object> cachedProducts = redisService.lRange(HOT_PRODUCTS_CACHE, 0, limit - 1);
|
||||
if (!cachedProducts.isEmpty()) {
|
||||
return cachedProducts.stream()
|
||||
.map(obj -> (ProductDTO) obj)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
Pageable pageable = PageRequest.of(0, limit);
|
||||
List<Product> hotProducts = productRepository.findHotProducts(pageable);
|
||||
|
||||
List<ProductDTO> productDTOs = hotProducts.stream()
|
||||
.map(product -> {
|
||||
ProductDTO dto = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, dto);
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 缓存热门商品
|
||||
if (!productDTOs.isEmpty()) {
|
||||
redisService.delete(HOT_PRODUCTS_CACHE);
|
||||
redisService.rPush(HOT_PRODUCTS_CACHE, productDTOs.toArray());
|
||||
redisService.expire(HOT_PRODUCTS_CACHE, 30, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
return productDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品信息
|
||||
*/
|
||||
@Transactional
|
||||
public ProductDTO updateProduct(Long productId, ProductDTO.UpdateDTO updateDTO) {
|
||||
log.info("更新商品信息: {}", productId);
|
||||
|
||||
Optional<Product> productOpt = productRepository.findById(productId);
|
||||
if (!productOpt.isPresent()) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
Product product = productOpt.get();
|
||||
|
||||
// 更新字段
|
||||
if (updateDTO.getName() != null) {
|
||||
product.setName(updateDTO.getName());
|
||||
}
|
||||
if (updateDTO.getDescription() != null) {
|
||||
product.setDescription(updateDTO.getDescription());
|
||||
}
|
||||
if (updateDTO.getPrice() != null) {
|
||||
product.setPrice(updateDTO.getPrice());
|
||||
}
|
||||
if (updateDTO.getStock() != null) {
|
||||
product.setStock(updateDTO.getStock());
|
||||
// 同步更新Redis中的库存
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
redisService.set(stockKey, updateDTO.getStock());
|
||||
}
|
||||
if (updateDTO.getImageUrl() != null) {
|
||||
product.setImageUrl(updateDTO.getImageUrl());
|
||||
}
|
||||
if (updateDTO.getStatus() != null) {
|
||||
product.setStatus(updateDTO.getStatus());
|
||||
}
|
||||
|
||||
product = productRepository.save(product);
|
||||
|
||||
// 更新缓存
|
||||
cacheProductInfo(product);
|
||||
clearProductListCache();
|
||||
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, productDTO);
|
||||
|
||||
log.info("商品信息更新成功: {}", productId);
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品库存
|
||||
*/
|
||||
@Transactional
|
||||
public boolean updateStock(Long productId, Integer quantity, String operation) {
|
||||
log.info("更新商品库存: {}, 数量: {}, 操作: {}", productId, quantity, operation);
|
||||
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
|
||||
try {
|
||||
if ("increase".equals(operation)) {
|
||||
// 增加库存
|
||||
redisService.incrBy(stockKey, quantity);
|
||||
productRepository.increaseStock(productId, quantity);
|
||||
} else if ("decrease".equals(operation)) {
|
||||
// 减少库存
|
||||
Long currentStock = (Long) redisService.get(stockKey);
|
||||
if (currentStock == null || currentStock < quantity) {
|
||||
log.warn("库存不足: 商品ID={}, 当前库存={}, 需要扣减={}", productId, currentStock, quantity);
|
||||
return false;
|
||||
}
|
||||
|
||||
redisService.decrBy(stockKey, quantity);
|
||||
int updatedRows = productRepository.updateStock(productId, quantity);
|
||||
if (updatedRows == 0) {
|
||||
// 数据库更新失败,回滚Redis
|
||||
redisService.incrBy(stockKey, quantity);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 清除商品缓存,强制下次查询时重新加载
|
||||
String productCacheKey = PRODUCT_CACHE_PREFIX + productId;
|
||||
redisService.delete(productCacheKey);
|
||||
|
||||
// 发布库存变更消息
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("productId", productId);
|
||||
message.put("quantity", quantity);
|
||||
message.put("operation", operation);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
redisService.publish("stock:change", message);
|
||||
|
||||
log.info("商品库存更新成功: {}", productId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("更新商品库存失败: {}", productId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品库存(从Redis)
|
||||
*/
|
||||
public Integer getProductStock(Long productId) {
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
Object stock = redisService.get(stockKey);
|
||||
return stock != null ? Integer.valueOf(stock.toString()) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热商品库存到Redis
|
||||
*/
|
||||
public void preloadProductStock(Long productId) {
|
||||
Optional<Product> productOpt = productRepository.findById(productId);
|
||||
if (productOpt.isPresent()) {
|
||||
Product product = productOpt.get();
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
redisService.set(stockKey, product.getStock());
|
||||
log.info("商品库存预热完成: 商品ID={}, 库存={}", productId, product.getStock());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量预热商品库存
|
||||
*/
|
||||
public void batchPreloadProductStock(List<Long> productIds) {
|
||||
log.info("开始批量预热商品库存: {}", productIds);
|
||||
|
||||
for (Long productId : productIds) {
|
||||
preloadProductStock(productId);
|
||||
}
|
||||
|
||||
log.info("批量预热商品库存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加商品销量排行榜分数
|
||||
*/
|
||||
public void increaseSalesRank(Long productId, Integer quantity) {
|
||||
redisService.zIncrBy(PRODUCT_SALES_RANK, productId, quantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品销量排行榜
|
||||
*/
|
||||
public List<Map<String, Object>> getSalesRank(int limit) {
|
||||
Set<Object> topProducts = redisService.zRevRange(PRODUCT_SALES_RANK, 0, limit - 1);
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
int rank = 1;
|
||||
for (Object productId : topProducts) {
|
||||
ProductDTO product = getProductById(Long.valueOf(productId.toString()));
|
||||
if (product != null) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("rank", rank++);
|
||||
item.put("product", product);
|
||||
item.put("sales", redisService.zIncrBy(PRODUCT_SALES_RANK, productId, 0)); // 获取分数
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存商品信息
|
||||
*/
|
||||
private void cacheProductInfo(Product product) {
|
||||
String cacheKey = PRODUCT_CACHE_PREFIX + product.getId();
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("description", product.getDescription());
|
||||
productMap.put("price", product.getPrice().toString());
|
||||
productMap.put("stock", product.getStock().toString());
|
||||
productMap.put("imageUrl", product.getImageUrl());
|
||||
productMap.put("status", product.getStatus().toString());
|
||||
|
||||
redisService.hMSet(cacheKey, productMap);
|
||||
redisService.expire(cacheKey, productCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存构建ProductDTO
|
||||
*/
|
||||
private ProductDTO buildProductDTOFromCache(Long productId, Map<Object, Object> productMap) {
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
productDTO.setId(productId);
|
||||
productDTO.setName((String) productMap.get("name"));
|
||||
productDTO.setDescription((String) productMap.get("description"));
|
||||
productDTO.setPrice(new java.math.BigDecimal((String) productMap.get("price")));
|
||||
productDTO.setStock(Integer.valueOf((String) productMap.get("stock")));
|
||||
productDTO.setImageUrl((String) productMap.get("imageUrl"));
|
||||
productDTO.setStatus(Integer.valueOf((String) productMap.get("status")));
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建商品列表缓存键
|
||||
*/
|
||||
private String buildProductListCacheKey(ProductDTO.QueryDTO queryDTO) {
|
||||
return PRODUCT_LIST_CACHE_PREFIX +
|
||||
(queryDTO.getName() != null ? queryDTO.getName() : "all") + ":" +
|
||||
(queryDTO.getStatus() != null ? queryDTO.getStatus() : "1") + ":" +
|
||||
queryDTO.getPage() + ":" + queryDTO.getSize() + ":" +
|
||||
queryDTO.getSortBy() + ":" + queryDTO.getSortDirection();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除商品列表缓存
|
||||
*/
|
||||
private void clearProductListCache() {
|
||||
// 使用通配符删除所有商品列表缓存
|
||||
// 注意:这里简化处理,实际生产环境可能需要更精确的缓存管理
|
||||
redisService.delete(HOT_PRODUCTS_CACHE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*/
|
||||
@Transactional
|
||||
public boolean deleteProduct(Long productId) {
|
||||
log.info("删除商品: {}", productId);
|
||||
|
||||
try {
|
||||
// 删除数据库记录
|
||||
productRepository.deleteById(productId);
|
||||
|
||||
// 删除相关缓存
|
||||
String productCacheKey = PRODUCT_CACHE_PREFIX + productId;
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
redisService.delete(productCacheKey);
|
||||
redisService.delete(stockKey);
|
||||
|
||||
// 从销量排行榜中移除
|
||||
redisService.zRem(PRODUCT_SALES_RANK, productId);
|
||||
|
||||
// 清除商品列表缓存
|
||||
clearProductListCache();
|
||||
|
||||
log.info("商品删除成功: {}", productId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("删除商品失败: {}", productId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 限流服务
|
||||
* 使用Redis实现接口限流,防止恶意刷单
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RateLimitService {
|
||||
|
||||
private static final String RATE_LIMIT_PREFIX = "rate_limit:";
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Value("${flashsale.seckill.rate-limit.max-requests-per-minute:10}")
|
||||
private int maxRequestsPerMinute;
|
||||
|
||||
/**
|
||||
* 检查是否超过限流
|
||||
*
|
||||
* @param key 限流键(通常是用户ID+接口标识)
|
||||
* @param maxRequests 最大请求次数
|
||||
* @param timeWindow 时间窗口(秒)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean isAllowed(String key, int maxRequests, int timeWindow) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
// 获取当前计数
|
||||
Long currentCount = redisService.incr(rateLimitKey);
|
||||
|
||||
if (currentCount == 1) {
|
||||
// 第一次请求,设置过期时间
|
||||
redisService.expire(rateLimitKey, timeWindow, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
boolean allowed = currentCount <= maxRequests;
|
||||
|
||||
if (!allowed) {
|
||||
log.warn("请求被限流: key={}, 当前计数={}, 最大允许={}", key, currentCount, maxRequests);
|
||||
}
|
||||
|
||||
return allowed;
|
||||
} catch (Exception e) {
|
||||
log.error("限流检查异常: key={}", key, e);
|
||||
// 异常情况下允许请求,避免影响正常业务
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户秒杀接口限流
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkFlashSaleRateLimit(Long userId) {
|
||||
String key = "flashsale:" + userId;
|
||||
return isAllowed(key, maxRequestsPerMinute, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户登录接口限流
|
||||
*
|
||||
* @param identifier 标识符(用户名或IP)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkLoginRateLimit(String identifier) {
|
||||
String key = "login:" + identifier;
|
||||
return isAllowed(key, 5, 300); // 5分钟内最多5次登录尝试
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户注册接口限流
|
||||
*
|
||||
* @param ip IP地址
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkRegisterRateLimit(String ip) {
|
||||
String key = "register:" + ip;
|
||||
return isAllowed(key, 3, 3600); // 1小时内最多3次注册
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查购物车操作限流
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkCartOperationRateLimit(Long userId) {
|
||||
String key = "cart:" + userId;
|
||||
return isAllowed(key, 100, 60); // 1分钟内最多100次购物车操作
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余请求次数
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param maxRequests 最大请求次数
|
||||
*
|
||||
* @return 剩余请求次数
|
||||
*/
|
||||
public int getRemainingRequests(String key, int maxRequests) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
Object countObj = redisService.get(rateLimitKey);
|
||||
if (countObj == null) {
|
||||
return maxRequests;
|
||||
}
|
||||
|
||||
int currentCount = Integer.parseInt(countObj.toString());
|
||||
return Math.max(0, maxRequests - currentCount);
|
||||
} catch (Exception e) {
|
||||
log.error("获取剩余请求次数异常: key={}", key, e);
|
||||
return maxRequests;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取限流重置时间
|
||||
*
|
||||
* @param key 限流键
|
||||
*
|
||||
* @return 重置时间(秒)
|
||||
*/
|
||||
public long getResetTime(String key) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
Long expireTime = redisService.getExpire(rateLimitKey);
|
||||
return expireTime != null ? expireTime : 0;
|
||||
} catch (Exception e) {
|
||||
log.error("获取限流重置时间异常: key={}", key, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除限流记录
|
||||
*
|
||||
* @param key 限流键
|
||||
*/
|
||||
public void clearRateLimit(String key) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
redisService.delete(rateLimitKey);
|
||||
log.info("清除限流记录: key={}", key);
|
||||
} catch (Exception e) {
|
||||
log.error("清除限流记录异常: key={}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑动窗口限流
|
||||
* 使用ZSet实现更精确的滑动窗口限流
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param maxRequests 最大请求次数
|
||||
* @param timeWindow 时间窗口(秒)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean slidingWindowRateLimit(String key, int maxRequests, int timeWindow) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + "sliding:" + key;
|
||||
long now = System.currentTimeMillis();
|
||||
long windowStart = now - timeWindow * 1000L;
|
||||
|
||||
try {
|
||||
// 移除过期的记录
|
||||
redisService.zRem(rateLimitKey, 0, windowStart);
|
||||
|
||||
// 获取当前窗口内的请求数
|
||||
Long currentCount = redisService.zCard(rateLimitKey);
|
||||
|
||||
if (currentCount != null && currentCount >= maxRequests) {
|
||||
log.warn("滑动窗口限流: key={}, 当前计数={}, 最大允许={}", key, currentCount, maxRequests);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加当前请求
|
||||
redisService.zAdd(rateLimitKey, String.valueOf(now), now);
|
||||
|
||||
// 设置过期时间
|
||||
redisService.expire(rateLimitKey, timeWindow + 1, TimeUnit.SECONDS);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("滑动窗口限流异常: key={}", key, e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌桶限流
|
||||
* 使用Redis实现令牌桶算法
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param capacity 桶容量
|
||||
* @param refillRate 令牌补充速率(每秒)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean tokenBucketRateLimit(String key, int capacity, double refillRate) {
|
||||
String bucketKey = RATE_LIMIT_PREFIX + "bucket:" + key;
|
||||
String lastRefillKey = bucketKey + ":last_refill";
|
||||
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// 获取当前令牌数和上次补充时间
|
||||
Object tokensObj = redisService.get(bucketKey);
|
||||
Object lastRefillObj = redisService.get(lastRefillKey);
|
||||
|
||||
double tokens = tokensObj != null ? Double.parseDouble(tokensObj.toString()) : capacity;
|
||||
long lastRefill = lastRefillObj != null ? Long.parseLong(lastRefillObj.toString()) : now;
|
||||
|
||||
// 计算需要补充的令牌数
|
||||
double timePassed = (now - lastRefill) / 1000.0;
|
||||
double tokensToAdd = timePassed * refillRate;
|
||||
tokens = Math.min(capacity, tokens + tokensToAdd);
|
||||
|
||||
if (tokens >= 1) {
|
||||
// 消耗一个令牌
|
||||
tokens -= 1;
|
||||
|
||||
// 更新令牌数和时间
|
||||
redisService.set(bucketKey, String.valueOf(tokens), 3600, TimeUnit.SECONDS);
|
||||
redisService.set(lastRefillKey, String.valueOf(now), 3600, TimeUnit.SECONDS);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
log.warn("令牌桶限流: key={}, 当前令牌数={}", key, tokens);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("令牌桶限流异常: key={}", key, e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.SessionCallback;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis管道技术服务
|
||||
* 实现批量操作优化,提高性能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedisPipelineService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 批量设置键值对
|
||||
*/
|
||||
public void batchSet(Map<String, Object> keyValues) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<String, Object> entry : keyValues.entrySet()) {
|
||||
ops.opsForValue().set(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量设置完成,数量: {}", keyValues.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置键值对并指定过期时间
|
||||
*/
|
||||
public void batchSetWithExpire(Map<String, Object> keyValues, long timeout, TimeUnit unit) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<String, Object> entry : keyValues.entrySet()) {
|
||||
ops.opsForValue().set(entry.getKey(), entry.getValue(), timeout, unit);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量设置带过期时间完成,数量: {}", keyValues.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取值
|
||||
*/
|
||||
public List<Object> batchGet(List<String> keys) {
|
||||
List<Object> results = redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.opsForValue().get(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量获取完成,数量: {}", keys.size());
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除键
|
||||
*/
|
||||
public void batchDelete(List<String> keys) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.delete(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量删除完成,数量: {}", keys.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量Hash操作
|
||||
*/
|
||||
public void batchHashSet(String key, Map<String, Object> hash) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<String, Object> entry : hash.entrySet()) {
|
||||
ops.opsForHash().put(key, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量Hash设置完成,key: {}, 字段数量: {}", key, hash.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量List操作
|
||||
*/
|
||||
public void batchListPush(String key, List<Object> values) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Object value : values) {
|
||||
ops.opsForList().rightPush(key, value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量List推入完成,key: {}, 数量: {}", key, values.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量Set操作
|
||||
*/
|
||||
public void batchSetAdd(String key, List<Object> values) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Object value : values) {
|
||||
ops.opsForSet().add(key, value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量Set添加完成,key: {}, 数量: {}", key, values.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量ZSet操作
|
||||
*/
|
||||
public void batchZSetAdd(String key, Map<Object, Double> scoreMembers) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<Object, Double> entry : scoreMembers.entrySet()) {
|
||||
ops.opsForZSet().add(key, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量ZSet添加完成,key: {}, 数量: {}", key, scoreMembers.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量递增操作
|
||||
*/
|
||||
public List<Object> batchIncrement(List<String> keys, long delta) {
|
||||
List<Object> results = redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.opsForValue().increment(key, delta);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量递增完成,数量: {}, 增量: {}", keys.size(), delta);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置过期时间
|
||||
*/
|
||||
public void batchExpire(List<String> keys, long timeout, TimeUnit unit) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.expire(key, timeout, unit);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量设置过期时间完成,数量: {}", keys.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 复杂批量操作示例:用户数据预热
|
||||
*/
|
||||
public void preloadUserData(List<Map<String, Object>> userData) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map<String, Object> user : userData) {
|
||||
String userId = user.get("id").toString();
|
||||
String userKey = "user:" + userId;
|
||||
|
||||
// 设置用户基本信息
|
||||
ops.opsForHash().putAll(userKey, user);
|
||||
|
||||
// 设置过期时间
|
||||
ops.expire(userKey, 30, TimeUnit.MINUTES);
|
||||
|
||||
// 添加到在线用户集合
|
||||
ops.opsForSet().add("online_users", userId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("用户数据预热完成,数量: {}", userData.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 复杂批量操作示例:商品数据预热
|
||||
*/
|
||||
public void preloadProductData(List<Map<String, Object>> productData) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map<String, Object> product : productData) {
|
||||
String productId = product.get("id").toString();
|
||||
String productKey = "product:" + productId;
|
||||
String stockKey = "product_stock:" + productId;
|
||||
|
||||
// 设置商品信息
|
||||
ops.opsForHash().putAll(productKey, product);
|
||||
|
||||
// 设置库存
|
||||
ops.opsForValue().set(stockKey, product.get("stock"));
|
||||
|
||||
// 设置过期时间
|
||||
ops.expire(productKey, 60, TimeUnit.MINUTES);
|
||||
|
||||
// 添加到销量排行榜(初始分数为0)
|
||||
ops.opsForZSet().add("product_sales_rank", productId, 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("商品数据预热完成,数量: {}", productData.size());
|
||||
}
|
||||
}
|
||||
359
src/main/java/com/org/flashsalesystem/service/RedisService.java
Normal file
359
src/main/java/com/org/flashsalesystem/service/RedisService.java
Normal file
@@ -0,0 +1,359 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis服务类
|
||||
* 封装Redis的各种操作,包括五种数据类型的应用
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedisService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<Long> flashSaleScript;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<String> lockScript;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<Long> unlockScript;
|
||||
|
||||
// ========== String类型操作 ==========
|
||||
|
||||
/**
|
||||
* 设置字符串值
|
||||
*/
|
||||
public void set(String key, Object value) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符串值并指定过期时间
|
||||
*/
|
||||
public void set(String key, Object value, long timeout, TimeUnit unit) {
|
||||
redisTemplate.opsForValue().set(key, value, timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串值
|
||||
*/
|
||||
public Object get(String key) {
|
||||
return redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
public Long incr(String key) {
|
||||
return redisTemplate.opsForValue().increment(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增指定步长
|
||||
*/
|
||||
public Long incrBy(String key, long delta) {
|
||||
return redisTemplate.opsForValue().increment(key, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减
|
||||
*/
|
||||
public Long decr(String key) {
|
||||
return redisTemplate.opsForValue().decrement(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减指定步长
|
||||
*/
|
||||
public Long decrBy(String key, long delta) {
|
||||
return redisTemplate.opsForValue().decrement(key, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值,仅当键不存在时
|
||||
*/
|
||||
public Boolean setNx(String key, Object value) {
|
||||
return redisTemplate.opsForValue().setIfAbsent(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值和过期时间,仅当键不存在时
|
||||
*/
|
||||
public Boolean setNx(String key, Object value, long timeout, TimeUnit unit) {
|
||||
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
|
||||
}
|
||||
|
||||
// ========== Hash类型操作 ==========
|
||||
|
||||
/**
|
||||
* 设置Hash字段值
|
||||
*/
|
||||
public void hSet(String key, String field, Object value) {
|
||||
redisTemplate.opsForHash().put(key, field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hash字段值
|
||||
*/
|
||||
public Object hGet(String key, String field) {
|
||||
return redisTemplate.opsForHash().get(key, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置Hash字段
|
||||
*/
|
||||
public void hMSet(String key, Map<String, Object> map) {
|
||||
redisTemplate.opsForHash().putAll(key, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取Hash字段值
|
||||
*/
|
||||
public List<Object> hMGet(String key, Collection<Object> fields) {
|
||||
return redisTemplate.opsForHash().multiGet(key, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hash所有字段和值
|
||||
*/
|
||||
public Map<Object, Object> hGetAll(String key) {
|
||||
return redisTemplate.opsForHash().entries(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Hash字段
|
||||
*/
|
||||
public Long hDel(String key, Object... fields) {
|
||||
return redisTemplate.opsForHash().delete(key, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Hash字段是否存在
|
||||
*/
|
||||
public Boolean hExists(String key, String field) {
|
||||
return redisTemplate.opsForHash().hasKey(key, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash字段值递增
|
||||
*/
|
||||
public Long hIncrBy(String key, String field, long delta) {
|
||||
return redisTemplate.opsForHash().increment(key, field, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hash长度
|
||||
*/
|
||||
public Long hLen(String key) {
|
||||
return redisTemplate.opsForHash().size(key);
|
||||
}
|
||||
|
||||
// ========== List类型操作 ==========
|
||||
|
||||
/**
|
||||
* 从左侧推入元素
|
||||
*/
|
||||
public Long lPush(String key, Object... values) {
|
||||
return redisTemplate.opsForList().leftPushAll(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从右侧推入元素
|
||||
*/
|
||||
public Long rPush(String key, Object... values) {
|
||||
return redisTemplate.opsForList().rightPushAll(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从左侧弹出元素
|
||||
*/
|
||||
public Object lPop(String key) {
|
||||
return redisTemplate.opsForList().leftPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从右侧弹出元素
|
||||
*/
|
||||
public Object rPop(String key) {
|
||||
return redisTemplate.opsForList().rightPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表长度
|
||||
*/
|
||||
public Long lLen(String key) {
|
||||
return redisTemplate.opsForList().size(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表指定范围的元素
|
||||
*/
|
||||
public List<Object> lRange(String key, long start, long end) {
|
||||
return redisTemplate.opsForList().range(key, start, end);
|
||||
}
|
||||
|
||||
// ========== Set类型操作 ==========
|
||||
|
||||
/**
|
||||
* 添加元素到集合
|
||||
*/
|
||||
public Long sAdd(String key, Object... values) {
|
||||
return redisTemplate.opsForSet().add(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从集合中移除元素
|
||||
*/
|
||||
public Long sRem(String key, Object... values) {
|
||||
return redisTemplate.opsForSet().remove(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素是否在集合中
|
||||
*/
|
||||
public Boolean sIsMember(String key, Object value) {
|
||||
return redisTemplate.opsForSet().isMember(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合所有元素
|
||||
*/
|
||||
public Set<Object> sMembers(String key) {
|
||||
return redisTemplate.opsForSet().members(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合大小
|
||||
*/
|
||||
public Long sCard(String key) {
|
||||
return redisTemplate.opsForSet().size(key);
|
||||
}
|
||||
|
||||
// ========== ZSet类型操作 ==========
|
||||
|
||||
/**
|
||||
* 添加元素到有序集合
|
||||
*/
|
||||
public Boolean zAdd(String key, Object value, double score) {
|
||||
return redisTemplate.opsForZSet().add(key, value, score);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从有序集合中移除元素
|
||||
*/
|
||||
public Long zRem(String key, Object... values) {
|
||||
return redisTemplate.opsForZSet().remove(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合指定范围的元素(按分数排序)
|
||||
*/
|
||||
public Set<Object> zRange(String key, long start, long end) {
|
||||
return redisTemplate.opsForZSet().range(key, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合指定范围的元素(按分数倒序)
|
||||
*/
|
||||
public Set<Object> zRevRange(String key, long start, long end) {
|
||||
return redisTemplate.opsForZSet().reverseRange(key, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加有序集合元素的分数
|
||||
*/
|
||||
public Double zIncrBy(String key, Object value, double delta) {
|
||||
return redisTemplate.opsForZSet().incrementScore(key, value, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合大小
|
||||
*/
|
||||
public Long zCard(String key) {
|
||||
return redisTemplate.opsForZSet().zCard(key);
|
||||
}
|
||||
|
||||
// ========== 通用操作 ==========
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
*/
|
||||
public Boolean delete(String key) {
|
||||
return redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除键
|
||||
*/
|
||||
public Long delete(Collection<String> keys) {
|
||||
return redisTemplate.delete(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*/
|
||||
public Boolean exists(String key) {
|
||||
return redisTemplate.hasKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键的过期时间
|
||||
*/
|
||||
public Boolean expire(String key, long timeout, TimeUnit unit) {
|
||||
return redisTemplate.expire(key, timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键的剩余过期时间
|
||||
*/
|
||||
public Long getExpire(String key) {
|
||||
return redisTemplate.getExpire(key);
|
||||
}
|
||||
|
||||
// ========== Lua脚本操作 ==========
|
||||
|
||||
/**
|
||||
* 执行秒杀脚本
|
||||
*/
|
||||
public Long executeFlashSaleScript(String stockKey, int quantity) {
|
||||
return redisTemplate.execute(flashSaleScript, Collections.singletonList(stockKey), String.valueOf(quantity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行分布式锁脚本
|
||||
*/
|
||||
public String executeLockScript(String lockKey, String lockValue, int expireSeconds) {
|
||||
return redisTemplate.execute(lockScript, Collections.singletonList(lockKey), lockValue,
|
||||
String.valueOf(expireSeconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行释放锁脚本
|
||||
*/
|
||||
public Long executeUnlockScript(String lockKey, String lockValue) {
|
||||
return redisTemplate.execute(unlockScript, Collections.singletonList(lockKey), lockValue);
|
||||
}
|
||||
|
||||
// ========== 发布订阅操作 ==========
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*/
|
||||
public void publish(String channel, Object message) {
|
||||
redisTemplate.convertAndSend(channel, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于Redisson的分布式锁服务
|
||||
* 提供更简洁和强大的分布式锁功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedissonLockService {
|
||||
|
||||
@Autowired
|
||||
private RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
* 尝试获取锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param waitTime 等待时间(秒)
|
||||
* @param leaseTime 锁持有时间(秒)
|
||||
*
|
||||
* @return 是否获取成功
|
||||
*/
|
||||
public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
|
||||
if (acquired) {
|
||||
log.debug("成功获取分布式锁: {}", lockKey);
|
||||
} else {
|
||||
log.debug("获取分布式锁失败: {}", lockKey);
|
||||
}
|
||||
return acquired;
|
||||
} catch (InterruptedException e) {
|
||||
log.error("获取分布式锁被中断: {}", lockKey, e);
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁(使用默认时间)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否获取成功
|
||||
*/
|
||||
public boolean tryLock(String lockKey) {
|
||||
return tryLock(lockKey, 3, 30); // 等待3秒,持有30秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*/
|
||||
public void unlock(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
log.debug("成功释放分布式锁: {}", lockKey);
|
||||
} else {
|
||||
log.warn("尝试释放不属于当前线程的锁: {}", lockKey);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("释放分布式锁失败: {}", lockKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否被持有
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否被持有
|
||||
*/
|
||||
public boolean isLocked(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
return lock.isLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否被当前线程持有
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否被当前线程持有
|
||||
*/
|
||||
public boolean isHeldByCurrentThread(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
return lock.isHeldByCurrentThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁的剩余时间
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 剩余时间(毫秒),-1表示永不过期,-2表示锁不存在
|
||||
*/
|
||||
public long getRemainingTimeToLive(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
return lock.remainTimeToLive();
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否释放成功
|
||||
*/
|
||||
public boolean forceUnlock(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
boolean result = lock.forceUnlock();
|
||||
if (result) {
|
||||
log.info("强制释放分布式锁成功: {}", lockKey);
|
||||
} else {
|
||||
log.warn("强制释放分布式锁失败,锁可能不存在: {}", lockKey);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("强制释放分布式锁异常: {}", lockKey, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行带锁的操作
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param waitTime 等待时间(秒)
|
||||
* @param leaseTime 锁持有时间(秒)
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, long waitTime, long leaseTime, Runnable task) {
|
||||
if (tryLock(lockKey, waitTime, leaseTime)) {
|
||||
try {
|
||||
task.run();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("执行带锁操作失败: {}", lockKey, e);
|
||||
return false;
|
||||
} finally {
|
||||
unlock(lockKey);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行带锁的操作(使用默认时间)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, Runnable task) {
|
||||
return executeWithLock(lockKey, 3, 30, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可重入锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return RLock对象
|
||||
*/
|
||||
public RLock getLock(String lockKey) {
|
||||
return redissonClient.getLock(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公平锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 公平锁对象
|
||||
*/
|
||||
public RLock getFairLock(String lockKey) {
|
||||
return redissonClient.getFairLock(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取读写锁的读锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 读锁对象
|
||||
*/
|
||||
public RLock getReadLock(String lockKey) {
|
||||
return redissonClient.getReadWriteLock(lockKey).readLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取读写锁的写锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 写锁对象
|
||||
*/
|
||||
public RLock getWriteLock(String lockKey) {
|
||||
return redissonClient.getReadWriteLock(lockKey).writeLock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.*;
|
||||
import org.redisson.api.listener.MessageListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于Redisson的增强Redis服务
|
||||
* 提供更丰富的Redis操作功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedissonService {
|
||||
|
||||
@Autowired
|
||||
private RedissonClient redissonClient;
|
||||
|
||||
// ==================== 字符串操作 ====================
|
||||
|
||||
/**
|
||||
* 设置字符串值
|
||||
*/
|
||||
public void set(String key, Object value) {
|
||||
RBucket<Object> bucket = redissonClient.getBucket(key);
|
||||
bucket.set(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符串值并指定过期时间
|
||||
*/
|
||||
public void set(String key, Object value, long timeout, TimeUnit unit) {
|
||||
RBucket<Object> bucket = redissonClient.getBucket(key);
|
||||
bucket.set(value, timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符串值并指定过期时间(Duration)
|
||||
*/
|
||||
public void set(String key, Object value, Duration duration) {
|
||||
RBucket<Object> bucket = redissonClient.getBucket(key);
|
||||
bucket.set(value, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串值
|
||||
*/
|
||||
public <T> T get(String key) {
|
||||
RBucket<T> bucket = redissonClient.getBucket(key);
|
||||
return bucket.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
*/
|
||||
public boolean delete(String key) {
|
||||
return redissonClient.getBucket(key).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*/
|
||||
public boolean exists(String key) {
|
||||
return redissonClient.getBucket(key).isExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*/
|
||||
public boolean expire(String key, long timeout, TimeUnit unit) {
|
||||
return redissonClient.getBucket(key).expire(timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余过期时间
|
||||
*/
|
||||
public long getExpire(String key) {
|
||||
return redissonClient.getBucket(key).remainTimeToLive();
|
||||
}
|
||||
|
||||
// ==================== 哈希操作 ====================
|
||||
|
||||
/**
|
||||
* 设置哈希字段值
|
||||
*/
|
||||
public <T> T hSet(String key, String field, Object value) {
|
||||
RMap<String, Object> map = redissonClient.getMap(key);
|
||||
return (T) map.put(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希字段值
|
||||
*/
|
||||
public <T> T hGet(String key, String field) {
|
||||
RMap<String, T> map = redissonClient.getMap(key);
|
||||
return map.get(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除哈希字段
|
||||
*/
|
||||
public <T> T hDel(String key, String field) {
|
||||
RMap<String, T> map = redissonClient.getMap(key);
|
||||
return map.remove(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查哈希字段是否存在
|
||||
*/
|
||||
public boolean hExists(String key, String field) {
|
||||
RMap<String, Object> map = redissonClient.getMap(key);
|
||||
return map.containsKey(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希所有字段和值
|
||||
*/
|
||||
public <T> Map<String, T> hGetAll(String key) {
|
||||
RMap<String, T> map = redissonClient.getMap(key);
|
||||
return map.readAllMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置哈希字段
|
||||
*/
|
||||
public void hMSet(String key, Map<String, Object> hash) {
|
||||
RMap<String, Object> map = redissonClient.getMap(key);
|
||||
map.putAll(hash);
|
||||
}
|
||||
|
||||
// ==================== 列表操作 ====================
|
||||
|
||||
/**
|
||||
* 左侧推入列表
|
||||
*/
|
||||
public int lPush(String key, Object... values) {
|
||||
RList<Object> list = redissonClient.getList(key);
|
||||
for (Object value : values) {
|
||||
list.add(0, value);
|
||||
}
|
||||
return list.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 右侧推入列表
|
||||
*/
|
||||
public int rPush(String key, Object... values) {
|
||||
RList<Object> list = redissonClient.getList(key);
|
||||
Collections.addAll(list, values);
|
||||
return list.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 左侧弹出列表
|
||||
*/
|
||||
public <T> T lPop(String key) {
|
||||
RList<T> list = redissonClient.getList(key);
|
||||
return list.isEmpty() ? null : list.remove(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 右侧弹出列表
|
||||
*/
|
||||
public <T> T rPop(String key) {
|
||||
RList<T> list = redissonClient.getList(key);
|
||||
return list.isEmpty() ? null : list.remove(list.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表长度
|
||||
*/
|
||||
public int lLen(String key) {
|
||||
RList<Object> list = redissonClient.getList(key);
|
||||
return list.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表范围内的元素
|
||||
*/
|
||||
public <T> List<T> lRange(String key, int start, int end) {
|
||||
RList<T> list = redissonClient.getList(key);
|
||||
if (end == -1) {
|
||||
end = list.size() - 1;
|
||||
}
|
||||
return list.range(start, end);
|
||||
}
|
||||
|
||||
// ==================== 集合操作 ====================
|
||||
|
||||
/**
|
||||
* 添加集合成员
|
||||
*/
|
||||
public boolean sAdd(String key, Object... values) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
boolean result = false;
|
||||
for (Object value : values) {
|
||||
result |= set.add(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除集合成员
|
||||
*/
|
||||
public boolean sRem(String key, Object... values) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
boolean result = false;
|
||||
for (Object value : values) {
|
||||
result |= set.remove(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为集合成员
|
||||
*/
|
||||
public boolean sIsMember(String key, Object value) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
return set.contains(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合所有成员
|
||||
*/
|
||||
public <T> Set<T> sMembers(String key) {
|
||||
RSet<T> set = redissonClient.getSet(key);
|
||||
return set.readAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合大小
|
||||
*/
|
||||
public int sCard(String key) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
return set.size();
|
||||
}
|
||||
|
||||
// ==================== 有序集合操作 ====================
|
||||
|
||||
/**
|
||||
* 添加有序集合成员
|
||||
*/
|
||||
public boolean zAdd(String key, double score, Object member) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.add(score, member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除有序集合成员
|
||||
*/
|
||||
public boolean zRem(String key, Object... members) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
boolean result = false;
|
||||
for (Object member : members) {
|
||||
result |= sortedSet.remove(member);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合成员分数
|
||||
*/
|
||||
public Double zScore(String key, Object member) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.getScore(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合范围内的成员
|
||||
*/
|
||||
public <T> Collection<T> zRange(String key, int start, int end) {
|
||||
RScoredSortedSet<T> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.valueRange(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合大小
|
||||
*/
|
||||
public int zCard(String key) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.size();
|
||||
}
|
||||
|
||||
// ==================== 发布订阅 ====================
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*/
|
||||
public long publish(String channel, Object message) {
|
||||
RTopic topic = redissonClient.getTopic(channel);
|
||||
return topic.publish(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅消息
|
||||
*/
|
||||
public void subscribe(String channel, MessageListener<Object> listener) {
|
||||
RTopic topic = redissonClient.getTopic(channel);
|
||||
topic.addListener(Object.class, listener);
|
||||
}
|
||||
|
||||
// ==================== 原子操作 ====================
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
public long incr(String key) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增指定值
|
||||
*/
|
||||
public long incrBy(String key, long delta) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.addAndGet(delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减
|
||||
*/
|
||||
public long decr(String key) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.decrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减指定值
|
||||
*/
|
||||
public long decrBy(String key, long delta) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.addAndGet(-delta);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取Redisson客户端
|
||||
*/
|
||||
public RedissonClient getRedissonClient() {
|
||||
return redissonClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行Lua脚本
|
||||
*/
|
||||
public <T> T evalSha(String shaDigest, RScript.ReturnType returnType, List<Object> keys, Object... values) {
|
||||
RScript script = redissonClient.getScript();
|
||||
return script.evalSha(RScript.Mode.READ_WRITE, shaDigest, returnType, keys, values);
|
||||
}
|
||||
}
|
||||
314
src/main/java/com/org/flashsalesystem/service/UserService.java
Normal file
314
src/main/java/com/org/flashsalesystem/service/UserService.java
Normal file
@@ -0,0 +1,314 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.User;
|
||||
import com.org.flashsalesystem.repository.UserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 用户服务类
|
||||
* 实现用户注册、登录、信息管理等功能
|
||||
* 使用Redis缓存用户信息和会话管理
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserService {
|
||||
|
||||
private static final String USER_CACHE_PREFIX = "user:";
|
||||
private static final String USER_TOKEN_PREFIX = "user_token:";
|
||||
private static final String ONLINE_USERS_SET = "online_users";
|
||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Value("${flashsale.cache.user-expire-minutes:30}")
|
||||
private int userCacheExpireMinutes;
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@Transactional
|
||||
public UserDTO register(UserDTO.RegisterDTO registerDTO) {
|
||||
log.info("用户注册: {}", registerDTO.getUsername());
|
||||
|
||||
// 验证确认密码
|
||||
if (!registerDTO.getPassword().equals(registerDTO.getConfirmPassword())) {
|
||||
throw new RuntimeException("两次输入的密码不一致");
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (userRepository.existsByUsername(registerDTO.getUsername())) {
|
||||
throw new RuntimeException("用户名已存在");
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (registerDTO.getEmail() != null && userRepository.existsByEmail(registerDTO.getEmail())) {
|
||||
throw new RuntimeException("邮箱已被注册");
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (registerDTO.getPhone() != null && userRepository.existsByPhone(registerDTO.getPhone())) {
|
||||
throw new RuntimeException("手机号已被注册");
|
||||
}
|
||||
|
||||
// 创建用户实体
|
||||
User user = new User();
|
||||
user.setUsername(registerDTO.getUsername());
|
||||
user.setPassword(encryptPassword(registerDTO.getPassword()));
|
||||
user.setEmail(registerDTO.getEmail());
|
||||
user.setPhone(registerDTO.getPhone());
|
||||
|
||||
// 保存到数据库
|
||||
user = userRepository.save(user);
|
||||
|
||||
// 缓存用户信息
|
||||
cacheUserInfo(user);
|
||||
|
||||
// 转换为DTO返回
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null); // 不返回密码
|
||||
|
||||
log.info("用户注册成功: {}, ID: {}", user.getUsername(), user.getId());
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
public Map<String, Object> login(UserDTO.LoginDTO loginDTO) {
|
||||
log.info("用户登录: {}", loginDTO.getUsername());
|
||||
|
||||
// 先根据用户名查找用户
|
||||
Optional<User> userOpt = userRepository.findByUsername(loginDTO.getUsername());
|
||||
if (!userOpt.isPresent()) {
|
||||
throw new RuntimeException("用户名或密码错误");
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// 验证密码
|
||||
if (!verifyPassword(loginDTO.getPassword(), user.getPassword())) {
|
||||
throw new RuntimeException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 生成token
|
||||
String token = generateToken();
|
||||
|
||||
// 缓存用户信息和token
|
||||
cacheUserInfo(user);
|
||||
cacheUserToken(token, user.getId());
|
||||
|
||||
// 添加到在线用户集合
|
||||
redisService.sAdd(ONLINE_USERS_SET, user.getId());
|
||||
|
||||
// 转换为DTO
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("token", token);
|
||||
result.put("user", userDTO);
|
||||
|
||||
log.info("用户登录成功: {}, ID: {}", user.getUsername(), user.getId());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
public void logout(String token) {
|
||||
if (token == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
Object userIdObj = redisService.get(USER_TOKEN_PREFIX + token);
|
||||
if (userIdObj != null) {
|
||||
Long userId = Long.valueOf(userIdObj.toString());
|
||||
|
||||
// 从在线用户集合中移除
|
||||
redisService.sRem(ONLINE_USERS_SET, userId);
|
||||
|
||||
log.info("用户登出: {}", userId);
|
||||
}
|
||||
|
||||
// 删除token
|
||||
redisService.delete(USER_TOKEN_PREFIX + token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据token获取用户信息
|
||||
*/
|
||||
public UserDTO getUserByToken(String token) {
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从Redis获取用户ID
|
||||
Object userIdObj = redisService.get(USER_TOKEN_PREFIX + token);
|
||||
if (userIdObj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long userId = Long.valueOf(userIdObj.toString());
|
||||
return getUserById(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息
|
||||
*/
|
||||
public UserDTO getUserById(Long userId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从缓存获取
|
||||
String cacheKey = USER_CACHE_PREFIX + userId;
|
||||
Map<Object, Object> userMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!userMap.isEmpty()) {
|
||||
// 从缓存构造用户对象
|
||||
UserDTO userDTO = new UserDTO();
|
||||
userDTO.setId(userId);
|
||||
userDTO.setUsername((String) userMap.get("username"));
|
||||
userDTO.setEmail((String) userMap.get("email"));
|
||||
userDTO.setPhone((String) userMap.get("phone"));
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (!userOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// 缓存用户信息
|
||||
cacheUserInfo(user);
|
||||
|
||||
// 转换为DTO
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null);
|
||||
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
@Transactional
|
||||
public UserDTO updateUser(Long userId, UserDTO.UpdateDTO updateDTO) {
|
||||
log.info("更新用户信息: {}", userId);
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (!userOpt.isPresent()) {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// 检查邮箱是否被其他用户使用
|
||||
if (updateDTO.getEmail() != null && !updateDTO.getEmail().equals(user.getEmail())) {
|
||||
if (userRepository.existsByEmail(updateDTO.getEmail())) {
|
||||
throw new RuntimeException("邮箱已被其他用户使用");
|
||||
}
|
||||
user.setEmail(updateDTO.getEmail());
|
||||
}
|
||||
|
||||
// 检查手机号是否被其他用户使用
|
||||
if (updateDTO.getPhone() != null && !updateDTO.getPhone().equals(user.getPhone())) {
|
||||
if (userRepository.existsByPhone(updateDTO.getPhone())) {
|
||||
throw new RuntimeException("手机号已被其他用户使用");
|
||||
}
|
||||
user.setPhone(updateDTO.getPhone());
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
user = userRepository.save(user);
|
||||
|
||||
// 更新缓存
|
||||
cacheUserInfo(user);
|
||||
|
||||
// 转换为DTO
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null);
|
||||
|
||||
log.info("用户信息更新成功: {}", userId);
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否在线
|
||||
*/
|
||||
public boolean isUserOnline(Long userId) {
|
||||
return redisService.sIsMember(ONLINE_USERS_SET, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户数量
|
||||
*/
|
||||
public long getOnlineUserCount() {
|
||||
return redisService.sCard(ONLINE_USERS_SET);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存用户信息
|
||||
*/
|
||||
private void cacheUserInfo(User user) {
|
||||
String cacheKey = USER_CACHE_PREFIX + user.getId();
|
||||
Map<String, Object> userMap = new HashMap<>();
|
||||
userMap.put("username", user.getUsername());
|
||||
userMap.put("email", user.getEmail());
|
||||
userMap.put("phone", user.getPhone());
|
||||
|
||||
redisService.hMSet(cacheKey, userMap);
|
||||
redisService.expire(cacheKey, userCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存用户token
|
||||
*/
|
||||
private void cacheUserToken(String token, Long userId) {
|
||||
String tokenKey = USER_TOKEN_PREFIX + token;
|
||||
redisService.set(tokenKey, userId, userCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成token
|
||||
*/
|
||||
private String generateToken() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
*/
|
||||
private String encryptPassword(String password) {
|
||||
return passwordEncoder.encode(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
private boolean verifyPassword(String rawPassword, String encodedPassword) {
|
||||
return passwordEncoder.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user