feat: 删除JSP视图层,完善评价和通知系统,新增拼团模块
- 删除所有 JSP 页面(20个文件),前端完全迁移至 Vue 3 SPA - 完善评价系统:ReviewDialog 组件、用户评价历史页、评价状态检查API - 新增通知系统:Notification 实体/仓库/服务/控制器,NotificationCenter 接入真实API - 新增拼团模块:GroupBuying 全套后端和前端页面 - 修复 review check API 参数双重包装导致请求格式错误 - 修复通知 API 路径缺少 /api 前缀和响应格式处理 - MessageListenerService 集成 NotificationService 创建持久化通知 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,10 @@ package com.org.flashsalesystem;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class FlashSaleSystemApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -292,4 +292,16 @@ public class RedissonConfig {
|
||||
log.info("加载购物车操作Lua脚本");
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼团库存扣减Lua脚本
|
||||
*/
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> groupBuyingStockScript() {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/groupbuying_stock.lua")));
|
||||
script.setResultType(Long.class);
|
||||
log.info("加载拼团库存扣减Lua脚本");
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,31 @@ public class FlashSaleController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动统计信息
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ResponseEntity<Map<String, Object>> getFlashSaleStatistics(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
Map<String, Object> stats = flashSaleService.getFlashSaleStatistics(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stats);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动详情
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.GroupBuyingDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.GroupBuyingService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "拼团管理", description = "拼团活动创建、参与、团组管理等接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/groupbuying")
|
||||
@Slf4j
|
||||
public class GroupBuyingController {
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingService groupBuyingService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
// ========== 用户端 ==========
|
||||
|
||||
@Operation(summary = "获取拼团活动列表")
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getList(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
try {
|
||||
Map<String, Object> result = groupBuyingService.getGroupBuyingList(page, size, status);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取拼团活动列表失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取拼团活动详情")
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getDetail(@PathVariable Long id) {
|
||||
try {
|
||||
GroupBuyingDTO detail = groupBuyingService.getGroupBuyingDetail(id);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", detail);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取拼团活动详情失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取活动下的团组列表")
|
||||
@GetMapping("/{id}/groups")
|
||||
public ResponseEntity<Map<String, Object>> getGroups(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
try {
|
||||
Map<String, Object> result = groupBuyingService.getGroupsByActivity(id, page, size);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取团组列表失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "参与拼团")
|
||||
@PostMapping("/join")
|
||||
public ResponseEntity<Map<String, Object>> joinGroup(
|
||||
@Validated @RequestBody GroupBuyingDTO.JoinGroupDTO joinDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
GroupBuyingDTO.JoinResultDTO result = groupBuyingService.joinGroupBuying(joinDTO, userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", result.getSuccess());
|
||||
response.put("message", result.getMessage());
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("参与拼团失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取团组详情")
|
||||
@GetMapping("/group/{groupId}")
|
||||
public ResponseEntity<Map<String, Object>> getGroupDetail(@PathVariable Long groupId) {
|
||||
try {
|
||||
GroupBuyingDTO.GroupInfoDTO detail = groupBuyingService.getGroupDetail(groupId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", detail);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取团组详情失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "退出团组")
|
||||
@PostMapping("/group/{groupId}/cancel")
|
||||
public ResponseEntity<Map<String, Object>> cancelMembership(
|
||||
@PathVariable Long groupId,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
groupBuyingService.cancelMembership(groupId, userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已退出团组");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("退出团组失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "我的团组")
|
||||
@GetMapping("/my-groups")
|
||||
public ResponseEntity<Map<String, Object>> getMyGroups(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
Map<String, Object> result = groupBuyingService.getMyGroups(userId, page, size);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取我的团组失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "拼团统计数据")
|
||||
@GetMapping("/statistics")
|
||||
public ResponseEntity<Map<String, Object>> getStatistics(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
GroupBuyingDTO.StatisticsDTO stats = groupBuyingService.getStatistics(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stats);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取拼团统计失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 管理员端 ==========
|
||||
|
||||
@Operation(summary = "创建拼团活动")
|
||||
@PostMapping("/admin/create")
|
||||
public ResponseEntity<Map<String, Object>> create(
|
||||
@Validated @RequestBody GroupBuyingDTO.CreateDTO createDTO) {
|
||||
try {
|
||||
GroupBuyingDTO result = groupBuyingService.createGroupBuying(createDTO);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "拼团活动创建成功");
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("创建拼团活动失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "更新拼团活动")
|
||||
@PutMapping("/admin/{id}")
|
||||
public ResponseEntity<Map<String, Object>> update(
|
||||
@PathVariable Long id,
|
||||
@Validated @RequestBody GroupBuyingDTO.UpdateDTO updateDTO) {
|
||||
try {
|
||||
GroupBuyingDTO result = groupBuyingService.updateGroupBuying(id, updateDTO);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "拼团活动更新成功");
|
||||
response.put("data", result);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新拼团活动失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除拼团活动")
|
||||
@DeleteMapping("/admin/{id}")
|
||||
public ResponseEntity<Map<String, Object>> delete(@PathVariable Long id) {
|
||||
try {
|
||||
boolean success = groupBuyingService.deleteGroupBuying(id);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", success);
|
||||
response.put("message", success ? "拼团活动删除成功" : "拼团活动删除失败");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("删除拼团活动失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "预热所有拼团活动库存")
|
||||
@PostMapping("/admin/preload-all")
|
||||
public ResponseEntity<Map<String, Object>> preloadAll() {
|
||||
try {
|
||||
groupBuyingService.preloadAllActiveStock();
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "拼团活动库存预热完成");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("预热拼团库存失败", e);
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 工具方法 ==========
|
||||
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) return null;
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> createUnauthorizedResponse() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> badRequest(String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", message);
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.NotificationService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/notification")
|
||||
public class NotificationController {
|
||||
|
||||
@Autowired
|
||||
private NotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getNotifications(
|
||||
@RequestParam(required = false) String type,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
if (type != null && !type.isEmpty()) {
|
||||
response.put("data", notificationService.getUserNotificationsByType(userId, type));
|
||||
} else {
|
||||
response.put("data", notificationService.getUserNotifications(userId));
|
||||
}
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/unread-count")
|
||||
public ResponseEntity<Map<String, Object>> getUnreadCount(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", notificationService.getUnreadCount(userId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/read")
|
||||
public ResponseEntity<Map<String, Object>> markAsRead(@PathVariable Long id,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
notificationService.markAsRead(id, userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已标记为已读");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PutMapping("/read-all")
|
||||
public ResponseEntity<Map<String, Object>> markAllAsRead(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
notificationService.markAllAsRead(userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已全部标记为已读");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@DeleteMapping("/clear")
|
||||
public ResponseEntity<Map<String, Object>> clearAll(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
notificationService.clearAll(userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "已清空所有通知");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> unauthorized() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@@ -41,10 +42,52 @@ public class ProductReviewController {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
try {
|
||||
response.put("success", true);
|
||||
response.put("message", "评价提交成功");
|
||||
response.put("data", productReviewService.createReview(userId, createDTO));
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (RuntimeException e) {
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<Map<String, Object>> checkReview(@RequestParam Long orderId,
|
||||
@RequestParam Long productId) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "评价提交成功");
|
||||
response.put("data", productReviewService.createReview(userId, createDTO));
|
||||
response.put("data", productReviewService.checkReviewStatus(orderId, productId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/my")
|
||||
public ResponseEntity<Map<String, Object>> getMyReviews(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", productReviewService.getUserReviews(userId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/order/{orderId}")
|
||||
public ResponseEntity<Map<String, Object>> getOrderReviews(@PathVariable Long orderId,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", productReviewService.getOrderReviews(orderId));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
173
src/main/java/com/org/flashsalesystem/dto/GroupBuyingDTO.java
Normal file
173
src/main/java/com/org/flashsalesystem/dto/GroupBuyingDTO.java
Normal file
@@ -0,0 +1,173 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuyingDTO {
|
||||
|
||||
private Long id;
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private String productImageUrl;
|
||||
private BigDecimal productPrice;
|
||||
private BigDecimal groupPrice;
|
||||
private Integer requiredMembers;
|
||||
private Integer durationMinutes;
|
||||
private Integer totalStock;
|
||||
private Integer remainingStock;
|
||||
private Integer maxPerUser;
|
||||
private Integer status;
|
||||
private String statusDescription;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
private Integer activeGroupCount;
|
||||
private BigDecimal discount;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CreateDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@NotNull(message = "拼团价格不能为空")
|
||||
@DecimalMin(value = "0.01", message = "拼团价格必须大于0")
|
||||
private BigDecimal groupPrice;
|
||||
|
||||
@Min(value = 2, message = "成团人数至少为2人")
|
||||
private Integer requiredMembers = 2;
|
||||
|
||||
@Min(value = 1, message = "有效期至少为1分钟")
|
||||
private Integer durationMinutes = 1440;
|
||||
|
||||
@Min(value = 1, message = "总库存至少为1")
|
||||
private Integer totalStock;
|
||||
|
||||
@Min(value = 1, message = "每人限购至少为1")
|
||||
private Integer maxPerUser = 1;
|
||||
|
||||
@NotNull(message = "开始时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@NotNull(message = "结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateDTO {
|
||||
private Long productId;
|
||||
private BigDecimal groupPrice;
|
||||
private Integer requiredMembers;
|
||||
private Integer durationMinutes;
|
||||
private Integer totalStock;
|
||||
private Integer maxPerUser;
|
||||
private Integer status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class JoinGroupDTO {
|
||||
@NotNull(message = "拼团活动ID不能为空")
|
||||
private Long groupBuyingId;
|
||||
|
||||
private Long groupId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class GroupInfoDTO {
|
||||
private Long id;
|
||||
private String groupNo;
|
||||
private Long groupBuyingId;
|
||||
private Long leaderUserId;
|
||||
private String leaderUsername;
|
||||
private Integer requiredMembers;
|
||||
private Integer currentMembers;
|
||||
private Integer status;
|
||||
private String statusDescription;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
private List<MemberDTO> members;
|
||||
private GroupBuyingDTO groupBuying;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class MemberDTO {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String avatar;
|
||||
private Long orderId;
|
||||
private Integer status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime joinedAt;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class JoinResultDTO {
|
||||
private Boolean success;
|
||||
private String message;
|
||||
private Long groupId;
|
||||
private String groupNo;
|
||||
private Long orderId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class StatisticsDTO {
|
||||
private Long totalActivities;
|
||||
private Long activeActivities;
|
||||
private Long myGroups;
|
||||
private Long successGroups;
|
||||
private BigDecimal totalSaved;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public class OrderDTO {
|
||||
private Long userId;
|
||||
private String username;
|
||||
private Long productId;
|
||||
private Long flashSaleId;
|
||||
private String productName;
|
||||
private String productImageUrl;
|
||||
private Integer quantity;
|
||||
|
||||
@@ -20,6 +20,8 @@ public class ProductReviewDTO {
|
||||
private Long userId;
|
||||
private Long orderId;
|
||||
private String username;
|
||||
private String productName;
|
||||
private String productImage;
|
||||
private Integer rating;
|
||||
private String content;
|
||||
private Integer status;
|
||||
@@ -64,4 +66,12 @@ public class ProductReviewDTO {
|
||||
private Integer status;
|
||||
private String adminReply;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CheckDTO {
|
||||
private boolean reviewed;
|
||||
private ProductReviewDTO review;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "group_buying")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuying {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "group_price", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal groupPrice;
|
||||
|
||||
@Column(name = "required_members", nullable = false)
|
||||
private Integer requiredMembers = 2;
|
||||
|
||||
@Column(name = "duration_minutes", nullable = false)
|
||||
private Integer durationMinutes = 1440;
|
||||
|
||||
@Column(name = "total_stock", nullable = false)
|
||||
private Integer totalStock;
|
||||
|
||||
@Column(name = "remaining_stock", nullable = false)
|
||||
private Integer remainingStock;
|
||||
|
||||
@Column(name = "max_per_user", nullable = false)
|
||||
private Integer maxPerUser = 1;
|
||||
|
||||
/**
|
||||
* 状态:0-草稿 1-未开始 2-进行中 3-已结束
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 0;
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "product_id", insertable = false, updatable = false)
|
||||
private Product product;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return now.isAfter(startTime) && now.isBefore(endTime) && status == 2;
|
||||
}
|
||||
|
||||
public enum GroupBuyingStatus {
|
||||
DRAFT(0, "草稿"),
|
||||
PENDING(1, "未开始"),
|
||||
ACTIVE(2, "进行中"),
|
||||
ENDED(3, "已结束");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
GroupBuyingStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() { return code; }
|
||||
public String getDescription() { return description; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "group_buying_group")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuyingGroup {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "group_no", nullable = false, unique = true, length = 64)
|
||||
private String groupNo;
|
||||
|
||||
@Column(name = "group_buying_id", nullable = false)
|
||||
private Long groupBuyingId;
|
||||
|
||||
@Column(name = "leader_user_id", nullable = false)
|
||||
private Long leaderUserId;
|
||||
|
||||
@Column(name = "required_members", nullable = false)
|
||||
private Integer requiredMembers;
|
||||
|
||||
@Column(name = "current_members", nullable = false)
|
||||
private Integer currentMembers = 1;
|
||||
|
||||
/**
|
||||
* 状态:1-拼团中 2-已成团 3-已失败(超时)
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "expire_time", nullable = false)
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "group_buying_id", insertable = false, updatable = false)
|
||||
private GroupBuying groupBuying;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "leader_user_id", insertable = false, updatable = false)
|
||||
private User leader;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public enum GroupStatus {
|
||||
FORMING(1, "拼团中"),
|
||||
SUCCESS(2, "已成团"),
|
||||
FAILED(3, "已失败");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
GroupStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() { return code; }
|
||||
public String getDescription() { return description; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "group_buying_member", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_group_user", columnNames = {"group_id", "user_id"})
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GroupBuyingMember {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "group_id", nullable = false)
|
||||
private Long groupId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "order_id")
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 状态:1-已加入 2-已成团 3-已退出
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "joined_at", nullable = false, updatable = false)
|
||||
private LocalDateTime joinedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "group_id", insertable = false, updatable = false)
|
||||
private GroupBuyingGroup group;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
joinedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "notifications", indexes = {
|
||||
@Index(name = "idx_notification_user_read", columnList = "user_id, is_read"),
|
||||
@Index(name = "idx_notification_user_created", columnList = "user_id, created_at")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Notification {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String type;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String message;
|
||||
|
||||
@Column(length = 255)
|
||||
private String link;
|
||||
|
||||
@Column(name = "is_read", nullable = false)
|
||||
private Boolean read = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,12 @@ public class Order {
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "flash_sale_id")
|
||||
private Long flashSaleId;
|
||||
|
||||
@Column(name = "group_buying_group_id")
|
||||
private Long groupBuyingGroupId;
|
||||
|
||||
@Min(value = 1, message = "商品数量必须大于0")
|
||||
@Column(nullable = false)
|
||||
private Integer quantity;
|
||||
@@ -145,7 +151,8 @@ public class Order {
|
||||
*/
|
||||
public enum OrderType {
|
||||
NORMAL(1, "普通订单"),
|
||||
FLASH_SALE(2, "秒杀订单");
|
||||
FLASH_SALE(2, "秒杀订单"),
|
||||
GROUP_BUYING(3, "拼团订单");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
@@ -11,7 +11,6 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 秒杀活动数据访问层
|
||||
@@ -19,16 +18,18 @@ import java.util.Optional;
|
||||
@Repository
|
||||
public interface FlashSaleRepository extends JpaRepository<FlashSale, Long> {
|
||||
|
||||
/**
|
||||
* 根据商品ID查找秒杀活动
|
||||
*/
|
||||
Optional<FlashSale> findByProductId(Long productId);
|
||||
|
||||
/**
|
||||
* 分页查找指定商品的秒杀活动
|
||||
*/
|
||||
Page<FlashSale> findByProductId(Long productId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找指定时间点覆盖的商品秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.productId = :productId AND f.startTime <= :targetTime AND f.endTime >= :targetTime")
|
||||
List<FlashSale> findByProductIdAndCoveringTime(@Param("productId") Long productId,
|
||||
@Param("targetTime") LocalDateTime targetTime);
|
||||
|
||||
/**
|
||||
* 根据商品ID和状态查找秒杀活动
|
||||
*/
|
||||
@@ -78,6 +79,13 @@ public interface FlashSaleRepository extends JpaRepository<FlashSale, Long> {
|
||||
" >= :quantity")
|
||||
int updateFlashStock(@Param("flashSaleId") Long flashSaleId, @Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 恢复秒杀库存(订单取消时使用)
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE FlashSale f SET f.flashStock = f.flashStock + :quantity WHERE f.id = :flashSaleId")
|
||||
int increaseFlashStock(@Param("flashSaleId") Long flashSaleId, @Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.GroupBuyingGroup;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface GroupBuyingGroupRepository extends JpaRepository<GroupBuyingGroup, Long> {
|
||||
|
||||
List<GroupBuyingGroup> findByGroupBuyingIdAndStatus(Long groupBuyingId, Integer status);
|
||||
|
||||
Optional<GroupBuyingGroup> findByGroupNo(String groupNo);
|
||||
|
||||
@Query("SELECT g FROM GroupBuyingGroup g WHERE g.status = 1 AND g.expireTime < :now")
|
||||
List<GroupBuyingGroup> findExpiredGroups(@Param("now") LocalDateTime now);
|
||||
|
||||
List<GroupBuyingGroup> findByLeaderUserId(Long userId);
|
||||
|
||||
Page<GroupBuyingGroup> findByGroupBuyingId(Long groupBuyingId, Pageable pageable);
|
||||
|
||||
@Query("SELECT g FROM GroupBuyingGroup g WHERE g.id IN " +
|
||||
"(SELECT m.groupId FROM GroupBuyingMember m WHERE m.userId = :userId AND m.status != 3)")
|
||||
Page<GroupBuyingGroup> findByMemberUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
long countByGroupBuyingIdAndStatus(Long groupBuyingId, Integer status);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingGroup g SET g.currentMembers = g.currentMembers + 1 WHERE g.id = :id")
|
||||
int incrementCurrentMembers(@Param("id") Long id);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingGroup g SET g.currentMembers = g.currentMembers - 1 WHERE g.id = :id AND g.currentMembers > 0")
|
||||
int decrementCurrentMembers(@Param("id") Long id);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingGroup g SET g.status = :status, g.completedAt = :completedAt WHERE g.id = :id")
|
||||
int updateStatusAndCompletedAt(@Param("id") Long id, @Param("status") Integer status, @Param("completedAt") LocalDateTime completedAt);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.GroupBuyingMember;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface GroupBuyingMemberRepository extends JpaRepository<GroupBuyingMember, Long> {
|
||||
|
||||
List<GroupBuyingMember> findByGroupId(Long groupId);
|
||||
|
||||
List<GroupBuyingMember> findByGroupIdAndStatus(Long groupId, Integer status);
|
||||
|
||||
boolean existsByGroupIdAndUserIdAndStatusNot(Long groupId, Long userId, Integer excludeStatus);
|
||||
|
||||
Optional<GroupBuyingMember> findByGroupIdAndUserId(Long groupId, Long userId);
|
||||
|
||||
long countByGroupIdAndStatusNot(Long groupId, Integer excludeStatus);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuyingMember m SET m.status = :status WHERE m.groupId = :groupId AND m.status = 1")
|
||||
int updateStatusByGroupId(@Param("groupId") Long groupId, @Param("status") Integer status);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.GroupBuying;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface GroupBuyingRepository extends JpaRepository<GroupBuying, Long> {
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.startTime <= :now AND g.endTime > :now AND g.status = 2")
|
||||
List<GroupBuying> findActiveGroupBuyings(@Param("now") LocalDateTime now);
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.startTime <= :now AND g.endTime > :now AND g.status = 2")
|
||||
Page<GroupBuying> findActiveGroupBuyings(@Param("now") LocalDateTime now, Pageable pageable);
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.startTime > :now AND g.status = 1")
|
||||
List<GroupBuying> findUpcomingGroupBuyings(@Param("now") LocalDateTime now);
|
||||
|
||||
@Query("SELECT g FROM GroupBuying g WHERE g.endTime <= :now OR g.status = 3")
|
||||
Page<GroupBuying> findEndedGroupBuyings(@Param("now") LocalDateTime now, Pageable pageable);
|
||||
|
||||
Page<GroupBuying> findByStatus(Integer status, Pageable pageable);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuying g SET g.remainingStock = g.remainingStock - :quantity WHERE g.id = :id AND g.remainingStock >= :quantity")
|
||||
int updateStock(@Param("id") Long id, @Param("quantity") Integer quantity);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuying g SET g.remainingStock = g.remainingStock + :quantity WHERE g.id = :id")
|
||||
int increaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE GroupBuying g SET g.status = :status WHERE g.id = :id")
|
||||
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
|
||||
|
||||
long countByStatus(Integer status);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.Notification;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface NotificationRepository extends JpaRepository<Notification, Long> {
|
||||
|
||||
List<Notification> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
|
||||
List<Notification> findByUserIdAndTypeOrderByCreatedAtDesc(Long userId, String type);
|
||||
|
||||
long countByUserIdAndReadFalse(Long userId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Notification n SET n.read = true WHERE n.userId = :userId AND n.read = false")
|
||||
int markAllAsRead(@Param("userId") Long userId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Notification n SET n.read = true WHERE n.id = :id AND n.userId = :userId")
|
||||
int markAsRead(@Param("id") Long id, @Param("userId") Long userId);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
@@ -49,6 +49,23 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
*/
|
||||
Page<Order> findByOrderType(Integer orderType, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 分页查找用户指定类型的订单
|
||||
*/
|
||||
Page<Order> findByUserIdAndOrderType(Long userId, Integer orderType, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计用户指定类型的订单数量
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.userId = :userId AND o.orderType = :orderType")
|
||||
Long countByUserIdAndOrderType(@Param("userId") Long userId, @Param("orderType") Integer orderType);
|
||||
|
||||
/**
|
||||
* 统计用户指定类型且非取消的订单数量(抢购成功)
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.userId = :userId AND o.orderType = :orderType AND o.status != 5")
|
||||
Long countByUserIdAndOrderTypeAndStatusNot5(@Param("userId") Long userId, @Param("orderType") Integer orderType);
|
||||
|
||||
/**
|
||||
* 查找秒杀订单
|
||||
*/
|
||||
@@ -116,10 +133,14 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
List<Order> findFlashSaleOrdersByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 检查用户是否已经购买过指定商品的秒杀
|
||||
* 检查用户是否已经参与过指定秒杀活动
|
||||
*/
|
||||
@Query("SELECT COUNT(o) > 0 FROM Order o WHERE o.userId = :userId AND o.productId = :productId AND o.orderType = 2")
|
||||
boolean existsFlashSaleOrder(@Param("userId") Long userId, @Param("productId") Long productId);
|
||||
boolean existsByUserIdAndFlashSaleIdAndOrderType(Long userId, Long flashSaleId, Integer orderType);
|
||||
|
||||
/**
|
||||
* 检查指定秒杀活动是否已有订单
|
||||
*/
|
||||
boolean existsByFlashSaleIdAndOrderType(Long flashSaleId, Integer orderType);
|
||||
|
||||
/**
|
||||
* 根据创建时间范围统计订单数量
|
||||
|
||||
@@ -18,6 +18,16 @@ public interface ProductReviewRepository extends JpaRepository<ProductReview, Lo
|
||||
|
||||
long countByProductId(Long productId);
|
||||
|
||||
long countByProductIdAndStatus(Long productId, Integer status);
|
||||
|
||||
@Query("SELECT AVG(r.rating) FROM ProductReview r WHERE r.productId = :productId")
|
||||
Double findAverageRatingByProductId(@Param("productId") Long productId);
|
||||
|
||||
List<ProductReview> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
|
||||
List<ProductReview> findByOrderId(Long orderId);
|
||||
|
||||
boolean existsByOrderIdAndProductId(Long orderId, Long productId);
|
||||
|
||||
Optional<ProductReview> findByOrderIdAndProductId(Long orderId, Long productId);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -318,6 +320,7 @@ public class CartService {
|
||||
/**
|
||||
* 购物车下单
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO checkoutCart(Long userId, List<Long> productIds) {
|
||||
log.info("购物车下单: 用户ID={}, 商品IDs={}", userId, productIds);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ 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.scheduling.annotation.Scheduled;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -90,10 +91,9 @@ public class FlashSaleService {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
|
||||
// 检查是否已有该商品的秒杀活动
|
||||
Optional<FlashSale> existingFlashSale = flashSaleRepository.findByProductId(createDTO.getProductId());
|
||||
if (existingFlashSale.isPresent()) {
|
||||
throw new RuntimeException("该商品已有秒杀活动");
|
||||
// 验证秒杀价格必须小于商品原价
|
||||
if (createDTO.getFlashPrice().compareTo(product.getPrice()) >= 0) {
|
||||
throw new RuntimeException("秒杀价格必须小于商品原价");
|
||||
}
|
||||
|
||||
// 创建秒杀活动
|
||||
@@ -157,8 +157,8 @@ public class FlashSaleService {
|
||||
}
|
||||
|
||||
// 检查数据库中是否已有订单
|
||||
if (orderRepository.existsFlashSaleOrder(userId, flashSale.getProductId())) {
|
||||
return createFailResult("您已经购买过该商品");
|
||||
if (orderRepository.existsByUserIdAndFlashSaleIdAndOrderType(userId, flashSale.getId(), 2)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
|
||||
// 检查购买数量限制
|
||||
@@ -174,6 +174,14 @@ public class FlashSaleService {
|
||||
}
|
||||
|
||||
try {
|
||||
// 二次校验:锁内重新检查用户是否已参与(防止并发竞态)
|
||||
if (redisService.sIsMember(successUsersKey, userId)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
if (orderRepository.existsByUserIdAndFlashSaleIdAndOrderType(userId, flashSale.getId(), 2)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
|
||||
// 检查并修复库存数据
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
String currentStock = redisService.getString(stockKey);
|
||||
@@ -300,9 +308,11 @@ public class FlashSaleService {
|
||||
// 验证排序字段
|
||||
String sortBy = validateSortField(queryDTO.getSortBy());
|
||||
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), sortBy);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Page<FlashSale> flashSalePage;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -621,6 +631,55 @@ public class FlashSaleService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复秒杀库存(订单取消时调用)
|
||||
* 恢复Redis库存、DB库存,并移除成功用户集合记录
|
||||
*/
|
||||
@Transactional
|
||||
public void restoreFlashSaleStock(Long flashSaleId, Long productId, LocalDateTime orderCreatedAt, Long userId,
|
||||
Integer quantity) {
|
||||
log.info("恢复秒杀库存: flashSaleId={}, productId={}, orderCreatedAt={}, userId={}, quantity={}",
|
||||
flashSaleId, productId, orderCreatedAt, userId, quantity);
|
||||
|
||||
Optional<FlashSale> flashSaleOpt = Optional.empty();
|
||||
if (flashSaleId == null) {
|
||||
List<FlashSale> matchedFlashSales = flashSaleRepository.findByProductIdAndCoveringTime(productId,
|
||||
orderCreatedAt);
|
||||
if (matchedFlashSales.size() == 1) {
|
||||
flashSaleOpt = Optional.of(matchedFlashSales.get(0));
|
||||
log.info("根据商品和下单时间回填秒杀活动: flashSaleId={}, productId={}",
|
||||
flashSaleOpt.get().getId(), productId);
|
||||
} else {
|
||||
log.warn("订单未记录秒杀活动ID且无法唯一匹配历史活动,跳过秒杀库存恢复: productId={}, matches={}",
|
||||
productId, matchedFlashSales.size());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
}
|
||||
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
log.warn("未找到对应的秒杀活动,跳过秒杀库存恢复: flashSaleId={}", flashSaleId);
|
||||
return;
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
|
||||
// 恢复Redis库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
redisService.incrBy(stockKey, quantity);
|
||||
|
||||
// 恢复DB库存
|
||||
flashSaleRepository.increaseFlashStock(flashSale.getId(), quantity);
|
||||
|
||||
// 移除成功用户集合记录
|
||||
String successUsersKey = FLASH_SALE_SUCCESS_USERS_PREFIX + flashSale.getId();
|
||||
redisService.sRem(successUsersKey, userId);
|
||||
|
||||
log.info("秒杀库存恢复成功: flashSaleId={}, userId={}, quantity={}",
|
||||
flashSale.getId(), userId, quantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动剩余库存
|
||||
*/
|
||||
@@ -712,7 +771,7 @@ public class FlashSaleService {
|
||||
}
|
||||
|
||||
// 检查是否有相关订单
|
||||
if (orderRepository.existsFlashSaleOrder(null, flashSale.getProductId())) {
|
||||
if (orderRepository.existsByFlashSaleIdAndOrderType(flashSaleId, 2)) {
|
||||
throw new RuntimeException("该秒杀活动已有订单,无法删除");
|
||||
}
|
||||
|
||||
@@ -886,6 +945,60 @@ public class FlashSaleService {
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动统计信息(即将开始、正在进行的全局数量 + 用户参与/成功数量)
|
||||
*/
|
||||
public Map<String, Object> getFlashSaleStatistics(Long userId) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
long upcoming = flashSaleRepository.findUpcomingFlashSales(now).size();
|
||||
long active = flashSaleRepository.findActiveFlashSales(now).size();
|
||||
|
||||
long participated = 0;
|
||||
long success = 0;
|
||||
if (userId != null) {
|
||||
participated = orderRepository.countByUserIdAndOrderType(userId, 2);
|
||||
success = orderRepository.countByUserIdAndOrderTypeAndStatusNot5(userId, 2);
|
||||
}
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("upcoming", upcoming);
|
||||
stats.put("active", active);
|
||||
stats.put("participated", participated);
|
||||
stats.put("success", success);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时预热即将开始的秒杀活动库存(每5分钟执行一次)
|
||||
*/
|
||||
@Scheduled(fixedRate = 300000)
|
||||
public void scheduledPreloadFlashSales() {
|
||||
log.info("定时任务:检查即将开始的秒杀活动并预热库存");
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime threshold = now.plusMinutes(30);
|
||||
List<FlashSale> upcomingFlashSales = flashSaleRepository.findUpcomingFlashSales(now);
|
||||
|
||||
int preloadCount = 0;
|
||||
for (FlashSale flashSale : upcomingFlashSales) {
|
||||
if (flashSale.getStartTime().isBefore(threshold)) {
|
||||
preloadFlashSale(flashSale.getId());
|
||||
preloadCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (preloadCount > 0) {
|
||||
log.info("定时预热完成:预热了{}个即将开始的秒杀活动", preloadCount);
|
||||
}
|
||||
|
||||
// 同时更新秒杀活动状态
|
||||
updateFlashSaleStatus();
|
||||
} catch (Exception e) {
|
||||
log.error("定时预热秒杀活动失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
@@ -1051,6 +1164,7 @@ public class FlashSaleService {
|
||||
order.setOrderNo("FS" + System.currentTimeMillis() + String.format("%03d", new java.util.Random().nextInt(1000)));
|
||||
order.setUserId(userId);
|
||||
order.setProductId(flashSale.getProductId());
|
||||
order.setFlashSaleId(flashSale.getId());
|
||||
order.setQuantity(participateDTO.getQuantity());
|
||||
order.setTotalPrice(flashSale.getFlashPrice().multiply(BigDecimal.valueOf(participateDTO.getQuantity())));
|
||||
order.setStatus(1); // 待支付
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.GroupBuyingDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.*;
|
||||
import com.org.flashsalesystem.repository.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
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.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
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;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class GroupBuyingService {
|
||||
|
||||
private static final String GB_STOCK_PREFIX = "groupbuying_stock:";
|
||||
private static final String GB_LOCK_PREFIX = "groupbuying_lock:";
|
||||
private static final String GB_MEMBERS_PREFIX = "groupbuying_members:";
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingRepository groupBuyingRepository;
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingGroupRepository groupBuyingGroupRepository;
|
||||
|
||||
@Autowired
|
||||
private GroupBuyingMemberRepository groupBuyingMemberRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
@Autowired
|
||||
private RedissonLockService redissonLockService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("customStringRedisTemplate")
|
||||
private RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<Long> groupBuyingStockScript;
|
||||
|
||||
// ========== 管理员操作 ==========
|
||||
|
||||
@Transactional
|
||||
public GroupBuyingDTO createGroupBuying(GroupBuyingDTO.CreateDTO createDTO) {
|
||||
log.info("创建拼团活动: productId={}", createDTO.getProductId());
|
||||
|
||||
Product product = productRepository.findById(createDTO.getProductId())
|
||||
.orElseThrow(() -> new RuntimeException("商品不存在"));
|
||||
|
||||
if (createDTO.getGroupPrice().compareTo(product.getPrice()) >= 0) {
|
||||
throw new RuntimeException("拼团价格必须低于商品原价");
|
||||
}
|
||||
|
||||
if (createDTO.getEndTime().isBefore(createDTO.getStartTime())) {
|
||||
throw new RuntimeException("结束时间不能早于开始时间");
|
||||
}
|
||||
|
||||
GroupBuying gb = new GroupBuying();
|
||||
gb.setProductId(createDTO.getProductId());
|
||||
gb.setGroupPrice(createDTO.getGroupPrice());
|
||||
gb.setRequiredMembers(createDTO.getRequiredMembers());
|
||||
gb.setDurationMinutes(createDTO.getDurationMinutes());
|
||||
gb.setTotalStock(createDTO.getTotalStock());
|
||||
gb.setRemainingStock(createDTO.getTotalStock());
|
||||
gb.setMaxPerUser(createDTO.getMaxPerUser());
|
||||
gb.setStatus(0); // 草稿
|
||||
gb.setStartTime(createDTO.getStartTime());
|
||||
gb.setEndTime(createDTO.getEndTime());
|
||||
|
||||
gb = groupBuyingRepository.save(gb);
|
||||
log.info("拼团活动创建成功: id={}", gb.getId());
|
||||
|
||||
return buildDTO(gb);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GroupBuyingDTO updateGroupBuying(Long id, GroupBuyingDTO.UpdateDTO updateDTO) {
|
||||
GroupBuying gb = groupBuyingRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
|
||||
if (gb.getStatus() == 2) {
|
||||
throw new RuntimeException("进行中的活动不能修改");
|
||||
}
|
||||
|
||||
if (updateDTO.getProductId() != null) gb.setProductId(updateDTO.getProductId());
|
||||
if (updateDTO.getGroupPrice() != null) gb.setGroupPrice(updateDTO.getGroupPrice());
|
||||
if (updateDTO.getRequiredMembers() != null) gb.setRequiredMembers(updateDTO.getRequiredMembers());
|
||||
if (updateDTO.getDurationMinutes() != null) gb.setDurationMinutes(updateDTO.getDurationMinutes());
|
||||
if (updateDTO.getTotalStock() != null) {
|
||||
int diff = updateDTO.getTotalStock() - gb.getTotalStock();
|
||||
gb.setTotalStock(updateDTO.getTotalStock());
|
||||
gb.setRemainingStock(gb.getRemainingStock() + diff);
|
||||
}
|
||||
if (updateDTO.getMaxPerUser() != null) gb.setMaxPerUser(updateDTO.getMaxPerUser());
|
||||
if (updateDTO.getStatus() != null) gb.setStatus(updateDTO.getStatus());
|
||||
if (updateDTO.getStartTime() != null) gb.setStartTime(updateDTO.getStartTime());
|
||||
if (updateDTO.getEndTime() != null) gb.setEndTime(updateDTO.getEndTime());
|
||||
|
||||
gb = groupBuyingRepository.save(gb);
|
||||
return buildDTO(gb);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean deleteGroupBuying(Long id) {
|
||||
GroupBuying gb = groupBuyingRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
|
||||
if (gb.getStatus() == 2) {
|
||||
throw new RuntimeException("进行中的活动不能删除");
|
||||
}
|
||||
|
||||
groupBuyingRepository.deleteById(id);
|
||||
redisService.delete(GB_STOCK_PREFIX + id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== 查询操作 ==========
|
||||
|
||||
public Map<String, Object> getGroupBuyingList(int page, int size, Integer status) {
|
||||
int pageSize = Math.min(size, 100);
|
||||
Pageable pageable = PageRequest.of(page, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
|
||||
Page<GroupBuying> gbPage;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (status != null) {
|
||||
if (status == 2) {
|
||||
gbPage = groupBuyingRepository.findActiveGroupBuyings(now, pageable);
|
||||
} else if (status == 3) {
|
||||
gbPage = groupBuyingRepository.findEndedGroupBuyings(now, pageable);
|
||||
} else {
|
||||
gbPage = groupBuyingRepository.findByStatus(status, pageable);
|
||||
}
|
||||
} else {
|
||||
gbPage = groupBuyingRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
List<GroupBuyingDTO> dtos = gbPage.getContent().stream()
|
||||
.map(this::buildDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", dtos);
|
||||
result.put("totalElements", gbPage.getTotalElements());
|
||||
result.put("totalPages", gbPage.getTotalPages());
|
||||
result.put("currentPage", gbPage.getNumber());
|
||||
result.put("size", gbPage.getSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
public GroupBuyingDTO getGroupBuyingDetail(Long id) {
|
||||
GroupBuying gb = groupBuyingRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
return buildDTO(gb);
|
||||
}
|
||||
|
||||
public Map<String, Object> getGroupsByActivity(Long groupBuyingId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, Math.min(size, 100), Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<GroupBuyingGroup> groupPage = groupBuyingGroupRepository.findByGroupBuyingId(groupBuyingId, pageable);
|
||||
|
||||
List<GroupBuyingDTO.GroupInfoDTO> dtos = groupPage.getContent().stream()
|
||||
.map(this::buildGroupInfoDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", dtos);
|
||||
result.put("totalElements", groupPage.getTotalElements());
|
||||
result.put("totalPages", groupPage.getTotalPages());
|
||||
result.put("currentPage", groupPage.getNumber());
|
||||
result.put("size", groupPage.getSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
public GroupBuyingDTO.GroupInfoDTO getGroupDetail(Long groupId) {
|
||||
GroupBuyingGroup group = groupBuyingGroupRepository.findById(groupId)
|
||||
.orElseThrow(() -> new RuntimeException("团组不存在"));
|
||||
return buildGroupInfoDTO(group);
|
||||
}
|
||||
|
||||
public Map<String, Object> getMyGroups(Long userId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, Math.min(size, 100), Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<GroupBuyingGroup> groupPage = groupBuyingGroupRepository.findByMemberUserId(userId, pageable);
|
||||
|
||||
List<GroupBuyingDTO.GroupInfoDTO> dtos = groupPage.getContent().stream()
|
||||
.map(this::buildGroupInfoDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", dtos);
|
||||
result.put("totalElements", groupPage.getTotalElements());
|
||||
result.put("totalPages", groupPage.getTotalPages());
|
||||
result.put("currentPage", groupPage.getNumber());
|
||||
result.put("size", groupPage.getSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
public GroupBuyingDTO.StatisticsDTO getStatistics(Long userId) {
|
||||
GroupBuyingDTO.StatisticsDTO stats = new GroupBuyingDTO.StatisticsDTO();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
stats.setTotalActivities(groupBuyingRepository.count());
|
||||
stats.setActiveActivities(groupBuyingRepository.countByStatus(2));
|
||||
|
||||
if (userId != null) {
|
||||
Page<GroupBuyingGroup> myGroups = groupBuyingGroupRepository.findByMemberUserId(userId, PageRequest.of(0, 1));
|
||||
stats.setMyGroups(myGroups.getTotalElements());
|
||||
|
||||
// Count successful groups where user participated
|
||||
long successCount = 0;
|
||||
BigDecimal totalSaved = BigDecimal.ZERO;
|
||||
Page<GroupBuyingGroup> allMyGroups = groupBuyingGroupRepository.findByMemberUserId(userId, PageRequest.of(0, 1000));
|
||||
for (GroupBuyingGroup g : allMyGroups.getContent()) {
|
||||
if (g.getStatus() == 2) {
|
||||
successCount++;
|
||||
GroupBuying gb = groupBuyingRepository.findById(g.getGroupBuyingId()).orElse(null);
|
||||
if (gb != null) {
|
||||
Product product = productRepository.findById(gb.getProductId()).orElse(null);
|
||||
if (product != null) {
|
||||
totalSaved = totalSaved.add(product.getPrice().subtract(gb.getGroupPrice()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.setSuccessGroups(successCount);
|
||||
stats.setTotalSaved(totalSaved);
|
||||
} else {
|
||||
stats.setMyGroups(0L);
|
||||
stats.setSuccessGroups(0L);
|
||||
stats.setTotalSaved(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ========== 核心拼团逻辑 ==========
|
||||
|
||||
@Transactional
|
||||
public GroupBuyingDTO.JoinResultDTO joinGroupBuying(GroupBuyingDTO.JoinGroupDTO joinDTO, Long userId) {
|
||||
Long activityId = joinDTO.getGroupBuyingId();
|
||||
String lockKey = GB_LOCK_PREFIX + activityId;
|
||||
|
||||
if (!redissonLockService.tryLock(lockKey, 5, 30)) {
|
||||
throw new RuntimeException("系统繁忙,请稍后重试");
|
||||
}
|
||||
|
||||
try {
|
||||
return doJoinGroupBuying(joinDTO, userId);
|
||||
} finally {
|
||||
redissonLockService.unlock(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
private GroupBuyingDTO.JoinResultDTO doJoinGroupBuying(GroupBuyingDTO.JoinGroupDTO joinDTO, Long userId) {
|
||||
Long activityId = joinDTO.getGroupBuyingId();
|
||||
|
||||
// 1. 校验活动状态
|
||||
GroupBuying gb = groupBuyingRepository.findById(activityId)
|
||||
.orElseThrow(() -> new RuntimeException("拼团活动不存在"));
|
||||
|
||||
if (!gb.isActive()) {
|
||||
throw new RuntimeException("拼团活动未在进行中");
|
||||
}
|
||||
|
||||
if (gb.getRemainingStock() <= 0) {
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
|
||||
// 2. 获取商品信息
|
||||
Product product = productRepository.findById(gb.getProductId())
|
||||
.orElseThrow(() -> new RuntimeException("商品不存在"));
|
||||
|
||||
GroupBuyingGroup group;
|
||||
|
||||
if (joinDTO.getGroupId() != null) {
|
||||
// 加入现有团组
|
||||
group = groupBuyingGroupRepository.findById(joinDTO.getGroupId())
|
||||
.orElseThrow(() -> new RuntimeException("团组不存在"));
|
||||
|
||||
if (group.getStatus() != 1) {
|
||||
throw new RuntimeException("该团组已成团或已过期");
|
||||
}
|
||||
|
||||
if (LocalDateTime.now().isAfter(group.getExpireTime())) {
|
||||
throw new RuntimeException("该团组已过期");
|
||||
}
|
||||
|
||||
if (group.getCurrentMembers() >= group.getRequiredMembers()) {
|
||||
throw new RuntimeException("该团组已满员");
|
||||
}
|
||||
|
||||
// 检查用户是否已经在该团组中
|
||||
if (groupBuyingMemberRepository.existsByGroupIdAndUserIdAndStatusNot(group.getId(), userId, 3)) {
|
||||
throw new RuntimeException("您已在该团组中");
|
||||
}
|
||||
} else {
|
||||
// 创建新团组
|
||||
group = new GroupBuyingGroup();
|
||||
group.setGroupNo("GB" + System.currentTimeMillis() + String.format("%03d", new Random().nextInt(1000)));
|
||||
group.setGroupBuyingId(activityId);
|
||||
group.setLeaderUserId(userId);
|
||||
group.setRequiredMembers(gb.getRequiredMembers());
|
||||
group.setCurrentMembers(0); // will be incremented below
|
||||
group.setStatus(1);
|
||||
group.setExpireTime(LocalDateTime.now().plusMinutes(gb.getDurationMinutes()));
|
||||
group = groupBuyingGroupRepository.save(group);
|
||||
}
|
||||
|
||||
// 3. Redis 原子扣库存
|
||||
String stockKey = GB_STOCK_PREFIX + activityId;
|
||||
Long result = stringRedisTemplate.execute(groupBuyingStockScript,
|
||||
Collections.singletonList(stockKey), "1");
|
||||
|
||||
if (result == null || result < 0) {
|
||||
// Redis stock exhausted, double check DB
|
||||
if (gb.getRemainingStock() <= 0) {
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. DB 扣库存
|
||||
int updated = groupBuyingRepository.updateStock(activityId, 1);
|
||||
if (updated == 0) {
|
||||
// Restore Redis stock
|
||||
redisService.incrBy(stockKey, 1);
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
|
||||
// 5. 创建订单
|
||||
Order order = new Order();
|
||||
order.setOrderNo("GB" + System.currentTimeMillis() + String.format("%03d", new Random().nextInt(1000)));
|
||||
order.setUserId(userId);
|
||||
order.setProductId(gb.getProductId());
|
||||
order.setQuantity(1);
|
||||
order.setTotalPrice(gb.getGroupPrice());
|
||||
order.setStatus(1); // 待支付
|
||||
order.setOrderType(3); // 拼团订单
|
||||
order.setGroupBuyingGroupId(group.getId());
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 6. 创建成员记录
|
||||
GroupBuyingMember member = new GroupBuyingMember();
|
||||
member.setGroupId(group.getId());
|
||||
member.setUserId(userId);
|
||||
member.setOrderId(order.getId());
|
||||
member.setStatus(1);
|
||||
groupBuyingMemberRepository.save(member);
|
||||
|
||||
// 7. 更新团组人数
|
||||
groupBuyingGroupRepository.incrementCurrentMembers(group.getId());
|
||||
|
||||
// Refresh group from DB
|
||||
group = groupBuyingGroupRepository.findById(group.getId()).orElse(group);
|
||||
|
||||
// 8. 检查是否满员 → 成团
|
||||
if (group.getCurrentMembers() >= group.getRequiredMembers()) {
|
||||
groupBuyingGroupRepository.updateStatusAndCompletedAt(group.getId(), 2, LocalDateTime.now());
|
||||
// Update all members status to SUCCESS
|
||||
groupBuyingMemberRepository.updateStatusByGroupId(group.getId(), 2);
|
||||
log.info("拼团成功: groupId={}, groupNo={}", group.getId(), group.getGroupNo());
|
||||
}
|
||||
|
||||
// Add to Redis members set
|
||||
redisService.sAdd(GB_MEMBERS_PREFIX + group.getId(), userId.toString());
|
||||
redisService.expire(GB_MEMBERS_PREFIX + group.getId(), 7, TimeUnit.DAYS);
|
||||
|
||||
GroupBuyingDTO.JoinResultDTO resultDTO = new GroupBuyingDTO.JoinResultDTO();
|
||||
resultDTO.setSuccess(true);
|
||||
resultDTO.setMessage(joinDTO.getGroupId() != null ? "加入拼团成功" : "开团成功");
|
||||
resultDTO.setGroupId(group.getId());
|
||||
resultDTO.setGroupNo(group.getGroupNo());
|
||||
resultDTO.setOrderId(order.getId());
|
||||
|
||||
log.info("用户{}参与拼团成功: activityId={}, groupId={}, orderId={}", userId, activityId, group.getId(), order.getId());
|
||||
return resultDTO;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancelMembership(Long groupId, Long userId) {
|
||||
log.info("退出拼团: groupId={}, userId={}", groupId, userId);
|
||||
|
||||
GroupBuyingGroup group = groupBuyingGroupRepository.findById(groupId)
|
||||
.orElseThrow(() -> new RuntimeException("团组不存在"));
|
||||
|
||||
if (group.getStatus() != 1) {
|
||||
throw new RuntimeException("已成团或已失败的团组不能退出");
|
||||
}
|
||||
|
||||
GroupBuyingMember member = groupBuyingMemberRepository.findByGroupIdAndUserId(groupId, userId)
|
||||
.orElseThrow(() -> new RuntimeException("您不在该团组中"));
|
||||
|
||||
if (member.getStatus() == 3) {
|
||||
throw new RuntimeException("您已退出该团组");
|
||||
}
|
||||
|
||||
// 更新成员状态
|
||||
member.setStatus(3);
|
||||
groupBuyingMemberRepository.save(member);
|
||||
|
||||
// 更新团组人数
|
||||
groupBuyingGroupRepository.decrementCurrentMembers(groupId);
|
||||
|
||||
// 恢复库存
|
||||
Long activityId = group.getGroupBuyingId();
|
||||
groupBuyingRepository.increaseStock(activityId, 1);
|
||||
redisService.incrBy(GB_STOCK_PREFIX + activityId, 1);
|
||||
|
||||
// 取消订单
|
||||
if (member.getOrderId() != null) {
|
||||
Optional<Order> orderOpt = orderRepository.findById(member.getOrderId());
|
||||
if (orderOpt.isPresent()) {
|
||||
Order order = orderOpt.get();
|
||||
if (order.getStatus() == 1) {
|
||||
order.setStatus(5);
|
||||
order.setRemark("退出拼团,订单取消");
|
||||
orderRepository.save(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from Redis set
|
||||
redisService.sRem(GB_MEMBERS_PREFIX + groupId, userId.toString());
|
||||
|
||||
log.info("退出拼团成功: groupId={}, userId={}", groupId, userId);
|
||||
}
|
||||
|
||||
// ========== 定时任务 ==========
|
||||
|
||||
@Scheduled(fixedRate = 60000) // 每分钟检查
|
||||
@Transactional
|
||||
public void scheduledCheckExpiredGroups() {
|
||||
List<GroupBuyingGroup> expiredGroups = groupBuyingGroupRepository.findExpiredGroups(LocalDateTime.now());
|
||||
for (GroupBuyingGroup group : expiredGroups) {
|
||||
try {
|
||||
handleExpiredGroup(group);
|
||||
} catch (Exception e) {
|
||||
log.error("处理超时团组失败: groupId={}", group.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void handleExpiredGroup(GroupBuyingGroup group) {
|
||||
log.info("处理超时团组: groupId={}, groupNo={}", group.getId(), group.getGroupNo());
|
||||
|
||||
// Mark group as FAILED
|
||||
groupBuyingGroupRepository.updateStatusAndCompletedAt(group.getId(), 3, LocalDateTime.now());
|
||||
|
||||
// Get active members
|
||||
List<GroupBuyingMember> members = groupBuyingMemberRepository.findByGroupIdAndStatus(group.getId(), 1);
|
||||
|
||||
for (GroupBuyingMember member : members) {
|
||||
// Update member status
|
||||
member.setStatus(3);
|
||||
groupBuyingMemberRepository.save(member);
|
||||
|
||||
// Restore stock
|
||||
groupBuyingRepository.increaseStock(group.getGroupBuyingId(), 1);
|
||||
redisService.incrBy(GB_STOCK_PREFIX + group.getGroupBuyingId(), 1);
|
||||
|
||||
// Cancel order
|
||||
if (member.getOrderId() != null) {
|
||||
Optional<Order> orderOpt = orderRepository.findById(member.getOrderId());
|
||||
if (orderOpt.isPresent()) {
|
||||
Order order = orderOpt.get();
|
||||
if (order.getStatus() == 1) {
|
||||
order.setStatus(5);
|
||||
order.setRemark("拼团超时,订单自动取消");
|
||||
orderRepository.save(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean Redis
|
||||
redisService.delete(GB_MEMBERS_PREFIX + group.getId());
|
||||
|
||||
log.info("超时团组处理完成: groupId={}", group.getId());
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 300000) // 每5分钟
|
||||
@Transactional
|
||||
public void scheduledUpdateStatus() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// Activate pending activities whose start time has passed
|
||||
List<GroupBuying> upcoming = groupBuyingRepository.findUpcomingGroupBuyings(now);
|
||||
// These are status=1 and startTime > now, so nothing to activate here
|
||||
|
||||
// Actually find activities that should be active: status=1, startTime <= now, endTime > now
|
||||
List<GroupBuying> all = groupBuyingRepository.findAll();
|
||||
for (GroupBuying gb : all) {
|
||||
if (gb.getStatus() == 1 && !now.isBefore(gb.getStartTime()) && now.isBefore(gb.getEndTime())) {
|
||||
groupBuyingRepository.updateStatus(gb.getId(), 2);
|
||||
preloadStock(gb);
|
||||
log.info("拼团活动已激活: id={}", gb.getId());
|
||||
} else if (gb.getStatus() == 2 && !now.isBefore(gb.getEndTime())) {
|
||||
groupBuyingRepository.updateStatus(gb.getId(), 3);
|
||||
log.info("拼团活动已结束: id={}", gb.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void preloadStock(GroupBuying gb) {
|
||||
String stockKey = GB_STOCK_PREFIX + gb.getId();
|
||||
redisService.setString(stockKey, String.valueOf(gb.getRemainingStock()));
|
||||
long ttl = java.time.Duration.between(LocalDateTime.now(), gb.getEndTime()).getSeconds() + 3600;
|
||||
redisService.expire(stockKey, ttl, TimeUnit.SECONDS);
|
||||
log.info("拼团库存预热: id={}, stock={}", gb.getId(), gb.getRemainingStock());
|
||||
}
|
||||
|
||||
public void preloadAllActiveStock() {
|
||||
List<GroupBuying> activeList = groupBuyingRepository.findActiveGroupBuyings(LocalDateTime.now());
|
||||
for (GroupBuying gb : activeList) {
|
||||
preloadStock(gb);
|
||||
}
|
||||
log.info("所有拼团活动库存预热完成, count={}", activeList.size());
|
||||
}
|
||||
|
||||
// ========== 构建 DTO ==========
|
||||
|
||||
private GroupBuyingDTO buildDTO(GroupBuying gb) {
|
||||
GroupBuyingDTO dto = new GroupBuyingDTO();
|
||||
dto.setId(gb.getId());
|
||||
dto.setProductId(gb.getProductId());
|
||||
dto.setGroupPrice(gb.getGroupPrice());
|
||||
dto.setRequiredMembers(gb.getRequiredMembers());
|
||||
dto.setDurationMinutes(gb.getDurationMinutes());
|
||||
dto.setTotalStock(gb.getTotalStock());
|
||||
dto.setRemainingStock(gb.getRemainingStock());
|
||||
dto.setMaxPerUser(gb.getMaxPerUser());
|
||||
dto.setStatus(gb.getStatus());
|
||||
dto.setStatusDescription(getStatusDescription(gb.getStatus()));
|
||||
dto.setStartTime(gb.getStartTime());
|
||||
dto.setEndTime(gb.getEndTime());
|
||||
dto.setCreatedAt(gb.getCreatedAt());
|
||||
dto.setUpdatedAt(gb.getUpdatedAt());
|
||||
|
||||
// Product info
|
||||
ProductDTO product = productService.getProductById(gb.getProductId());
|
||||
if (product != null) {
|
||||
dto.setProductName(product.getName());
|
||||
dto.setProductImageUrl(product.getImageUrl());
|
||||
dto.setProductPrice(product.getPrice());
|
||||
dto.setDiscount(product.getPrice().subtract(gb.getGroupPrice()));
|
||||
}
|
||||
|
||||
// Active group count
|
||||
long activeGroups = groupBuyingGroupRepository.countByGroupBuyingIdAndStatus(gb.getId(), 1);
|
||||
dto.setActiveGroupCount((int) activeGroups);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private GroupBuyingDTO.GroupInfoDTO buildGroupInfoDTO(GroupBuyingGroup group) {
|
||||
GroupBuyingDTO.GroupInfoDTO dto = new GroupBuyingDTO.GroupInfoDTO();
|
||||
dto.setId(group.getId());
|
||||
dto.setGroupNo(group.getGroupNo());
|
||||
dto.setGroupBuyingId(group.getGroupBuyingId());
|
||||
dto.setLeaderUserId(group.getLeaderUserId());
|
||||
dto.setRequiredMembers(group.getRequiredMembers());
|
||||
dto.setCurrentMembers(group.getCurrentMembers());
|
||||
dto.setStatus(group.getStatus());
|
||||
dto.setStatusDescription(getGroupStatusDescription(group.getStatus()));
|
||||
dto.setExpireTime(group.getExpireTime());
|
||||
dto.setCreatedAt(group.getCreatedAt());
|
||||
dto.setCompletedAt(group.getCompletedAt());
|
||||
|
||||
// Leader info
|
||||
UserDTO leader = userService.getUserById(group.getLeaderUserId());
|
||||
if (leader != null) {
|
||||
dto.setLeaderUsername(leader.getUsername());
|
||||
}
|
||||
|
||||
// Members
|
||||
List<GroupBuyingMember> members = groupBuyingMemberRepository.findByGroupId(group.getId());
|
||||
List<GroupBuyingDTO.MemberDTO> memberDTOs = members.stream()
|
||||
.filter(m -> m.getStatus() != 3) // exclude exited
|
||||
.map(m -> {
|
||||
GroupBuyingDTO.MemberDTO memberDTO = new GroupBuyingDTO.MemberDTO();
|
||||
memberDTO.setId(m.getId());
|
||||
memberDTO.setUserId(m.getUserId());
|
||||
memberDTO.setOrderId(m.getOrderId());
|
||||
memberDTO.setStatus(m.getStatus());
|
||||
memberDTO.setJoinedAt(m.getJoinedAt());
|
||||
UserDTO user = userService.getUserById(m.getUserId());
|
||||
if (user != null) {
|
||||
memberDTO.setUsername(user.getUsername());
|
||||
memberDTO.setAvatar(user.getAvatar());
|
||||
}
|
||||
return memberDTO;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
dto.setMembers(memberDTOs);
|
||||
|
||||
// Activity info
|
||||
GroupBuying gb = groupBuyingRepository.findById(group.getGroupBuyingId()).orElse(null);
|
||||
if (gb != null) {
|
||||
dto.setGroupBuying(buildDTO(gb));
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private String getStatusDescription(Integer status) {
|
||||
if (status == null) return "未知";
|
||||
switch (status) {
|
||||
case 0: return "草稿";
|
||||
case 1: return "未开始";
|
||||
case 2: return "进行中";
|
||||
case 3: return "已结束";
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
private String getGroupStatusDescription(Integer status) {
|
||||
if (status == null) return "未知";
|
||||
switch (status) {
|
||||
case 1: return "拼团中";
|
||||
case 2: return "已成团";
|
||||
case 3: return "已失败";
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public class MessageListenerService {
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private NotificationService notificationService;
|
||||
|
||||
/**
|
||||
* 初始化消息监听器
|
||||
*/
|
||||
@@ -62,31 +65,43 @@ public class MessageListenerService {
|
||||
* 处理订单状态变更
|
||||
*/
|
||||
private void handleOrderStatusChange(Long orderId, Long userId, Integer status, String action) {
|
||||
// 可以在这里实现:
|
||||
// 1. 发送邮件通知
|
||||
// 2. 推送消息
|
||||
// 3. 更新统计数据
|
||||
// 4. 触发其他业务流程
|
||||
if (userId == null) {
|
||||
log.warn("订单状态变更缺少用户ID: orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
String title;
|
||||
String message;
|
||||
String link = "/order/" + orderId;
|
||||
|
||||
switch (action) {
|
||||
case "created":
|
||||
log.info("订单创建通知处理: 订单ID={}", orderId);
|
||||
title = "订单创建成功";
|
||||
message = "您的订单 #" + orderId + " 已创建,请尽快完成支付";
|
||||
break;
|
||||
case "paid":
|
||||
log.info("订单支付通知处理: 订单ID={}", orderId);
|
||||
title = "订单支付成功";
|
||||
message = "您的订单 #" + orderId + " 已支付成功,等待商家发货";
|
||||
break;
|
||||
case "shipped":
|
||||
log.info("订单发货通知处理: 订单ID={}", orderId);
|
||||
title = "订单已发货";
|
||||
message = "您的订单 #" + orderId + " 已发货,请注意查收";
|
||||
break;
|
||||
case "completed":
|
||||
log.info("订单完成通知处理: 订单ID={}", orderId);
|
||||
title = "订单已完成";
|
||||
message = "您的订单 #" + orderId + " 已完成,欢迎评价";
|
||||
break;
|
||||
case "cancelled":
|
||||
log.info("订单取消通知处理: 订单ID={}", orderId);
|
||||
title = "订单已取消";
|
||||
message = "您的订单 #" + orderId + " 已取消";
|
||||
break;
|
||||
default:
|
||||
log.info("未知订单状态变更: {}", action);
|
||||
return;
|
||||
}
|
||||
|
||||
notificationService.createNotification(userId, "order", title, message, link);
|
||||
log.info("订单状态变更通知已创建: 订单ID={}, 操作={}", orderId, action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,20 +127,23 @@ public class MessageListenerService {
|
||||
* 处理秒杀结果
|
||||
*/
|
||||
private void handleFlashSaleResult(Long userId, Long flashSaleId, Boolean success, Map<String, Object> data) {
|
||||
// 可以在这里实现:
|
||||
// 1. 实时通知用户
|
||||
// 2. 统计秒杀数据
|
||||
// 3. 风控分析
|
||||
// 4. 营销推荐
|
||||
if (userId == null) {
|
||||
log.warn("秒杀结果缺少用户ID: flashSaleId={}", flashSaleId);
|
||||
return;
|
||||
}
|
||||
|
||||
String link = "/flashsale/" + flashSaleId;
|
||||
|
||||
if (success) {
|
||||
log.info("秒杀成功处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 发送成功通知
|
||||
sendFlashSaleSuccessNotification(userId, flashSaleId);
|
||||
String title = "秒杀成功";
|
||||
String message = "恭喜您成功抢购秒杀商品,请尽快完成支付!";
|
||||
notificationService.createNotification(userId, "flashsale", title, message, link);
|
||||
log.info("秒杀成功通知已创建: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
} else {
|
||||
log.info("秒杀失败处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 可以推荐其他商品
|
||||
recommendAlternativeProducts(userId, flashSaleId);
|
||||
String title = "秒杀未中";
|
||||
String message = "很遗憾,本次秒杀未能抢购成功,下次再来吧!";
|
||||
notificationService.createNotification(userId, "flashsale", title, message, link);
|
||||
log.info("秒杀失败通知已创建: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,26 +179,9 @@ public class MessageListenerService {
|
||||
* 检查库存预警
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取Long值
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.entity.Notification;
|
||||
import com.org.flashsalesystem.repository.NotificationRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class NotificationService {
|
||||
|
||||
@Autowired
|
||||
private NotificationRepository notificationRepository;
|
||||
|
||||
public void createNotification(Long userId, String type, String title, String message, String link) {
|
||||
Notification notification = new Notification();
|
||||
notification.setUserId(userId);
|
||||
notification.setType(type);
|
||||
notification.setTitle(title);
|
||||
notification.setMessage(message);
|
||||
notification.setLink(link);
|
||||
notification.setRead(false);
|
||||
notificationRepository.save(notification);
|
||||
log.debug("通知已创建: userId={}, type={}, title={}", userId, type, title);
|
||||
}
|
||||
|
||||
public List<Notification> getUserNotifications(Long userId) {
|
||||
return notificationRepository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
public List<Notification> getUserNotificationsByType(Long userId, String type) {
|
||||
return notificationRepository.findByUserIdAndTypeOrderByCreatedAtDesc(userId, type);
|
||||
}
|
||||
|
||||
public long getUnreadCount(Long userId) {
|
||||
return notificationRepository.countByUserIdAndReadFalse(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAsRead(Long notificationId, Long userId) {
|
||||
notificationRepository.markAsRead(notificationId, userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAllAsRead(Long userId) {
|
||||
notificationRepository.markAllAsRead(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void clearAll(Long userId) {
|
||||
notificationRepository.deleteByUserId(userId);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ public class OrderService {
|
||||
private UserService userService;
|
||||
@Autowired
|
||||
private UserAddressRepository userAddressRepository;
|
||||
@Autowired
|
||||
private FlashSaleService flashSaleService;
|
||||
@Autowired
|
||||
private GroupBuyingService groupBuyingService;
|
||||
|
||||
/**
|
||||
* 创建普通订单
|
||||
@@ -219,14 +223,20 @@ public class OrderService {
|
||||
* 获取用户订单列表
|
||||
*/
|
||||
public Map<String, Object> getUserOrders(Long userId, OrderDTO.QueryDTO queryDTO) {
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
// 根据查询条件获取订单
|
||||
if (queryDTO.getStatus() != null) {
|
||||
if (queryDTO.getStatus() != null && queryDTO.getOrderType() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndStatus(userId, queryDTO.getStatus(), pageable);
|
||||
} else if (queryDTO.getOrderType() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndOrderType(userId, queryDTO.getOrderType(), pageable);
|
||||
} else if (queryDTO.getStatus() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndStatus(userId, queryDTO.getStatus(), pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findByUserId(userId, pageable);
|
||||
@@ -259,9 +269,11 @@ public class OrderService {
|
||||
* 获取所有订单列表(管理员)
|
||||
*/
|
||||
public Map<String, Object> getAllOrders(OrderDTO.QueryDTO queryDTO) {
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
@@ -402,6 +414,21 @@ public class OrderService {
|
||||
// 恢复库存
|
||||
productService.updateStock(order.getProductId(), order.getQuantity(), "increase");
|
||||
|
||||
// 秒杀订单额外恢复秒杀库存
|
||||
if (order.getOrderType() != null && order.getOrderType() == 2) {
|
||||
flashSaleService.restoreFlashSaleStock(order.getFlashSaleId(), order.getProductId(), order.getCreatedAt(),
|
||||
order.getUserId(), order.getQuantity());
|
||||
}
|
||||
|
||||
// 拼团订单额外处理
|
||||
if (order.getOrderType() != null && order.getOrderType() == 3 && order.getGroupBuyingGroupId() != null) {
|
||||
try {
|
||||
groupBuyingService.cancelMembership(order.getGroupBuyingGroupId(), order.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("拼团退出处理失败: orderId={}, error={}", orderId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
cacheOrderInfo(order);
|
||||
|
||||
@@ -569,6 +596,7 @@ public class OrderService {
|
||||
orderMap.put("groupNo", order.getGroupNo() == null ? "" : order.getGroupNo());
|
||||
orderMap.put("userId", order.getUserId().toString());
|
||||
orderMap.put("productId", order.getProductId().toString());
|
||||
orderMap.put("flashSaleId", order.getFlashSaleId() == null ? "" : order.getFlashSaleId().toString());
|
||||
orderMap.put("quantity", order.getQuantity().toString());
|
||||
orderMap.put("totalPrice", order.getTotalPrice().toString());
|
||||
orderMap.put("status", order.getStatus().toString());
|
||||
@@ -603,6 +631,8 @@ public class OrderService {
|
||||
orderDTO.setGroupNo((String) orderMap.get("groupNo"));
|
||||
orderDTO.setUserId(Long.valueOf((String) orderMap.get("userId")));
|
||||
orderDTO.setProductId(Long.valueOf((String) orderMap.get("productId")));
|
||||
String flashSaleId = (String) orderMap.get("flashSaleId");
|
||||
if (flashSaleId != null && !flashSaleId.isEmpty()) { orderDTO.setFlashSaleId(Long.valueOf(flashSaleId)); }
|
||||
orderDTO.setQuantity(Integer.valueOf((String) orderMap.get("quantity")));
|
||||
orderDTO.setTotalPrice(new BigDecimal((String) orderMap.get("totalPrice")));
|
||||
orderDTO.setStatus(Integer.valueOf((String) orderMap.get("status")));
|
||||
@@ -849,6 +879,8 @@ public class OrderService {
|
||||
return "普通订单";
|
||||
case 2:
|
||||
return "秒杀订单";
|
||||
case 3:
|
||||
return "拼团订单";
|
||||
default:
|
||||
return "未知类型";
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import com.org.flashsalesystem.dto.ProductReviewDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.OrderItem;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.entity.ProductReview;
|
||||
import com.org.flashsalesystem.repository.OrderItemRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import com.org.flashsalesystem.repository.ProductReviewRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -15,6 +17,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -30,6 +33,9 @@ public class ProductReviewService {
|
||||
@Autowired
|
||||
private OrderItemRepository orderItemRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@@ -68,7 +74,7 @@ public class ProductReviewService {
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
Double average = productReviewRepository.findAverageRatingByProductId(productId);
|
||||
Long total = productReviewRepository.countByProductId(productId);
|
||||
Long total = productReviewRepository.countByProductIdAndStatus(productId, 1);
|
||||
return new ProductReviewDTO.SummaryDTO(average == null ? 0.0 : average, total, reviews);
|
||||
}
|
||||
|
||||
@@ -89,12 +95,39 @@ public class ProductReviewService {
|
||||
return toDTO(review);
|
||||
}
|
||||
|
||||
public ProductReviewDTO.CheckDTO checkReviewStatus(Long orderId, Long productId) {
|
||||
ProductReviewDTO.CheckDTO checkDTO = new ProductReviewDTO.CheckDTO();
|
||||
Optional<ProductReview> review = productReviewRepository.findByOrderIdAndProductId(orderId, productId);
|
||||
checkDTO.setReviewed(review.isPresent());
|
||||
review.ifPresent(r -> checkDTO.setReview(toDTO(r)));
|
||||
return checkDTO;
|
||||
}
|
||||
|
||||
public List<ProductReviewDTO> getUserReviews(Long userId) {
|
||||
return productReviewRepository.findByUserIdOrderByCreatedAtDesc(userId)
|
||||
.stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<ProductReviewDTO> getOrderReviews(Long orderId) {
|
||||
return productReviewRepository.findByOrderId(orderId)
|
||||
.stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private ProductReviewDTO toDTO(ProductReview review) {
|
||||
ProductReviewDTO dto = new ProductReviewDTO();
|
||||
BeanUtils.copyProperties(review, dto);
|
||||
UserDTO user = userService.getUserById(review.getUserId());
|
||||
dto.setUsername(user != null ? user.getUsername() : "匿名用户");
|
||||
dto.setStatusText(review.getStatus() != null && review.getStatus() == 1 ? "显示" : "隐藏");
|
||||
Optional<Product> product = productRepository.findById(review.getProductId());
|
||||
if (product.isPresent()) {
|
||||
dto.setProductName(product.get().getName());
|
||||
dto.setProductImage(product.get().getImageUrl());
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +120,11 @@ public class ProductService {
|
||||
return (Map<String, Object>) cachedResult;
|
||||
}
|
||||
|
||||
// 限制分页大小
|
||||
int pageSize = Math.min(queryDTO.getSize(), 100);
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), pageSize, sort);
|
||||
|
||||
Integer status = queryDTO.getStatus() != null ? queryDTO.getStatus() : 1;
|
||||
String keyword = queryDTO.getKeyword() != null && !queryDTO.getKeyword().trim().isEmpty()
|
||||
|
||||
@@ -2,6 +2,8 @@ server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
session:
|
||||
timeout: 30m
|
||||
|
||||
spring:
|
||||
application:
|
||||
|
||||
32
src/main/resources/lua/groupbuying_stock.lua
Normal file
32
src/main/resources/lua/groupbuying_stock.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
-- 拼团库存扣减Lua脚本
|
||||
-- 功能:原子性地检查库存并扣减,防止超卖
|
||||
-- 参数:KEYS[1] = 库存key, ARGV[1] = 扣减数量
|
||||
-- 返回值:成功返回剩余库存,失败返回负数
|
||||
|
||||
local stock_key = KEYS[1]
|
||||
local quantity_str = ARGV[1]
|
||||
local quantity = tonumber(quantity_str)
|
||||
|
||||
if quantity == nil or quantity <= 0 then
|
||||
return -3
|
||||
end
|
||||
|
||||
local current_stock = redis.call('GET', stock_key)
|
||||
|
||||
if current_stock == false then
|
||||
return -1
|
||||
end
|
||||
|
||||
local current_stock_num = tonumber(current_stock)
|
||||
|
||||
if current_stock_num == nil then
|
||||
return -1
|
||||
end
|
||||
|
||||
if current_stock_num < quantity then
|
||||
return -2
|
||||
end
|
||||
|
||||
local remaining_stock = redis.call('DECRBY', stock_key, quantity)
|
||||
|
||||
return remaining_stock
|
||||
@@ -1,22 +1,17 @@
|
||||
-- 演示账号快速创建脚本
|
||||
-- 密码都是明文对应的值:demo1/demo2/admin的密码分别是123456/123456/admin123
|
||||
-- 演示账号初始化脚本
|
||||
-- 账号:demo1 / 123456,demo2 / 123456,admin / admin123
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 插入演示用户(密码已加密)
|
||||
INSERT INTO users (username, password, email, phone, role, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo1@example.com', '13800138001', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo2@example.com', '13800138002', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 'ADMIN', 1,
|
||||
NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE username = VALUES(username),
|
||||
email = VALUES(email),
|
||||
phone = VALUES(phone),
|
||||
updated_at = NOW();
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
INSERT INTO users (username, password, email, phone, avatar, role, status, created_at, updated_at)
|
||||
VALUES
|
||||
('demo1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo1@example.com', '13800138001', '', 'USER', 1, NOW(), NOW()),
|
||||
('demo2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo2@example.com', '13800138002', '', 'USER', 1, NOW(), NOW()),
|
||||
('admin', '$2a$10$DOwVJZHH.5PkZKJKJKJKJOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', '', 'ADMIN', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
email = VALUES(email),
|
||||
phone = VALUES(phone),
|
||||
avatar = VALUES(avatar),
|
||||
role = VALUES(role),
|
||||
status = VALUES(status),
|
||||
updated_at = NOW();
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
-- 修复演示账号密码问题
|
||||
-- 使用正确的BCrypt加密密码
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 删除现有的演示用户(如果存在)
|
||||
DELETE
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 插入正确的演示用户
|
||||
-- demo1/demo2 密码: 123456
|
||||
-- admin 密码: admin123
|
||||
INSERT INTO users (username, password, email, phone, role, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo1@example.com', '13800138001', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo2@example.com', '13800138002', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$DOwVJZHH.5PkZKJKJKJKJOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 'ADMIN', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 显示密码提示
|
||||
SELECT '演示账号密码信息:' as info;
|
||||
SELECT 'demo1 / 123456' as account_info
|
||||
UNION ALL
|
||||
SELECT 'demo2 / 123456'
|
||||
UNION ALL
|
||||
SELECT 'admin / admin123';
|
||||
@@ -1,31 +1,32 @@
|
||||
-- 秒杀系统数据库表结构
|
||||
-- 创建数据库和所有必要的表
|
||||
-- 秒杀系统数据库结构
|
||||
-- 说明:本脚本只负责数据库对象定义,不包含演示数据。
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS flash_sale_db
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 创建数据库
|
||||
CREATE DATABASE IF NOT EXISTS flash_sale_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE flash_sale_db;
|
||||
|
||||
-- ================================
|
||||
-- 1. 用户表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
|
||||
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||
password VARCHAR(255) NOT NULL COMMENT '密码(加密)',
|
||||
email VARCHAR(100) COMMENT '邮箱',
|
||||
phone VARCHAR(20) COMMENT '手机号',
|
||||
avatar VARCHAR(500) COMMENT '头像',
|
||||
role VARCHAR(20) DEFAULT 'USER' COMMENT '角色:ADMIN/USER',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'USER' COMMENT '角色:ADMIN/USER',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
|
||||
last_login TIMESTAMP NULL COMMENT '最后登录时间',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
INDEX idx_users_username (username),
|
||||
INDEX idx_users_email (email),
|
||||
INDEX idx_users_phone (phone),
|
||||
INDEX idx_users_status (status),
|
||||
INDEX idx_users_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='用户表';
|
||||
@@ -33,8 +34,7 @@ CREATE TABLE IF NOT EXISTS users
|
||||
-- ================================
|
||||
-- 2. 商品表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS products
|
||||
(
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
|
||||
name VARCHAR(200) NOT NULL COMMENT '商品名称',
|
||||
description TEXT COMMENT '商品描述',
|
||||
@@ -42,15 +42,15 @@ CREATE TABLE IF NOT EXISTS products
|
||||
category VARCHAR(100) COMMENT '商品分类',
|
||||
stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
|
||||
image_url VARCHAR(500) COMMENT '商品图片URL',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-上架,0-下架',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_price (price),
|
||||
INDEX idx_stock (stock),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-上架,0-下架',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
INDEX idx_products_name (name),
|
||||
INDEX idx_products_category (category),
|
||||
INDEX idx_products_price (price),
|
||||
INDEX idx_products_stock (stock),
|
||||
INDEX idx_products_status (status),
|
||||
INDEX idx_products_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='商品表';
|
||||
@@ -58,42 +58,41 @@ CREATE TABLE IF NOT EXISTS products
|
||||
-- ================================
|
||||
-- 3. 秒杀活动表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS flash_sales
|
||||
(
|
||||
CREATE TABLE IF NOT EXISTS flash_sales (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '秒杀活动ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
flash_price DECIMAL(10, 2) NOT NULL COMMENT '秒杀价格',
|
||||
flash_stock INT NOT NULL COMMENT '秒杀库存',
|
||||
start_time TIMESTAMP NOT NULL COMMENT '开始时间',
|
||||
end_time TIMESTAMP NOT NULL COMMENT '结束时间',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-未开始,2-进行中,3-已结束',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_product_id (product_id),
|
||||
INDEX idx_start_time (start_time),
|
||||
INDEX idx_end_time (end_time),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-未开始,2-进行中,3-已结束',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
CONSTRAINT fk_flash_sales_product FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_flash_sales_product_id (product_id),
|
||||
INDEX idx_flash_sales_start_time (start_time),
|
||||
INDEX idx_flash_sales_end_time (end_time),
|
||||
INDEX idx_flash_sales_status (status),
|
||||
INDEX idx_flash_sales_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='秒杀活动表';
|
||||
|
||||
-- ================================
|
||||
-- 4. 订单表
|
||||
-- 4. 订单主表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS orders
|
||||
(
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID',
|
||||
order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '订单号',
|
||||
group_no VARCHAR(64) COMMENT '聚合订单号',
|
||||
group_no VARCHAR(64) COMMENT '聚合订单号(兼容旧数据)',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量',
|
||||
total_price DECIMAL(10, 2) NOT NULL COMMENT '总价',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消',
|
||||
order_type TINYINT DEFAULT 1 COMMENT '订单类型:1-普通订单,2-秒杀订单',
|
||||
product_id BIGINT NOT NULL COMMENT '兼容字段:主商品ID',
|
||||
flash_sale_id BIGINT COMMENT '秒杀活动ID',
|
||||
group_buying_group_id BIGINT COMMENT '拼团团组ID',
|
||||
quantity INT NOT NULL DEFAULT 1 COMMENT '兼容字段:总购买数量',
|
||||
total_price DECIMAL(10, 2) NOT NULL COMMENT '订单总价',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消',
|
||||
order_type TINYINT NOT NULL DEFAULT 1 COMMENT '订单类型:1-普通订单,2-秒杀订单',
|
||||
receiver_name VARCHAR(100) COMMENT '收货人',
|
||||
receiver_phone VARCHAR(20) COMMENT '收货手机号',
|
||||
receiver_address VARCHAR(255) COMMENT '收货地址',
|
||||
@@ -102,30 +101,26 @@ CREATE TABLE IF NOT EXISTS orders
|
||||
paid_at TIMESTAMP NULL COMMENT '支付时间',
|
||||
shipped_at TIMESTAMP NULL COMMENT '发货时间',
|
||||
completed_at TIMESTAMP NULL COMMENT '完成时间',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_product_id (product_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_order_type (order_type),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_user_product (user_id, product_id)
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_orders_product FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_orders_order_no (order_no),
|
||||
INDEX idx_orders_group_no (group_no),
|
||||
INDEX idx_orders_user_id (user_id),
|
||||
INDEX idx_orders_product_id (product_id),
|
||||
INDEX idx_orders_flash_sale_id (flash_sale_id),
|
||||
INDEX idx_orders_status (status),
|
||||
INDEX idx_orders_order_type (order_type),
|
||||
INDEX idx_orders_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='订单表';
|
||||
|
||||
|
||||
|
||||
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='订单主表';
|
||||
|
||||
-- ================================
|
||||
-- 5. 订单明细表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS order_items
|
||||
(
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '明细ID',
|
||||
order_id BIGINT NOT NULL COMMENT '主订单ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
@@ -134,10 +129,9 @@ CREATE TABLE IF NOT EXISTS order_items
|
||||
price DECIMAL(10, 2) NOT NULL COMMENT '下单单价',
|
||||
quantity INT NOT NULL COMMENT '购买数量',
|
||||
subtotal DECIMAL(10, 2) NOT NULL COMMENT '小计',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_order_items_order_id (order_id),
|
||||
INDEX idx_order_items_product_id (product_id)
|
||||
) ENGINE = InnoDB
|
||||
@@ -147,8 +141,7 @@ CREATE TABLE IF NOT EXISTS order_items
|
||||
-- ================================
|
||||
-- 6. 用户地址表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS user_addresses
|
||||
(
|
||||
CREATE TABLE IF NOT EXISTS user_addresses (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '地址ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
name VARCHAR(100) NOT NULL COMMENT '收货人',
|
||||
@@ -157,13 +150,12 @@ CREATE TABLE IF NOT EXISTS user_addresses
|
||||
city VARCHAR(50) COMMENT '城市',
|
||||
district VARCHAR(50) COMMENT '区县',
|
||||
address VARCHAR(255) NOT NULL COMMENT '详细地址',
|
||||
is_default TINYINT DEFAULT 0 COMMENT '是否默认地址',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
INDEX idx_address_user_id (user_id),
|
||||
INDEX idx_address_default (is_default)
|
||||
is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认地址',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
CONSTRAINT fk_user_addresses_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
INDEX idx_user_addresses_user_id (user_id),
|
||||
INDEX idx_user_addresses_default (is_default)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='用户地址表';
|
||||
@@ -171,61 +163,127 @@ CREATE TABLE IF NOT EXISTS user_addresses
|
||||
-- ================================
|
||||
-- 7. 商品评价表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS product_reviews
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '评价ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
order_id BIGINT NOT NULL COMMENT '订单ID',
|
||||
rating TINYINT NOT NULL DEFAULT 5 COMMENT '评分',
|
||||
content TEXT NOT NULL COMMENT '评价内容',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-显示,0-隐藏',
|
||||
CREATE TABLE IF NOT EXISTS product_reviews (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '评价ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
order_id BIGINT NOT NULL COMMENT '订单ID',
|
||||
rating TINYINT NOT NULL DEFAULT 5 COMMENT '评分',
|
||||
content TEXT NOT NULL COMMENT '评价内容',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-显示,0-隐藏',
|
||||
admin_reply TEXT COMMENT '管理员回复',
|
||||
replied_at TIMESTAMP NULL COMMENT '回复时间',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE,
|
||||
INDEX idx_review_product_id (product_id),
|
||||
INDEX idx_review_user_id (user_id),
|
||||
UNIQUE KEY uk_review_order_user (order_id, user_id)
|
||||
replied_at TIMESTAMP NULL COMMENT '回复时间',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
CONSTRAINT fk_product_reviews_product FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_product_reviews_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_product_reviews_order FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_review_order_user_product (order_id, user_id, product_id),
|
||||
INDEX idx_product_reviews_product_id (product_id),
|
||||
INDEX idx_product_reviews_user_id (user_id),
|
||||
INDEX idx_product_reviews_status (status)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='商品评价表';
|
||||
|
||||
|
||||
|
||||
-- ================================
|
||||
-- 8. 用户收藏表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS user_favorites
|
||||
(
|
||||
CREATE TABLE IF NOT EXISTS user_favorites (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '收藏ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
CONSTRAINT fk_user_favorites_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_user_favorites_product FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_favorite_user_product (user_id, product_id),
|
||||
INDEX idx_favorite_user_id (user_id),
|
||||
INDEX idx_favorite_product_id (product_id)
|
||||
INDEX idx_user_favorites_user_id (user_id),
|
||||
INDEX idx_user_favorites_product_id (product_id)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='用户收藏表';
|
||||
|
||||
-- ================================
|
||||
-- 9. 创建视图(可选)
|
||||
-- 9. 视图
|
||||
-- ================================
|
||||
-- ================================
|
||||
-- 9. 拼团活动表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS group_buying (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '拼团活动ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
group_price DECIMAL(10, 2) NOT NULL COMMENT '拼团价格',
|
||||
required_members INT NOT NULL DEFAULT 2 COMMENT '成团人数',
|
||||
duration_minutes INT NOT NULL DEFAULT 1440 COMMENT '拼团有效期(分钟)',
|
||||
total_stock INT NOT NULL COMMENT '总库存',
|
||||
remaining_stock INT NOT NULL COMMENT '剩余库存',
|
||||
max_per_user INT NOT NULL DEFAULT 1 COMMENT '每人限购',
|
||||
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-草稿 1-未开始 2-进行中 3-已结束',
|
||||
start_time DATETIME NOT NULL COMMENT '开始时间',
|
||||
end_time DATETIME NOT NULL COMMENT '结束时间',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
CONSTRAINT fk_group_buying_product FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_group_buying_product_id (product_id),
|
||||
INDEX idx_group_buying_status (status),
|
||||
INDEX idx_group_buying_start_time (start_time),
|
||||
INDEX idx_group_buying_end_time (end_time)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='拼团活动表';
|
||||
|
||||
-- 活跃秒杀活动视图
|
||||
-- ================================
|
||||
-- 10. 拼团团组表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS group_buying_group (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '团组ID',
|
||||
group_no VARCHAR(64) NOT NULL UNIQUE COMMENT '团号',
|
||||
group_buying_id BIGINT NOT NULL COMMENT '关联拼团活动',
|
||||
leader_user_id BIGINT NOT NULL COMMENT '团长用户ID',
|
||||
required_members INT NOT NULL COMMENT '需要人数',
|
||||
current_members INT NOT NULL DEFAULT 1 COMMENT '当前人数',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-拼团中 2-已成团 3-已失败(超时)',
|
||||
expire_time DATETIME NOT NULL COMMENT '过期时间',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
completed_at TIMESTAMP NULL COMMENT '成团时间',
|
||||
CONSTRAINT fk_gbg_group_buying FOREIGN KEY (group_buying_id) REFERENCES group_buying (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_gbg_leader FOREIGN KEY (leader_user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
INDEX idx_gbg_group_no (group_no),
|
||||
INDEX idx_gbg_group_buying_id (group_buying_id),
|
||||
INDEX idx_gbg_status (status),
|
||||
INDEX idx_gbg_expire_time (expire_time)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='拼团团组表';
|
||||
|
||||
-- ================================
|
||||
-- 11. 拼团成员表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS group_buying_member (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '成员ID',
|
||||
group_id BIGINT NOT NULL COMMENT '关联团组',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
order_id BIGINT COMMENT '关联订单',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-已加入 2-已成团 3-已退出',
|
||||
joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
|
||||
CONSTRAINT fk_gbm_group FOREIGN KEY (group_id) REFERENCES group_buying_group (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_gbm_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_group_user (group_id, user_id),
|
||||
INDEX idx_gbm_group_id (group_id),
|
||||
INDEX idx_gbm_user_id (user_id),
|
||||
INDEX idx_gbm_order_id (order_id)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='拼团成员表';
|
||||
|
||||
-- ================================
|
||||
-- 12. 视图
|
||||
-- ================================
|
||||
CREATE OR REPLACE VIEW active_flash_sales AS
|
||||
SELECT fs.id,
|
||||
fs.product_id,
|
||||
p.name as product_name,
|
||||
p.price as original_price,
|
||||
p.name AS product_name,
|
||||
p.price AS original_price,
|
||||
fs.flash_price,
|
||||
fs.flash_stock,
|
||||
fs.start_time,
|
||||
@@ -233,35 +291,20 @@ SELECT fs.id,
|
||||
fs.status,
|
||||
p.image_url
|
||||
FROM flash_sales fs
|
||||
JOIN products p ON fs.product_id = p.id
|
||||
JOIN products p ON fs.product_id = p.id
|
||||
WHERE fs.status = 2
|
||||
AND fs.start_time <= NOW()
|
||||
AND fs.end_time > NOW()
|
||||
AND p.status = 1;
|
||||
|
||||
-- 订单统计视图
|
||||
CREATE OR REPLACE VIEW order_statistics AS
|
||||
SELECT DATE(created_at) as order_date,
|
||||
COUNT(*) as total_orders,
|
||||
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as pending_orders,
|
||||
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as paid_orders,
|
||||
SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as completed_orders,
|
||||
SUM(CASE WHEN order_type = 2 THEN 1 ELSE 0 END) as flash_sale_orders,
|
||||
SUM(total_price) as total_amount
|
||||
SELECT DATE(created_at) AS order_date,
|
||||
COUNT(*) AS total_orders,
|
||||
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS pending_orders,
|
||||
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS paid_orders,
|
||||
SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) AS completed_orders,
|
||||
SUM(CASE WHEN order_type = 2 THEN 1 ELSE 0 END) AS flash_sale_orders,
|
||||
SUM(total_price) AS total_amount
|
||||
FROM orders
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY order_date DESC;
|
||||
|
||||
-- ================================
|
||||
-- 6. 显示表结构
|
||||
-- ================================
|
||||
SHOW TABLES;
|
||||
|
||||
-- 显示表结构信息
|
||||
SELECT TABLE_NAME as '表名',
|
||||
TABLE_COMMENT as '表注释',
|
||||
TABLE_ROWS as '估计行数'
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = 'flash_sale_db'
|
||||
AND TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY TABLE_NAME;
|
||||
|
||||
@@ -1,161 +1,126 @@
|
||||
-- 秒杀系统测试数据SQL脚本
|
||||
-- 包含演示账号、测试商品、秒杀活动等数据
|
||||
-- 测试业务数据初始化脚本
|
||||
-- 依赖:请先执行 schema.sql 和 demo-users.sql
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS flash_sale_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 清理现有数据(谨慎使用)
|
||||
-- DELETE FROM orders WHERE id > 0;
|
||||
-- DELETE FROM flash_sales WHERE id > 0;
|
||||
-- DELETE FROM products WHERE id > 0;
|
||||
-- DELETE FROM users WHERE id > 0;
|
||||
|
||||
-- 重置自增ID
|
||||
-- ALTER TABLE users AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE products AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE flash_sales AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE orders AUTO_INCREMENT = 1;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
DELETE FROM user_favorites;
|
||||
DELETE FROM product_reviews;
|
||||
DELETE FROM user_addresses;
|
||||
DELETE FROM order_items;
|
||||
DELETE FROM orders;
|
||||
DELETE FROM flash_sales;
|
||||
DELETE FROM products;
|
||||
DELETE FROM users WHERE username LIKE 'testuser%';
|
||||
ALTER TABLE products AUTO_INCREMENT = 1;
|
||||
ALTER TABLE flash_sales AUTO_INCREMENT = 1;
|
||||
ALTER TABLE orders AUTO_INCREMENT = 1;
|
||||
ALTER TABLE order_items AUTO_INCREMENT = 1;
|
||||
ALTER TABLE user_addresses AUTO_INCREMENT = 1;
|
||||
ALTER TABLE product_reviews AUTO_INCREMENT = 1;
|
||||
ALTER TABLE user_favorites AUTO_INCREMENT = 1;
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ================================
|
||||
-- 1. 插入测试用户数据
|
||||
-- 1. 测试用户
|
||||
-- ================================
|
||||
|
||||
INSERT INTO users (username, password, email, phone, role, status, created_at, updated_at)
|
||||
INSERT INTO users (username, password, email, phone, avatar, role, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 演示账号(密码都是明文,实际应用中应该加密)
|
||||
('demo1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo1@example.com', '13800138001', 'USER', 1, NOW(),
|
||||
NOW()),
|
||||
('demo2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo2@example.com', '13800138002', 'USER', 1, NOW(),
|
||||
NOW()),
|
||||
('admin', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 'ADMIN', 1, NOW(),
|
||||
NOW()),
|
||||
|
||||
-- 普通测试用户
|
||||
('testuser1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test1@example.com', '13800138003', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test2@example.com', '13800138004', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser3', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test3@example.com', '13800138005', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser4', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test4@example.com', '13800138006', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser5', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test5@example.com', '13800138007', 'USER', 1,
|
||||
NOW(), NOW());
|
||||
('testuser1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'test1@example.com', '13800138003', '', 'USER', 1, NOW(), NOW()),
|
||||
('testuser2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'test2@example.com', '13800138004', '', 'USER', 1, NOW(), NOW()),
|
||||
('testuser3', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'test3@example.com', '13800138005', '', 'USER', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
email = VALUES(email),
|
||||
phone = VALUES(phone),
|
||||
updated_at = NOW();
|
||||
|
||||
-- ================================
|
||||
-- 2. 插入测试商品数据
|
||||
-- 2. 商品
|
||||
-- ================================
|
||||
|
||||
INSERT INTO products (name, description, price, category, stock, image_url, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 电子产品类
|
||||
('iPhone 15 Pro Max', '苹果最新旗舰手机,A17 Pro芯片,钛金属设计', 9999.00, '电子产品', 100, '/images/iphone15.jpg', 1, NOW(), NOW()),
|
||||
('MacBook Pro 16英寸', 'M3 Max芯片,36GB内存,1TB存储', 25999.00, '电子产品', 50, '/images/macbook.jpg', 1, NOW(), NOW()),
|
||||
('iPad Air', '10.9英寸液晶显示屏,M1芯片', 4399.00, '电子产品', 80, '/images/ipad.jpg', 1, NOW(), NOW()),
|
||||
('AirPods Pro 2', '主动降噪无线耳机,空间音频', 1899.00, '电子产品', 200, '/images/airpods.jpg', 1, NOW(), NOW()),
|
||||
('Apple Watch Series 9', '健康监测,GPS+蜂窝网络', 3199.00, '电子产品', 150, '/images/watch.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 家电类
|
||||
('小米电视 65英寸', '4K超高清,120Hz刷新率', 2999.00, '家电', 60, '/images/tv.jpg', 1, NOW(), NOW()),
|
||||
('戴森吸尘器 V15', '激光显微尘,强劲吸力', 4690.00, '家电', 40, '/images/dyson.jpg', 1, NOW(), NOW()),
|
||||
('美的空调 1.5匹', '变频节能,静音运行', 2599.00, '家电', 80, '/images/airconditioner.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 服装类
|
||||
('Nike Air Jordan 1', '经典篮球鞋,限量版配色', 1299.00, '服饰鞋包', 120, '/images/jordan.jpg', 1, NOW(), NOW()),
|
||||
('Adidas Ultra Boost', '缓震跑鞋,Boost中底', 1599.00, '服饰鞋包', 100, '/images/ultraboost.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 图书类
|
||||
('深入理解Java虚拟机', 'JVM原理与实践,第3版', 89.00, '图书音像', 500, '/images/jvm-book.jpg', 1, NOW(), NOW()),
|
||||
('Redis设计与实现', 'Redis内部机制详解', 79.00, '图书音像', 300, '/images/redis-book.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 食品类
|
||||
('茅台酒 53度 500ml', '国酒茅台,收藏佳品', 2680.00, '食品饮料', 30, '/images/maotai.jpg', 1, NOW(), NOW()),
|
||||
('五常大米 10kg', '东北优质大米,香甜可口', 168.00, '食品饮料', 200, '/images/rice.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 美妆类
|
||||
('SK-II神仙水 230ml', '护肤精华,改善肌肤', 1690.00, '美妆个护', 80, '/images/skii.jpg', 1, NOW(), NOW());
|
||||
('iPhone 15 Pro Max', '苹果最新旗舰手机,A17 Pro 芯片,钛金属设计。', 9999.00, '电子产品', 100, '/images/iphone15.svg', 1, NOW(), NOW()),
|
||||
('MacBook Pro 16英寸', 'M3 Max 芯片,36GB 内存,1TB 存储。', 25999.00, '电子产品', 50, '/images/macbook.svg', 1, NOW(), NOW()),
|
||||
('iPad Air', '10.9 英寸显示屏,轻薄便携。', 4399.00, '电子产品', 80, '/images/ipad.svg', 1, NOW(), NOW()),
|
||||
('AirPods Pro 2', '主动降噪无线耳机。', 1899.00, '电子产品', 200, '/images/default-product.svg', 1, NOW(), NOW()),
|
||||
('Apple Watch Series 9', '健康监测与运动记录。', 3199.00, '电子产品', 150, '/images/default-product.svg', 1, NOW(), NOW()),
|
||||
('小米电视 65英寸', '4K 超高清,120Hz 刷新率。', 2999.00, '家电', 60, '/images/default-product.svg', 1, NOW(), NOW()),
|
||||
('戴森吸尘器 V15', '激光显微尘,强劲吸力。', 4690.00, '家电', 40, '/images/default-product.svg', 1, NOW(), NOW()),
|
||||
('Nike Air Jordan 1', '经典篮球鞋,限量版配色。', 1299.00, '服饰鞋包', 120, '/images/default-product.svg', 1, NOW(), NOW()),
|
||||
('深入理解Java虚拟机', 'JVM 原理与实践,第 3 版。', 89.00, '图书音像', 500, '/images/default-product.svg', 1, NOW(), NOW()),
|
||||
('五常大米 10kg', '东北优质大米,香甜可口。', 168.00, '食品饮料', 200, '/images/default-product.svg', 1, NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 3. 插入秒杀活动数据
|
||||
-- 3. 秒杀活动
|
||||
-- ================================
|
||||
|
||||
INSERT INTO flash_sales (product_id, flash_price, flash_stock, start_time, end_time, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 正在进行的秒杀活动
|
||||
(1, 7999.00, 20, DATE_SUB(NOW(), INTERVAL 10 MINUTE), DATE_ADD(NOW(), INTERVAL 2 HOUR), 2, NOW(), NOW()),
|
||||
(4, 1299.00, 50, DATE_SUB(NOW(), INTERVAL 5 MINUTE), DATE_ADD(NOW(), INTERVAL 1 HOUR), 2, NOW(), NOW()),
|
||||
(6, 1999.00, 15, DATE_SUB(NOW(), INTERVAL 1 MINUTE), DATE_ADD(NOW(), INTERVAL 3 HOUR), 2, NOW(), NOW()),
|
||||
|
||||
-- 即将开始的秒杀活动
|
||||
(2, 19999.00, 10, DATE_ADD(NOW(), INTERVAL 30 MINUTE), DATE_ADD(NOW(), INTERVAL 4 HOUR), 1, NOW(), NOW()),
|
||||
(9, 899.00, 30, DATE_ADD(NOW(), INTERVAL 1 HOUR), DATE_ADD(NOW(), INTERVAL 5 HOUR), 1, NOW(), NOW()),
|
||||
(13, 1999.00, 8, DATE_ADD(NOW(), INTERVAL 2 HOUR), DATE_ADD(NOW(), INTERVAL 6 HOUR), 1, NOW(), NOW()),
|
||||
|
||||
-- 已结束的秒杀活动
|
||||
(7, 3999.00, 10, DATE_SUB(NOW(), INTERVAL 2 HOUR), DATE_SUB(NOW(), INTERVAL 30 MINUTE), 3, NOW(), NOW()),
|
||||
(11, 59.00, 100, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 22 HOUR), 3, NOW(), NOW());
|
||||
(1, 7999.00, 20, DATE_SUB(NOW(), INTERVAL 10 MINUTE), DATE_ADD(NOW(), INTERVAL 2 HOUR), 2, NOW(), NOW()),
|
||||
(4, 1299.00, 50, DATE_SUB(NOW(), INTERVAL 5 MINUTE), DATE_ADD(NOW(), INTERVAL 1 HOUR), 2, NOW(), NOW()),
|
||||
(6, 1999.00, 15, DATE_SUB(NOW(), INTERVAL 1 MINUTE), DATE_ADD(NOW(), INTERVAL 3 HOUR), 2, NOW(), NOW()),
|
||||
(2, 19999.00, 10, DATE_ADD(NOW(), INTERVAL 30 MINUTE), DATE_ADD(NOW(), INTERVAL 4 HOUR), 1, NOW(), NOW()),
|
||||
(8, 899.00, 30, DATE_ADD(NOW(), INTERVAL 1 HOUR), DATE_ADD(NOW(), INTERVAL 5 HOUR), 1, NOW(), NOW()),
|
||||
(9, 59.00, 100, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 22 HOUR), 3, NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 4. 插入测试订单数据
|
||||
-- 4. 地址
|
||||
-- ================================
|
||||
INSERT INTO user_addresses (user_id, name, phone, province, city, district, address, is_default, created_at, updated_at)
|
||||
SELECT id, '演示用户一', '13800138001', '上海市', '上海市', '浦东新区', '张江高科技园区 100 号', 1, NOW(), NOW() FROM users WHERE username = 'demo1'
|
||||
UNION ALL
|
||||
SELECT id, '演示用户二', '13800138002', '浙江省', '杭州市', '西湖区', '文三路 88 号', 1, NOW(), NOW() FROM users WHERE username = 'demo2'
|
||||
UNION ALL
|
||||
SELECT id, '测试用户一', '13800138003', '广东省', '深圳市', '南山区', '科技园科苑路 18 号', 1, NOW(), NOW() FROM users WHERE username = 'testuser1';
|
||||
|
||||
INSERT INTO orders (user_id, product_id, quantity, total_price, status, order_type, created_at, updated_at)
|
||||
-- ================================
|
||||
-- 5. 订单主表
|
||||
-- ================================
|
||||
INSERT INTO orders (
|
||||
order_no, group_no, user_id, product_id, flash_sale_id, quantity, total_price, status, order_type,
|
||||
receiver_name, receiver_phone, receiver_address, remark, payment_method,
|
||||
paid_at, shipped_at, completed_at, created_at, updated_at
|
||||
)
|
||||
VALUES
|
||||
-- demo1用户的订单
|
||||
(1, 11, 1, 89.00, 4, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
(1, 12, 1, 79.00, 2, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
|
||||
-- demo2用户的订单
|
||||
(2, 14, 1, 168.00, 3, 1, DATE_SUB(NOW(), INTERVAL 3 HOUR), DATE_SUB(NOW(), INTERVAL 2 HOUR)),
|
||||
(2, 7, 1, 3999.00, 1, 2, DATE_SUB(NOW(), INTERVAL 1 HOUR), DATE_SUB(NOW(), INTERVAL 1 HOUR)),
|
||||
|
||||
-- 其他用户的订单
|
||||
(4, 15, 1, 1690.00, 2, 1, DATE_SUB(NOW(), INTERVAL 6 HOUR), DATE_SUB(NOW(), INTERVAL 5 HOUR)),
|
||||
(5, 10, 1, 1599.00, 4, 1, DATE_SUB(NOW(), INTERVAL 12 HOUR), DATE_SUB(NOW(), INTERVAL 10 HOUR)),
|
||||
(6, 8, 1, 2599.00, 3, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 20 HOUR)),
|
||||
(7, 5, 1, 3199.00, 2, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY));
|
||||
('ORD202603110001', NULL, (SELECT id FROM users WHERE username = 'demo1'), 9, NULL, 1, 89.00, 4, 1, '演示用户一', '13800138001', '上海市 上海市 浦东新区 张江高科技园区 100 号', '已完成测试订单', 'ALIPAY', DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
('ORD202603110002', NULL, (SELECT id FROM users WHERE username = 'demo1'), 4, NULL, 1, 1899.00, 2, 1, '演示用户一', '13800138001', '上海市 上海市 浦东新区 张江高科技园区 100 号', '待发货测试订单', 'WECHAT', DATE_SUB(NOW(), INTERVAL 1 DAY), NULL, NULL, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
('ORD202603110003', NULL, (SELECT id FROM users WHERE username = 'demo2'), 10, NULL, 1, 168.00, 3, 1, '演示用户二', '13800138002', '浙江省 杭州市 西湖区 文三路 88 号', '已发货测试订单', 'ONLINE', DATE_SUB(NOW(), INTERVAL 4 HOUR), DATE_SUB(NOW(), INTERVAL 2 HOUR), NULL, DATE_SUB(NOW(), INTERVAL 6 HOUR), DATE_SUB(NOW(), INTERVAL 2 HOUR)),
|
||||
('ORD202603110004', NULL, (SELECT id FROM users WHERE username = 'demo2'), 4, 2, 1, 1299.00, 1, 2, '演示用户二', '13800138002', '浙江省 杭州市 西湖区 文三路 88 号', '秒杀待支付订单', NULL, NULL, NULL, NULL, DATE_SUB(NOW(), INTERVAL 1 HOUR), DATE_SUB(NOW(), INTERVAL 1 HOUR)),
|
||||
('ORD202603110005', NULL, (SELECT id FROM users WHERE username = 'testuser1'), 1, NULL, 2, 11798.00, 2, 1, '测试用户一', '13800138003', '广东省 深圳市 南山区 科技园科苑路 18 号', '多商品主订单', 'ONLINE', DATE_SUB(NOW(), INTERVAL 5 HOUR), NULL, NULL, DATE_SUB(NOW(), INTERVAL 5 HOUR), DATE_SUB(NOW(), INTERVAL 5 HOUR));
|
||||
|
||||
-- ================================
|
||||
-- 5. 查询验证数据
|
||||
-- 6. 订单明细
|
||||
-- ================================
|
||||
|
||||
-- 查看用户数据
|
||||
SELECT 'Users:' as table_name;
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
ORDER BY id;
|
||||
|
||||
-- 查看商品数据
|
||||
SELECT 'Products:' as table_name;
|
||||
SELECT id, name, price, stock, status
|
||||
FROM products
|
||||
ORDER BY id
|
||||
LIMIT 10;
|
||||
|
||||
-- 查看秒杀活动数据
|
||||
SELECT 'Flash Sales:' as table_name;
|
||||
SELECT fs.id, p.name as product_name, fs.flash_price, fs.flash_stock, fs.start_time, fs.end_time, fs.status
|
||||
FROM flash_sales fs
|
||||
JOIN products p ON fs.product_id = p.id
|
||||
ORDER BY fs.id;
|
||||
|
||||
-- 查看订单数据
|
||||
SELECT 'Orders:' as table_name;
|
||||
SELECT o.id, u.username, p.name as product_name, o.quantity, o.total_price, o.status, o.order_type
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id
|
||||
JOIN products p ON o.product_id = p.id
|
||||
ORDER BY o.id;
|
||||
INSERT INTO order_items (order_id, product_id, product_name, product_image_url, price, quantity, subtotal, created_at)
|
||||
SELECT o.id, 9, '深入理解Java虚拟机', '/images/default-product.svg', 89.00, 1, 89.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110001'
|
||||
UNION ALL
|
||||
SELECT o.id, 4, 'AirPods Pro 2', '/images/default-product.svg', 1899.00, 1, 1899.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110002'
|
||||
UNION ALL
|
||||
SELECT o.id, 10, '五常大米 10kg', '/images/default-product.svg', 168.00, 1, 168.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110003'
|
||||
UNION ALL
|
||||
SELECT o.id, 4, 'AirPods Pro 2', '/images/default-product.svg', 1299.00, 1, 1299.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110004'
|
||||
UNION ALL
|
||||
SELECT o.id, 1, 'iPhone 15 Pro Max', '/images/iphone15.svg', 9999.00, 1, 9999.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110005'
|
||||
UNION ALL
|
||||
SELECT o.id, 9, '深入理解Java虚拟机', '/images/default-product.svg', 89.00, 2, 178.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110005'
|
||||
UNION ALL
|
||||
SELECT o.id, 10, '五常大米 10kg', '/images/default-product.svg', 168.00, 1, 168.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110005'
|
||||
UNION ALL
|
||||
SELECT o.id, 4, 'AirPods Pro 2', '/images/default-product.svg', 1899.00, 1, 1899.00, o.created_at FROM orders o WHERE o.order_no = 'ORD202603110005';
|
||||
|
||||
-- ================================
|
||||
-- 6. 统计信息
|
||||
-- 7. 评价
|
||||
-- ================================
|
||||
INSERT INTO product_reviews (product_id, user_id, order_id, rating, content, status, admin_reply, replied_at, created_at, updated_at)
|
||||
VALUES
|
||||
(9, (SELECT id FROM users WHERE username = 'demo1'), (SELECT id FROM orders WHERE order_no = 'ORD202603110001'), 5, '内容很扎实,适合深入学习 JVM。', 1, '感谢支持,后续会持续补充相关图书。', NOW(), DATE_SUB(NOW(), INTERVAL 1 DAY), NOW()),
|
||||
(4, (SELECT id FROM users WHERE username = 'demo1'), (SELECT id FROM orders WHERE order_no = 'ORD202603110002'), 4, '耳机效果不错,降噪很明显。', 1, NULL, NULL, DATE_SUB(NOW(), INTERVAL 12 HOUR), DATE_SUB(NOW(), INTERVAL 12 HOUR));
|
||||
|
||||
SELECT 'Statistics:' as info;
|
||||
SELECT (SELECT COUNT(*) FROM users) as total_users,
|
||||
(SELECT COUNT(*) FROM products) as total_products,
|
||||
(SELECT COUNT(*) FROM flash_sales) as total_flash_sales,
|
||||
(SELECT COUNT(*) FROM orders) as total_orders,
|
||||
(SELECT COUNT(*) FROM flash_sales WHERE status = 2) as active_flash_sales,
|
||||
(SELECT COUNT(*) FROM orders WHERE status = 1) as pending_orders;
|
||||
-- ================================
|
||||
-- 8. 收藏
|
||||
-- ================================
|
||||
INSERT INTO user_favorites (user_id, product_id, created_at)
|
||||
VALUES
|
||||
((SELECT id FROM users WHERE username = 'demo1'), 1, NOW()),
|
||||
((SELECT id FROM users WHERE username = 'demo1'), 4, NOW()),
|
||||
((SELECT id FROM users WHERE username = 'demo2'), 2, NOW()),
|
||||
((SELECT id FROM users WHERE username = 'testuser1'), 9, NOW());
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
-- 更新演示账号密码为BCrypt格式
|
||||
-- 这些是使用BCryptPasswordEncoder生成的正确哈希值
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 删除现有演示用户(如果存在)
|
||||
DELETE
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 插入使用BCrypt加密的演示用户
|
||||
-- demo1/demo2 密码: 123456 (BCrypt哈希)
|
||||
-- admin 密码: admin123 (BCrypt哈希)
|
||||
INSERT INTO users (username, password, email, phone, role, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo1@example.com', '13800138001', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo2@example.com', '13800138002', 'USER', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$DOwVJZHH.5PkZKJKJKJKJOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 'ADMIN', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id,
|
||||
username,
|
||||
email,
|
||||
phone,
|
||||
status,
|
||||
SUBSTRING(password, 1, 30) as password_hash_preview,
|
||||
created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin')
|
||||
ORDER BY username;
|
||||
|
||||
-- 显示账号信息
|
||||
SELECT '=== 演示账号信息 ===' as info;
|
||||
SELECT CONCAT(username, ' / ', CASE
|
||||
WHEN username = 'admin' THEN 'admin123'
|
||||
ELSE '123456'
|
||||
END) as '用户名/密码'
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin')
|
||||
ORDER BY username;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,464 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="管理后台"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">管理后台仪表盘</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshData()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新数据
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||
总用户数
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="totalUsers">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-users fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
||||
总商品数
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="totalProducts">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-box fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||
活跃秒杀
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="activeFlashSales">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-bolt fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-warning shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||
今日订单
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="todayOrders">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-shopping-cart fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-rocket"></i> 快速操作</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/products"
|
||||
class="btn btn-primary btn-block">
|
||||
<i class="fas fa-plus"></i> 添加商品
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/flashsales"
|
||||
class="btn btn-success btn-block">
|
||||
<i class="fas fa-bolt"></i> 创建秒杀
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/orders"
|
||||
class="btn btn-info btn-block">
|
||||
<i class="fas fa-list"></i> 查看订单
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/monitor"
|
||||
class="btn btn-warning btn-block">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-clock"></i> 最近订单</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>用户</th>
|
||||
<th>商品</th>
|
||||
<th>金额</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentOrders">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-fire"></i> 热门商品</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="hotProducts">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.border-left-primary {
|
||||
border-left: 0.25rem solid #4e73df !important;
|
||||
}
|
||||
|
||||
.border-left-success {
|
||||
border-left: 0.25rem solid #1cc88a !important;
|
||||
}
|
||||
|
||||
.border-left-info {
|
||||
border-left: 0.25rem solid #36b9cc !important;
|
||||
}
|
||||
|
||||
.border-left-warning {
|
||||
border-left: 0.25rem solid #f6c23e !important;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
loadDashboardData();
|
||||
});
|
||||
|
||||
function loadDashboardData() {
|
||||
// 加载统计数据
|
||||
loadStatistics();
|
||||
|
||||
// 加载最近订单
|
||||
loadRecentOrders();
|
||||
|
||||
// 加载热门商品
|
||||
loadHotProducts();
|
||||
}
|
||||
|
||||
function loadStatistics() {
|
||||
// 调用真实API获取统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/dashboard/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateDashboardStats(response.data);
|
||||
} else {
|
||||
console.error('获取仪表盘数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateDashboardStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取仪表盘数据请求失败');
|
||||
// 显示默认值
|
||||
updateDashboardStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新仪表盘统计数据
|
||||
function updateDashboardStats(stats) {
|
||||
$('#totalUsers').text(formatNumber(stats.totalUsers || 0));
|
||||
$('#totalProducts').text(formatNumber(stats.totalProducts || 0));
|
||||
$('#activeFlashSales').text(formatNumber(stats.activeFlashSales || 0));
|
||||
$('#todayOrders').text(formatNumber(stats.todayOrders || 0));
|
||||
|
||||
// 更新订单统计卡片
|
||||
$('#totalOrders').text(formatNumber(stats.totalOrders || 0));
|
||||
$('#paidOrders').text(formatNumber(stats.paidOrders || 0));
|
||||
$('#pendingOrders').text(formatNumber(stats.pendingOrders || 0));
|
||||
$('#totalAmount').text('¥' + formatNumber(stats.totalAmount || 0));
|
||||
}
|
||||
|
||||
function loadRecentOrders() {
|
||||
// 调用真实API获取最近订单
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders/recent?limit=10')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateRecentOrders(response.data);
|
||||
} else {
|
||||
console.error('获取最近订单失败:', response.message);
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center">获取订单数据失败</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取最近订单请求失败');
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center">网络请求失败</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新最近订单列表
|
||||
function updateRecentOrders(orders) {
|
||||
let html = '';
|
||||
if (orders && orders.length > 0) {
|
||||
orders.forEach(function (order) {
|
||||
let statusClass = getOrderStatusClass(order.status);
|
||||
let statusText = getOrderStatusText(order.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + order.id + `</td>
|
||||
<td>` + order.username + `</td>
|
||||
<td>` + order.productName + `</td>
|
||||
<td>¥` + formatNumber(order.totalAmount) + `</td>
|
||||
<td><span class="badge ` + statusClass + `">` + statusText + `</span></td>
|
||||
<td>` + formatDateTime(order.createdAt) + `</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<tr><td colspan="6" class="text-center">暂无订单数据</td></tr>';
|
||||
}
|
||||
$('#recentOrders').html(html);
|
||||
}
|
||||
|
||||
function loadHotProducts() {
|
||||
// 模拟数据,实际应该调用API
|
||||
setTimeout(function () {
|
||||
const products = [
|
||||
{name: 'iPhone 15 Pro Max', sales: 156},
|
||||
{name: 'MacBook Pro 16英寸', sales: 89},
|
||||
{name: 'iPad Air', sales: 67},
|
||||
{name: 'AirPods Pro 2', sales: 234}
|
||||
];
|
||||
|
||||
let html = '';
|
||||
products.forEach((product, index) => {
|
||||
html += `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<small class="text-muted">#${index + 1}</small>
|
||||
<span class="ms-2">${product.name}</span>
|
||||
</div>
|
||||
<span class="badge bg-success">${product.sales}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#hotProducts').html(html);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
// 显示加载状态
|
||||
$('#totalUsers, #totalProducts, #activeFlashSales, #todayOrders').html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#hotProducts').html('<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>');
|
||||
|
||||
// 重新加载数据
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 待支付
|
||||
case 2:
|
||||
return 'bg-success'; // 已支付
|
||||
case 3:
|
||||
return 'bg-info'; // 已发货
|
||||
case 4:
|
||||
return 'bg-primary'; // 已完成
|
||||
case 5:
|
||||
return 'bg-danger'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
@@ -1,515 +0,0 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>系统监控 - 管理后台</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
.monitor-card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.metric-item {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.metric-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-online { background-color: #28a745; }
|
||||
.status-warning { background-color: #ffc107; }
|
||||
.status-offline { background-color: #dc3545; }
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}
|
||||
.log-container {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.log-line {
|
||||
margin-bottom: 5px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.log-error { color: #dc3545; }
|
||||
.log-warn { color: #ffc107; }
|
||||
.log-info { color: #17a2b8; }
|
||||
.log-debug { color: #6c757d; }
|
||||
.refresh-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/admin">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>管理后台
|
||||
</a>
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a class="nav-link" href="/admin">
|
||||
<i class="fas fa-arrow-left me-1"></i>返回首页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2>
|
||||
<i class="fas fa-chart-line me-2"></i>系统监控
|
||||
<button class="btn btn-outline-primary btn-sm ms-3 refresh-btn" onclick="refreshAll()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态卡片 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card monitor-card bg-primary text-white">
|
||||
<div class="card-body metric-item">
|
||||
<div class="metric-value" id="cpu-usage">0%</div>
|
||||
<div class="metric-label">CPU 使用率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card monitor-card bg-info text-white">
|
||||
<div class="card-body metric-item">
|
||||
<div class="metric-value" id="memory-usage">0%</div>
|
||||
<div class="metric-label">内存使用率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card monitor-card bg-success text-white">
|
||||
<div class="card-body metric-item">
|
||||
<div class="metric-value" id="active-users">0</div>
|
||||
<div class="metric-label">在线用户</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card monitor-card bg-warning text-white">
|
||||
<div class="card-body metric-item">
|
||||
<div class="metric-value" id="total-requests">0</div>
|
||||
<div class="metric-label">今日请求</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务状态 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card monitor-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-server me-2"></i>服务状态
|
||||
</h5>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="checkServices()">
|
||||
<i class="fas fa-check"></i> 检查
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush" id="service-status">
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="status-indicator status-online"></span>
|
||||
应用服务
|
||||
</div>
|
||||
<span class="badge bg-success rounded-pill">运行中</span>
|
||||
</div>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="status-indicator" id="redis-indicator"></span>
|
||||
Redis 服务
|
||||
</div>
|
||||
<span class="badge rounded-pill" id="redis-status">检查中...</span>
|
||||
</div>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="status-indicator" id="mysql-indicator"></span>
|
||||
MySQL 服务
|
||||
</div>
|
||||
<span class="badge rounded-pill" id="mysql-status">检查中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card monitor-card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-chart-area me-2"></i>系统性能趋势
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container">
|
||||
<canvas id="performance-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 业务监控 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card monitor-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>秒杀活动监控
|
||||
</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="loadFlashSaleStats()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="flashsale-monitor">
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="mt-3 text-muted">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card monitor-card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>系统告警
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="system-alerts">
|
||||
<div class="alert alert-success" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
系统运行正常
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时日志 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<div class="card monitor-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-file-alt me-2"></i>实时日志
|
||||
</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary" onclick="clearLogs()">
|
||||
<i class="fas fa-trash"></i> 清空
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" onclick="toggleAutoRefresh()">
|
||||
<i class="fas fa-play" id="auto-refresh-icon"></i>
|
||||
<span id="auto-refresh-text">自动刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="log-container" id="log-container">
|
||||
<div class="log-line log-info">[INFO] 系统监控页面已加载</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
let performanceChart;
|
||||
let autoRefreshInterval;
|
||||
let isAutoRefreshEnabled = false;
|
||||
|
||||
$(document).ready(function() {
|
||||
initPerformanceChart();
|
||||
loadSystemMetrics();
|
||||
checkServices();
|
||||
loadFlashSaleStats();
|
||||
loadSystemLogs();
|
||||
});
|
||||
|
||||
function initPerformanceChart() {
|
||||
const ctx = document.getElementById('performance-chart').getContext('2d');
|
||||
performanceChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'CPU使用率',
|
||||
data: [],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
tension: 0.1
|
||||
}, {
|
||||
label: '内存使用率',
|
||||
data: [],
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadSystemMetrics() {
|
||||
$.get('/api/admin/system/metrics', function(data) {
|
||||
$('#cpu-usage').text(data.cpuUsage + '%');
|
||||
$('#memory-usage').text(data.memoryUsage + '%');
|
||||
$('#active-users').text(data.activeUsers);
|
||||
$('#total-requests').text(data.totalRequests.toLocaleString());
|
||||
|
||||
// 更新图表
|
||||
updatePerformanceChart(data.cpuUsage, data.memoryUsage);
|
||||
|
||||
addLogEntry('info', '系统指标已更新');
|
||||
}).fail(function() {
|
||||
// 模拟数据
|
||||
const mockData = {
|
||||
cpuUsage: Math.floor(Math.random() * 80) + 10,
|
||||
memoryUsage: Math.floor(Math.random() * 70) + 20,
|
||||
activeUsers: Math.floor(Math.random() * 100) + 50,
|
||||
totalRequests: Math.floor(Math.random() * 10000) + 5000
|
||||
};
|
||||
|
||||
$('#cpu-usage').text(mockData.cpuUsage + '%');
|
||||
$('#memory-usage').text(mockData.memoryUsage + '%');
|
||||
$('#active-users').text(mockData.activeUsers);
|
||||
$('#total-requests').text(mockData.totalRequests.toLocaleString());
|
||||
|
||||
updatePerformanceChart(mockData.cpuUsage, mockData.memoryUsage);
|
||||
|
||||
addLogEntry('warn', '无法连接到监控API,显示模拟数据');
|
||||
});
|
||||
}
|
||||
|
||||
function updatePerformanceChart(cpuUsage, memoryUsage) {
|
||||
const now = new Date().toLocaleTimeString();
|
||||
|
||||
performanceChart.data.labels.push(now);
|
||||
performanceChart.data.datasets[0].data.push(cpuUsage);
|
||||
performanceChart.data.datasets[1].data.push(memoryUsage);
|
||||
|
||||
// 保持最近20个数据点
|
||||
if (performanceChart.data.labels.length > 20) {
|
||||
performanceChart.data.labels.shift();
|
||||
performanceChart.data.datasets[0].data.shift();
|
||||
performanceChart.data.datasets[1].data.shift();
|
||||
}
|
||||
|
||||
performanceChart.update();
|
||||
}
|
||||
|
||||
function checkServices() {
|
||||
// 检查Redis服务
|
||||
$.get('/api/admin/health/redis').done(function() {
|
||||
updateServiceStatus('redis', true);
|
||||
}).fail(function() {
|
||||
updateServiceStatus('redis', false);
|
||||
});
|
||||
|
||||
// 检查MySQL服务
|
||||
$.get('/api/admin/health/mysql').done(function() {
|
||||
updateServiceStatus('mysql', true);
|
||||
}).fail(function() {
|
||||
updateServiceStatus('mysql', false);
|
||||
});
|
||||
}
|
||||
|
||||
function updateServiceStatus(service, isOnline) {
|
||||
const indicator = $('#' + service + '-indicator');
|
||||
const status = $('#' + service + '-status');
|
||||
|
||||
if (isOnline) {
|
||||
indicator.removeClass('status-warning status-offline').addClass('status-online');
|
||||
status.removeClass('bg-warning bg-danger').addClass('bg-success').text('运行中');
|
||||
addLogEntry('info', service.toUpperCase() + ' 服务状态正常');
|
||||
} else {
|
||||
indicator.removeClass('status-online status-warning').addClass('status-offline');
|
||||
status.removeClass('bg-success bg-warning').addClass('bg-danger').text('离线');
|
||||
addLogEntry('error', service.toUpperCase() + ' 服务连接失败');
|
||||
}
|
||||
}
|
||||
|
||||
function loadFlashSaleStats() {
|
||||
$.get('/api/admin/flashsale/monitor', function(data) {
|
||||
let html = '<div class="row">';
|
||||
|
||||
if (data && data.length > 0) {
|
||||
data.forEach(function(flashSale) {
|
||||
const progressPercentage = Math.max(0, (flashSale.flashStock - flashSale.remainingStock) / flashSale.flashStock * 100);
|
||||
|
||||
html += `
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">${flashSale.productName}</h6>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<small>已售:${flashSale.flashStock - flashSale.remainingStock}</small>
|
||||
<small>剩余:${flashSale.remainingStock}</small>
|
||||
</div>
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar ${progressPercentage > 80 ? 'bg-warning' : 'bg-success'}"
|
||||
style="width: ${progressPercentage}%"></div>
|
||||
</div>
|
||||
<small class="text-muted mt-1 d-block">状态: ${flashSale.statusDescription}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html += '<div class="col-12"><p class="text-muted text-center">暂无活跃的秒杀活动</p></div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('#flashsale-monitor').html(html);
|
||||
|
||||
addLogEntry('info', '秒杀活动监控数据已更新');
|
||||
}).fail(function() {
|
||||
$('#flashsale-monitor').html('<div class="text-center py-4"><p class="text-danger">加载失败</p></div>');
|
||||
addLogEntry('error', '加载秒杀监控数据失败');
|
||||
});
|
||||
}
|
||||
|
||||
function loadSystemLogs() {
|
||||
// 模拟实时日志
|
||||
const logTypes = ['info', 'warn', 'error', 'debug'];
|
||||
const logMessages = [
|
||||
'用户登录成功',
|
||||
'秒杀活动开始',
|
||||
'Redis连接池满',
|
||||
'数据库查询耗时较长',
|
||||
'缓存命中率下降',
|
||||
'用户注册完成',
|
||||
'订单支付成功',
|
||||
'库存更新完成'
|
||||
];
|
||||
|
||||
// 添加一些初始日志
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const type = logTypes[Math.floor(Math.random() * logTypes.length)];
|
||||
const message = logMessages[Math.floor(Math.random() * logMessages.length)];
|
||||
addLogEntry(type, message);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function addLogEntry(level, message) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logLine = `<div class="log-line log-${level}">[${timestamp}] [${level.toUpperCase()}] ${message}</div>`;
|
||||
|
||||
$('#log-container').prepend(logLine);
|
||||
|
||||
// 保持最多100条日志
|
||||
const logLines = $('#log-container .log-line');
|
||||
if (logLines.length > 100) {
|
||||
logLines.slice(100).remove();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
loadSystemMetrics();
|
||||
checkServices();
|
||||
loadFlashSaleStats();
|
||||
addLogEntry('info', '手动刷新所有监控数据');
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
$('#log-container').empty();
|
||||
addLogEntry('info', '日志已清空');
|
||||
}
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
if (isAutoRefreshEnabled) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
$('#auto-refresh-icon').removeClass('fa-stop').addClass('fa-play');
|
||||
$('#auto-refresh-text').text('自动刷新');
|
||||
isAutoRefreshEnabled = false;
|
||||
addLogEntry('info', '自动刷新已停止');
|
||||
} else {
|
||||
autoRefreshInterval = setInterval(function() {
|
||||
loadSystemMetrics();
|
||||
checkServices();
|
||||
loadFlashSaleStats();
|
||||
}, 10000); // 每10秒刷新
|
||||
|
||||
$('#auto-refresh-icon').removeClass('fa-play').addClass('fa-stop');
|
||||
$('#auto-refresh-text').text('停止刷新');
|
||||
isAutoRefreshEnabled = true;
|
||||
addLogEntry('info', '自动刷新已启动');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面离开时清理定时器
|
||||
$(window).on('beforeunload', function() {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,545 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="订单管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">订单管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshOrders()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportOrders()">
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索订单号/用户...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchOrders()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterOrders()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待支付</option>
|
||||
<option value="paid">已支付</option>
|
||||
<option value="shipped">已发货</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="cancelled">已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" id="dateFilter" onchange="filterOrders()">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortOrders()">
|
||||
<option value="created_at">按创建时间</option>
|
||||
<option value="total_amount">按订单金额</option>
|
||||
<option value="status">按订单状态</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单统计 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="totalOrders">0</h5>
|
||||
<p class="card-text">总订单数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="paidOrders">0</h5>
|
||||
<p class="card-text">已支付订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="pendingOrders">0</h5>
|
||||
<p class="card-text">待处理订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="totalAmount">¥0</h5>
|
||||
<p class="card-text">总交易额</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>用户</th>
|
||||
<th>商品信息</th>
|
||||
<th>数量</th>
|
||||
<th>总金额</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersTableBody">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="订单分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情模态框 -->
|
||||
<div class="modal fade" id="orderDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">订单详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="orderDetailContent">
|
||||
<!-- 订单详情内容 -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadOrders();
|
||||
loadOrderStats();
|
||||
});
|
||||
|
||||
function loadOrders(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建请求参数
|
||||
let params = {
|
||||
page: page,
|
||||
size: 10
|
||||
};
|
||||
|
||||
const keyword = $('#searchKeyword').val();
|
||||
const status = $('#statusFilter').val();
|
||||
|
||||
if (keyword && keyword.trim()) {
|
||||
params.keyword = keyword.trim();
|
||||
}
|
||||
|
||||
if (status && status !== '') {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders', params)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
renderOrdersTable(response.data.orders);
|
||||
renderPagination(response.data.currentPage, response.data.totalPages);
|
||||
} else {
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center text-danger">获取订单数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
function loadOrderStats() {
|
||||
// 调用真实API获取订单统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateOrderStats(response.data);
|
||||
} else {
|
||||
console.error('获取订单统计数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateOrderStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取订单统计数据请求失败');
|
||||
// 显示默认值
|
||||
updateOrderStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新订单统计数据
|
||||
function updateOrderStats(stats) {
|
||||
$('#totalOrders').text(formatNumber(stats.totalOrders || 0));
|
||||
$('#paidOrders').text(formatNumber(stats.paidOrders || 0));
|
||||
$('#pendingOrders').text(formatNumber(stats.pendingOrders || 0));
|
||||
$('#totalAmount').text('¥' + formatNumber(stats.totalAmount || 0));
|
||||
}
|
||||
|
||||
function renderOrdersTable(orders) {
|
||||
let html = '';
|
||||
|
||||
if (orders.length === 0) {
|
||||
html = '<tr><td colspan="8" class="text-center">暂无订单数据</td></tr>';
|
||||
} else {
|
||||
orders.forEach(order => {
|
||||
const statusText = getOrderStatusText(order.status);
|
||||
const statusClass = getOrderStatusClass(order.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">` + order.id + `</div>
|
||||
` + (order.isFlashSale ? '<small class="text-danger"><i class="fas fa-bolt"></i> 秒杀订单</small>' : '') + `
|
||||
</td>
|
||||
<td>` + order.username + `</td>
|
||||
<td>` + order.productName + `</td>
|
||||
<td>` + order.quantity + `</td>
|
||||
<td class="fw-bold">¥` + formatNumber(order.totalAmount || 0) + `</td>
|
||||
<td>
|
||||
<span class="badge ` + statusClass + `">
|
||||
` + statusText + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(order.createdAt) + `</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="viewOrderDetail('` + order.id + `')" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
` + (order.status === 2 ?
|
||||
'<button class="btn btn-outline-success" onclick="shipOrder(\'' + order.id + '\')" title="发货"><i class="fas fa-shipping-fast"></i></button>' : '') + `
|
||||
` + (order.status === 1 ?
|
||||
'<button class="btn btn-outline-danger" onclick="cancelOrder(\'' + order.id + '\')" title="取消"><i class="fas fa-times"></i></button>' : '') + `
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#ordersTableBody').html(html);
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待支付';
|
||||
case 'paid':
|
||||
return '已支付';
|
||||
case 'shipped':
|
||||
return '已发货';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-warning';
|
||||
case 'paid':
|
||||
return 'bg-success';
|
||||
case 'shipped':
|
||||
return 'bg-info';
|
||||
case 'completed':
|
||||
return 'bg-primary';
|
||||
case 'cancelled':
|
||||
return 'bg-secondary';
|
||||
default:
|
||||
return 'bg-light';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshOrders() {
|
||||
$('#ordersTableBody').html('<tr><td colspan="8" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadOrders(currentPage);
|
||||
loadOrderStats();
|
||||
}
|
||||
|
||||
function searchOrders() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索订单:', keyword);
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function filterOrders() {
|
||||
const status = $('#statusFilter').val();
|
||||
const date = $('#dateFilter').val();
|
||||
console.log('筛选订单:', {status, date});
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function sortOrders() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function viewOrderDetail(orderId) {
|
||||
console.log('查看订单详情:', orderId);
|
||||
|
||||
// 模拟获取订单详情
|
||||
const orderDetail = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>订单信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>订单号:</td><td>` + orderId + `</td></tr>
|
||||
<tr><td>用户:</td><td>demo1</td></tr>
|
||||
<tr><td>状态:</td><td><span class="badge bg-success">已支付</span></td></tr>
|
||||
<tr><td>创建时间:</td><td>2025-06-29 10:30:15</td></tr>
|
||||
<tr><td>支付时间:</td><td>2025-06-29 10:31:20</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>商品信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>商品名称:</td><td>iPhone 15 Pro Max</td></tr>
|
||||
<tr><td>单价:</td><td>¥8,888.00</td></tr>
|
||||
<tr><td>数量:</td><td>1</td></tr>
|
||||
<tr><td>总金额:</td><td class="fw-bold">¥8,888.00</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6>收货地址</h6>
|
||||
<p>北京市朝阳区xxx街道xxx号xxx小区xxx楼xxx室<br>
|
||||
收货人: 张三 13800138001</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#orderDetailContent').html(orderDetail);
|
||||
$('#orderDetailModal').modal('show');
|
||||
}
|
||||
|
||||
function shipOrder(orderId) {
|
||||
if (confirm('确定要将此订单标记为已发货吗?')) {
|
||||
console.log('发货订单:', orderId);
|
||||
|
||||
setTimeout(function () {
|
||||
alert('订单已标记为已发货!');
|
||||
refreshOrders();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelOrder(orderId) {
|
||||
if (confirm('确定要取消此订单吗?此操作不可恢复。')) {
|
||||
console.log('取消订单:', orderId);
|
||||
|
||||
setTimeout(function () {
|
||||
alert('订单已取消!');
|
||||
refreshOrders();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function exportOrders() {
|
||||
console.log('导出订单数据');
|
||||
alert('订单数据导出功能开发中...');
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 待支付
|
||||
case 2:
|
||||
return 'bg-success'; // 已支付
|
||||
case 3:
|
||||
return 'bg-info'; // 已发货
|
||||
case 4:
|
||||
return 'bg-primary'; // 已完成
|
||||
case 5:
|
||||
return 'bg-danger'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,404 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">用户管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshUsers()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportUsers()">
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索用户名/邮箱/手机...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchUsers()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterUsers()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="1">正常</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" id="dateFilter" onchange="filterUsers()">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortUsers()">
|
||||
<option value="created_at">按注册时间</option>
|
||||
<option value="username">按用户名</option>
|
||||
<option value="last_login">按最后登录</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户统计 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="totalUsers">0</h5>
|
||||
<p class="card-text">总用户数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="activeUsers">0</h5>
|
||||
<p class="card-text">活跃用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="newUsers">0</h5>
|
||||
<p class="card-text">今日新增</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="onlineUsers">0</h5>
|
||||
<p class="card-text">在线用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>手机号</th>
|
||||
<th>状态</th>
|
||||
<th>注册时间</th>
|
||||
<th>最后登录</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="用户分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadUsers();
|
||||
loadUserStats();
|
||||
});
|
||||
|
||||
function loadUsers(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建请求参数
|
||||
let params = {
|
||||
page: page,
|
||||
size: 10
|
||||
};
|
||||
|
||||
const keyword = $('#searchKeyword').val();
|
||||
const status = $('#statusFilter').val();
|
||||
|
||||
if (keyword && keyword.trim()) {
|
||||
params.keyword = keyword.trim();
|
||||
}
|
||||
|
||||
if (status && status !== '') {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.get('${pageContext.request.contextPath}/api/admin/users', params)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
renderUsersTable(response.data.users);
|
||||
renderPagination(response.data.currentPage, response.data.totalPages);
|
||||
} else {
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center text-danger">获取用户数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
function loadUserStats() {
|
||||
// 调用真实API获取用户统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/users/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateUserStats(response.data);
|
||||
} else {
|
||||
console.error('获取用户统计数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateUserStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取用户统计数据请求失败');
|
||||
// 显示默认值
|
||||
updateUserStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户统计数据
|
||||
function updateUserStats(stats) {
|
||||
$('#totalUsers').text(formatNumber(stats.totalUsers || 0));
|
||||
$('#activeUsers').text(formatNumber(stats.activeUsers || 0));
|
||||
$('#newUsers').text(formatNumber(stats.newUsers || 0));
|
||||
$('#onlineUsers').text(formatNumber(stats.onlineUsers || 0));
|
||||
}
|
||||
|
||||
function renderUsersTable(users) {
|
||||
let html = '';
|
||||
|
||||
if (users.length === 0) {
|
||||
html = '<tr><td colspan="7" class="text-center">暂无用户数据</td></tr>';
|
||||
} else {
|
||||
users.forEach(user => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + user.id + `</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-2">` + user.username + `</span>
|
||||
` + (user.isOnline ? '<span class="badge bg-success">在线</span>' : '') + `
|
||||
</div>
|
||||
</td>
|
||||
<td>` + (user.email || '-') + `</td>
|
||||
<td>` + (user.phone || '-') + `</td>
|
||||
<td>
|
||||
<span class="badge ` + getUserStatusClass(user.status) + `">
|
||||
` + getUserStatusText(user.status) + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(user.createdAt) + `</td>
|
||||
<td>` + (user.lastLogin ? formatDateTime(user.lastLogin) : '从未登录') + `</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#usersTableBody').html(html);
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshUsers() {
|
||||
$('#usersTableBody').html('<tr><td colspan="7" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadUsers(currentPage);
|
||||
loadUserStats();
|
||||
}
|
||||
|
||||
function searchUsers() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索用户:', keyword);
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
function filterUsers() {
|
||||
const status = $('#statusFilter').val();
|
||||
const date = $('#dateFilter').val();
|
||||
console.log('筛选用户:', {status, date});
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
function sortUsers() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
|
||||
function exportUsers() {
|
||||
console.log('导出用户数据');
|
||||
alert('用户数据导出功能开发中...');
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getUserStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-success'; // 正常
|
||||
case 0:
|
||||
return 'bg-danger'; // 禁用
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getUserStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '正常';
|
||||
case 0:
|
||||
return '禁用';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
@@ -1,658 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="购物车"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item active">购物车</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 购物车内容 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shopping-cart text-primary"></i> 我的购物车
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="clearCart()" id="clearCartBtn"
|
||||
style="display: none;">
|
||||
<i class="fas fa-trash"></i> 清空购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingCart" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载购物车中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空购物车状态 -->
|
||||
<div id="emptyCart" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-shopping-cart fa-4x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">购物车空空如也</h5>
|
||||
<p class="text-muted">快去挑选您喜欢的商品吧~</p>
|
||||
<a href="${pageContext.request.contextPath}/" class="btn btn-primary">
|
||||
<i class="fas fa-shopping-bag"></i> 去购物
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 购物车商品列表 -->
|
||||
<div id="cartItems" style="display: none;">
|
||||
<!-- 全选区域 -->
|
||||
<div class="row mb-3 border-bottom pb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="selectAll">
|
||||
<label class="form-check-label" for="selectAll">
|
||||
全选
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="batchRemoveSelected()">
|
||||
<i class="fas fa-trash"></i> 删除选中
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表容器 -->
|
||||
<div id="cartItemsList"></div>
|
||||
|
||||
<!-- 结算区域 -->
|
||||
<div class="row mt-4 pt-3 border-top">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted me-3">已选择 <span id="selectedCount">0</span> 件商品</span>
|
||||
<span class="text-muted">总计:<span id="totalQuantity">0</span> 件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<span class="h5 text-danger me-3 mb-0">
|
||||
¥<span id="totalPrice">0.00</span>
|
||||
</span>
|
||||
<button class="btn btn-danger btn-lg" onclick="checkout()" id="checkoutBtn"
|
||||
disabled>
|
||||
<i class="fas fa-credit-card"></i> 去结算
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推荐商品 -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-heart text-danger"></i> 猜你喜欢
|
||||
</h5>
|
||||
<div id="recommendedProducts" class="row">
|
||||
<!-- 推荐商品将通过AJAX加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
loadCart();
|
||||
loadRecommendedProducts();
|
||||
});
|
||||
|
||||
// 加载购物车
|
||||
function loadCart() {
|
||||
$('#loadingCart').show();
|
||||
$('#emptyCart').hide();
|
||||
$('#cartItems').hide();
|
||||
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart',
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data) {
|
||||
renderCart(response.data);
|
||||
} else {
|
||||
showEmptyCart();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('加载购物车失败,请刷新页面重试', 'error');
|
||||
showEmptyCart();
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingCart').hide();
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
$('#loadingCart').hide();
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 渲染购物车
|
||||
function renderCart(cart) {
|
||||
if (!cart.items || cart.items.length === 0) {
|
||||
showEmptyCart();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
cart.items.forEach(function (item) {
|
||||
html += `
|
||||
<div class="row align-items-center py-3 border-bottom cart-item" data-product-id="` + item.productId + `">
|
||||
<div class="col-md-1">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input item-checkbox" type="checkbox" value="` + item.productId + `">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<img src="` + getProductImageUrl(item.productImageUrl) + `"
|
||||
class="img-fluid rounded" alt="` + item.productName + `" style="max-height: 80px;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="mb-1">` + item.productName + `</h6>
|
||||
<small class="text-muted">库存:` + item.stock + ` 件</small>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<span class="text-danger fw-bold">¥` + item.productPrice.toFixed(2) + `</span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="input-group input-group-sm" style="width: 120px;">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="updateQuantity(` + item.productId + `, ` + (item.quantity - 1) + `)">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
<input type="number" class="form-control text-center" value="` + item.quantity + `"
|
||||
min="1" max="` + item.stock + `"
|
||||
onchange="updateQuantity(` + item.productId + `, this.value)">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="updateQuantity(` + item.productId + `, ` + (item.quantity + 1) + `)">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<span class="fw-bold text-danger">¥` + item.subtotal.toFixed(2) + `</span>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="removeFromCart(` + item.productId + `)" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#cartItemsList').html(html);
|
||||
$('#cartItems').show();
|
||||
$('#clearCartBtn').show();
|
||||
|
||||
// 更新总计
|
||||
updateCartSummary(cart);
|
||||
|
||||
// 绑定事件
|
||||
bindCartEvents();
|
||||
}
|
||||
|
||||
// 显示空购物车
|
||||
function showEmptyCart() {
|
||||
$('#emptyCart').show();
|
||||
$('#cartItems').hide();
|
||||
$('#clearCartBtn').hide();
|
||||
}
|
||||
|
||||
// 更新购物车摘要
|
||||
function updateCartSummary(cart) {
|
||||
$('#totalQuantity').text(cart.totalQuantity || 0);
|
||||
$('#totalPrice').text((cart.totalPrice || 0).toFixed(2));
|
||||
}
|
||||
|
||||
// 绑定购物车事件
|
||||
function bindCartEvents() {
|
||||
// 全选/反选
|
||||
$('#selectAll').on('change', function () {
|
||||
const isChecked = $(this).is(':checked');
|
||||
$('.item-checkbox').prop('checked', isChecked);
|
||||
updateSelectedSummary();
|
||||
});
|
||||
|
||||
// 单个选择
|
||||
$('.item-checkbox').on('change', function () {
|
||||
updateSelectedSummary();
|
||||
|
||||
// 检查是否全选
|
||||
const totalItems = $('.item-checkbox').length;
|
||||
const selectedItems = $('.item-checkbox:checked').length;
|
||||
$('#selectAll').prop('checked', totalItems === selectedItems);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新已选中商品摘要
|
||||
function updateSelectedSummary() {
|
||||
const selectedItems = $('.item-checkbox:checked');
|
||||
let selectedCount = 0;
|
||||
let selectedTotal = 0;
|
||||
|
||||
selectedItems.each(function () {
|
||||
const productId = $(this).val();
|
||||
const cartItem = $(this).closest('.cart-item');
|
||||
const quantity = parseInt(cartItem.find('input[type="number"]').val());
|
||||
const price = parseFloat(cartItem.find('.text-danger.fw-bold').text().replace('¥', ''));
|
||||
|
||||
selectedCount += quantity;
|
||||
selectedTotal += price;
|
||||
});
|
||||
|
||||
$('#selectedCount').text(selectedCount);
|
||||
$('#checkoutBtn').prop('disabled', selectedItems.length === 0);
|
||||
}
|
||||
|
||||
// 更新商品数量
|
||||
function updateQuantity(productId, newQuantity) {
|
||||
newQuantity = parseInt(newQuantity);
|
||||
|
||||
if (newQuantity < 1) {
|
||||
if (confirm('确定要从购物车中移除这个商品吗?')) {
|
||||
removeFromCart(productId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
const cartItem = $(`.cart-item[data-product-id="` + productId + `"]`);
|
||||
const maxStock = parseInt(cartItem.find('input[type="number"]').attr('max'));
|
||||
|
||||
if (newQuantity > maxStock) {
|
||||
showMessage('数量不能超过库存:' + maxStock, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/update',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: newQuantity
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
loadCart(); // 重新加载购物车
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('更新失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 从购物车移除商品
|
||||
function removeFromCart(productId) {
|
||||
if (!confirm('确定要从购物车中移除这个商品吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/remove',
|
||||
type: 'DELETE',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已从购物车移除', 'success');
|
||||
loadCart(); // 重新加载购物车
|
||||
updateCartCount(); // 更新导航栏购物车数量
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('移除失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 批量移除选中商品
|
||||
function batchRemoveSelected() {
|
||||
const selectedItems = $('.item-checkbox:checked');
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
showMessage('请先选择要删除的商品', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('确定要删除选中的 ' + selectedItems.length + ' 个商品吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const productIds = [];
|
||||
selectedItems.each(function () {
|
||||
productIds.push(parseInt($(this).val()));
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/batch-remove',
|
||||
type: 'DELETE',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productIds: productIds
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('选中商品已删除', 'success');
|
||||
loadCart(); // 重新加载购物车
|
||||
updateCartCount(); // 更新导航栏购物车数量
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('删除失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空购物车
|
||||
function clearCart() {
|
||||
if (!confirm('确定要清空购物车吗?此操作不可恢复。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/clear',
|
||||
type: 'DELETE',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('购物车已清空', 'success');
|
||||
showEmptyCart();
|
||||
updateCartCount(); // 更新导航栏购物车数量
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('清空失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 去结算
|
||||
function checkout() {
|
||||
const selectedItems = $('.item-checkbox:checked');
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
showMessage('请先选择要结算的商品', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集选中的商品ID
|
||||
const productIds = [];
|
||||
selectedItems.each(function () {
|
||||
productIds.push(parseInt($(this).val()));
|
||||
});
|
||||
|
||||
// 显示确认对话框
|
||||
if (!confirm('确定要结算选中的 ' + selectedItems.length + ' 个商品吗?\n\n结算后将生成订单,请及时支付。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用结算按钮防止重复点击
|
||||
const checkoutBtn = $('#checkoutBtn');
|
||||
const originalText = checkoutBtn.html();
|
||||
checkoutBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 结算中...');
|
||||
|
||||
// 通过AJAX调用购物车结算接口
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/checkout',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productIds: productIds
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('✅ 订单生成成功!订单号:' + response.data.orderNo, 'success');
|
||||
|
||||
// 清空购物车显示
|
||||
loadCart();
|
||||
|
||||
// 3秒后跳转到订单详情页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/order/' + response.data.id;
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage('❌ 下单失败:' + response.message, 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
checkoutBtn.prop('disabled', false).html(originalText);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
let errorMessage = '网络异常,请重试';
|
||||
|
||||
if (xhr.status === 401) {
|
||||
errorMessage = '登录已过期,请重新登录';
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login?returnUrl=' + encodeURIComponent(window.location.pathname);
|
||||
}, 1500);
|
||||
} else if (xhr.status === 400 && xhr.responseJSON) {
|
||||
errorMessage = xhr.responseJSON.message || '请求参数错误';
|
||||
}
|
||||
|
||||
showMessage('❌ 结算失败:' + errorMessage, 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
checkoutBtn.prop('disabled', false).html(originalText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载推荐商品
|
||||
function loadRecommendedProducts() {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/product/hot?limit=4',
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderRecommendedProducts(response.data);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
console.log('加载推荐商品失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染推荐商品
|
||||
function renderRecommendedProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function (product) {
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<img src="` + getProductImageUrl(product.imageUrl) + `"
|
||||
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>
|
||||
<p class="card-text text-muted small text-truncate">` + (product.description || '暂无描述') + `</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold">¥` + (product.price ? product.price.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted">库存: ` + (product.stock || 0) + `</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="addToCartFromRecommend(` + product.id + `)">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#recommendedProducts').html(html);
|
||||
}
|
||||
|
||||
// 从推荐商品添加到购物车
|
||||
function addToCartFromRecommend(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
loadCart(); // 重新加载购物车
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.cart-item {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.cart-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.input-group-sm .form-control {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cart-item .col-md-1,
|
||||
.cart-item .col-md-2,
|
||||
.cart-item .col-md-3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
@@ -1,215 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="bg-dark text-light mt-5 py-4">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-bolt"></i> 秒杀系统</h5>
|
||||
<p class="mb-2">基于Spring Boot + Redis构建的高并发秒杀系统</p>
|
||||
<p class="text-muted small">
|
||||
<i class="fas fa-server"></i> Redis集群 |
|
||||
<i class="fas fa-lock"></i> 分布式锁 |
|
||||
<i class="fas fa-tachometer-alt"></i> 高性能
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>核心功能</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-fire text-danger"></i> 秒杀抢购</li>
|
||||
<li><i class="fas fa-shopping-cart text-primary"></i> 购物车</li>
|
||||
<li><i class="fas fa-list-alt text-success"></i> 订单管理</li>
|
||||
<li><i class="fas fa-chart-line text-warning"></i> 销量排行</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>技术特性</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-database text-info"></i> Redis缓存</li>
|
||||
<li><i class="fas fa-shield-alt text-success"></i> 防超卖机制</li>
|
||||
<li><i class="fas fa-stopwatch text-warning"></i> 接口限流</li>
|
||||
<li><i class="fas fa-code text-primary"></i> Lua脚本</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<p class="mb-0 text-muted">
|
||||
© 2025 秒杀系统演示项目.
|
||||
<span class="text-danger">❤</span>
|
||||
基于Redis集群构建
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<div class="d-flex justify-content-md-end align-items-center">
|
||||
<span class="me-3 text-muted small">
|
||||
<i class="fas fa-users"></i>
|
||||
在线用户: <span id="onlineUserCount">-</span>
|
||||
</span>
|
||||
<span class="me-3 text-muted small">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="currentTime"></span>
|
||||
</span>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-light btn-sm" onclick="checkSystemStatus()">
|
||||
<i class="fas fa-heartbeat"></i> 系统状态
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 系统状态模态框 -->
|
||||
<div class="modal fade" id="systemStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-heartbeat text-success"></i> 系统状态
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-database fa-2x text-success mb-2"></i>
|
||||
<h6>Redis集群</h6>
|
||||
<span class="badge bg-success">正常</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-server fa-2x text-primary mb-2"></i>
|
||||
<h6>应用服务</h6>
|
||||
<span class="badge bg-primary">运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="refreshSystemStatus()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString('zh-CN');
|
||||
$('#currentTime').text(timeString);
|
||||
}
|
||||
|
||||
// 获取在线用户数
|
||||
function updateOnlineUserCount() {
|
||||
// 只有在用户登录时才更新在线用户数,并且不在登录页面执行
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
// 检查当前页面是否为登录页面
|
||||
if (window.location.pathname.indexOf('/login') === -1) {
|
||||
$.get('${pageContext.request.contextPath}/api/user/online-stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#onlineUserCount').text(response.data.onlineUserCount);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#onlineUserCount').text('N/A');
|
||||
});
|
||||
}
|
||||
</c:if>
|
||||
}
|
||||
|
||||
// 检查系统状态
|
||||
function checkSystemStatus() {
|
||||
$('#systemStatusModal').modal('show');
|
||||
refreshSystemStatus();
|
||||
}
|
||||
|
||||
// 刷新系统状态
|
||||
function refreshSystemStatus() {
|
||||
// 获取订单统计
|
||||
$.get('${pageContext.request.contextPath}/api/order/statistics')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
const stats = response.data;
|
||||
$('#totalOrders').text(stats.totalOrders || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取活跃秒杀数量
|
||||
$.get('${pageContext.request.contextPath}/api/flashsale/active')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#activeFlashSales').text(response.data.length || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 模拟其他统计数据
|
||||
$('#totalUsers').text(Math.floor(Math.random() * 1000) + 500);
|
||||
$('#totalProducts').text(Math.floor(Math.random() * 100) + 50);
|
||||
}
|
||||
|
||||
// 页面加载完成后执行
|
||||
$(document).ready(function () {
|
||||
// 立即更新时间
|
||||
updateCurrentTime();
|
||||
|
||||
// 只有在用户登录时才更新在线用户数
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
updateOnlineUserCount();
|
||||
// 每2分钟更新一次在线用户数(减少频率)
|
||||
setInterval(updateOnlineUserCount, 120000);
|
||||
</c:if>
|
||||
|
||||
// 每秒更新时间
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!document.hidden) {
|
||||
// 页面变为可见时,更新数据
|
||||
updateOnlineUserCount();
|
||||
}
|
||||
});
|
||||
|
||||
// 全局错误处理
|
||||
$(document).ajaxError(function (event, xhr, settings, thrownError) {
|
||||
if (xhr.status === 401) {
|
||||
showMessage('登录已过期,请重新登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 2000);
|
||||
} else if (xhr.status >= 500) {
|
||||
showMessage('服务器错误,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 添加加载动画
|
||||
function showLoading(element) {
|
||||
$(element).html('<i class="fas fa-spinner fa-spin"></i> 加载中...');
|
||||
}
|
||||
|
||||
// 隐藏加载动画
|
||||
function hideLoading(element, originalText) {
|
||||
$(element).html(originalText);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,288 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageTitle} - 秒杀系统</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<style>
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.flash-sale-badge {
|
||||
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 0.7em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.online-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #28a745;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="${pageContext.request.contextPath}/">
|
||||
<i class="fas fa-bolt"></i> 秒杀系统
|
||||
<span class="flash-sale-badge">FLASH SALE</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/">
|
||||
<i class="fas fa-home"></i> 首页
|
||||
</a>
|
||||
</li>
|
||||
<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 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>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
<!-- 购物车 -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link position-relative" href="${pageContext.request.contextPath}/cart">
|
||||
<i class="fas fa-shopping-cart"></i> 购物车
|
||||
<span class="cart-badge" id="cartCount">0</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- 用户菜单 -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="userDropdown"
|
||||
role="button" data-bs-toggle="dropdown">
|
||||
<div class="user-avatar me-2">
|
||||
${sessionScope.user.username.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<span class="online-indicator"></span>
|
||||
${sessionScope.user.username}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<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>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#" onclick="logout()">
|
||||
<i class="fas fa-sign-out-alt"></i> 退出登录
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/login">
|
||||
<i class="fas fa-sign-in-alt"></i> 登录
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/register">
|
||||
<i class="fas fa-user-plus"></i> 注册
|
||||
</a>
|
||||
</li>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 消息提示区域 -->
|
||||
<div id="messageContainer" class="container mt-3">
|
||||
<!-- 动态消息将在这里显示 -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局JavaScript函数
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertClass = {
|
||||
'success': 'alert-success',
|
||||
'error': 'alert-danger',
|
||||
'warning': 'alert-warning',
|
||||
'info': 'alert-info'
|
||||
}[type] || 'alert-info';
|
||||
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#messageContainer').html(alertHtml);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
$('.alert').alert('close');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
$.post('${pageContext.request.contextPath}/api/user/logout')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showMessage('退出登录失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
function updateCartCount() {
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
$.get('${pageContext.request.contextPath}/api/cart/count')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#cartCount').text(response.data.count);
|
||||
}
|
||||
});
|
||||
</c:if>
|
||||
}
|
||||
|
||||
// 页面加载完成后执行
|
||||
$(document).ready(function () {
|
||||
updateCartCount();
|
||||
|
||||
// 每30秒更新一次购物车数量
|
||||
setInterval(updateCartCount, 30000);
|
||||
});
|
||||
|
||||
// 格式化价格
|
||||
function formatPrice(price) {
|
||||
return '¥' + parseFloat(price).toFixed(2);
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 倒计时函数
|
||||
function countdown(endTime, elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
function updateCountdown() {
|
||||
const now = new Date().getTime();
|
||||
const distance = endTime - now;
|
||||
|
||||
if (distance < 0) {
|
||||
element.innerHTML = "已结束";
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
|
||||
let countdownText = "";
|
||||
if (days > 0) countdownText += days + "天 ";
|
||||
countdownText += String(hours).padStart(2, '0') + ":" +
|
||||
String(minutes).padStart(2, '0') + ":" +
|
||||
String(seconds).padStart(2, '0');
|
||||
|
||||
element.innerHTML = countdownText;
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 1000);
|
||||
|
||||
// 返回清理函数
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
</script>
|
||||
@@ -1,83 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="系统错误"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle"></i> 系统错误
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-bug fa-5x text-danger mb-3"></i>
|
||||
<h5>抱歉,系统遇到了一个错误</h5>
|
||||
<p class="text-muted">我们正在努力修复这个问题,请稍后再试。</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息(仅在开发环境显示) -->
|
||||
<c:if test="${not empty error}">
|
||||
<div class="alert alert-warning text-start">
|
||||
<h6><i class="fas fa-info-circle"></i> 错误详情:</h6>
|
||||
<p class="mb-0">${error}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${not empty exception}">
|
||||
<div class="alert alert-danger text-start">
|
||||
<h6><i class="fas fa-bug"></i> 异常信息:</h6>
|
||||
<p class="mb-0">${exception.message}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-primary" onclick="history.back()">
|
||||
<i class="fas fa-arrow-left"></i> 返回上页
|
||||
</button>
|
||||
<a href="${pageContext.request.contextPath}/" class="btn btn-success">
|
||||
<i class="fas fa-home"></i> 返回首页
|
||||
</a>
|
||||
<button class="btn btn-info" onclick="location.reload()">
|
||||
<i class="fas fa-redo"></i> 刷新页面
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常见问题解决方案 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h6><i class="fas fa-question-circle"></i> 常见问题解决方案</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>如果页面无法加载:</h6>
|
||||
<ul class="small">
|
||||
<li>检查网络连接</li>
|
||||
<li>清除浏览器缓存</li>
|
||||
<li>尝试刷新页面</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>如果功能异常:</h6>
|
||||
<ul class="small">
|
||||
<li>重新登录账号</li>
|
||||
<li>检查输入信息</li>
|
||||
<li>联系系统管理员</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
@@ -1,873 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="秒杀详情"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/flashsales">秒杀活动</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">秒杀详情</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingDetail" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载秒杀详情中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div id="errorDetail" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle fa-4x text-warning mb-3"></i>
|
||||
<h5 class="text-muted">秒杀活动不存在或已被删除</h5>
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-primary mt-3">
|
||||
<i class="fas fa-arrow-left"></i> 返回秒杀列表
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<div id="flashSaleDetail" style="display: none;">
|
||||
<div class="row">
|
||||
<!-- 左侧:商品信息 -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="position-relative">
|
||||
<img id="productImage" src="" class="card-img-top" alt=""
|
||||
style="height: 400px; object-fit: cover;">
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<div class="position-absolute top-0 start-0 m-3">
|
||||
<span id="statusBadge" class="badge fs-6"></span>
|
||||
</div>
|
||||
|
||||
<!-- 折扣标签 -->
|
||||
<div id="discountBadge" class="position-absolute top-0 end-0 m-3" style="display: none;">
|
||||
<span class="badge bg-warning text-dark fs-6"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<h4 id="productName" class="card-title fw-bold"></h4>
|
||||
<p id="productDescription" class="card-text text-muted"></p>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<div class="price-section mb-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-6">
|
||||
<span class="text-muted small">秒杀价</span>
|
||||
<div class="text-danger fw-bold" style="font-size: 2.5rem;" id="flashPrice">¥0.00
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<span class="text-muted small">原价</span>
|
||||
<div class="text-muted text-decoration-line-through" style="font-size: 1.5rem;"
|
||||
id="originalPrice">¥0.00
|
||||
</div>
|
||||
<div class="text-success fw-bold" id="savings">节省 ¥0.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:秒杀信息 -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-fire"></i> 秒杀信息
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 倒计时 -->
|
||||
<div id="countdownSection" class="text-center mb-4 p-4 bg-light rounded">
|
||||
<div id="countdownLabel" class="text-muted mb-2"></div>
|
||||
<div id="countdown" class="display-4 fw-bold text-danger"
|
||||
style="font-family: 'Courier New', monospace;">
|
||||
--:--:--
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 库存信息 -->
|
||||
<div class="stock-section mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="fw-bold">剩余库存</span>
|
||||
<span id="stockInfo" class="fw-bold text-danger">0/0</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div id="stockProgress" class="progress-bar bg-danger" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-muted">已抢</small>
|
||||
<small class="text-muted">剩余</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动时间 -->
|
||||
<div class="time-section mb-4">
|
||||
<h6 class="fw-bold mb-3">活动时间</h6>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">开始时间</small>
|
||||
<div id="startTime" class="fw-bold">--</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">结束时间</small>
|
||||
<div id="endTime" class="fw-bold">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 购买限制 -->
|
||||
<div class="limit-section mb-4">
|
||||
<h6 class="fw-bold mb-2">购买限制</h6>
|
||||
<ul class="list-unstyled small text-muted">
|
||||
<li><i class="fas fa-check text-success me-2"></i>每人限购 1 件</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>不支持退换货</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>限时限量,售完即止</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-section">
|
||||
<div class="d-grid gap-2">
|
||||
<button id="actionButton" class="btn btn-lg" onclick="handleAction()">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" onclick="addToCart()">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车(原价)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分享和收藏 -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">分享给好友</span>
|
||||
<div>
|
||||
<button class="btn btn-outline-secondary btn-sm me-2" onclick="shareWeChat()">
|
||||
<i class="fab fa-weixin text-success"></i> 微信
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm me-2" onclick="shareWeibo()">
|
||||
<i class="fab fa-weibo text-danger"></i> 微博
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="copyLink()">
|
||||
<i class="fas fa-link"></i> 复制链接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动规则说明 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-info-circle"></i> 活动规则
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="fw-bold text-primary">参与条件</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check text-success me-2"></i>需要登录账户</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>每个用户限购一件</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>先到先得,售完即止</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="fw-bold text-primary">注意事项</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-exclamation-circle text-warning me-2"></i>秒杀商品不支持退换
|
||||
</li>
|
||||
<li><i class="fas fa-exclamation-circle text-warning me-2"></i>请在规定时间内完成支付
|
||||
</li>
|
||||
<li><i class="fas fa-exclamation-circle text-warning me-2"></i>恶意刷单将被系统拦截
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推荐商品 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h5 class="fw-bold mb-3">
|
||||
<i class="fas fa-heart text-danger"></i> 推荐商品
|
||||
</h5>
|
||||
<div id="recommendedProducts" class="row">
|
||||
<!-- 推荐商品将通过AJAX加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let flashSaleId = ${flashSaleId};
|
||||
let flashSaleData = null;
|
||||
let countdownInterval = null;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadFlashSaleDetail();
|
||||
loadRecommendedProducts();
|
||||
});
|
||||
|
||||
// 加载秒杀详情
|
||||
function loadFlashSaleDetail() {
|
||||
$('#loadingDetail').show();
|
||||
$('#errorDetail').hide();
|
||||
$('#flashSaleDetail').hide();
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + flashSaleId,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data) {
|
||||
flashSaleData = response.data;
|
||||
renderFlashSaleDetail(flashSaleData);
|
||||
$('#flashSaleDetail').show();
|
||||
} else {
|
||||
showErrorDetail();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showErrorDetail();
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingDetail').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
function showErrorDetail() {
|
||||
$('#errorDetail').show();
|
||||
$('#flashSaleDetail').hide();
|
||||
}
|
||||
|
||||
// 渲染秒杀详情
|
||||
function renderFlashSaleDetail(data) {
|
||||
// 更新页面标题
|
||||
document.title = data.productName + ' - 秒杀详情';
|
||||
|
||||
// 商品信息
|
||||
$('#productImage').attr('src', data.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg')
|
||||
.attr('alt', data.productName);
|
||||
$('#productName').text(data.productName || '商品名称');
|
||||
$('#productDescription').text(data.productDescription || '暂无描述');
|
||||
|
||||
// 价格信息
|
||||
const flashPrice = data.flashPrice || 0;
|
||||
const originalPrice = data.originalPrice || 0;
|
||||
const savings = originalPrice - flashPrice;
|
||||
|
||||
$('#flashPrice').text('¥' + flashPrice.toFixed(2));
|
||||
$('#originalPrice').text('¥' + originalPrice.toFixed(2));
|
||||
$('#savings').text('节省 ¥' + savings.toFixed(2));
|
||||
|
||||
// 折扣标签
|
||||
if (originalPrice > flashPrice && originalPrice > 0) {
|
||||
const discountPercent = Math.round((1 - flashPrice / originalPrice) * 100);
|
||||
$('#discountBadge span').text(discountPercent + '% OFF');
|
||||
$('#discountBadge').show();
|
||||
}
|
||||
|
||||
// 库存信息
|
||||
const remainingStock = data.remainingStock || 0;
|
||||
const totalStock = data.flashStock || 0;
|
||||
const soldStock = totalStock - remainingStock;
|
||||
const stockPercent = totalStock > 0 ? (remainingStock / totalStock * 100) : 0;
|
||||
|
||||
$('#stockInfo').text(remainingStock + '/' + totalStock);
|
||||
$('#stockProgress').css('width', Math.max(5, stockPercent) + '%');
|
||||
|
||||
if (stockPercent > 50) {
|
||||
$('#stockProgress').removeClass('bg-warning bg-danger').addClass('bg-success');
|
||||
} else if (stockPercent > 20) {
|
||||
$('#stockProgress').removeClass('bg-success bg-danger').addClass('bg-warning');
|
||||
} else {
|
||||
$('#stockProgress').removeClass('bg-success bg-warning').addClass('bg-danger');
|
||||
}
|
||||
|
||||
// 时间信息
|
||||
$('#startTime').text(formatDateTime(data.startTime));
|
||||
$('#endTime').text(formatDateTime(data.endTime));
|
||||
|
||||
// 状态和按钮
|
||||
updateStatusAndButton(data);
|
||||
|
||||
// 启动倒计时
|
||||
startCountdown(data);
|
||||
}
|
||||
|
||||
// 更新状态和按钮
|
||||
function updateStatusAndButton(data) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(data.startTime);
|
||||
const endTime = new Date(data.endTime);
|
||||
const remainingStock = data.remainingStock || 0;
|
||||
|
||||
let status, buttonText, buttonClass, buttonIcon, buttonDisabled = false;
|
||||
|
||||
if (now < startTime) {
|
||||
// 未开始
|
||||
status = {text: '即将开始', class: 'bg-warning text-dark', icon: 'fas fa-clock'};
|
||||
buttonText = '活动未开始';
|
||||
buttonClass = 'btn-outline-primary';
|
||||
buttonIcon = 'fas fa-clock';
|
||||
buttonDisabled = true;
|
||||
} else if (now >= startTime && now < endTime) {
|
||||
if (remainingStock > 0) {
|
||||
// 进行中
|
||||
status = {text: '正在抢购', class: 'bg-danger', icon: 'fas fa-fire'};
|
||||
buttonText = '立即抢购';
|
||||
buttonClass = 'btn-danger';
|
||||
buttonIcon = 'fas fa-bolt';
|
||||
} else {
|
||||
// 已售罄
|
||||
status = {text: '已售罄', class: 'bg-secondary', icon: 'fas fa-times'};
|
||||
buttonText = '已售罄';
|
||||
buttonClass = 'btn-secondary';
|
||||
buttonIcon = 'fas fa-times';
|
||||
buttonDisabled = true;
|
||||
}
|
||||
} else {
|
||||
// 已结束
|
||||
status = {text: '已结束', class: 'bg-secondary', icon: 'fas fa-check'};
|
||||
buttonText = '活动已结束';
|
||||
buttonClass = 'btn-secondary';
|
||||
buttonIcon = 'fas fa-check';
|
||||
buttonDisabled = true;
|
||||
}
|
||||
|
||||
// 更新状态标签
|
||||
$('#statusBadge').attr('class', 'badge fs-6 ' + status.class)
|
||||
.html('<i class="' + status.icon + '"></i> ' + status.text);
|
||||
|
||||
// 更新按钮
|
||||
$('#actionButton').attr('class', 'btn btn-lg ' + buttonClass)
|
||||
.prop('disabled', buttonDisabled)
|
||||
.html('<i class="' + buttonIcon + '"></i> ' + buttonText);
|
||||
}
|
||||
|
||||
// 启动倒计时
|
||||
function startCountdown(data) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(data.startTime);
|
||||
const endTime = new Date(data.endTime);
|
||||
|
||||
let targetTime, labelText;
|
||||
|
||||
if (now < startTime) {
|
||||
targetTime = startTime.getTime();
|
||||
labelText = '距离开始还有';
|
||||
$('#countdown').removeClass('text-danger').addClass('text-primary');
|
||||
} else if (now >= startTime && now < endTime) {
|
||||
targetTime = endTime.getTime();
|
||||
labelText = '距离结束还有';
|
||||
$('#countdown').removeClass('text-primary').addClass('text-danger');
|
||||
} else {
|
||||
$('#countdownLabel').text('活动已结束');
|
||||
$('#countdown').text('00:00:00').removeClass('text-primary text-danger').addClass('text-muted');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#countdownLabel').text(labelText);
|
||||
|
||||
// 清除之前的定时器
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
|
||||
// 更新倒计时
|
||||
function updateCountdown() {
|
||||
const now = Date.now();
|
||||
const timeLeft = targetTime - now;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
// 重新加载页面数据
|
||||
loadFlashSaleDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
|
||||
|
||||
let timeString;
|
||||
if (days > 0) {
|
||||
timeString = days + '天 ' + hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
} else {
|
||||
timeString = hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
$('#countdown').text(timeString);
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
countdownInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
// 处理主要操作(抢购)
|
||||
function handleAction() {
|
||||
if (!flashSaleData) return;
|
||||
|
||||
const now = new Date();
|
||||
const startTime = new Date(flashSaleData.startTime);
|
||||
const endTime = new Date(flashSaleData.endTime);
|
||||
const remainingStock = flashSaleData.remainingStock || 0;
|
||||
|
||||
// 检查活动状态
|
||||
if (now < startTime) {
|
||||
showMessage('活动还未开始,请耐心等待', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (now >= endTime) {
|
||||
showMessage('活动已结束', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingStock <= 0) {
|
||||
showMessage('商品已售罄', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 参与秒杀
|
||||
participateFlashSale();
|
||||
}
|
||||
|
||||
// 参与秒杀(优化版)
|
||||
function participateFlashSale() {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
// 防止重复点击
|
||||
if (window.flashSaleInProgress) {
|
||||
showMessage('操作进行中,请稍候...', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
if (!confirm('确定要参与这个秒杀活动吗?\n\n注意:每人限购一件,确认后将立即抢购!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置全局锁
|
||||
window.flashSaleInProgress = true;
|
||||
|
||||
const button = $('#actionButton');
|
||||
const originalText = button.html();
|
||||
const originalClass = button.attr('class');
|
||||
|
||||
// 更新按钮状态
|
||||
button.prop('disabled', true);
|
||||
button.attr('class', 'btn btn-warning btn-lg');
|
||||
button.html('<i class="fas fa-spinner fa-spin"></i> 抢购中...');
|
||||
|
||||
// 添加视觉反馈
|
||||
button.css('transform', 'scale(0.95)');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
timeout: 10000, // 10秒超时
|
||||
data: JSON.stringify({
|
||||
flashSaleId: flashSaleId,
|
||||
quantity: 1,
|
||||
timestamp: startTime
|
||||
}),
|
||||
success: function (response) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (response.success) {
|
||||
// 成功状态
|
||||
button.attr('class', 'btn btn-success btn-lg');
|
||||
button.html('<i class="fas fa-check"></i> 抢购成功!');
|
||||
button.css('transform', 'scale(1.05)');
|
||||
|
||||
showMessage(`🎉 恭喜您!秒杀成功,订单已生成 (耗时: ${duration}ms)`, 'success');
|
||||
|
||||
// 重新加载详情数据
|
||||
setTimeout(() => {
|
||||
loadFlashSaleDetail();
|
||||
}, 1000);
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 3000);
|
||||
} else {
|
||||
// 失败状态
|
||||
button.attr('class', 'btn btn-danger btn-lg');
|
||||
button.html('<i class="fas fa-times"></i> ' + (response.message || '抢购失败'));
|
||||
|
||||
showMessage(response.message || '抢购失败,请重试', 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreDetailButton(button, originalText, originalClass);
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// 错误状态
|
||||
button.attr('class', 'btn btn-danger btn-lg');
|
||||
|
||||
let errorMessage = '网络异常,请重试';
|
||||
if (status === 'timeout') {
|
||||
errorMessage = '请求超时,请检查网络连接';
|
||||
button.html('<i class="fas fa-clock"></i> 请求超时');
|
||||
} else if (xhr.status === 429) {
|
||||
errorMessage = '请求过于频繁,请稍后再试';
|
||||
button.html('<i class="fas fa-ban"></i> 请求频繁');
|
||||
} else {
|
||||
button.html('<i class="fas fa-exclamation-triangle"></i> 网络异常');
|
||||
}
|
||||
|
||||
showMessage(errorMessage, 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreDetailButton(button, originalText, originalClass);
|
||||
}, 3000);
|
||||
},
|
||||
complete: function () {
|
||||
// 释放全局锁
|
||||
setTimeout(() => {
|
||||
window.flashSaleInProgress = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录后参与秒杀', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login?returnUrl=' +
|
||||
encodeURIComponent('/flashsale/' + flashSaleId);
|
||||
}, 1500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 恢复详情页按钮状态
|
||||
function restoreDetailButton(button, originalText, originalClass) {
|
||||
button.prop('disabled', false);
|
||||
button.attr('class', originalClass);
|
||||
button.html(originalText);
|
||||
button.css('transform', 'scale(1)');
|
||||
}
|
||||
|
||||
// 加入购物车(原价)
|
||||
function addToCart() {
|
||||
if (!flashSaleData) return;
|
||||
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: flashSaleData.productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 分享功能
|
||||
function shareWeChat() {
|
||||
showMessage('请使用微信扫一扫功能分享', 'info');
|
||||
}
|
||||
|
||||
function shareWeibo() {
|
||||
const text = encodeURIComponent('发现一个超值秒杀:' + (flashSaleData ? flashSaleData.productName : '') + ' 限时抢购!');
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
window.open('https://service.weibo.com/share/share.php?title=' + text + '&url=' + url, '_blank');
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
navigator.clipboard.writeText(window.location.href).then(function () {
|
||||
showMessage('链接已复制到剪贴板', 'success');
|
||||
}, function () {
|
||||
showMessage('复制失败,请手动复制链接', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载推荐商品
|
||||
function loadRecommendedProducts() {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/product/hot?limit=4',
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderRecommendedProducts(response.data);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
console.log('加载推荐商品失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染推荐商品
|
||||
function renderRecommendedProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function (product) {
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-3">
|
||||
<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;"
|
||||
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>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold">¥` + (product.price ? product.price.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted">库存: ` + (product.stock || 0) + `</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="addProductToCart(` + product.id + `)">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#recommendedProducts').html(html);
|
||||
}
|
||||
|
||||
// 添加推荐商品到购物车
|
||||
function addProductToCart(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
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);
|
||||
}
|
||||
|
||||
// 页面离开时清理定时器
|
||||
$(window).on('beforeunload', function () {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.price-section {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%);
|
||||
}
|
||||
|
||||
#countdown {
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stock-section {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.time-section,
|
||||
.limit-section {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.price-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-section .col-6 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#countdown {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.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" %>
|
||||
@@ -1,915 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="秒杀活动"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<!-- 页面标题 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item active">秒杀活动</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-fire text-danger"></i> 秒杀活动
|
||||
<small class="text-muted fs-6">限时抢购,先到先得</small>
|
||||
</h2>
|
||||
<div class="text-end">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="currentTime"></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="statusFilter" onchange="filterFlashSales()">
|
||||
<option value="">全部活动</option>
|
||||
<option value="upcoming">即将开始</option>
|
||||
<option value="active">进行中</option>
|
||||
<option value="ended">已结束</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="sortBy" onchange="sortFlashSales()">
|
||||
<option value="startTime">按开始时间</option>
|
||||
<option value="endTime">按结束时间</option>
|
||||
<option value="flashPrice">按价格</option>
|
||||
<option value="remainingStock">按剩余库存</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索商品名称...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchFlashSales()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-outline-primary w-100" onclick="refreshFlashSales()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-warning">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-clock fa-2x text-warning mb-2"></i>
|
||||
<h5 class="card-title text-warning" id="upcomingCount">0</h5>
|
||||
<p class="card-text">即将开始</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-success">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-fire fa-2x text-success mb-2"></i>
|
||||
<h5 class="card-title text-success" id="activeCount">0</h5>
|
||||
<p class="card-text">正在抢购</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-danger">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-bolt fa-2x text-danger mb-2"></i>
|
||||
<h5 class="card-title text-danger" id="hotCount">0</h5>
|
||||
<p class="card-text">热门活动</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-secondary">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-check fa-2x text-secondary mb-2"></i>
|
||||
<h5 class="card-title text-secondary" id="endedCount">0</h5>
|
||||
<p class="card-text">已结束</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingFlashSales" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载秒杀活动中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div id="emptyFlashSales" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-fire fa-4x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">暂无秒杀活动</h5>
|
||||
<p class="text-muted">敬请期待更多精彩活动~</p>
|
||||
</div>
|
||||
|
||||
<!-- 活动网格 -->
|
||||
<div id="flashSalesGrid" class="row" style="display: none;">
|
||||
<!-- 活动卡片将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="活动分页" class="mt-4">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 12;
|
||||
let totalPages = 1;
|
||||
let currentFilters = {};
|
||||
|
||||
$(document).ready(function () {
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000); // 每秒更新时间
|
||||
|
||||
loadFlashSales();
|
||||
|
||||
// 每30秒刷新一次数据
|
||||
setInterval(function () {
|
||||
refreshFlashSales();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
$('#currentTime').text(timeString);
|
||||
}
|
||||
|
||||
// 加载秒杀活动
|
||||
function loadFlashSales(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
$('#loadingFlashSales').show();
|
||||
$('#emptyFlashSales').hide();
|
||||
$('#flashSalesGrid').hide();
|
||||
|
||||
// 构建查询参数
|
||||
const queryData = {
|
||||
page: page - 1,
|
||||
size: pageSize,
|
||||
sortBy: $('#sortBy').val() || 'startTime',
|
||||
sortDirection: 'asc',
|
||||
...currentFilters
|
||||
};
|
||||
|
||||
// 添加搜索关键词
|
||||
const keyword = $('#searchInput').val().trim();
|
||||
if (keyword) {
|
||||
queryData.keyword = keyword;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/list',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(queryData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
const flashSales = response.data.content || response.data.flashSales || [];
|
||||
renderFlashSales(flashSales);
|
||||
renderPagination(response.data.totalElements || response.data.total || 0, pageSize);
|
||||
updateStatistics(flashSales);
|
||||
} else {
|
||||
showEmptyFlashSales();
|
||||
showMessage('获取秒杀活动失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showEmptyFlashSales();
|
||||
showMessage('网络请求失败,请稍后重试', 'error');
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingFlashSales').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染秒杀活动
|
||||
function renderFlashSales(flashSales) {
|
||||
if (flashSales.length === 0) {
|
||||
showEmptyFlashSales();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
flashSales.forEach(function (flashSale) {
|
||||
const discountPercent = flashSale.originalPrice > 0 ?
|
||||
Math.round((1 - flashSale.flashPrice / flashSale.originalPrice) * 100) : 0;
|
||||
|
||||
const status = getFlashSaleStatus(flashSale);
|
||||
const stockPercent = flashSale.flashStock > 0 ?
|
||||
(flashSale.remainingStock / flashSale.flashStock * 100) : 0;
|
||||
|
||||
html += `
|
||||
<div class="col-lg-4 col-md-6 mb-4">
|
||||
<div class="card h-100 flashsale-card" data-flashsale-id="` + flashSale.id + `">
|
||||
<div class="position-relative">
|
||||
<img src="` + getProductImageUrl(flashSale.productImageUrl) + `"
|
||||
class="card-img-top" alt="` + flashSale.productName + `" style="height: 220px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<div class="position-absolute top-0 start-0">
|
||||
<span class="badge ` + status.badgeClass + ` m-2">
|
||||
<i class="` + status.icon + `"></i> ` + status.text + `
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 折扣标签 -->
|
||||
` + (discountPercent > 0 ? `
|
||||
<div class="position-absolute top-0 end-0">
|
||||
<span class="badge bg-warning text-dark m-2">
|
||||
` + discountPercent + `% OFF
|
||||
</span>
|
||||
</div>
|
||||
` : '') + `
|
||||
</div>
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h6 class="card-title text-truncate" title="` + flashSale.productName + `">
|
||||
` + flashSale.productName + `
|
||||
</h6>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<div class="price-section mb-3">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<span class="text-danger fw-bold fs-4">¥` + (flashSale.flashPrice || 0).toFixed(2) + `</span>
|
||||
` + (flashSale.originalPrice > flashSale.flashPrice ? `
|
||||
<small class="text-muted text-decoration-line-through ms-2">
|
||||
¥` + (flashSale.originalPrice || 0).toFixed(2) + `
|
||||
</small>
|
||||
` : '') + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 库存进度 -->
|
||||
<div class="stock-section mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<small class="text-muted">剩余库存</small>
|
||||
<small class="text-muted">` + (flashSale.remainingStock || 0) + `/` + (flashSale.flashStock || 0) + `</small>
|
||||
</div>
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar ` + (stockPercent > 50 ? 'bg-success' : stockPercent > 20 ? 'bg-warning' : 'bg-danger') + `"
|
||||
style="width: ` + Math.max(5, stockPercent) + `%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<div class="time-section mb-3">
|
||||
` + getTimeDisplay(flashSale, status) + `
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-auto">
|
||||
` + getActionButton(flashSale, status) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#flashSalesGrid').html(html);
|
||||
$('#flashSalesGrid').show();
|
||||
|
||||
// 启动倒计时
|
||||
startCountdowns();
|
||||
}
|
||||
|
||||
// 显示空状态
|
||||
function showEmptyFlashSales() {
|
||||
$('#emptyFlashSales').show();
|
||||
$('#flashSalesGrid').hide();
|
||||
}
|
||||
|
||||
// 获取秒杀状态
|
||||
function getFlashSaleStatus(flashSale) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(flashSale.startTime);
|
||||
const endTime = new Date(flashSale.endTime);
|
||||
|
||||
if (now < startTime) {
|
||||
return {
|
||||
key: 'upcoming',
|
||||
text: '即将开始',
|
||||
badgeClass: 'bg-warning text-dark',
|
||||
icon: 'fas fa-clock'
|
||||
};
|
||||
} else if (now >= startTime && now < endTime) {
|
||||
if ((flashSale.remainingStock || 0) > 0) {
|
||||
return {
|
||||
key: 'active',
|
||||
text: '正在抢购',
|
||||
badgeClass: 'bg-danger',
|
||||
icon: 'fas fa-fire'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
key: 'soldout',
|
||||
text: '已售罄',
|
||||
badgeClass: 'bg-secondary',
|
||||
icon: 'fas fa-times'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
key: 'ended',
|
||||
text: '已结束',
|
||||
badgeClass: 'bg-secondary',
|
||||
icon: 'fas fa-check'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取时间显示
|
||||
function getTimeDisplay(flashSale, status) {
|
||||
if (status.key === 'upcoming') {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">距开始还有</small>
|
||||
<div class="countdown text-primary fw-bold" data-target="` + new Date(flashSale.startTime).getTime() + `">
|
||||
计算中...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (status.key === 'active') {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">距结束还有</small>
|
||||
<div class="countdown text-danger fw-bold" data-target="` + new Date(flashSale.endTime).getTime() + `">
|
||||
计算中...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">活动时间</small>
|
||||
<div class="small">` + formatDateTime(flashSale.startTime) + ` - ` + formatDateTime(flashSale.endTime) + `</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取操作按钮
|
||||
function getActionButton(flashSale, status) {
|
||||
if (status.key === 'upcoming') {
|
||||
return `
|
||||
<button class="btn btn-outline-primary w-100" disabled>
|
||||
<i class="fas fa-clock"></i> 活动未开始
|
||||
</button>
|
||||
`;
|
||||
} else if (status.key === 'active') {
|
||||
return `
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-danger btn-lg flash-sale-btn"
|
||||
onclick="participateFlashSale(` + flashSale.id + `)"
|
||||
data-flashsale-id="` + flashSale.id + `"
|
||||
onmouseover="this.style.transform='scale(1.02)'"
|
||||
onmouseout="this.style.transform='scale(1)'">
|
||||
<i class="fas fa-bolt"></i> 立即抢购
|
||||
</button>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="viewFlashSaleDetail(` + flashSale.id + `)">
|
||||
<i class="fas fa-eye"></i> 查看详情
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else if (status.key === 'soldout') {
|
||||
return `
|
||||
<button class="btn btn-secondary w-100" disabled>
|
||||
<i class="fas fa-times"></i> 已售罄
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<button class="btn btn-outline-secondary w-100" onclick="viewFlashSaleDetail(` + flashSale.id + `)">
|
||||
<i class="fas fa-eye"></i> 查看详情
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动倒计时
|
||||
function startCountdowns() {
|
||||
$('.countdown').each(function () {
|
||||
const element = $(this);
|
||||
const targetTime = parseInt(element.data('target'));
|
||||
|
||||
if (targetTime) {
|
||||
updateCountdown(element, targetTime);
|
||||
|
||||
// 每秒更新倒计时
|
||||
element.data('interval', setInterval(function () {
|
||||
updateCountdown(element, targetTime);
|
||||
}, 1000));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新倒计时
|
||||
function updateCountdown(element, targetTime) {
|
||||
const now = Date.now();
|
||||
const timeLeft = targetTime - now;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
element.text('时间到');
|
||||
element.removeClass('text-primary text-danger').addClass('text-muted');
|
||||
clearInterval(element.data('interval'));
|
||||
// 刷新页面数据
|
||||
setTimeout(refreshFlashSales, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
|
||||
|
||||
let timeString = '';
|
||||
if (days > 0) {
|
||||
timeString = days + '天 ' + hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
} else {
|
||||
timeString = hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
element.text(timeString);
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStatistics(flashSales) {
|
||||
let upcoming = 0, active = 0, hot = 0, ended = 0;
|
||||
|
||||
flashSales.forEach(function (flashSale) {
|
||||
const status = getFlashSaleStatus(flashSale);
|
||||
|
||||
switch (status.key) {
|
||||
case 'upcoming':
|
||||
upcoming++;
|
||||
break;
|
||||
case 'active':
|
||||
active++;
|
||||
if ((flashSale.remainingStock / flashSale.flashStock) < 0.3) {
|
||||
hot++; // 库存少于30%认为是热门
|
||||
}
|
||||
break;
|
||||
case 'ended':
|
||||
case 'soldout':
|
||||
ended++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$('#upcomingCount').text(upcoming);
|
||||
$('#activeCount').text(active);
|
||||
$('#hotCount').text(hot);
|
||||
$('#endedCount').text(ended);
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('#pagination').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
// 筛选活动
|
||||
function filterFlashSales() {
|
||||
const status = $('#statusFilter').val();
|
||||
currentFilters.status = status;
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
// 排序活动
|
||||
function sortFlashSales() {
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
// 搜索活动
|
||||
function searchFlashSales() {
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
// 刷新活动
|
||||
function refreshFlashSales() {
|
||||
// 清除所有倒计时
|
||||
$('.countdown').each(function () {
|
||||
clearInterval($(this).data('interval'));
|
||||
});
|
||||
|
||||
loadFlashSales(currentPage);
|
||||
}
|
||||
|
||||
// 参与秒杀(优化版)
|
||||
function participateFlashSale(flashSaleId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
// 防止重复点击
|
||||
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-lg';
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 抢购中...';
|
||||
|
||||
// 添加视觉反馈
|
||||
button.style.transform = 'scale(0.95)';
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
timeout: 10000, // 10秒超时
|
||||
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-lg';
|
||||
button.innerHTML = '<i class="fas fa-check"></i> 抢购成功!';
|
||||
button.style.transform = 'scale(1.05)';
|
||||
|
||||
showMessage(`🎉 恭喜您!秒杀成功,订单已生成 (耗时: ${duration}ms)`, 'success');
|
||||
|
||||
// 刷新页面数据
|
||||
setTimeout(() => {
|
||||
refreshFlashSales();
|
||||
}, 1000);
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 3000);
|
||||
} else {
|
||||
// 失败状态
|
||||
button.className = 'btn btn-danger btn-lg';
|
||||
button.innerHTML = '<i class="fas fa-times"></i> ' + (response.message || '抢购失败');
|
||||
|
||||
showMessage(response.message || '抢购失败,请重试', 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreButton(button, originalText, originalClass);
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// 错误状态
|
||||
button.className = 'btn btn-danger btn-lg';
|
||||
|
||||
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> 网络异常';
|
||||
}
|
||||
|
||||
showMessage(errorMessage, 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreButton(button, originalText, originalClass);
|
||||
}, 3000);
|
||||
},
|
||||
complete: function () {
|
||||
// 释放全局锁
|
||||
setTimeout(() => {
|
||||
window.flashSaleInProgress = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录后参与秒杀', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login?returnUrl=' +
|
||||
encodeURIComponent('/flashsales');
|
||||
}, 1500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 恢复按钮状态
|
||||
function restoreButton(button, originalText, originalClass) {
|
||||
button.disabled = false;
|
||||
button.className = originalClass;
|
||||
button.innerHTML = originalText;
|
||||
button.style.transform = 'scale(1)';
|
||||
}
|
||||
|
||||
// 查看秒杀详情
|
||||
function viewFlashSaleDetail(flashSaleId) {
|
||||
window.location.href = '${pageContext.request.contextPath}/flashsale/' + flashSaleId;
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
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);
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.flashsale-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.flashsale-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.flashsale-card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 1.1em;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.price-section .fs-4 {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flashsale-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.flashsale-card:hover .card-img-top {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.price-section .fs-4 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 抢购按钮样式优化 */
|
||||
.flash-sale-btn {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.flash-sale-btn:hover {
|
||||
box-shadow: 0 8px 25px rgba(220, 53, 69, 0.4);
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
}
|
||||
|
||||
.flash-sale-btn:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
.flash-sale-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.flash-sale-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* 按钮禁用状态 */
|
||||
.flash-sale-btn:disabled {
|
||||
opacity: 0.8;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fa-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 按钮状态动画 */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(220, 53, 69, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 53, 69, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger.flash-sale-btn:not(:disabled) {
|
||||
animation: pulse 2s 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" %>
|
||||
@@ -1,703 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="首页" />
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<!-- 轮播图 -->
|
||||
<div id="heroCarousel" class="carousel slide" data-bs-ride="carousel">
|
||||
<div class="carousel-indicators">
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0" class="active"></button>
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="1"></button>
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="2"></button>
|
||||
</div>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<div class="bg-gradient-danger text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-bolt"></i> 秒杀系统
|
||||
</h1>
|
||||
<p class="lead mb-4">基于Redis集群构建的高并发秒杀系统,支持分布式锁、接口限流、库存预热等核心功能。</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-fire"></i> 立即抢购
|
||||
</a>
|
||||
<a href="${pageContext.request.contextPath}/products" class="btn btn-outline-light btn-lg">
|
||||
<i class="fas fa-shopping-bag"></i> 浏览商品
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-rocket fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<div class="bg-gradient-primary text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-shield-alt"></i> 防超卖机制
|
||||
</h1>
|
||||
<p class="lead mb-4">采用Redis分布式锁和Lua脚本,确保高并发场景下的数据一致性,彻底解决超卖问题。</p>
|
||||
<a href="#features" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-info-circle"></i> 了解更多
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-lock fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<div class="bg-gradient-success text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-tachometer-alt"></i> 高性能缓存
|
||||
</h1>
|
||||
<p class="lead mb-4">Redis集群架构,支持五种数据类型应用,实现毫秒级响应,轻松应对高并发访问。</p>
|
||||
<a href="#performance" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-chart-line"></i> 性能指标
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-database fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#heroCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon"></span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#heroCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container my-5">
|
||||
<!-- 正在进行的秒杀活动 -->
|
||||
<section class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-fire text-danger"></i> 正在秒杀
|
||||
</h2>
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-outline-danger">
|
||||
查看全部 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="activeFlashSales" class="row">
|
||||
<!-- 动态加载秒杀活动 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 热门商品 -->
|
||||
<section class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-star text-warning"></i> 热门商品
|
||||
</h2>
|
||||
<a href="${pageContext.request.contextPath}/products" class="btn btn-outline-primary">
|
||||
查看全部 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="hotProducts" class="row">
|
||||
<!-- 动态加载热门商品 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 系统特性 -->
|
||||
<section id="features" class="mb-5">
|
||||
<h2 class="text-center fw-bold mb-5">
|
||||
<i class="fas fa-cogs"></i> 系统特性
|
||||
</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-bolt fa-3x text-danger mb-3"></i>
|
||||
<h5 class="card-title">秒杀抢购</h5>
|
||||
<p class="card-text text-muted">高并发秒杀系统,支持大量用户同时抢购</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-shield-alt fa-3x text-success mb-3"></i>
|
||||
<h5 class="card-title">防超卖</h5>
|
||||
<p class="card-text text-muted">分布式锁机制,确保库存数据一致性</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-database fa-3x text-info mb-3"></i>
|
||||
<h5 class="card-title">Redis缓存</h5>
|
||||
<p class="card-text text-muted">五种数据类型应用,毫秒级响应</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-tachometer-alt fa-3x text-warning mb-3"></i>
|
||||
<h5 class="card-title">接口限流</h5>
|
||||
<p class="card-text text-muted">多种限流策略,防止恶意刷单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// 加载正在进行的秒杀活动
|
||||
loadActiveFlashSales();
|
||||
|
||||
// 加载热门商品
|
||||
loadHotProducts();
|
||||
|
||||
// 启动性能指标动画
|
||||
animateCounters();
|
||||
|
||||
// 更新购物车数量(如果用户已登录)
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
updateCartCount();
|
||||
</c:if>
|
||||
});
|
||||
|
||||
// 加载正在进行的秒杀活动
|
||||
function loadActiveFlashSales() {
|
||||
$.get('${pageContext.request.contextPath}/api/flashsale/active')
|
||||
.done(function(response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderFlashSales(response.data.slice(0, 4)); // 只显示前4个
|
||||
} else {
|
||||
$('#activeFlashSales').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">暂无进行中的秒杀活动</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
$('#activeFlashSales').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
||||
<p class="text-muted mt-2">加载失败,请刷新页面重试</p>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染秒杀活动
|
||||
function renderFlashSales(flashSales) {
|
||||
let html = '';
|
||||
|
||||
flashSales.forEach(function(flashSale) {
|
||||
const discountPercent = Math.round((1 - flashSale.flashPrice / flashSale.originalPrice) * 100);
|
||||
const imageUrl = getProductImageUrl(flashSale.productImageUrl);
|
||||
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100 border-danger">
|
||||
<div class="position-relative">
|
||||
<img src="` + imageUrl + `"
|
||||
class="card-img-top" alt="` + flashSale.productName + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="position-absolute top-0 start-0 bg-danger text-white px-2 py-1 rounded-end">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + flashSale.productName + `</h6>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<span class="text-danger fw-bold fs-5">¥` + (flashSale.flashPrice ? flashSale.flashPrice.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted text-decoration-line-through ms-2">¥` + (flashSale.originalPrice ? flashSale.originalPrice.toFixed(2) : '0.00') + `</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">剩余: ` + (flashSale.remainingStock || 0) + `件</small>
|
||||
<div class="progress" style="height: 4px;">
|
||||
<div class="progress-bar bg-danger" style="width: ` + ((flashSale.remainingStock || 0) / (flashSale.flashStock || 1) * 100) + `%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-danger fw-bold mb-2" id="countdown_${flashSale.id}">
|
||||
计算中...
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 启动倒计时
|
||||
setTimeout(() => {
|
||||
if (flashSale.timeToEnd > 0) {
|
||||
countdown(Date.now() + flashSale.timeToEnd, 'countdown_' + flashSale.id);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
$('#activeFlashSales').html(html);
|
||||
}
|
||||
|
||||
// 加载热门商品
|
||||
function loadHotProducts() {
|
||||
$.get('${pageContext.request.contextPath}/api/product/hot?limit=8')
|
||||
.done(function(response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderHotProducts(response.data);
|
||||
} else {
|
||||
$('#hotProducts').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">暂无热门商品</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
$('#hotProducts').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
||||
<p class="text-muted mt-2">加载失败,请刷新页面重试</p>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染热门商品
|
||||
function renderHotProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function(product) {
|
||||
const imageUrl = getProductImageUrl(product.imageUrl);
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<img src="` + imageUrl + `"
|
||||
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>
|
||||
<p class="card-text text-muted small text-truncate">` + (product.description || '暂无描述') + `</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold">¥` + (product.price ? product.price.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted">库存: ` + (product.stock || 0) + `</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="addToCart(` + product.id + `)">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#hotProducts').html(html);
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
|
||||
// 参与秒杀(首页版)
|
||||
function participateFlashSale(flashSaleId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
// 防止重复点击
|
||||
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');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 添加到购物车
|
||||
function addToCart(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 性能指标动画
|
||||
function animateCounters() {
|
||||
const counters = [
|
||||
{ id: 'qpsCounter', target: 10000, suffix: '+' },
|
||||
{ id: 'concurrentUsers', target: 50000, suffix: '+' }
|
||||
];
|
||||
|
||||
counters.forEach(counter => {
|
||||
animateCounter(counter.id, counter.target, counter.suffix);
|
||||
});
|
||||
}
|
||||
|
||||
function animateCounter(elementId, target, suffix = '') {
|
||||
const element = document.getElementById(elementId);
|
||||
let current = 0;
|
||||
const increment = target / 100;
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= target) {
|
||||
current = target;
|
||||
clearInterval(timer);
|
||||
}
|
||||
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" %>
|
||||
@@ -1,248 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户登录"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-sign-in-alt"></i> 用户登录
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user"></i> 用户名
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="请输入用户名" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock"></i> 密码
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="请输入密码" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="rememberMe">
|
||||
<label class="form-check-label" for="rememberMe">
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">
|
||||
<i class="fas fa-sign-in-alt"></i> 登录
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-2">还没有账号?</p>
|
||||
<a href="${pageContext.request.contextPath}/register" class="btn btn-outline-success">
|
||||
<i class="fas fa-user-plus"></i> 立即注册
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统特性介绍 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle"></i> 系统特性
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-3">
|
||||
<i class="fas fa-bolt fa-2x text-danger mb-2"></i>
|
||||
<h6>秒杀抢购</h6>
|
||||
<small class="text-muted">高并发秒杀系统</small>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<i class="fas fa-shield-alt fa-2x text-success mb-2"></i>
|
||||
<h6>防超卖</h6>
|
||||
<small class="text-muted">分布式锁机制</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<i class="fas fa-database fa-2x text-info mb-2"></i>
|
||||
<h6>Redis缓存</h6>
|
||||
<small class="text-muted">高性能缓存</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<i class="fas fa-tachometer-alt fa-2x text-warning mb-2"></i>
|
||||
<h6>接口限流</h6>
|
||||
<small class="text-muted">防刷机制</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码显示/隐藏切换
|
||||
$('#togglePassword').click(function () {
|
||||
const passwordField = $('#password');
|
||||
const icon = $(this).find('i');
|
||||
|
||||
if (passwordField.attr('type') === 'password') {
|
||||
passwordField.attr('type', 'text');
|
||||
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.attr('type', 'password');
|
||||
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
$('#loginForm').submit(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = $('#username').val().trim();
|
||||
const password = $('#password').val();
|
||||
|
||||
// 基本验证
|
||||
if (!username) {
|
||||
showFieldError('username', '请输入用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showFieldError('password', '请输入密码');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showFieldError('password', '密码长度至少6位');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的错误状态
|
||||
clearFieldErrors();
|
||||
|
||||
// 显示加载状态
|
||||
const loginBtn = $('#loginBtn');
|
||||
const originalText = loginBtn.html();
|
||||
loginBtn.html('<i class="fas fa-spinner fa-spin"></i> 登录中...').prop('disabled', true);
|
||||
|
||||
// 发送登录请求
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/user/login',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('登录成功,正在跳转...', 'success');
|
||||
|
||||
// 保存记住我状态
|
||||
if ($('#rememberMe').is(':checked')) {
|
||||
localStorage.setItem('rememberedUsername', username);
|
||||
} else {
|
||||
localStorage.removeItem('rememberedUsername');
|
||||
}
|
||||
|
||||
// 跳转到首页或之前访问的页面
|
||||
setTimeout(() => {
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('returnUrl');
|
||||
window.location.href = returnUrl || '${pageContext.request.contextPath}/';
|
||||
}, 1000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.status === 400) {
|
||||
const response = xhr.responseJSON;
|
||||
showMessage(response.message || '登录失败', 'error');
|
||||
} else {
|
||||
showMessage('网络错误,请稍后重试', 'error');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
// 恢复按钮状态
|
||||
loginBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面加载时检查记住的用户名
|
||||
const rememberedUsername = localStorage.getItem('rememberedUsername');
|
||||
if (rememberedUsername) {
|
||||
$('#username').val(rememberedUsername);
|
||||
$('#rememberMe').prop('checked', true);
|
||||
$('#password').focus();
|
||||
} else {
|
||||
$('#username').focus();
|
||||
}
|
||||
|
||||
// 回车键快速登录
|
||||
$(document).keypress(function (e) {
|
||||
if (e.which === 13) { // Enter键
|
||||
$('#loginForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 快速登录演示账号
|
||||
function quickLogin(username, password) {
|
||||
$('#username').val(username);
|
||||
$('#password').val(password);
|
||||
$('#loginForm').submit();
|
||||
}
|
||||
|
||||
// 显示字段错误
|
||||
function showFieldError(fieldName, message) {
|
||||
const field = $('#' + fieldName);
|
||||
field.addClass('is-invalid');
|
||||
field.siblings('.invalid-feedback').text(message);
|
||||
}
|
||||
|
||||
// 清除字段错误
|
||||
function clearFieldErrors() {
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
$('.invalid-feedback').text('');
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
function checkLoginStatus() {
|
||||
$.get('${pageContext.request.contextPath}/api/user/current')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
// 已登录,跳转到首页
|
||||
window.location.href = '${pageContext.request.contextPath}/';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态(延迟执行,避免影响用户输入)
|
||||
// 注释掉自动检查,避免页面刷新影响用户输入
|
||||
// setTimeout(function() {
|
||||
// checkLoginStatus();
|
||||
// }, 5000);
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
@@ -1,571 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="订单详情"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/orders">我的订单</a></li>
|
||||
<li class="breadcrumb-item active">订单详情</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingOrder" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="text-muted mt-2">正在加载订单详情...</p>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情内容 -->
|
||||
<div id="orderDetail" style="display: none;">
|
||||
<!-- 订单状态和基本信息 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-receipt text-primary"></i> 订单信息
|
||||
</h5>
|
||||
<div id="orderActions">
|
||||
<!-- 订单操作按钮将动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td class="text-muted" style="width: 120px;">订单号:</td>
|
||||
<td id="orderNo">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">订单状态:</td>
|
||||
<td><span id="orderStatus" class="badge">-</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">订单类型:</td>
|
||||
<td id="orderType">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">下单时间:</td>
|
||||
<td id="createdAt">-</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td class="text-muted" style="width: 120px;">商品数量:</td>
|
||||
<td id="quantity">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">订单金额:</td>
|
||||
<td class="text-danger fw-bold fs-5" id="totalPrice">¥0.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">更新时间:</td>
|
||||
<td id="updatedAt">-</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品信息 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-box text-success"></i> 商品信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body" id="productInfo">
|
||||
<!-- 商品信息将动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单操作历史 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-history text-info"></i> 订单状态记录
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="statusHistory">
|
||||
<!-- 状态历史将动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div id="errorState" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle fa-4x text-warning mb-3"></i>
|
||||
<h4 class="text-muted">订单信息加载失败</h4>
|
||||
<p class="text-muted" id="errorMessage">请稍后重试或联系客服</p>
|
||||
<a href="${pageContext.request.contextPath}/orders" class="btn btn-primary">
|
||||
<i class="fas fa-arrow-left"></i> 返回订单列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付确认模态框 -->
|
||||
<div class="modal fade" id="paymentModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">确认支付</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center mb-3">
|
||||
<i class="fas fa-credit-card fa-3x text-primary"></i>
|
||||
</div>
|
||||
<p class="text-center">确定要支付此订单吗?</p>
|
||||
<div class="alert alert-info">
|
||||
<small>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
这是模拟支付,99%概率成功
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="processPayment()">
|
||||
<i class="fas fa-credit-card"></i> 确认支付
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const orderId = ${orderId};
|
||||
let currentOrder = null;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadOrderDetail();
|
||||
});
|
||||
|
||||
// 加载订单详情
|
||||
function loadOrderDetail() {
|
||||
$('#loadingOrder').show();
|
||||
$('#orderDetail').hide();
|
||||
$('#errorState').hide();
|
||||
|
||||
console.log('Loading order detail for orderId:', orderId);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
console.log('Order detail response:', response);
|
||||
if (response.success) {
|
||||
currentOrder = response.data;
|
||||
console.log('Current order data:', currentOrder);
|
||||
renderOrderDetail(currentOrder);
|
||||
$('#orderDetail').show();
|
||||
} else {
|
||||
console.error('Failed to get order:', response.message);
|
||||
showError(response.message || '订单信息获取失败');
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('Error loading order:', xhr.status, xhr.responseText);
|
||||
let errorMessage = '网络错误,请稍后重试';
|
||||
if (xhr.status === 404) {
|
||||
errorMessage = '订单不存在或已被删除';
|
||||
} else if (xhr.status === 401) {
|
||||
errorMessage = '请先登录';
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login?returnUrl=' + encodeURIComponent(window.location.pathname);
|
||||
}, 1500);
|
||||
} else if (xhr.status === 403) {
|
||||
errorMessage = '无权限查看此订单';
|
||||
}
|
||||
showError(errorMessage);
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingOrder').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染订单详情
|
||||
function renderOrderDetail(order) {
|
||||
if (!order) {
|
||||
console.error('Order data is null or undefined');
|
||||
showError('订单数据为空');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Rendering order detail:', order);
|
||||
|
||||
// 基本信息
|
||||
$('#orderNo').text('#' + (order.id || 'unknown'));
|
||||
$('#quantity').text((order.quantity || 0) + ' 件');
|
||||
$('#totalPrice').text('¥' + (order.totalPrice ? parseFloat(order.totalPrice).toFixed(2) : '0.00'));
|
||||
$('#createdAt').text(formatDateTime(order.createdAt));
|
||||
$('#updatedAt').text(formatDateTime(order.updatedAt));
|
||||
|
||||
// 订单状态
|
||||
const statusInfo = getStatusInfo(order.status || 0);
|
||||
$('#orderStatus').removeClass().addClass('badge ' + statusInfo.class).text(statusInfo.text);
|
||||
|
||||
// 订单类型
|
||||
const typeText = (order.orderType === 2) ? '秒杀订单' : '普通订单';
|
||||
$('#orderType').text(typeText);
|
||||
|
||||
// 商品信息
|
||||
renderProductInfo(order);
|
||||
|
||||
// 订单操作按钮
|
||||
renderOrderActions(order);
|
||||
|
||||
// 状态历史
|
||||
renderStatusHistory(order);
|
||||
}
|
||||
|
||||
// 渲染商品信息
|
||||
function renderProductInfo(order) {
|
||||
// 计算单价
|
||||
const totalPrice = order.totalPrice ? parseFloat(order.totalPrice) : 0;
|
||||
const quantity = order.quantity || 1;
|
||||
const unitPrice = totalPrice / quantity;
|
||||
|
||||
const productName = order.productName || '未知商品';
|
||||
const productId = order.productId || '-';
|
||||
const productImageUrl = order.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
const isFlashSale = order.orderType === 2;
|
||||
|
||||
const productHtml = `
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-2">
|
||||
<img src="` + productImageUrl + `"
|
||||
class="img-fluid rounded" alt="` + productName + `"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;"
|
||||
style="max-height: 100px; object-fit: cover;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-1">` + productName + `</h6>
|
||||
<p class="text-muted small mb-0">商品ID: ` + productId + `</p>
|
||||
` + (isFlashSale ? '<span class="badge bg-danger small">秒杀商品</span>' : '<span class="badge bg-primary small">普通商品</span>') + `
|
||||
</div>
|
||||
<div class="col-md-2 text-center">
|
||||
<span class="text-primary fw-bold">¥` + unitPrice.toFixed(2) + `</span>
|
||||
<br><small class="text-muted">单价</small>
|
||||
</div>
|
||||
<div class="col-md-2 text-center">
|
||||
<span class="badge bg-light text-dark fs-6">× ` + quantity + `</span>
|
||||
<br><small class="text-muted">数量</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#productInfo').html(productHtml);
|
||||
}
|
||||
|
||||
// 渲染订单操作按钮
|
||||
function renderOrderActions(order) {
|
||||
let actionsHtml = '';
|
||||
|
||||
switch (order.status) {
|
||||
case 1: // 待支付
|
||||
actionsHtml = `
|
||||
<button class="btn btn-primary btn-sm me-2" onclick="showPaymentModal()">
|
||||
<i class="fas fa-credit-card"></i> 立即支付
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="cancelOrder()">
|
||||
<i class="fas fa-times"></i> 取消订单
|
||||
</button>
|
||||
`;
|
||||
break;
|
||||
case 2: // 已支付
|
||||
actionsHtml = `
|
||||
<button class="btn btn-info btn-sm" onclick="confirmReceipt()" disabled>
|
||||
<i class="fas fa-truck"></i> 等待发货
|
||||
</button>
|
||||
`;
|
||||
break;
|
||||
case 3: // 已发货
|
||||
actionsHtml = `
|
||||
<button class="btn btn-success btn-sm" onclick="confirmReceipt()">
|
||||
<i class="fas fa-check"></i> 确认收货
|
||||
</button>
|
||||
`;
|
||||
break;
|
||||
case 4: // 已完成
|
||||
actionsHtml = `
|
||||
<span class="text-success">
|
||||
<i class="fas fa-check-circle"></i> 订单已完成
|
||||
</span>
|
||||
`;
|
||||
break;
|
||||
case 5: // 已取消
|
||||
actionsHtml = `
|
||||
<span class="text-muted">
|
||||
<i class="fas fa-ban"></i> 订单已取消
|
||||
</span>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
$('#orderActions').html(actionsHtml);
|
||||
}
|
||||
|
||||
// 渲染状态历史
|
||||
function renderStatusHistory(order) {
|
||||
const statusSteps = [
|
||||
{status: 1, text: '订单创建', icon: 'fas fa-plus-circle', time: order.createdAt},
|
||||
{status: 2, text: '支付完成', icon: 'fas fa-credit-card'},
|
||||
{status: 3, text: '商品发货', icon: 'fas fa-truck'},
|
||||
{status: 4, text: '订单完成', icon: 'fas fa-check-circle'}
|
||||
];
|
||||
|
||||
let historyHtml = '<div class="timeline">';
|
||||
statusSteps.forEach((step, index) => {
|
||||
const isActive = order.status >= step.status;
|
||||
const isCurrent = order.status === step.status;
|
||||
const statusClass = isActive ? 'text-success' : 'text-muted';
|
||||
|
||||
historyHtml += `
|
||||
<div class="timeline-item ` + (isActive ? 'active' : '') + `">
|
||||
<div class="timeline-marker">
|
||||
<i class="` + step.icon + ` ` + statusClass + `"></i>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<h6 class="` + statusClass + `">` + step.text + `</h6>
|
||||
` + (step.time ? `<small class="text-muted">` + formatDateTime(step.time) + `</small>` : '') + `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
historyHtml += '</div>';
|
||||
|
||||
$('#statusHistory').html(historyHtml);
|
||||
}
|
||||
|
||||
// 显示支付模态框
|
||||
function showPaymentModal() {
|
||||
$('#paymentModal').modal('show');
|
||||
}
|
||||
|
||||
// 处理支付
|
||||
function processPayment() {
|
||||
$('#paymentModal').modal('hide');
|
||||
|
||||
const payBtn = $('button[onclick="processPayment()"]');
|
||||
const originalText = payBtn.html();
|
||||
payBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 支付中...');
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/pay',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('💳 支付成功!', 'success');
|
||||
setTimeout(() => {
|
||||
loadOrderDetail(); // 重新加载订单详情
|
||||
}, 1000);
|
||||
} else {
|
||||
showMessage('❌ 支付失败:' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('❌ 支付失败,请重试', 'error');
|
||||
},
|
||||
complete: function () {
|
||||
payBtn.prop('disabled', false).html(originalText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
function cancelOrder() {
|
||||
if (!confirm('确定要取消这个订单吗?\n\n取消后无法恢复。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/cancel',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('订单已取消', 'success');
|
||||
setTimeout(() => {
|
||||
loadOrderDetail(); // 重新加载订单详情
|
||||
}, 1000);
|
||||
} else {
|
||||
showMessage('取消失败:' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('取消失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认收货
|
||||
function confirmReceipt() {
|
||||
if (!confirm('确定已收到商品吗?\n\n确认后订单将标记为完成。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/confirm',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('✅ 确认收货成功!', 'success');
|
||||
setTimeout(() => {
|
||||
loadOrderDetail(); // 重新加载订单详情
|
||||
}, 1000);
|
||||
} else {
|
||||
showMessage('确认失败:' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('确认失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取状态信息
|
||||
function getStatusInfo(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return {text: '待支付', class: 'bg-warning'};
|
||||
case 2:
|
||||
return {text: '已支付', class: 'bg-info'};
|
||||
case 3:
|
||||
return {text: '已发货', class: 'bg-primary'};
|
||||
case 4:
|
||||
return {text: '已完成', class: 'bg-success'};
|
||||
case 5:
|
||||
return {text: '已取消', class: 'bg-secondary'};
|
||||
default:
|
||||
return {text: '未知状态', class: 'bg-secondary'};
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 显示错误
|
||||
function showError(message) {
|
||||
$('#errorMessage').text(message);
|
||||
$('#errorState').show();
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.timeline-item:not(:last-child)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -22px;
|
||||
top: 20px;
|
||||
bottom: -20px;
|
||||
width: 2px;
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item.active:not(:last-child)::before {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.timeline-item.active .timeline-marker {
|
||||
border-color: #28a745;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
@@ -1,861 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="我的订单"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item active">我的订单</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单筛选和搜索 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="statusFilter" onchange="filterOrders()">
|
||||
<option value="">全部订单</option>
|
||||
<option value="1">待支付</option>
|
||||
<option value="2">已支付</option>
|
||||
<option value="3">已发货</option>
|
||||
<option value="4">已完成</option>
|
||||
<option value="5">已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="typeFilter" onchange="filterOrders()">
|
||||
<option value="">全部类型</option>
|
||||
<option value="1">普通订单</option>
|
||||
<option value="2">秒杀订单</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput"
|
||||
placeholder="搜索订单号或商品名称...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchOrders()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-outline-primary w-100" onclick="refreshOrders()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list-alt text-primary"></i> 我的订单
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingOrders" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载订单中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空订单状态 -->
|
||||
<div id="emptyOrders" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-receipt fa-4x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">暂无订单</h5>
|
||||
<p class="text-muted">快去下单购买您喜欢的商品吧~</p>
|
||||
<a href="${pageContext.request.contextPath}/" class="btn btn-primary">
|
||||
<i class="fas fa-shopping-bag"></i> 去购物
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表容器 -->
|
||||
<div id="ordersList"></div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="订单分页" class="mt-4">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情模态框 -->
|
||||
<div class="modal fade" id="orderDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">订单详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="orderDetailContent">
|
||||
<!-- 订单详情内容将动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
loadOrders();
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
$('#loadingOrders').hide();
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
});
|
||||
|
||||
// 加载订单列表
|
||||
function loadOrders(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
$('#loadingOrders').show();
|
||||
$('#emptyOrders').hide();
|
||||
$('#ordersList').hide();
|
||||
|
||||
// 构建查询参数
|
||||
const queryData = {
|
||||
page: page - 1, // 后端使用0基索引
|
||||
size: pageSize,
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc'
|
||||
};
|
||||
|
||||
// 添加状态筛选
|
||||
const statusFilter = $('#statusFilter').val();
|
||||
if (statusFilter) {
|
||||
queryData.status = parseInt(statusFilter);
|
||||
}
|
||||
|
||||
// 添加类型筛选
|
||||
const typeFilter = $('#typeFilter').val();
|
||||
if (typeFilter) {
|
||||
queryData.orderType = parseInt(typeFilter);
|
||||
}
|
||||
|
||||
// 添加搜索关键词
|
||||
const keyword = $('#searchInput').val().trim();
|
||||
if (keyword) {
|
||||
queryData.keyword = keyword;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/my-orders',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(queryData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
renderOrders(response.data.content || response.data.orders || []);
|
||||
renderPagination(response.data.totalElements || response.data.total || 0, pageSize);
|
||||
} else {
|
||||
showEmptyOrders();
|
||||
showMessage('获取订单数据失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showEmptyOrders();
|
||||
showMessage('网络请求失败,请稍后重试', 'error');
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingOrders').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染订单列表
|
||||
function renderOrders(orders) {
|
||||
if (orders.length === 0) {
|
||||
showEmptyOrders();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
orders.forEach(function (order) {
|
||||
html += `
|
||||
<div class="order-item border rounded mb-3 p-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-1">
|
||||
<img src="` + getProductImageUrl(order.productImageUrl) + `"
|
||||
class="img-fluid rounded" alt="` + (order.productName || '商品') + `" style="max-height: 60px;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">订单号</small>
|
||||
<div class="fw-bold text-truncate" style="font-size: 0.9rem;">#` + order.id + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">商品</small>
|
||||
<div class="fw-bold text-truncate">` + (order.productName || '商品信息') + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div>
|
||||
<small class="text-muted">数量</small>
|
||||
<div class="fw-bold">` + order.quantity + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">总价</small>
|
||||
<div class="fw-bold text-danger">¥` + (order.totalPrice ? order.totalPrice.toFixed(2) : '0.00') + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div>
|
||||
<small class="text-muted">状态</small>
|
||||
<div>
|
||||
<span class="badge ` + getStatusBadgeClass(order.status) + `">
|
||||
` + getStatusText(order.status) + `
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div>
|
||||
<small class="text-muted">类型</small>
|
||||
<div>
|
||||
<span class="badge ` + getTypeBadgeClass(order.orderType) + `">
|
||||
` + getTypeText(order.orderType) + `
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="btn-group btn-group-sm d-flex">
|
||||
<button class="btn btn-outline-primary" onclick="viewOrderDetail(` + order.id + `)" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
` + getOrderActionButtons(order) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-12">
|
||||
<small class="text-muted">
|
||||
下单时间:` + formatDateTime(order.createdAt) + `
|
||||
` + (order.orderType == 2 ? ' | <i class="fas fa-bolt text-danger"></i> 秒杀订单' : '') + `
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#ordersList').html(html);
|
||||
$('#ordersList').show();
|
||||
}
|
||||
|
||||
// 显示空订单状态
|
||||
function showEmptyOrders() {
|
||||
$('#emptyOrders').show();
|
||||
$('#ordersList').hide();
|
||||
}
|
||||
|
||||
// 获取状态徽章样式
|
||||
function getStatusBadgeClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning text-dark'; // 待支付
|
||||
case 2:
|
||||
return 'bg-info'; // 已支付
|
||||
case 3:
|
||||
return 'bg-primary'; // 已发货
|
||||
case 4:
|
||||
return 'bg-success'; // 已完成
|
||||
case 5:
|
||||
return 'bg-secondary'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类型徽章样式
|
||||
function getTypeBadgeClass(orderType) {
|
||||
switch (orderType) {
|
||||
case 1:
|
||||
return 'bg-light text-dark'; // 普通订单
|
||||
case 2:
|
||||
return 'bg-danger'; // 秒杀订单
|
||||
default:
|
||||
return 'bg-light text-dark';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
function getTypeText(orderType) {
|
||||
switch (orderType) {
|
||||
case 1:
|
||||
return '普通';
|
||||
case 2:
|
||||
return '秒杀';
|
||||
default:
|
||||
return '普通';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单操作按钮
|
||||
function getOrderActionButtons(order) {
|
||||
let buttons = '';
|
||||
|
||||
switch (order.status) {
|
||||
case 1: // 待支付
|
||||
buttons += `<button class="btn btn-outline-success" onclick="payOrder(` + order.id + `)" title="去支付">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
</button>`;
|
||||
buttons += `<button class="btn btn-outline-danger" onclick="cancelOrder(` + order.id + `)" title="取消订单">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 2: // 已支付
|
||||
buttons += `<button class="btn btn-outline-info" onclick="remindShipping(` + order.id + `)" title="提醒发货">
|
||||
<i class="fas fa-truck"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 3: // 已发货
|
||||
buttons += `<button class="btn btn-outline-success" onclick="confirmReceipt(` + order.id + `)" title="确认收货">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 4: // 已完成
|
||||
buttons += `<button class="btn btn-outline-warning" onclick="reviewOrder(` + order.id + `)" title="评价">
|
||||
<i class="fas fa-star"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 5: // 已取消
|
||||
// 已取消的订单不显示操作按钮
|
||||
break;
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('#pagination').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
// 筛选订单
|
||||
function filterOrders() {
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
// 搜索订单
|
||||
function searchOrders() {
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
// 刷新订单
|
||||
function refreshOrders() {
|
||||
loadOrders(currentPage);
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
function viewOrderDetail(orderId) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
renderOrderDetail(response.data);
|
||||
$('#orderDetailModal').modal('show');
|
||||
} else {
|
||||
showMessage('获取订单详情失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('获取订单详情失败,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染订单详情
|
||||
function renderOrderDetail(order) {
|
||||
const html = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>基本信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>订单号:</td><td>#` + order.id + `</td></tr>
|
||||
<tr><td>订单类型:</td><td><span class="badge ` + getTypeBadgeClass(order.orderType) + `">` + getTypeText(order.orderType) + `</span></td></tr>
|
||||
<tr><td>订单状态:</td><td><span class="badge ` + getStatusBadgeClass(order.status) + `">` + getStatusText(order.status) + `</span></td></tr>
|
||||
<tr><td>创建时间:</td><td>` + formatDateTime(order.createdAt) + `</td></tr>
|
||||
<tr><td>更新时间:</td><td>` + formatDateTime(order.updatedAt) + `</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>商品信息</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-4">
|
||||
<img src="` + getProductImageUrl(order.productImageUrl) + `"
|
||||
class="img-fluid rounded" alt="` + (order.productName || '商品') + `"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<table class="table table-sm">
|
||||
<tr><td>商品名称:</td><td>` + (order.productName || '商品信息') + `</td></tr>
|
||||
<tr><td>购买数量:</td><td>` + order.quantity + ` 件</td></tr>
|
||||
<tr><td>商品单价:</td><td>¥` + (order.totalPrice / order.quantity).toFixed(2) + `</td></tr>
|
||||
<tr><td>订单总价:</td><td class="text-danger fw-bold">¥` + (order.totalPrice ? order.totalPrice.toFixed(2) : '0.00') + `</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
` + (order.remark ? `
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6>订单备注</h6>
|
||||
<p class="text-muted">` + order.remark + `</p>
|
||||
</div>
|
||||
</div>
|
||||
` : '') + `
|
||||
`;
|
||||
|
||||
$('#orderDetailContent').html(html);
|
||||
}
|
||||
|
||||
// 支付订单
|
||||
function payOrder(orderId) {
|
||||
if (confirm('确定要支付这个订单吗?')) {
|
||||
// 显示支付方式选择模态框
|
||||
showPaymentModal(orderId);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示支付方式选择模态框
|
||||
function showPaymentModal(orderId) {
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="paymentModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">选择支付方式</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="payment-methods">
|
||||
<div class="form-check mb-3" data-payment-method="alipay">
|
||||
<input class="form-check-input" type="radio" name="paymentMethod" id="alipay" value="alipay">
|
||||
<label class="form-check-label" for="alipay">
|
||||
<i class="fab fa-alipay text-primary"></i> 支付宝
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3" data-payment-method="wechat">
|
||||
<input class="form-check-input" type="radio" name="paymentMethod" id="wechat" value="wechat">
|
||||
<label class="form-check-label" for="wechat">
|
||||
<i class="fab fa-weixin text-success"></i> 微信支付
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3" data-payment-method="unionpay">
|
||||
<input class="form-check-input" type="radio" name="paymentMethod" id="unionpay" value="unionpay">
|
||||
<label class="form-check-label" for="unionpay">
|
||||
<i class="fas fa-credit-card text-info"></i> 银联支付
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<div id="paymentStatus" class="text-muted me-auto">
|
||||
<small>选择支付方式后将自动处理支付</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 移除现有的支付模态框
|
||||
$('#paymentModal').remove();
|
||||
|
||||
// 添加新的支付模态框
|
||||
$('body').append(modalHtml);
|
||||
|
||||
// 设置订单ID到模态框数据属性中
|
||||
$('#paymentModal').attr('data-order-id', orderId);
|
||||
|
||||
// 添加支付方式点击事件
|
||||
$('#paymentModal').on('click', '.form-check[data-payment-method]', function () {
|
||||
const paymentMethod = $(this).attr('data-payment-method');
|
||||
const currentOrderId = $('#paymentModal').attr('data-order-id');
|
||||
selectPaymentMethod(paymentMethod, currentOrderId);
|
||||
});
|
||||
|
||||
// 在模态框关闭时移除事件监听器
|
||||
$('#paymentModal').on('hidden.bs.modal', function () {
|
||||
$(this).off('click');
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
// 显示模态框
|
||||
$('#paymentModal').modal('show');
|
||||
}
|
||||
|
||||
// 选择支付方式并直接处理支付
|
||||
function selectPaymentMethod(paymentMethod, orderId) {
|
||||
// 如果orderId未定义,从模态框数据属性中获取
|
||||
if (!orderId || orderId === 'undefined') {
|
||||
orderId = $('#paymentModal').attr('data-order-id');
|
||||
}
|
||||
|
||||
// 验证orderId
|
||||
if (!orderId || orderId === 'undefined') {
|
||||
showMessage('订单ID错误,请重新尝试', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Processing payment for order:', orderId, 'with method:', paymentMethod);
|
||||
|
||||
// 选中对应的单选框
|
||||
$('#paymentModal input[value="' + paymentMethod + '"]').prop('checked', true);
|
||||
|
||||
// 显示支付处理中状态
|
||||
$('#paymentStatus').html('<i class="fas fa-spinner fa-spin text-primary"></i> <small class="text-primary">正在处理支付...</small>');
|
||||
|
||||
// 禁用所有支付方式选项
|
||||
$('.payment-methods .form-check').css('pointer-events', 'none').css('opacity', '0.6');
|
||||
|
||||
// 模拟短暂延迟后调用后端支付接口
|
||||
setTimeout(function () {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/pay',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
paymentMethod: paymentMethod
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#paymentStatus').html('<i class="fas fa-check-circle text-success"></i> <small class="text-success">支付成功!</small>');
|
||||
|
||||
// 2秒后关闭模态框并刷新订单列表
|
||||
setTimeout(function () {
|
||||
$('#paymentModal').modal('hide');
|
||||
showMessage('支付成功!订单状态已更新为已支付', 'success');
|
||||
refreshOrders();
|
||||
}, 2000);
|
||||
} else {
|
||||
$('#paymentStatus').html('<i class="fas fa-times-circle text-danger"></i> <small class="text-danger">支付失败: ' + response.message + '</small>');
|
||||
// 恢复支付方式选项
|
||||
$('.payment-methods .form-check').css('pointer-events', 'auto').css('opacity', '1');
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
let errorMessage = '支付失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
$('#paymentStatus').html('<i class="fas fa-times-circle text-danger"></i> <small class="text-danger">' + errorMessage + '</small>');
|
||||
// 恢复支付方式选项
|
||||
$('.payment-methods .form-check').css('pointer-events', 'auto').css('opacity', '1');
|
||||
}
|
||||
});
|
||||
}, 1000); // 1秒延迟模拟支付处理
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
function cancelOrder(orderId) {
|
||||
if (confirm('确定要取消这个订单吗?')) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/cancel',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('订单已取消', 'success');
|
||||
refreshOrders();
|
||||
} else {
|
||||
showMessage('取消订单失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('取消订单失败,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 提醒发货
|
||||
function remindShipping(orderId) {
|
||||
showMessage('已提醒商家发货', 'success');
|
||||
}
|
||||
|
||||
// 确认收货
|
||||
function confirmReceipt(orderId) {
|
||||
if (confirm('确定已收到商品吗?确认后订单将完成。')) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/confirm',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('确认收货成功,订单已完成', 'success');
|
||||
refreshOrders();
|
||||
} else {
|
||||
showMessage('确认收货失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('确认收货失败,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 评价订单
|
||||
function reviewOrder(orderId) {
|
||||
showMessage('评价功能开发中...', 'info');
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
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);
|
||||
}
|
||||
|
||||
// 获取商品图片URL
|
||||
function getProductImageUrl(imageUrl) {
|
||||
// 如果没有图片URL或为空,返回默认图片
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
}
|
||||
|
||||
// 如果是相对路径,添加上下文路径
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是上传的图片(以/uploads/开头)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return '${pageContext.request.contextPath}' + imageUrl;
|
||||
}
|
||||
|
||||
// 如果是完整的URL(http或https),直接返回
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 其他情况,当作相对路径处理
|
||||
return '${pageContext.request.contextPath}/images/' + imageUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.order-item {
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.order-item:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.text-truncate {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.order-item .col-md-1,
|
||||
.order-item .col-md-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-group-sm .btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table td {
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.table td:first-child {
|
||||
font-weight: 500;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
/* 支付方式样式 */
|
||||
.payment-methods .form-check {
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.payment-methods .form-check:hover {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 4px 12px rgba(13, 110, 253, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.payment-methods .form-check-input {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.payment-methods .form-check-input:checked + .form-check-label {
|
||||
color: #0d6efd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.payment-methods .form-check-input:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.payment-methods .form-check:has(.form-check-input:checked) {
|
||||
border-color: #0d6efd;
|
||||
background-color: rgba(13, 110, 253, 0.05);
|
||||
}
|
||||
|
||||
.payment-methods .form-check-label {
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.payment-methods .form-check-label i {
|
||||
font-size: 1.3rem;
|
||||
margin-right: 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#paymentStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
@@ -1,383 +0,0 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>商品详情 - 秒杀系统</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.product-image {
|
||||
max-width: 100%;
|
||||
height: 400px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.price {
|
||||
color: #e74c3c;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.original-price {
|
||||
color: #7f8c8d;
|
||||
text-decoration: line-through;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.flash-sale-badge {
|
||||
background: linear-gradient(45deg, #ff4757, #ff3838);
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.stock-info {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.btn-flash-sale {
|
||||
background: linear-gradient(45deg, #ff4757, #ff3838);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 12px 30px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
.btn-flash-sale:hover {
|
||||
background: linear-gradient(45deg, #ff3838, #e84118);
|
||||
color: white;
|
||||
}
|
||||
.countdown-timer {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.countdown-item {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.countdown-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
.countdown-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.flash-sale-ended {
|
||||
background: #7f8c8d;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<jsp:include page="common/header.jsp" />
|
||||
|
||||
<div class="container mt-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">首页</a></li>
|
||||
<li class="breadcrumb-item"><a href="/products">商品列表</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">${product.name}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<img src="${product.imageUrl != null ? product.imageUrl : '/static/images/default-product.svg'}"
|
||||
alt="${product.name}" class="product-image">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h1 class="h2 mb-3">${product.name}</h1>
|
||||
|
||||
<c:if test="${flashSale != null}">
|
||||
<div class="flash-sale-badge">
|
||||
<i class="fas fa-bolt"></i> 限时秒杀
|
||||
</div>
|
||||
|
||||
<div class="price-section">
|
||||
<span class="price">¥<fmt:formatNumber value="${flashSale.flashPrice}" pattern="#,##0.00" /></span>
|
||||
<span class="original-price ms-3">原价:¥<fmt:formatNumber value="${product.price}" pattern="#,##0.00" /></span>
|
||||
</div>
|
||||
|
||||
<c:choose>
|
||||
<c:when test="${flashSale.statusDescription == '未开始'}">
|
||||
<div class="countdown-timer">
|
||||
<h5 class="mb-3">距离秒杀开始还有</h5>
|
||||
<div id="countdown-container">
|
||||
<div class="countdown-item">
|
||||
<span class="countdown-number" id="days">00</span>
|
||||
<span class="countdown-label">天</span>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<span class="countdown-number" id="hours">00</span>
|
||||
<span class="countdown-label">时</span>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<span class="countdown-number" id="minutes">00</span>
|
||||
<span class="countdown-label">分</span>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<span class="countdown-number" id="seconds">00</span>
|
||||
<span class="countdown-label">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c:when>
|
||||
<c:when test="${flashSale.statusDescription == '进行中'}">
|
||||
<div class="countdown-timer">
|
||||
<h5 class="mb-3">距离秒杀结束还有</h5>
|
||||
<div id="countdown-container">
|
||||
<div class="countdown-item">
|
||||
<span class="countdown-number" id="hours">00</span>
|
||||
<span class="countdown-label">时</span>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<span class="countdown-number" id="minutes">00</span>
|
||||
<span class="countdown-label">分</span>
|
||||
</div>
|
||||
<div class="countdown-item">
|
||||
<span class="countdown-number" id="seconds">00</span>
|
||||
<span class="countdown-label">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="flash-sale-ended">
|
||||
<h5 class="mb-0">秒杀活动已结束</h5>
|
||||
</div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${flashSale == null}">
|
||||
<div class="price-section">
|
||||
<span class="price">¥<fmt:formatNumber value="${product.price}" pattern="#,##0.00" /></span>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<div class="stock-info">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<strong>库存:</strong>
|
||||
<c:choose>
|
||||
<c:when test="${flashSale != null}">
|
||||
<span class="text-primary">${flashSale.remainingStock} 件</span>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<span class="text-primary">${product.stock} 件</span>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>销量:</strong>
|
||||
<span class="text-info">${product.sales} 件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<c:choose>
|
||||
<c:when test="${flashSale != null && flashSale.canParticipate}">
|
||||
<button type="button" class="btn btn-flash-sale btn-lg me-3" onclick="participateFlashSale()">
|
||||
<i class="fas fa-bolt"></i> 立即秒杀
|
||||
</button>
|
||||
</c:when>
|
||||
<c:when test="${flashSale != null}">
|
||||
<button type="button" class="btn btn-secondary btn-lg me-3" disabled>
|
||||
秒杀已结束或库存不足
|
||||
</button>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="input-group mb-3" style="max-width: 150px; display: inline-block;">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="decreaseQuantity()">-</button>
|
||||
<input type="number" class="form-control text-center" id="quantity" value="1" min="1" max="${product.stock}">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="increaseQuantity()">+</button>
|
||||
</div>
|
||||
<br>
|
||||
<button type="button" class="btn btn-primary btn-lg me-3" onclick="addToCart()">
|
||||
<i class="fas fa-shopping-cart"></i> 加入购物车
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-lg" onclick="buyNow()">
|
||||
<i class="fas fa-credit-card"></i> 立即购买
|
||||
</button>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
|
||||
<button type="button" class="btn btn-outline-danger ms-2" onclick="toggleFavorite()">
|
||||
<i class="far fa-heart"></i> 收藏
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h5>商品描述</h5>
|
||||
<p class="text-muted">${product.description != null ? product.description : '暂无描述'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<jsp:include page="common/footer.jsp" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
// 倒计时功能
|
||||
<c:if test="${flashSale != null && (flashSale.timeToStart > 0 || flashSale.timeToEnd > 0)}">
|
||||
let countdownTime = ${flashSale.timeToStart > 0 ? flashSale.timeToStart : flashSale.timeToEnd};
|
||||
|
||||
function updateCountdown() {
|
||||
if (countdownTime <= 0) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
let days = Math.floor(countdownTime / (1000 * 60 * 60 * 24));
|
||||
let hours = Math.floor((countdownTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
let minutes = Math.floor((countdownTime % (1000 * 60 * 60)) / (1000 * 60));
|
||||
let seconds = Math.floor((countdownTime % (1000 * 60)) / 1000);
|
||||
|
||||
$('#days').text(String(days).padStart(2, '0'));
|
||||
$('#hours').text(String(hours).padStart(2, '0'));
|
||||
$('#minutes').text(String(minutes).padStart(2, '0'));
|
||||
$('#seconds').text(String(seconds).padStart(2, '0'));
|
||||
|
||||
countdownTime -= 1000;
|
||||
}
|
||||
|
||||
setInterval(updateCountdown, 1000);
|
||||
updateCountdown();
|
||||
</c:if>
|
||||
|
||||
function increaseQuantity() {
|
||||
let quantityInput = $('#quantity');
|
||||
let currentValue = parseInt(quantityInput.val());
|
||||
let maxValue = parseInt(quantityInput.attr('max'));
|
||||
|
||||
if (currentValue < maxValue) {
|
||||
quantityInput.val(currentValue + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function decreaseQuantity() {
|
||||
let quantityInput = $('#quantity');
|
||||
let currentValue = parseInt(quantityInput.val());
|
||||
|
||||
if (currentValue > 1) {
|
||||
quantityInput.val(currentValue - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function participateFlashSale() {
|
||||
<c:choose>
|
||||
<c:when test="${sessionScope.user == null}">
|
||||
alert('请先登录');
|
||||
location.href = '/login';
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
$.ajax({
|
||||
url: '/api/flashsale/participate',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
flashSaleId: ${flashSale.id},
|
||||
quantity: 1
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('秒杀成功!订单ID:' + response.orderId);
|
||||
location.href = '/orders/' + response.orderId;
|
||||
} else {
|
||||
alert('秒杀失败:' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('秒杀失败,请重试');
|
||||
}
|
||||
});
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
function addToCart() {
|
||||
<c:choose>
|
||||
<c:when test="${sessionScope.user == null}">
|
||||
alert('请先登录');
|
||||
location.href = '/login';
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
let quantity = parseInt($('#quantity').val());
|
||||
|
||||
$.ajax({
|
||||
url: '/api/cart/add',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: ${product.id},
|
||||
quantity: quantity
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('商品已添加到购物车');
|
||||
} else {
|
||||
alert('添加失败:' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('添加失败,请重试');
|
||||
}
|
||||
});
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
function buyNow() {
|
||||
<c:choose>
|
||||
<c:when test="${sessionScope.user == null}">
|
||||
alert('请先登录');
|
||||
location.href = '/login';
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
// 实现立即购买逻辑
|
||||
addToCart();
|
||||
setTimeout(() => {
|
||||
location.href = '/cart';
|
||||
}, 500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
function toggleFavorite() {
|
||||
<c:choose>
|
||||
<c:when test="${sessionScope.user == null}">
|
||||
alert('请先登录');
|
||||
location.href = '/login';
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
// 实现收藏功能
|
||||
alert('收藏功能开发中...');
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,505 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="商品列表"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<style>
|
||||
.product-card {
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.stock-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 卡片悬停效果 */
|
||||
.card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container my-4">
|
||||
<!-- 页面标题 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="page-title">
|
||||
<i class="fas fa-shopping-bag text-primary"></i>
|
||||
商品列表
|
||||
</h1>
|
||||
<p class="text-muted">发现更多优质商品</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="search-filters">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" id="searchKeyword" placeholder="搜索商品名称...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="categoryFilter">
|
||||
<option value="">全部分类</option>
|
||||
<option value="electronics">数码电子</option>
|
||||
<option value="clothing">服装鞋包</option>
|
||||
<option value="home">家居用品</option>
|
||||
<option value="books">图书文具</option>
|
||||
<option value="sports">运动户外</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="sortBy">
|
||||
<option value="id,desc">最新上架</option>
|
||||
<option value="price,asc">价格从低到高</option>
|
||||
<option value="price,desc">价格从高到低</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-primary w-100" onclick="searchProducts()">
|
||||
<i class="fas fa-search"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div class="loading-spinner" id="loadingSpinner">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载商品...</p>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<div class="row" id="productList">
|
||||
<!-- 商品卡片将通过JavaScript动态加载 -->
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="商品分页" class="mt-4">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript动态生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="text-center py-5" id="emptyState" style="display: none;">
|
||||
<i class="fas fa-shopping-basket fa-4x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无商品</h4>
|
||||
<p class="text-muted">请尝试调整搜索条件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品详情模态框 -->
|
||||
<div class="modal fade" id="productModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">商品详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="productModalBody">
|
||||
<!-- 商品详情内容 -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" id="addToCartBtn">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 0;
|
||||
let currentSize = 12;
|
||||
let totalPages = 0;
|
||||
let currentProductId = null;
|
||||
|
||||
// 页面加载完成后获取商品列表
|
||||
$(document).ready(function () {
|
||||
loadProducts();
|
||||
|
||||
// 搜索框回车事件
|
||||
$('#searchKeyword').on('keypress', function (e) {
|
||||
if (e.which === 13) {
|
||||
searchProducts();
|
||||
}
|
||||
});
|
||||
|
||||
// 更新购物车数量(如果用户已登录)
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
updateCartCount();
|
||||
</c:if>
|
||||
});
|
||||
|
||||
// 加载商品列表
|
||||
function loadProducts(page = 0) {
|
||||
currentPage = page;
|
||||
showLoading(true);
|
||||
|
||||
const keyword = $('#searchKeyword').val().trim();
|
||||
const category = $('#categoryFilter').val();
|
||||
const sortValue = $('#sortBy').val().split(',');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
size: currentSize
|
||||
});
|
||||
|
||||
if (keyword) params.append('keyword', keyword);
|
||||
if (category) params.append('category', category);
|
||||
if (sortValue.length === 2) {
|
||||
params.append('sortBy', sortValue[0]);
|
||||
params.append('sortDirection', sortValue[1]);
|
||||
}
|
||||
|
||||
$.get('${pageContext.request.contextPath}/api/product/list?' + params.toString())
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
displayProducts(response.data.content);
|
||||
updatePagination(response.data);
|
||||
} else {
|
||||
showError('获取商品列表失败:' + response.message);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showError('网络错误,请稍后重试');
|
||||
})
|
||||
.always(function () {
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索商品
|
||||
function searchProducts() {
|
||||
loadProducts(0);
|
||||
}
|
||||
|
||||
// 显示商品列表
|
||||
function displayProducts(products) {
|
||||
const productList = $('#productList');
|
||||
productList.empty();
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
$('#emptyState').show();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#emptyState').hide();
|
||||
|
||||
products.forEach(function (product) {
|
||||
const productCard = createProductCard(product);
|
||||
productList.append(productCard);
|
||||
});
|
||||
}
|
||||
|
||||
// 创建商品卡片(参考热门商品样式)
|
||||
function createProductCard(product) {
|
||||
const imageUrl = product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
const productName = product.name;
|
||||
const productDescription = product.description || '暂无描述';
|
||||
const price = product.price ? product.price.toFixed(2) : '0.00';
|
||||
const stock = product.stock || 0;
|
||||
|
||||
var cardHtml = '<div class="col-lg-3 col-md-4 col-sm-6 mb-4">' +
|
||||
'<div class="card product-card h-100">' +
|
||||
'<div class="position-relative">' +
|
||||
'<img src="' + imageUrl + '" class="card-img-top product-image" alt="' + productName + '" ' +
|
||||
'onerror="this.src=\'${pageContext.request.contextPath}/images/default-product.svg\'; this.onerror=null;">' +
|
||||
'<span class="stock-badge ' + (stock > 0 ? 'bg-success' : 'bg-danger') + '">' +
|
||||
(stock > 0 ? '库存 ' + stock : '无库存') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="card-body d-flex flex-column">' +
|
||||
'<h6 class="card-title text-truncate" title="' + productName + '">' + productName + '</h6>' +
|
||||
'<p class="card-text text-muted small flex-grow-1 text-truncate" title="' + productDescription + '">' +
|
||||
productDescription +
|
||||
'</p>' +
|
||||
'<div class="d-flex justify-content-between align-items-center mb-2">' +
|
||||
'<span class="text-primary fw-bold">¥' + price + '</span>' +
|
||||
'<small class="text-muted">库存: ' + stock + '</small>' +
|
||||
'</div>' +
|
||||
'<div class="d-flex gap-2">' +
|
||||
(stock > 0 ?
|
||||
'<button class="btn btn-primary btn-sm flex-grow-1" onclick="addToCart(' + product.id + ')"><i class="fas fa-cart-plus"></i> 加入购物车</button>' :
|
||||
'<button class="btn btn-secondary btn-sm flex-grow-1" disabled><i class="fas fa-ban"></i> 暂时缺货</button>'
|
||||
) +
|
||||
'<button class="btn btn-outline-secondary btn-sm" onclick="viewProductDetail(' + product.id + ')" title="查看详情">' +
|
||||
'<i class="fas fa-eye"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
return cardHtml;
|
||||
}
|
||||
|
||||
// 更新分页
|
||||
function updatePagination(pageData) {
|
||||
totalPages = pageData.totalPages;
|
||||
const pagination = $('#pagination');
|
||||
pagination.empty();
|
||||
|
||||
if (totalPages <= 1) return;
|
||||
|
||||
// 上一页
|
||||
const prevDisabled = currentPage === 0 ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${prevDisabled}">
|
||||
<a class="page-link" onclick="loadProducts(${currentPage - 1})" href="javascript:void(0)">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
|
||||
// 页码
|
||||
let startPage = Math.max(0, currentPage - 2);
|
||||
let endPage = Math.min(totalPages - 1, currentPage + 2);
|
||||
|
||||
if (startPage > 0) {
|
||||
pagination.append('<li class="page-item"><a class="page-link" onclick="loadProducts(0)" href="javascript:void(0)">1</a></li>');
|
||||
if (startPage > 1) {
|
||||
pagination.append('<li class="page-item disabled"><span class="page-link">...</span></li>');
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${active}">
|
||||
<a class="page-link" onclick="loadProducts(${i})" href="javascript:void(0)">${i + 1}</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
if (endPage < totalPages - 1) {
|
||||
if (endPage < totalPages - 2) {
|
||||
pagination.append('<li class="page-item disabled"><span class="page-link">...</span></li>');
|
||||
}
|
||||
pagination.append(`<li class="page-item"><a class="page-link" onclick="loadProducts(${totalPages - 1})" href="javascript:void(0)">${totalPages}</a></li>`);
|
||||
}
|
||||
|
||||
// 下一页
|
||||
const nextDisabled = currentPage === totalPages - 1 ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${nextDisabled}">
|
||||
<a class="page-link" onclick="loadProducts(${currentPage + 1})" href="javascript:void(0)">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
// 查看商品详情
|
||||
function viewProductDetail(productId) {
|
||||
currentProductId = productId;
|
||||
|
||||
$.get('${pageContext.request.contextPath}/api/product/' + productId)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
displayProductDetail(response.data);
|
||||
$('#productModal').modal('show');
|
||||
} else {
|
||||
showError('获取商品详情失败:' + response.message);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showError('网络错误,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 显示商品详情
|
||||
function displayProductDetail(product) {
|
||||
const modalBody = $('#productModalBody');
|
||||
const imageUrl = product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg';
|
||||
const stock = product.stock || 0;
|
||||
const price = product.price ? product.price.toFixed(2) : '0.00';
|
||||
|
||||
var modalHtml = '<div class="row">' +
|
||||
'<div class="col-md-6">' +
|
||||
'<img src="' + imageUrl + '" class="img-fluid rounded" alt="' + product.name + '" ' +
|
||||
'onerror="this.src=\'${pageContext.request.contextPath}/images/default-product.svg\'; this.onerror=null;">' +
|
||||
'</div>' +
|
||||
'<div class="col-md-6">' +
|
||||
'<h4>' + product.name + '</h4>' +
|
||||
'<p class="text-muted">' + (product.description || '暂无详细描述') + '</p>' +
|
||||
'<div class="mb-3">' +
|
||||
'<span class="text-primary fw-bold h4">¥' + price + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="mb-3">' +
|
||||
'<span class="badge ' + (stock > 0 ? 'bg-success' : 'bg-danger') + '">' +
|
||||
(stock > 0 ? '库存 ' + stock : '暂时缺货') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="mb-3">' +
|
||||
'<strong>商品状态:</strong> ' +
|
||||
'<span class="badge ' + (product.status === 1 ? 'bg-success' : 'bg-secondary') + '">' +
|
||||
(product.status === 1 ? '上架中' : '已下架') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
modalBody.html(modalHtml);
|
||||
|
||||
// 更新加入购物车按钮状态
|
||||
const addToCartBtn = $('#addToCartBtn');
|
||||
if (stock > 0 && product.status === 1) {
|
||||
addToCartBtn.prop('disabled', false).html('<i class="fas fa-cart-plus"></i> 加入购物车');
|
||||
} else {
|
||||
addToCartBtn.prop('disabled', true).html('<i class="fas fa-ban"></i> 暂时无法购买');
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到购物车(参考首页实现)
|
||||
function addToCart(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
// 如果是从模态框添加的,关闭模态框
|
||||
$('#productModal').modal('hide');
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login?returnUrl=' + encodeURIComponent(window.location.pathname);
|
||||
}, 1500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading(show) {
|
||||
if (show) {
|
||||
$('#loadingSpinner').show();
|
||||
$('#productList').hide();
|
||||
$('#emptyState').hide();
|
||||
} else {
|
||||
$('#loadingSpinner').hide();
|
||||
$('#productList').show();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息(参考首页实现)
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
function showError(message) {
|
||||
showMessage(message, 'error');
|
||||
}
|
||||
|
||||
// 成功提示
|
||||
function showSuccess(message) {
|
||||
showMessage(message, 'success');
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
@@ -1,493 +0,0 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>个人中心 - 秒杀系统</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.profile-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px 0;
|
||||
}
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid white;
|
||||
object-fit: cover;
|
||||
}
|
||||
.profile-stats {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin-top: -50px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.nav-tabs .nav-link {
|
||||
border: none;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #667eea;
|
||||
border-bottom: 2px solid #667eea;
|
||||
background: none;
|
||||
}
|
||||
.form-floating label {
|
||||
color: #6c757d;
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6b4190 100%);
|
||||
}
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.order-item {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.order-status {
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-pending { background: #fff3cd; color: #856404; }
|
||||
.status-paid { background: #d1ecf1; color: #0c5460; }
|
||||
.status-shipped { background: #d4edda; color: #155724; }
|
||||
.status-completed { background: #cce5ff; color: #004085; }
|
||||
.status-cancelled { background: #f8d7da; color: #721c24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<jsp:include page="common/header.jsp" />
|
||||
|
||||
<!-- 用户信息头部 -->
|
||||
<div class="profile-header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<img src="${user.avatar != null ? user.avatar : 'https://via.placeholder.com/120x120'}"
|
||||
alt="头像" class="avatar">
|
||||
</div>
|
||||
<div class="col">
|
||||
<h2 class="mb-2">${user.username}</h2>
|
||||
<p class="mb-0 opacity-75">
|
||||
<i class="fas fa-envelope me-2"></i>${user.email}
|
||||
<span class="ms-4">
|
||||
<i class="fas fa-calendar me-2"></i>
|
||||
加入时间:<fmt:formatDate value="${user.createdAt}" pattern="yyyy年MM月dd日"/>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- 统计数据 -->
|
||||
<div class="profile-stats">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="totalOrders">0</div>
|
||||
<div class="stat-label">总订单数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="totalAmount">¥0.00</div>
|
||||
<div class="stat-label">累计消费</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="flashSaleSuccess">0</div>
|
||||
<div class="stat-label">秒杀成功</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="favoriteCount">0</div>
|
||||
<div class="stat-label">收藏商品</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选项卡 -->
|
||||
<div class="mt-5">
|
||||
<ul class="nav nav-tabs" id="profileTabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#profile-info">
|
||||
<i class="fas fa-user me-2"></i>个人信息
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#order-history">
|
||||
<i class="fas fa-shopping-bag me-2"></i>订单历史
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#flash-sale-history">
|
||||
<i class="fas fa-bolt me-2"></i>秒杀记录
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#account-settings">
|
||||
<i class="fas fa-cog me-2"></i>账户设置
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mt-4">
|
||||
<!-- 个人信息 -->
|
||||
<div class="tab-pane fade show active" id="profile-info">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-user me-2"></i>个人信息
|
||||
</h5>
|
||||
<form id="profileForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" id="username"
|
||||
value="${user.username}" readonly>
|
||||
<label for="username">用户名</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="email" class="form-control" id="email"
|
||||
value="${user.email}">
|
||||
<label for="email">邮箱</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" id="phone"
|
||||
value="${user.phone != null ? user.phone : ''}">
|
||||
<label for="phone">手机号码</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<select class="form-select" id="gender">
|
||||
<option value="" ${user.gender == null ? 'selected' : ''}>请选择</option>
|
||||
<option value="male" ${user.gender == 'male' ? 'selected' : ''}>男</option>
|
||||
<option value="female" ${user.gender == 'female' ? 'selected' : ''}>女</option>
|
||||
<option value="other" ${user.gender == 'other' ? 'selected' : ''}>其他</option>
|
||||
</select>
|
||||
<label for="gender">性别</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<textarea class="form-control" id="address" style="height: 100px">${user.address != null ? user.address : ''}</textarea>
|
||||
<label for="address">地址</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>保存修改
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单历史 -->
|
||||
<div class="tab-pane fade" id="order-history">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-shopping-bag me-2"></i>订单历史
|
||||
</h5>
|
||||
<div id="orderHistoryContent">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="mt-3 text-muted">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 秒杀记录 -->
|
||||
<div class="tab-pane fade" id="flash-sale-history">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-bolt me-2"></i>秒杀记录
|
||||
</h5>
|
||||
<div id="flashSaleHistoryContent">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="mt-3 text-muted">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 账户设置 -->
|
||||
<div class="tab-pane fade" id="account-settings">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-cog me-2"></i>账户设置
|
||||
</h5>
|
||||
<form id="passwordForm">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control" id="currentPassword" required>
|
||||
<label for="currentPassword">当前密码</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control" id="newPassword" required>
|
||||
<label for="newPassword">新密码</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control" id="confirmPassword" required>
|
||||
<label for="confirmPassword">确认新密码</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-key me-2"></i>修改密码
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<jsp:include page="common/footer.jsp" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
loadUserStats();
|
||||
|
||||
// 选项卡切换事件
|
||||
$('#profileTabs button[data-bs-toggle="tab"]').on('shown.bs.tab', function(e) {
|
||||
const target = $(e.target).data('bs-target');
|
||||
if (target === '#order-history') {
|
||||
loadOrderHistory();
|
||||
} else if (target === '#flash-sale-history') {
|
||||
loadFlashSaleHistory();
|
||||
}
|
||||
});
|
||||
|
||||
// 个人信息表单提交
|
||||
$('#profileForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
updateProfile();
|
||||
});
|
||||
|
||||
// 密码修改表单提交
|
||||
$('#passwordForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
changePassword();
|
||||
});
|
||||
});
|
||||
|
||||
function loadUserStats() {
|
||||
$.get('/api/user/stats', function(data) {
|
||||
$('#totalOrders').text(data.totalOrders || 0);
|
||||
$('#totalAmount').text('¥' + (data.totalAmount || 0).toFixed(2));
|
||||
$('#flashSaleSuccess').text(data.flashSaleSuccess || 0);
|
||||
$('#favoriteCount').text(data.favoriteCount || 0);
|
||||
}).fail(function() {
|
||||
console.error('加载用户统计失败');
|
||||
});
|
||||
}
|
||||
|
||||
function loadOrderHistory() {
|
||||
$.get('/api/orders/user', function(data) {
|
||||
let html = '';
|
||||
if (data && data.length > 0) {
|
||||
data.forEach(function(order) {
|
||||
html += `
|
||||
<div class="order-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">订单号:${order.orderNumber || order.id}</h6>
|
||||
<small class="text-muted">
|
||||
创建时间:${new Date(order.createdAt).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="order-status status-${getStatusClass(order.status)}">
|
||||
${getStatusText(order.status)}
|
||||
</span>
|
||||
<div class="mt-1">
|
||||
<strong>¥${order.totalPrice.toFixed(2)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">
|
||||
商品:${order.productName || '商品ID:' + order.productId} × ${order.quantity}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<div class="text-center py-5"><p class="text-muted">暂无订单记录</p></div>';
|
||||
}
|
||||
$('#orderHistoryContent').html(html);
|
||||
}).fail(function() {
|
||||
$('#orderHistoryContent').html('<div class="text-center py-5"><p class="text-danger">加载订单历史失败</p></div>');
|
||||
});
|
||||
}
|
||||
|
||||
function loadFlashSaleHistory() {
|
||||
$.get('/api/orders/user?type=2', function(data) {
|
||||
let html = '';
|
||||
if (data && data.length > 0) {
|
||||
data.forEach(function(order) {
|
||||
html += `
|
||||
<div class="order-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">
|
||||
<i class="fas fa-bolt text-warning me-1"></i>
|
||||
秒杀订单:${order.orderNumber || order.id}
|
||||
</h6>
|
||||
<small class="text-muted">
|
||||
秒杀时间:${new Date(order.createdAt).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="order-status status-${getStatusClass(order.status)}">
|
||||
${getStatusText(order.status)}
|
||||
</span>
|
||||
<div class="mt-1">
|
||||
<strong class="text-danger">¥${order.totalPrice.toFixed(2)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<div class="text-center py-5"><p class="text-muted">暂无秒杀记录</p></div>';
|
||||
}
|
||||
$('#flashSaleHistoryContent').html(html);
|
||||
}).fail(function() {
|
||||
$('#flashSaleHistoryContent').html('<div class="text-center py-5"><p class="text-danger">加载秒杀记录失败</p></div>');
|
||||
});
|
||||
}
|
||||
|
||||
function updateProfile() {
|
||||
const profileData = {
|
||||
email: $('#email').val(),
|
||||
phone: $('#phone').val(),
|
||||
gender: $('#gender').val(),
|
||||
address: $('#address').val()
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/api/user/profile',
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(profileData),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('个人信息更新成功');
|
||||
} else {
|
||||
alert('更新失败:' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('更新失败,请重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword() {
|
||||
const passwordData = {
|
||||
currentPassword: $('#currentPassword').val(),
|
||||
newPassword: $('#newPassword').val(),
|
||||
confirmPassword: $('#confirmPassword').val()
|
||||
};
|
||||
|
||||
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
||||
alert('新密码和确认密码不匹配');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/user/password',
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(passwordData),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('密码修改成功');
|
||||
$('#passwordForm')[0].reset();
|
||||
} else {
|
||||
alert('修改失败:' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('修改失败,请重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
switch(status) {
|
||||
case 1: return 'pending'; // 待支付
|
||||
case 2: return 'paid'; // 已支付
|
||||
case 3: return 'shipped'; // 已发货
|
||||
case 4: return 'completed'; // 已完成
|
||||
case 5: return 'cancelled'; // 已取消
|
||||
default: return 'pending';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
switch(status) {
|
||||
case 1: return '待支付';
|
||||
case 2: return '已支付';
|
||||
case 3: return '已发货';
|
||||
case 4: return '已完成';
|
||||
case 5: return '已取消';
|
||||
default: return '未知状态';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,383 +0,0 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户注册"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-success text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-user-plus"></i> 用户注册
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="registerForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user"></i> 用户名 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="3-50个字符,支持字母数字下划线" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle"></i> 用户名将作为您的登录凭证
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock"></i> 密码 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="至少6位字符" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
<div class="progress mt-1" style="height: 3px;">
|
||||
<div class="progress-bar" id="passwordStrength" role="progressbar"
|
||||
style="width: 0%"></div>
|
||||
</div>
|
||||
<small class="form-text text-muted" id="passwordStrengthText">密码强度:无</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmPassword" class="form-label">
|
||||
<i class="fas fa-lock"></i> 确认密码 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword"
|
||||
placeholder="请再次输入密码" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope"></i> 邮箱
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
placeholder="example@domain.com">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">
|
||||
<i class="fas fa-phone"></i> 手机号
|
||||
</label>
|
||||
<input type="tel" class="form-control" id="phone" name="phone"
|
||||
placeholder="请输入手机号">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="agreeTerms" required>
|
||||
<label class="form-check-label" for="agreeTerms">
|
||||
我已阅读并同意 <a href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#termsModal">用户协议</a> 和
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#privacyModal">隐私政策</a>
|
||||
</label>
|
||||
<div class="invalid-feedback">请同意用户协议和隐私政策</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success" id="registerBtn">
|
||||
<i class="fas fa-user-plus"></i> 注册
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-2">已有账号?</p>
|
||||
<a href="${pageContext.request.contextPath}/login" class="btn btn-outline-primary">
|
||||
<i class="fas fa-sign-in-alt"></i> 立即登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户协议模态框 -->
|
||||
<div class="modal fade" id="termsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">用户协议</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>1. 服务条款</h6>
|
||||
<p>本系统为秒杀演示系统,仅供学习和演示使用。</p>
|
||||
|
||||
<h6>2. 用户责任</h6>
|
||||
<p>用户应当合理使用系统功能,不得进行恶意操作。</p>
|
||||
|
||||
<h6>3. 隐私保护</h6>
|
||||
<p>我们承诺保护用户隐私,不会泄露用户个人信息。</p>
|
||||
|
||||
<h6>4. 免责声明</h6>
|
||||
<p>本系统仅为演示目的,不承担任何商业责任。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
onclick="$('#agreeTerms').prop('checked', true)">同意
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐私政策模态框 -->
|
||||
<div class="modal fade" id="privacyModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">隐私政策</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>信息收集</h6>
|
||||
<p>我们仅收集必要的用户信息用于系统功能实现。</p>
|
||||
|
||||
<h6>信息使用</h6>
|
||||
<p>收集的信息仅用于系统功能,不会用于其他目的。</p>
|
||||
|
||||
<h6>信息保护</h6>
|
||||
<p>我们采用适当的技术措施保护用户信息安全。</p>
|
||||
|
||||
<h6>信息共享</h6>
|
||||
<p>我们不会与第三方共享用户个人信息。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
onclick="$('#agreeTerms').prop('checked', true)">同意
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码显示/隐藏切换
|
||||
$('#togglePassword').click(function () {
|
||||
const passwordField = $('#password');
|
||||
const icon = $(this).find('i');
|
||||
|
||||
if (passwordField.attr('type') === 'password') {
|
||||
passwordField.attr('type', 'text');
|
||||
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.attr('type', 'password');
|
||||
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// 密码强度检测
|
||||
$('#password').on('input', function () {
|
||||
const password = $(this).val();
|
||||
const strength = calculatePasswordStrength(password);
|
||||
updatePasswordStrengthUI(strength);
|
||||
});
|
||||
|
||||
// 确认密码验证
|
||||
$('#confirmPassword').on('input', function () {
|
||||
const password = $('#password').val();
|
||||
const confirmPassword = $(this).val();
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
$(this).addClass('is-invalid');
|
||||
$(this).siblings('.invalid-feedback').text('两次输入的密码不一致');
|
||||
} else {
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
// 用户名实时验证
|
||||
$('#username').on('input', function () {
|
||||
const username = $(this).val();
|
||||
if (username.length >= 3) {
|
||||
checkUsernameAvailability(username);
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
$('#registerForm').submit(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
username: $('#username').val().trim(),
|
||||
password: $('#password').val(),
|
||||
confirmPassword: $('#confirmPassword').val(),
|
||||
email: $('#email').val().trim() || null,
|
||||
phone: $('#phone').val().trim() || null
|
||||
};
|
||||
|
||||
// 显示加载状态
|
||||
const registerBtn = $('#registerBtn');
|
||||
const originalText = registerBtn.html();
|
||||
registerBtn.html('<i class="fas fa-spinner fa-spin"></i> 注册中...').prop('disabled', true);
|
||||
|
||||
// 发送注册请求
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/user/register',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('注册成功!正在跳转到登录页面...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.status === 400) {
|
||||
const response = xhr.responseJSON;
|
||||
showMessage(response.message || '注册失败', 'error');
|
||||
} else {
|
||||
showMessage('网络错误,请稍后重试', 'error');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
// 恢复按钮状态
|
||||
registerBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
function validateForm() {
|
||||
let isValid = true;
|
||||
|
||||
// 清除之前的错误状态
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
|
||||
// 用户名验证
|
||||
const username = $('#username').val().trim();
|
||||
if (!username) {
|
||||
showFieldError('username', '请输入用户名');
|
||||
isValid = false;
|
||||
} else if (username.length < 3 || username.length > 50) {
|
||||
showFieldError('username', '用户名长度必须在3-50个字符之间');
|
||||
isValid = false;
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
showFieldError('username', '用户名只能包含字母、数字和下划线');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 密码验证
|
||||
const password = $('#password').val();
|
||||
if (!password) {
|
||||
showFieldError('password', '请输入密码');
|
||||
isValid = false;
|
||||
} else if (password.length < 6) {
|
||||
showFieldError('password', '密码长度至少6位');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 确认密码验证
|
||||
const confirmPassword = $('#confirmPassword').val();
|
||||
if (!confirmPassword) {
|
||||
showFieldError('confirmPassword', '请确认密码');
|
||||
isValid = false;
|
||||
} else if (password !== confirmPassword) {
|
||||
showFieldError('confirmPassword', '两次输入的密码不一致');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 邮箱验证(可选)
|
||||
const email = $('#email').val().trim();
|
||||
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
showFieldError('email', '请输入有效的邮箱地址');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 手机号验证(可选)
|
||||
const phone = $('#phone').val().trim();
|
||||
if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
showFieldError('phone', '请输入有效的手机号');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 协议同意验证
|
||||
if (!$('#agreeTerms').is(':checked')) {
|
||||
$('#agreeTerms').addClass('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// 计算密码强度
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
|
||||
if (password.length >= 6) score += 20;
|
||||
if (password.length >= 8) score += 20;
|
||||
if (/[a-z]/.test(password)) score += 20;
|
||||
if (/[A-Z]/.test(password)) score += 20;
|
||||
if (/[0-9]/.test(password)) score += 10;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score += 10;
|
||||
|
||||
return Math.min(score, 100);
|
||||
}
|
||||
|
||||
// 更新密码强度UI
|
||||
function updatePasswordStrengthUI(strength) {
|
||||
const progressBar = $('#passwordStrength');
|
||||
const strengthText = $('#passwordStrengthText');
|
||||
|
||||
let color, text;
|
||||
|
||||
if (strength < 30) {
|
||||
color = 'bg-danger';
|
||||
text = '弱';
|
||||
} else if (strength < 60) {
|
||||
color = 'bg-warning';
|
||||
text = '中等';
|
||||
} else if (strength < 80) {
|
||||
color = 'bg-info';
|
||||
text = '强';
|
||||
} else {
|
||||
color = 'bg-success';
|
||||
text = '很强';
|
||||
}
|
||||
|
||||
progressBar.removeClass('bg-danger bg-warning bg-info bg-success').addClass(color);
|
||||
progressBar.css('width', strength + '%');
|
||||
strengthText.text('密码强度:' + text);
|
||||
}
|
||||
|
||||
// 检查用户名可用性
|
||||
function checkUsernameAvailability(username) {
|
||||
// 这里可以添加实时检查用户名是否已存在的逻辑
|
||||
// 为了演示,暂时省略
|
||||
}
|
||||
|
||||
// 显示字段错误
|
||||
function showFieldError(fieldName, message) {
|
||||
const field = $('#' + fieldName);
|
||||
field.addClass('is-invalid');
|
||||
field.siblings('.invalid-feedback').text(message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
Reference in New Issue
Block a user