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:
2026-03-14 16:40:26 +08:00
parent b684ea38d4
commit c4582655d9
115 changed files with 5968 additions and 12623 deletions

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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);
}
}
/**
* 获取秒杀活动详情
*/

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View 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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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);
/**
* 更新秒杀活动状态
*/

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
/**
* 根据创建时间范围统计订单数量

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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); // 待支付

View File

@@ -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 "未知";
}
}
}

View File

@@ -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值
*/

View File

@@ -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);
}
}

View File

@@ -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 "未知类型";
}

View File

@@ -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;
}
}

View File

@@ -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()

View File

@@ -2,6 +2,8 @@ server:
port: 8080
servlet:
context-path: /
session:
timeout: 30m
spring:
application:

View 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

View File

@@ -1,22 +1,17 @@
-- 演示账号快速创建脚本
-- 密码都是明文对应的值demo1/demo2/admin的密码分别是123456/123456/admin123
-- 演示账号初始化脚本
-- 账号demo1 / 123456demo2 / 123456admin / 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();

View File

@@ -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';

View File

@@ -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;

View File

@@ -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());

View File

@@ -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

View File

@@ -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" %>

View File

@@ -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>

View File

@@ -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

View File

@@ -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" %>

View File

@@ -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;
}
// 如果是完整的URLhttp或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" %>

View File

@@ -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">
&copy; 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>

View File

@@ -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>

View File

@@ -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" %>

View File

@@ -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" %>

View File

@@ -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;
}
// 如果是完整的URLhttp或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" %>

View File

@@ -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;
}
// 如果是完整的URLhttp或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" %>

View File

@@ -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" %>

View File

@@ -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" %>

View File

@@ -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;
}
// 如果是完整的URLhttp或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" %>

View File

@@ -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>

View File

@@ -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" %>

View File

@@ -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>

View File

@@ -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" %>