修复文件
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.CartDTO;
|
||||
import com.org.flashsalesystem.dto.OrderDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.CartService;
|
||||
import com.org.flashsalesystem.service.OrderService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -29,6 +31,9 @@ public class CartController {
|
||||
@Autowired
|
||||
private CartService cartService;
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@@ -66,7 +71,7 @@ public class CartController {
|
||||
/**
|
||||
* 更新购物车商品数量
|
||||
*/
|
||||
@PutMapping("/update-quantity")
|
||||
@PutMapping("/update")
|
||||
public ResponseEntity<Map<String, Object>> updateQuantity(@Validated @RequestBody CartDTO.UpdateQuantityDTO updateDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
@@ -125,6 +130,43 @@ public class CartController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除购物车商品
|
||||
*/
|
||||
@DeleteMapping("/batch-remove")
|
||||
public ResponseEntity<Map<String, Object>> batchRemove(@RequestBody Map<String, Object> requestBody,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Integer> productIdInts = (java.util.List<Integer>) requestBody.get("productIds");
|
||||
java.util.List<Long> productIds = productIdInts.stream()
|
||||
.map(Long::valueOf)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
CartDTO cart = cartService.batchRemove(userId, productIds);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "批量删除商品成功");
|
||||
response.put("data", cart);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("批量删除购物车商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作购物车
|
||||
*/
|
||||
@@ -315,6 +357,47 @@ public class CartController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车下单
|
||||
*/
|
||||
@PostMapping("/checkout")
|
||||
public ResponseEntity<Map<String, Object>> checkoutCart(@RequestBody(required = false) Map<String, Object> requestBody,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
// 获取要下单的商品IDs,如果为空则下单所有商品
|
||||
java.util.List<Long> productIds = null;
|
||||
if (requestBody != null && requestBody.containsKey("productIds")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Integer> productIdInts = (java.util.List<Integer>) requestBody.get("productIds");
|
||||
productIds = productIdInts.stream()
|
||||
.map(Long::valueOf)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
OrderDTO order = cartService.checkoutCart(userId, productIds);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "下单成功,请及时支付");
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("购物车下单失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*/
|
||||
|
||||
@@ -182,6 +182,30 @@ public class FlashSaleController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热所有秒杀活动库存(管理员功能)
|
||||
*/
|
||||
@PostMapping("/admin/preload-all")
|
||||
public ResponseEntity<Map<String, Object>> preloadAllFlashSales() {
|
||||
try {
|
||||
flashSaleService.preloadAllActiveFlashSales();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "所有秒杀活动库存预热完成");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("预热所有秒杀活动库存失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "预热失败: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动
|
||||
*/
|
||||
@@ -319,6 +343,110 @@ public class FlashSaleController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布秒杀活动
|
||||
*/
|
||||
@Operation(summary = "发布秒杀活动", description = "将秒杀活动状态设置为可参与")
|
||||
@PostMapping("/{id}/publish")
|
||||
public ResponseEntity<Map<String, Object>> publishFlashSale(@Parameter(description = "秒杀活动ID", required = true) @PathVariable Long id) {
|
||||
try {
|
||||
FlashSaleDTO flashSale = flashSaleService.publishFlashSale(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动发布成功");
|
||||
response.put("data", flashSale);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("发布秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停秒杀活动
|
||||
*/
|
||||
@Operation(summary = "暂停秒杀活动", description = "暂停正在进行的秒杀活动")
|
||||
@PostMapping("/{id}/pause")
|
||||
public ResponseEntity<Map<String, Object>> pauseFlashSale(@Parameter(description = "秒杀活动ID", required = true) @PathVariable Long id) {
|
||||
try {
|
||||
FlashSaleDTO flashSale = flashSaleService.pauseFlashSale(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动暂停成功");
|
||||
response.put("data", flashSale);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("暂停秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复秒杀活动
|
||||
*/
|
||||
@Operation(summary = "恢复秒杀活动", description = "恢复已暂停的秒杀活动")
|
||||
@PostMapping("/{id}/resume")
|
||||
public ResponseEntity<Map<String, Object>> resumeFlashSale(@Parameter(description = "秒杀活动ID", required = true) @PathVariable Long id) {
|
||||
try {
|
||||
FlashSaleDTO flashSale = flashSaleService.resumeFlashSale(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动恢复成功");
|
||||
response.put("data", flashSale);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("恢复秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束秒杀活动
|
||||
*/
|
||||
@Operation(summary = "结束秒杀活动", description = "提前结束秒杀活动")
|
||||
@PostMapping("/{id}/end")
|
||||
public ResponseEntity<Map<String, Object>> endFlashSale(@Parameter(description = "秒杀活动ID", required = true) @PathVariable Long id) {
|
||||
try {
|
||||
FlashSaleDTO flashSale = flashSaleService.endFlashSale(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动结束成功");
|
||||
response.put("data", flashSale);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("结束秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀压力测试接口
|
||||
*/
|
||||
|
||||
@@ -275,25 +275,129 @@ public class OrderController {
|
||||
* 支付订单(模拟)
|
||||
*/
|
||||
@PostMapping("/{id}/pay")
|
||||
public ResponseEntity<Map<String, Object>> payOrder(@PathVariable Long id, HttpServletRequest request) {
|
||||
public ResponseEntity<Map<String, Object>> payOrder(@PathVariable Long id,
|
||||
@RequestBody(required = false) Map<String, Object> requestBody,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
// 这里可以集成真实的支付接口
|
||||
// 目前只是简单地更新订单状态为已支付
|
||||
OrderDTO order = orderService.updateOrderStatus(id, 2, "用户支付");
|
||||
// 验证订单归属
|
||||
OrderDTO order = orderService.getOrderById(id);
|
||||
if (order == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "订单不存在");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!order.getUserId().equals(userId)) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "无权限操作此订单");
|
||||
return ResponseEntity.status(403).body(response);
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (order.getStatus() != 1) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "订单状态不正确,无法支付");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
// 模拟支付处理时间(可选)
|
||||
String paymentMethod = "default";
|
||||
if (requestBody != null && requestBody.containsKey("paymentMethod")) {
|
||||
paymentMethod = (String) requestBody.get("paymentMethod");
|
||||
}
|
||||
|
||||
// 模拟支付成功(在实际项目中这里会调用支付接口)
|
||||
boolean paymentSuccess = simulatePayment(order, paymentMethod);
|
||||
|
||||
if (paymentSuccess) {
|
||||
// 更新订单状态为已支付
|
||||
OrderDTO updatedOrder = orderService.updateOrderStatus(id, 2, "模拟支付成功 - " + paymentMethod);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付成功");
|
||||
response.put("data", updatedOrder);
|
||||
response.put("paymentMethod", paymentMethod);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} else {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "支付失败,请重试");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("支付订单失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟支付处理
|
||||
*/
|
||||
private boolean simulatePayment(OrderDTO order, String paymentMethod) {
|
||||
try {
|
||||
log.info("模拟支付处理: 订单ID={}, 金额={}, 支付方式={}",
|
||||
order.getId(), order.getTotalPrice(), paymentMethod);
|
||||
|
||||
// 模拟支付处理时间
|
||||
Thread.sleep(1000); // 1秒延迟模拟网络请求
|
||||
|
||||
// 99%的支付成功率(模拟)
|
||||
return Math.random() > 0.01;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟发货
|
||||
*/
|
||||
@PostMapping("/{id}/ship")
|
||||
public ResponseEntity<Map<String, Object>> shipOrder(@PathVariable Long id) {
|
||||
try {
|
||||
// 验证订单状态是否为已支付
|
||||
OrderDTO order = orderService.getOrderById(id);
|
||||
if (order == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "订单不存在");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (order.getStatus() != 2) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "订单状态不正确,无法发货");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
// 更新订单状态为已发货
|
||||
OrderDTO updatedOrder = orderService.updateOrderStatus(id, 3, "商家发货");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付成功");
|
||||
response.put("data", order);
|
||||
response.put("message", "订单发货成功");
|
||||
response.put("data", updatedOrder);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("支付订单失败", e);
|
||||
log.error("订单发货失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
@@ -314,12 +418,36 @@ public class OrderController {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
OrderDTO order = orderService.updateOrderStatus(id, 4, "用户确认收货");
|
||||
// 验证订单归属和状态
|
||||
OrderDTO order = orderService.getOrderById(id);
|
||||
if (order == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "订单不存在");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!order.getUserId().equals(userId)) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "无权限操作此订单");
|
||||
return ResponseEntity.status(403).body(response);
|
||||
}
|
||||
|
||||
if (order.getStatus() != 3) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "订单状态不正确,无法确认收货");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
// 更新订单状态为已完成
|
||||
OrderDTO updatedOrder = orderService.updateOrderStatus(id, 4, "用户确认收货");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "确认收货成功");
|
||||
response.put("data", order);
|
||||
response.put("message", "确认收货成功,订单已完成");
|
||||
response.put("data", updatedOrder);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -248,6 +248,93 @@ public class PageController {
|
||||
return "about";
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索页面
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public String search(Model model, @RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) String category) {
|
||||
model.addAttribute("pageTitle", "搜索结果");
|
||||
model.addAttribute("keyword", q);
|
||||
model.addAttribute("category", category);
|
||||
return "search";
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类页面
|
||||
*/
|
||||
@GetMapping("/category/{id}")
|
||||
public String category(@PathVariable Long id, Model model) {
|
||||
model.addAttribute("pageTitle", "商品分类");
|
||||
model.addAttribute("categoryId", id);
|
||||
return "category";
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏夹页面
|
||||
*/
|
||||
@GetMapping("/favorites")
|
||||
public String favorites(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/favorites";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "我的收藏");
|
||||
return "favorites";
|
||||
}
|
||||
|
||||
/**
|
||||
* 地址管理页面
|
||||
*/
|
||||
@GetMapping("/addresses")
|
||||
public String addresses(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/addresses";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "地址管理");
|
||||
return "addresses";
|
||||
}
|
||||
|
||||
/**
|
||||
* 优惠券页面
|
||||
*/
|
||||
@GetMapping("/coupons")
|
||||
public String coupons(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/coupons";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "我的优惠券");
|
||||
return "coupons";
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计报表页面
|
||||
*/
|
||||
@GetMapping("/admin/reports")
|
||||
public String adminReports(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin/reports";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "统计报表");
|
||||
return "admin/reports";
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统设置页面
|
||||
*/
|
||||
@GetMapping("/admin/settings")
|
||||
public String adminSettings(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin/settings";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "系统设置");
|
||||
return "admin/settings";
|
||||
}
|
||||
|
||||
/**
|
||||
* 404错误页面
|
||||
*/
|
||||
|
||||
@@ -97,7 +97,45 @@ public class ProductController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
* 获取商品列表(GET方法,用于页面展示)
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getProductListGet(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "12") int size,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(defaultValue = "id") String sortBy,
|
||||
@RequestParam(defaultValue = "desc") String sortDirection) {
|
||||
try {
|
||||
ProductDTO.QueryDTO queryDTO = new ProductDTO.QueryDTO();
|
||||
queryDTO.setPage(page);
|
||||
queryDTO.setSize(size);
|
||||
queryDTO.setKeyword(keyword);
|
||||
queryDTO.setCategory(category);
|
||||
queryDTO.setSortBy(sortBy);
|
||||
queryDTO.setSortDirection(sortDirection);
|
||||
|
||||
Map<String, Object> result = productService.getProductList(queryDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表(POST方法,用于复杂查询)
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getProductList(@RequestBody ProductDTO.QueryDTO queryDTO) {
|
||||
|
||||
@@ -37,14 +37,25 @@ public class ProductDTO {
|
||||
@DecimalMin(value = "0.01", message = "商品价格必须大于0")
|
||||
private BigDecimal price;
|
||||
|
||||
@Schema(description = "商品原价", example = "9999.00")
|
||||
private BigDecimal originalPrice;
|
||||
|
||||
@Schema(description = "商品分类", example = "electronics")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "库存数量", example = "100")
|
||||
@Min(value = 0, message = "库存不能为负数")
|
||||
private Integer stock;
|
||||
|
||||
@Schema(description = "商品图片URL")
|
||||
private String imageUrl;
|
||||
|
||||
@Schema(description = "商品状态:1-上架,0-下架")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "销量")
|
||||
private Integer sales;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@@ -96,6 +107,8 @@ public class ProductDTO {
|
||||
@AllArgsConstructor
|
||||
public static class QueryDTO {
|
||||
private String name;
|
||||
private String keyword;
|
||||
private String category;
|
||||
private BigDecimal minPrice;
|
||||
private BigDecimal maxPrice;
|
||||
private Integer status;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.CartDTO;
|
||||
import com.org.flashsalesystem.dto.OrderDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -29,6 +30,8 @@ public class CartService {
|
||||
private RedisService redisService;
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
@Value("${flashsale.cart.expire-days:7}")
|
||||
private int cartExpireDays;
|
||||
@Value("${flashsale.cart.max-items:20}")
|
||||
@@ -311,4 +314,83 @@ public class CartService {
|
||||
String cartKey = buildCartKey(userId);
|
||||
redisService.expire(cartKey, cartExpireDays, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车下单
|
||||
*/
|
||||
public OrderDTO checkoutCart(Long userId, List<Long> productIds) {
|
||||
log.info("购物车下单: 用户ID={}, 商品IDs={}", userId, productIds);
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
CartDTO cart = getCart(userId);
|
||||
|
||||
if (cart.getItems().isEmpty()) {
|
||||
throw new RuntimeException("购物车为空,无法下单");
|
||||
}
|
||||
|
||||
// 过滤要下单的商品
|
||||
List<CartDTO.CartItemDTO> itemsToOrder;
|
||||
if (productIds != null && !productIds.isEmpty()) {
|
||||
itemsToOrder = cart.getItems().stream()
|
||||
.filter(item -> productIds.contains(item.getProductId()))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
} else {
|
||||
itemsToOrder = cart.getItems();
|
||||
}
|
||||
|
||||
if (itemsToOrder.isEmpty()) {
|
||||
throw new RuntimeException("没有选择要下单的商品");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
for (CartDTO.CartItemDTO item : itemsToOrder) {
|
||||
Integer currentStock = productService.getProductStock(item.getProductId());
|
||||
if (currentStock < item.getQuantity()) {
|
||||
throw new RuntimeException("商品 " + item.getProductName() + " 库存不足,当前库存:" + currentStock);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算订单总价
|
||||
BigDecimal orderTotalPrice = itemsToOrder.stream()
|
||||
.map(CartDTO.CartItemDTO::getSubtotal)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 根据商品数量创建订单
|
||||
if (itemsToOrder.size() == 1) {
|
||||
// 单商品订单
|
||||
CartDTO.CartItemDTO item = itemsToOrder.get(0);
|
||||
OrderDTO.CreateDTO createDTO = new OrderDTO.CreateDTO();
|
||||
createDTO.setProductId(item.getProductId());
|
||||
createDTO.setQuantity(item.getQuantity());
|
||||
|
||||
OrderDTO order = orderService.createOrder(userId, createDTO);
|
||||
|
||||
// 从购物车中移除已下单的商品
|
||||
redisService.hDel(cartKey, item.getProductId().toString());
|
||||
|
||||
log.info("单商品购物车下单成功: 用户ID={}, 订单ID={}", userId, order.getId());
|
||||
return order;
|
||||
} else {
|
||||
// 多商品订单 - 创建多个订单
|
||||
List<OrderDTO> orders = new ArrayList<>();
|
||||
for (CartDTO.CartItemDTO item : itemsToOrder) {
|
||||
OrderDTO.CreateDTO createDTO = new OrderDTO.CreateDTO();
|
||||
createDTO.setProductId(item.getProductId());
|
||||
createDTO.setQuantity(item.getQuantity());
|
||||
|
||||
OrderDTO order = orderService.createOrder(userId, createDTO);
|
||||
orders.add(order);
|
||||
|
||||
// 从购物车中移除已下单的商品
|
||||
redisService.hDel(cartKey, item.getProductId().toString());
|
||||
}
|
||||
|
||||
log.info("多商品购物车下单成功: 用户ID={}, 订单数量={}", userId, orders.size());
|
||||
|
||||
// 返回第一个订单作为代表(实际项目中可能需要创建主订单)
|
||||
OrderDTO firstOrder = orders.get(0);
|
||||
firstOrder.setTotalPrice(orderTotalPrice); // 设置总价为所有订单的总和
|
||||
return firstOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +106,10 @@ public class FlashSaleService {
|
||||
// 缓存秒杀活动信息
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 预热库存到Redis
|
||||
// 预热库存到Redis - 确保存储为数字类型
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
redisService.delete(stockKey); // 先删除可能存在的异常数据
|
||||
redisService.setString(stockKey, flashSale.getFlashStock().toString()); // 确保存储为字符串数字
|
||||
|
||||
log.info("秒杀活动创建成功: ID={}", flashSale.getId());
|
||||
|
||||
@@ -163,15 +164,42 @@ public class FlashSaleService {
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用Lua脚本原子性扣减库存
|
||||
// 检查并修复库存数据
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
String currentStock = redisService.getString(stockKey);
|
||||
log.info("秒杀前库存检查: flashSaleId={}, stockKey={}, currentStock={}",
|
||||
flashSale.getId(), stockKey, currentStock);
|
||||
|
||||
if (currentStock == null || currentStock.trim().isEmpty()) {
|
||||
log.warn("检测到库存数据异常,尝试修复: flashSaleId={}", flashSale.getId());
|
||||
repairFlashSaleStock(flashSale.getId());
|
||||
|
||||
// 修复后重新获取库存
|
||||
currentStock = redisService.getString(stockKey);
|
||||
log.info("修复后库存: flashSaleId={}, stockKey={}, currentStock={}",
|
||||
flashSale.getId(), stockKey, currentStock);
|
||||
}
|
||||
|
||||
// 使用Lua脚本原子性扣减库存
|
||||
log.info("准备执行秒杀脚本: stockKey={}, quantity={}, userId={}",
|
||||
stockKey, participateDTO.getQuantity(), userId);
|
||||
Long remainingStock = redisService.executeFlashSaleScript(stockKey, participateDTO.getQuantity());
|
||||
log.info("秒杀脚本执行完成: stockKey={}, remainingStock={}", stockKey, remainingStock);
|
||||
|
||||
if (remainingStock < 0) {
|
||||
if (remainingStock == -1) {
|
||||
return createFailResult("秒杀活动库存信息异常");
|
||||
} else {
|
||||
log.warn("秒杀库存key不存在或数据异常: flashSaleId={}, stockKey={}", flashSale.getId(), stockKey);
|
||||
return createFailResult("秒杀活动库存信息异常,请刷新页面重试");
|
||||
} else if (remainingStock == -2) {
|
||||
log.info("秒杀库存不足: flashSaleId={}, 剩余库存不足", flashSale.getId());
|
||||
return createFailResult("商品已售罄");
|
||||
} else if (remainingStock == -3) {
|
||||
log.error("秒杀参数异常: flashSaleId={}, quantity={}", flashSale.getId(),
|
||||
participateDTO.getQuantity());
|
||||
return createFailResult("参数异常,请检查购买数量");
|
||||
} else {
|
||||
log.error("秒杀脚本执行异常: flashSaleId={}, returnValue={}", flashSale.getId(), remainingStock);
|
||||
return createFailResult("系统异常,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,13 +373,97 @@ public class FlashSaleService {
|
||||
// 缓存秒杀活动信息
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 预热库存
|
||||
// 预热库存 - 确保存储为数字类型
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
redisService.delete(stockKey); // 先删除可能存在的异常数据
|
||||
redisService.setString(stockKey, flashSale.getFlashStock().toString()); // 确保存储为字符串数字
|
||||
|
||||
log.info("秒杀活动预热完成: {}", flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复Redis中的库存数据
|
||||
*/
|
||||
public void repairFlashSaleStock(Long flashSaleId) {
|
||||
log.info("修复秒杀活动库存数据: {}", flashSaleId);
|
||||
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
log.warn("秒杀活动不存在: {}", flashSaleId);
|
||||
return;
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
|
||||
// 获取当前Redis中的库存值
|
||||
String currentStock = redisService.getString(stockKey);
|
||||
log.info("当前Redis库存值: key={}, value={}", stockKey, currentStock);
|
||||
|
||||
// 如果库存值无效,则重新设置
|
||||
try {
|
||||
if (currentStock == null || currentStock.trim().isEmpty()) {
|
||||
log.warn("库存值为空,重新设置: key={}, resetTo={}", stockKey, flashSale.getFlashStock());
|
||||
redisService.setString(stockKey, flashSale.getFlashStock().toString());
|
||||
|
||||
// 验证设置是否成功
|
||||
String verifyStock = redisService.getString(stockKey);
|
||||
log.info("库存设置验证: key={}, setTo={}, actualValue={}",
|
||||
stockKey, flashSale.getFlashStock(), verifyStock);
|
||||
} else {
|
||||
Integer stockNumber = Integer.parseInt(currentStock.trim());
|
||||
if (stockNumber < 0 || stockNumber > flashSale.getFlashStock()) {
|
||||
log.warn("库存值异常,重新设置: key={}, currentValue={}, resetTo={}",
|
||||
stockKey, currentStock, flashSale.getFlashStock());
|
||||
redisService.setString(stockKey, flashSale.getFlashStock().toString());
|
||||
|
||||
// 验证设置是否成功
|
||||
String verifyStock = redisService.getString(stockKey);
|
||||
log.info("异常库存修复验证: key={}, setTo={}, actualValue={}",
|
||||
stockKey, flashSale.getFlashStock(), verifyStock);
|
||||
} else {
|
||||
log.info("库存值正常: key={}, value={}", stockKey, stockNumber);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("库存值格式异常,重新设置: key={}, currentValue={}", stockKey, currentStock, e);
|
||||
redisService.delete(stockKey);
|
||||
redisService.setString(stockKey, flashSale.getFlashStock().toString());
|
||||
|
||||
// 验证设置是否成功
|
||||
String verifyStock = redisService.getString(stockKey);
|
||||
log.info("格式异常修复验证: key={}, setTo={}, actualValue={}",
|
||||
stockKey, flashSale.getFlashStock(), verifyStock);
|
||||
}
|
||||
|
||||
log.info("库存数据修复完成: {}", flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新预热所有活跃的秒杀活动库存
|
||||
*/
|
||||
public void preloadAllActiveFlashSales() {
|
||||
log.info("开始预热所有活跃秒杀活动库存");
|
||||
|
||||
List<FlashSale> activeFlashSales = flashSaleRepository.findAll();
|
||||
|
||||
for (FlashSale flashSale : activeFlashSales) {
|
||||
try {
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
|
||||
// 使用set方法先存储,让系统能识别
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
|
||||
log.info("预热秒杀活动库存: flashSaleId={}, stock={}",
|
||||
flashSale.getId(), flashSale.getFlashStock());
|
||||
} catch (Exception e) {
|
||||
log.error("预热秒杀活动库存失败: flashSaleId={}", flashSale.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("所有活跃秒杀活动库存预热完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动剩余库存
|
||||
*/
|
||||
@@ -458,6 +570,165 @@ public class FlashSaleService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO publishFlashSale(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("只有未开始的秒杀活动才能发布");
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (flashSale.getStartTime().isBefore(now)) {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
if (flashSale.getStartTime().isAfter(flashSale.getEndTime())) {
|
||||
throw new RuntimeException("开始时间不能晚于结束时间");
|
||||
}
|
||||
|
||||
// 验证商品存在
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
if (product == null) {
|
||||
throw new RuntimeException("关联商品不存在");
|
||||
}
|
||||
|
||||
// 验证库存
|
||||
if (flashSale.getFlashStock() <= 0) {
|
||||
throw new RuntimeException("秒杀库存必须大于0");
|
||||
}
|
||||
|
||||
// 预热缓存
|
||||
preloadFlashSale(flashSaleId);
|
||||
|
||||
log.info("秒杀活动发布成功: ID={}", flashSaleId);
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO pauseFlashSale(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() != 2) {
|
||||
throw new RuntimeException("只有进行中的秒杀活动才能暂停");
|
||||
}
|
||||
|
||||
// 更新状态为暂停 (status = 4)
|
||||
flashSaleRepository.updateStatus(flashSaleId, 4);
|
||||
flashSale.setStatus(4);
|
||||
|
||||
// 更新缓存
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
log.info("秒杀活动暂停成功: ID={}", flashSaleId);
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO resumeFlashSale(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() != 4) {
|
||||
throw new RuntimeException("只有暂停的秒杀活动才能恢复");
|
||||
}
|
||||
|
||||
// 检查是否已结束
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (flashSale.getEndTime().isBefore(now)) {
|
||||
throw new RuntimeException("秒杀活动已结束,无法恢复");
|
||||
}
|
||||
|
||||
// 更新状态为进行中 (status = 2)
|
||||
flashSaleRepository.updateStatus(flashSaleId, 2);
|
||||
flashSale.setStatus(2);
|
||||
|
||||
// 更新缓存
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
log.info("秒杀活动恢复成功: ID={}", flashSaleId);
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO endFlashSale(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() == 3) {
|
||||
throw new RuntimeException("秒杀活动已经结束");
|
||||
}
|
||||
if (flashSale.getStatus() == 1) {
|
||||
throw new RuntimeException("秒杀活动尚未开始,无法结束");
|
||||
}
|
||||
|
||||
// 更新状态为已结束 (status = 3)
|
||||
flashSaleRepository.updateStatus(flashSaleId, 3);
|
||||
flashSale.setStatus(3);
|
||||
|
||||
// 清除相关缓存
|
||||
clearFlashSaleCache(flashSaleId);
|
||||
|
||||
// 更新缓存
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
log.info("秒杀活动结束成功: ID={}", flashSaleId);
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
|
||||
@@ -100,6 +100,7 @@ public class ProductService {
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, productDTO);
|
||||
|
||||
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
@@ -142,6 +143,7 @@ public class ProductService {
|
||||
.map(product -> {
|
||||
ProductDTO dto = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, dto);
|
||||
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
@@ -295,12 +297,28 @@ public class ProductService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品库存(从Redis)
|
||||
* 获取商品库存(优先从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;
|
||||
|
||||
if (stock != null) {
|
||||
return Integer.valueOf(stock.toString());
|
||||
}
|
||||
|
||||
// Redis中没有库存数据,从数据库获取并同步到Redis
|
||||
Optional<Product> productOpt = productRepository.findById(productId);
|
||||
if (productOpt.isPresent()) {
|
||||
Product product = productOpt.get();
|
||||
// 同步到Redis
|
||||
redisService.set(stockKey, product.getStock());
|
||||
log.info("从数据库同步商品库存到Redis: 商品ID={}, 库存={}", productId, product.getStock());
|
||||
return product.getStock();
|
||||
}
|
||||
|
||||
log.warn("商品不存在: 商品ID={}", productId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -21,6 +22,7 @@ public class RedisService {
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("customStringRedisTemplate")
|
||||
private RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
@@ -41,6 +43,13 @@ public class RedisService {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纯字符串值(用于Lua脚本兼容)
|
||||
*/
|
||||
public void setString(String key, String value) {
|
||||
stringRedisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符串值并指定过期时间
|
||||
*/
|
||||
@@ -55,6 +64,13 @@ public class RedisService {
|
||||
return redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取字符串值
|
||||
*/
|
||||
public String getString(String key) {
|
||||
return stringRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
@@ -330,7 +346,18 @@ public class RedisService {
|
||||
* 执行秒杀脚本
|
||||
*/
|
||||
public Long executeFlashSaleScript(String stockKey, int quantity) {
|
||||
return redisTemplate.execute(flashSaleScript, Collections.singletonList(stockKey), String.valueOf(quantity));
|
||||
log.info("执行秒杀脚本: stockKey={}, quantity={}", stockKey, quantity);
|
||||
|
||||
try {
|
||||
Long result = stringRedisTemplate.execute(flashSaleScript, Collections.singletonList(stockKey),
|
||||
String.valueOf(quantity));
|
||||
log.info("秒杀脚本执行结果: result={}", result);
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("执行秒杀脚本异常: stockKey={}, quantity={}", stockKey, quantity, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
-- 返回值:成功返回剩余库存,失败返回负数
|
||||
|
||||
local stock_key = KEYS[1]
|
||||
local quantity = tonumber(ARGV[1])
|
||||
local quantity_str = ARGV[1]
|
||||
local quantity = tonumber(quantity_str)
|
||||
|
||||
-- 检查quantity参数是否有效
|
||||
if quantity == nil or quantity <= 0 then
|
||||
return -3
|
||||
end
|
||||
|
||||
-- 获取当前库存
|
||||
local current_stock = redis.call('GET', stock_key)
|
||||
@@ -15,10 +21,15 @@ if current_stock == false then
|
||||
end
|
||||
|
||||
-- 转换为数字
|
||||
current_stock = tonumber(current_stock)
|
||||
local current_stock_num = tonumber(current_stock)
|
||||
|
||||
-- 检查转换是否成功
|
||||
if current_stock_num == nil then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 检查库存是否足够
|
||||
if current_stock < quantity then
|
||||
if current_stock_num < quantity then
|
||||
return -2
|
||||
end
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<option value="pending">未开始</option>
|
||||
<option value="active">进行中</option>
|
||||
<option value="ended">已结束</option>
|
||||
<option value="paused">已暂停</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
@@ -468,6 +469,9 @@
|
||||
case 'ended':
|
||||
queryData.status = 3; // 已结束
|
||||
break;
|
||||
case 'paused':
|
||||
queryData.status = 4; // 已暂停
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,15 +532,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="editFlashSale(` + flashSale.id + `)" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="deleteFlashSale(` + flashSale.id + `)" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="viewFlashSale(` + flashSale.id + `)" title="查看">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
` + getActionButtons(flashSale) + `
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -555,6 +551,8 @@
|
||||
return '进行中';
|
||||
case 3:
|
||||
return '已结束';
|
||||
case 4:
|
||||
return '已暂停';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
@@ -568,6 +566,8 @@
|
||||
return 'bg-success'; // 进行中
|
||||
case 3:
|
||||
return 'bg-secondary'; // 已结束
|
||||
case 4:
|
||||
return 'bg-info'; // 已暂停
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
@@ -849,6 +849,163 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 生成操作按钮
|
||||
function getActionButtons(flashSale) {
|
||||
let buttons = '';
|
||||
|
||||
// 查看按钮 - 始终显示
|
||||
buttons += `<button class="btn btn-outline-info" onclick="viewFlashSale(` + flashSale.id + `)" title="查看">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>`;
|
||||
|
||||
// 根据状态显示不同的操作按钮
|
||||
switch (flashSale.status) {
|
||||
case 1: // 未开始
|
||||
buttons += `<button class="btn btn-outline-success" onclick="publishFlashSale(` + flashSale.id + `)" title="发布">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>`;
|
||||
buttons += `<button class="btn btn-outline-primary" onclick="editFlashSale(` + flashSale.id + `)" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>`;
|
||||
buttons += `<button class="btn btn-outline-danger" onclick="deleteFlashSale(` + flashSale.id + `)" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 2: // 进行中
|
||||
buttons += `<button class="btn btn-outline-warning" onclick="pauseFlashSale(` + flashSale.id + `)" title="暂停">
|
||||
<i class="fas fa-pause"></i>
|
||||
</button>`;
|
||||
buttons += `<button class="btn btn-outline-danger" onclick="endFlashSale(` + flashSale.id + `)" title="结束">
|
||||
<i class="fas fa-stop"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 3: // 已结束
|
||||
// 已结束的活动只能查看
|
||||
break;
|
||||
case 4: // 已暂停
|
||||
buttons += `<button class="btn btn-outline-success" onclick="resumeFlashSale(` + flashSale.id + `)" title="恢复">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>`;
|
||||
buttons += `<button class="btn btn-outline-danger" onclick="endFlashSale(` + flashSale.id + `)" title="结束">
|
||||
<i class="fas fa-stop"></i>
|
||||
</button>`;
|
||||
break;
|
||||
default:
|
||||
// 未知状态,只显示查看按钮
|
||||
break;
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// 发布秒杀活动
|
||||
function publishFlashSale(id) {
|
||||
if (confirm('确定要发布这个秒杀活动吗?发布后活动将生效并开始接受用户参与。')) {
|
||||
console.log('发布秒杀活动:', id);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id + '/publish',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
alert('秒杀活动发布成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('发布失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '发布失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停秒杀活动
|
||||
function pauseFlashSale(id) {
|
||||
if (confirm('确定要暂停这个秒杀活动吗?暂停后用户将无法参与秒杀。')) {
|
||||
console.log('暂停秒杀活动:', id);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id + '/pause',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
alert('秒杀活动暂停成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('暂停失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '暂停失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复秒杀活动
|
||||
function resumeFlashSale(id) {
|
||||
if (confirm('确定要恢复这个秒杀活动吗?恢复后用户将可以继续参与秒杀。')) {
|
||||
console.log('恢复秒杀活动:', id);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id + '/resume',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
alert('秒杀活动恢复成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('恢复失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '恢复失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 结束秒杀活动
|
||||
function endFlashSale(id) {
|
||||
if (confirm('确定要结束这个秒杀活动吗?结束后活动将无法恢复。')) {
|
||||
console.log('结束秒杀活动:', id);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id + '/end',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
alert('秒杀活动结束成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('结束失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '结束失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
@@ -88,14 +88,30 @@
|
||||
<i class="fas fa-home"></i> 首页
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/products">
|
||||
<i class="fas fa-shopping-bag"></i> 商品列表
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="productsDropdown" role="button"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-shopping-bag"></i> 商品
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/products">
|
||||
<i class="fas fa-th-large"></i> 商品列表
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/search">
|
||||
<i class="fas fa-search"></i> 商品搜索
|
||||
</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/category/1">
|
||||
<i class="fas fa-tags"></i> 商品分类
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/flashsales">
|
||||
<a class="nav-link text-warning fw-bold" href="${pageContext.request.contextPath}/flashsales">
|
||||
<i class="fas fa-fire"></i> 秒杀活动
|
||||
<span class="badge bg-danger ms-1">HOT</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -125,6 +141,9 @@
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/orders">
|
||||
<i class="fas fa-list-alt"></i> 我的订单
|
||||
</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/profile">
|
||||
<i class="fas fa-user-cog"></i> 个人设置
|
||||
</a></li>
|
||||
|
||||
@@ -221,12 +221,17 @@
|
||||
$(document).ready(function() {
|
||||
// 加载正在进行的秒杀活动
|
||||
loadActiveFlashSales();
|
||||
|
||||
|
||||
// 加载热门商品
|
||||
loadHotProducts();
|
||||
|
||||
|
||||
// 启动性能指标动画
|
||||
animateCounters();
|
||||
|
||||
// 更新购物车数量(如果用户已登录)
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
updateCartCount();
|
||||
</c:if>
|
||||
});
|
||||
|
||||
// 加载正在进行的秒杀活动
|
||||
@@ -272,7 +277,7 @@ function renderFlashSales(flashSales) {
|
||||
<small><i class="fas fa-fire"></i> 秒杀中</small>
|
||||
</div>
|
||||
<div class="position-absolute top-0 end-0 bg-warning text-dark px-2 py-1 rounded-start">
|
||||
<small>${discountPercent}% OFF</small>
|
||||
<small>` + discountPercent + `% OFF</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -293,7 +298,9 @@ function renderFlashSales(flashSales) {
|
||||
<div class="text-danger fw-bold mb-2" id="countdown_${flashSale.id}">
|
||||
计算中...
|
||||
</div>
|
||||
<button class="btn btn-danger btn-sm w-100" onclick="participateFlashSale(${flashSale.id})">
|
||||
<button class="btn btn-danger btn-sm w-100 flash-sale-btn"
|
||||
onclick="participateFlashSale(` + flashSale.id + `)"
|
||||
data-flashsale-id="` + flashSale.id + `">
|
||||
<i class="fas fa-bolt"></i> 立即抢购
|
||||
</button>
|
||||
</div>
|
||||
@@ -346,8 +353,8 @@ function renderHotProducts(products) {
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<img src="${product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg'}"
|
||||
class="card-img-top" alt="${product.name}" style="height: 200px; object-fit: cover;"
|
||||
<img src="` + (product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
class="card-img-top" alt="` + product.name + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + product.name + `</h6>
|
||||
@@ -370,40 +377,118 @@ function renderHotProducts(products) {
|
||||
$('#hotProducts').html(html);
|
||||
}
|
||||
|
||||
// 参与秒杀
|
||||
// 参与秒杀(首页版)
|
||||
function participateFlashSale(flashSaleId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
if (confirm('确定要参与这个秒杀活动吗?')) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
flashSaleId: flashSaleId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showMessage('秒杀成功!订单已生成', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showMessage('秒杀失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
// 防止重复点击
|
||||
if (window.flashSaleInProgress) {
|
||||
showMessage('操作进行中,请稍候...', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
if (!confirm('确定要参与这个秒杀活动吗?\n\n注意:每人限购一件,确认后将立即抢购!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到按钮元素
|
||||
const button = event.target.closest('button');
|
||||
if (!button) return;
|
||||
|
||||
// 设置全局锁
|
||||
window.flashSaleInProgress = true;
|
||||
|
||||
// 保存原始状态
|
||||
const originalText = button.innerHTML;
|
||||
const originalClass = button.className;
|
||||
|
||||
// 更新按钮状态
|
||||
button.disabled = true;
|
||||
button.className = 'btn btn-warning btn-sm w-100';
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 抢购中...';
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
timeout: 10000,
|
||||
data: JSON.stringify({
|
||||
flashSaleId: flashSaleId,
|
||||
quantity: 1,
|
||||
timestamp: startTime
|
||||
}),
|
||||
success: function (response) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (response.success) {
|
||||
// 成功状态
|
||||
button.className = 'btn btn-success btn-sm w-100';
|
||||
button.innerHTML = '<i class="fas fa-check"></i> 抢购成功!';
|
||||
|
||||
showMessage(`🎉 恭喜您!秒杀成功,订单已生成 (耗时: ${duration}ms)`, 'success');
|
||||
|
||||
// 刷新活动数据
|
||||
setTimeout(() => {
|
||||
loadActiveFlashSales();
|
||||
}, 1000);
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 3000);
|
||||
} else {
|
||||
// 失败状态
|
||||
button.className = 'btn btn-danger btn-sm w-100';
|
||||
button.innerHTML = '<i class="fas fa-times"></i> ' + (response.message || '抢购失败');
|
||||
|
||||
showMessage(response.message || '抢购失败,请重试', 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
button.disabled = false;
|
||||
button.className = originalClass;
|
||||
button.innerHTML = originalText;
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
let errorMessage = '网络异常,请重试';
|
||||
if (status === 'timeout') {
|
||||
errorMessage = '请求超时,请检查网络连接';
|
||||
button.innerHTML = '<i class="fas fa-clock"></i> 请求超时';
|
||||
} else if (xhr.status === 429) {
|
||||
errorMessage = '请求过于频繁,请稍后再试';
|
||||
button.innerHTML = '<i class="fas fa-ban"></i> 请求频繁';
|
||||
} else {
|
||||
button.innerHTML = '<i class="fas fa-exclamation-triangle"></i> 网络异常';
|
||||
}
|
||||
|
||||
button.className = 'btn btn-danger btn-sm w-100';
|
||||
showMessage(errorMessage, 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
button.disabled = false;
|
||||
button.className = originalClass;
|
||||
button.innerHTML = originalText;
|
||||
}, 3000);
|
||||
},
|
||||
complete: function () {
|
||||
// 释放全局锁
|
||||
setTimeout(() => {
|
||||
window.flashSaleInProgress = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
showMessage('请先登录后参与秒杀', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
}, 1500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
@@ -467,6 +552,166 @@ function animateCounter(elementId, target, suffix = '') {
|
||||
element.textContent = Math.floor(current).toLocaleString() + suffix;
|
||||
}, 20);
|
||||
}
|
||||
|
||||
// 倒计时函数
|
||||
function countdown(endTime, elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const timeLeft = endTime - now;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
element.innerHTML = '<span class="text-muted">已结束</span>';
|
||||
clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
|
||||
const hours = Math.floor(timeLeft / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
|
||||
|
||||
element.innerHTML = `
|
||||
<i class="fas fa-clock"></i>
|
||||
${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}
|
||||
`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
// 创建消息元素
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type == 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
function updateCartCount() {
|
||||
$.get('${pageContext.request.contextPath}/api/cart/count')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
const cartBadge = document.querySelector('.cart-count');
|
||||
if (cartBadge) {
|
||||
const count = response.data.count || 0;
|
||||
cartBadge.textContent = count;
|
||||
cartBadge.style.display = count > 0 ? 'inline' : 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 秒杀活动卡片样式 */
|
||||
.card.border-danger {
|
||||
border-width: 2px !important;
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.card.border-danger:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
/* 热门商品卡片样式 */
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
}
|
||||
|
||||
/* 倒计时样式 */
|
||||
.text-danger.fw-bold {
|
||||
font-family: 'Courier New', monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* 折扣标签样式 */
|
||||
.position-absolute.bg-warning {
|
||||
font-weight: bold;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* 商品图片样式 */
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 按钮悬停效果 */
|
||||
.btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.card.border-danger:hover,
|
||||
.card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.card:hover .card-img-top {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fa-spinner.fa-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 消息提示样式 */
|
||||
.alert.position-fixed {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
|
||||
Reference in New Issue
Block a user