后端功能增强:全局异常处理、API控制器、JSP视图和单元测试

- 添加 GlobalExceptionHandler 全局异常处理
- 添加 ApiController REST API 控制器
- 更新 WebConfig 跨域配置和 ProductRepository 查询方法
- 新增 monitor/product-detail/profile JSP 视图页面
- 添加 FlashSaleServiceTest 秒杀服务单元测试
- 更新 application.yml 配置
This commit is contained in:
2026-03-05 20:30:48 +08:00
parent 923e877759
commit 989c2741a2
63 changed files with 15508 additions and 1 deletions

View File

@@ -0,0 +1,414 @@
package com.org.flashsalesystem.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;
/**
* 全局异常处理器
* 统一处理应用中的各种异常,提供一致的错误响应格式
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常处理
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.warn("业务异常: {} - {}", e.getErrorCode(), e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Business Error")
.message(e.getMessage())
.errorCode(e.getErrorCode())
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 秒杀相关异常处理
*/
@ExceptionHandler(FlashSaleException.class)
public ResponseEntity<ErrorResponse> handleFlashSaleException(FlashSaleException e, HttpServletRequest request) {
log.warn("秒杀异常: {} - {}", e.getErrorCode(), e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Flash Sale Error")
.message(e.getMessage())
.errorCode(e.getErrorCode())
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 限流异常处理
*/
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<ErrorResponse> handleRateLimitException(RateLimitException e, HttpServletRequest request) {
log.warn("限流异常: {}", e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.TOO_MANY_REQUESTS.value())
.error("Rate Limit Exceeded")
.message(e.getMessage())
.errorCode("RATE_LIMIT_EXCEEDED")
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(errorResponse);
}
/**
* 数据验证异常处理
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
log.warn("数据验证异常: {}", e.getMessage());
StringBuilder errorMessages = new StringBuilder();
for (ObjectError error : e.getBindingResult().getAllErrors()) {
if (errorMessages.length() > 0) {
errorMessages.append("; ");
}
errorMessages.append(error.getDefaultMessage());
}
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Validation Error")
.message(errorMessages.toString())
.errorCode("VALIDATION_ERROR")
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 参数绑定异常处理
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<ErrorResponse> handleBindException(BindException e, HttpServletRequest request) {
log.warn("参数绑定异常: {}", e.getMessage());
StringBuilder errorMessages = new StringBuilder();
for (ObjectError error : e.getBindingResult().getAllErrors()) {
if (errorMessages.length() > 0) {
errorMessages.append("; ");
}
errorMessages.append(error.getDefaultMessage());
}
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Parameter Binding Error")
.message(errorMessages.toString())
.errorCode("BINDING_ERROR")
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 约束违规异常处理
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
log.warn("约束违规异常: {}", e.getMessage());
StringBuilder errorMessages = new StringBuilder();
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
for (ConstraintViolation<?> violation : violations) {
if (errorMessages.length() > 0) {
errorMessages.append("; ");
}
errorMessages.append(violation.getMessage());
}
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Constraint Violation")
.message(errorMessages.toString())
.errorCode("CONSTRAINT_VIOLATION")
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 超时异常处理
*/
@ExceptionHandler(TimeoutException.class)
public ResponseEntity<ErrorResponse> handleTimeoutException(TimeoutException e, HttpServletRequest request) {
log.error("超时异常: {}", e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.REQUEST_TIMEOUT.value())
.error("Timeout Error")
.message("请求超时,请稍后重试")
.errorCode("TIMEOUT_ERROR")
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(errorResponse);
}
/**
* 非法参数异常处理
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
log.warn("非法参数异常: {}", e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Illegal Argument")
.message(e.getMessage())
.errorCode("ILLEGAL_ARGUMENT")
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 空指针异常处理
*/
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
log.error("空指针异常: {}", e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Null Pointer Error")
.message("系统内部错误,请联系管理员")
.errorCode("NULL_POINTER_ERROR")
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
/**
* 运行时异常处理
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error("运行时异常: {}", e.getMessage(), e);
// 对于已知的业务异常,使用友好的错误信息
String message = e.getMessage();
if (message == null || message.trim().isEmpty()) {
message = "系统繁忙,请稍后重试";
}
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Runtime Error")
.message(message)
.errorCode("RUNTIME_ERROR")
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
/**
* 通用异常处理
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception e, HttpServletRequest request) {
log.error("系统异常: {}", e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("System Error")
.message("系统异常,请联系管理员")
.errorCode("SYSTEM_ERROR")
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
/**
* 统一错误响应格式
*/
public static class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String errorCode;
private String path;
private Map<String, Object> details;
public ErrorResponse() {
this.details = new HashMap<>();
}
public static ErrorResponseBuilder builder() {
return new ErrorResponseBuilder();
}
// Getters and Setters
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getErrorCode() { return errorCode; }
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
public Map<String, Object> getDetails() { return details; }
public void setDetails(Map<String, Object> details) { this.details = details; }
public static class ErrorResponseBuilder {
private ErrorResponse errorResponse = new ErrorResponse();
public ErrorResponseBuilder timestamp(LocalDateTime timestamp) {
errorResponse.setTimestamp(timestamp);
return this;
}
public ErrorResponseBuilder status(int status) {
errorResponse.setStatus(status);
return this;
}
public ErrorResponseBuilder error(String error) {
errorResponse.setError(error);
return this;
}
public ErrorResponseBuilder message(String message) {
errorResponse.setMessage(message);
return this;
}
public ErrorResponseBuilder errorCode(String errorCode) {
errorResponse.setErrorCode(errorCode);
return this;
}
public ErrorResponseBuilder path(String path) {
errorResponse.setPath(path);
return this;
}
public ErrorResponseBuilder detail(String key, Object value) {
errorResponse.getDetails().put(key, value);
return this;
}
public ErrorResponse build() {
return errorResponse;
}
}
}
/**
* 业务异常类
*/
public static class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String message) {
super(message);
this.errorCode = "BUSINESS_ERROR";
}
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.errorCode = "BUSINESS_ERROR";
}
public String getErrorCode() {
return errorCode;
}
}
/**
* 秒杀异常类
*/
public static class FlashSaleException extends RuntimeException {
private final String errorCode;
public FlashSaleException(String message) {
super(message);
this.errorCode = "FLASH_SALE_ERROR";
}
public FlashSaleException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
/**
* 限流异常类
*/
public static class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}
public RateLimitException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -76,4 +77,17 @@ public class WebConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
}
/**
* CORS跨域配置
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:*", "http://127.0.0.1:*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

View File

@@ -0,0 +1,230 @@
package com.org.flashsalesystem.controller;
import com.org.flashsalesystem.dto.FlashSaleDTO;
import com.org.flashsalesystem.dto.ProductDTO;
import com.org.flashsalesystem.entity.Product;
import com.org.flashsalesystem.repository.ProductRepository;
import com.org.flashsalesystem.service.FlashSaleService;
import com.org.flashsalesystem.service.ProductService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* API控制器 - 为Vue前端提供REST接口
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "API接口", description = "前端API接口")
public class ApiController {
private final ProductRepository productRepository;
private final ProductService productService;
private final FlashSaleService flashSaleService;
/**
* 获取热门商品
*/
@GetMapping("/products/hot")
@Operation(summary = "获取热门商品")
public ResponseEntity<Map<String, Object>> getHotProducts(
@RequestParam(defaultValue = "8") int limit) {
Map<String, Object> response = new HashMap<>();
try {
// 获取前N个商品作为热门商品
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "id"));
Page<Product> productPage = productRepository.findAll(pageable);
List<Map<String, Object>> products = new ArrayList<>();
for (Product product : productPage.getContent()) {
Map<String, Object> item = new HashMap<>();
item.put("id", product.getId());
item.put("name", product.getName());
item.put("description", product.getDescription());
item.put("price", product.getPrice());
item.put("stock", product.getStock());
item.put("image", product.getImageUrl());
item.put("category", "");
products.add(item);
}
response.put("success", true);
response.put("data", products);
} catch (Exception e) {
log.error("获取热门商品失败", e);
response.put("success", false);
response.put("message", "获取热门商品失败");
}
return ResponseEntity.ok(response);
}
/**
* 获取活跃的秒杀活动
*/
@GetMapping("/flashsales/active")
@Operation(summary = "获取活跃的秒杀活动")
public ResponseEntity<Map<String, Object>> getActiveFlashSales() {
Map<String, Object> response = new HashMap<>();
try {
List<FlashSaleDTO> flashSales = flashSaleService.getActiveFlashSales();
// 转换数据格式
List<Map<String, Object>> result = new ArrayList<>();
for (FlashSaleDTO flashSale : flashSales) {
Map<String, Object> item = new HashMap<>();
item.put("id", flashSale.getId());
item.put("productId", flashSale.getProductId());
item.put("productName", flashSale.getProductName());
item.put("productImage", "");
item.put("originalPrice", flashSale.getOriginalPrice());
item.put("flashPrice", flashSale.getFlashPrice());
item.put("flashStock", flashSale.getFlashStock());
item.put("startTime", flashSale.getStartTime());
item.put("endTime", flashSale.getEndTime());
item.put("status", flashSale.getStatus());
result.add(item);
}
response.put("success", true);
response.put("data", result);
} catch (Exception e) {
log.error("获取活跃秒杀活动失败", e);
response.put("success", false);
response.put("message", "获取活跃秒杀活动失败");
}
return ResponseEntity.ok(response);
}
/**
* 参与秒杀
*/
@PostMapping("/flashsales/participate")
@Operation(summary = "参与秒杀")
public ResponseEntity<Map<String, Object>> participate(
@RequestBody Map<String, Object> request,
HttpServletRequest httpRequest) {
Map<String, Object> response = new HashMap<>();
try {
// 从session获取用户ID
Long userId = (Long) httpRequest.getSession().getAttribute("userId");
if (userId == null) {
response.put("success", false);
response.put("message", "请先登录");
return ResponseEntity.ok(response);
}
Long flashSaleId = Long.valueOf(request.get("flashSaleId").toString());
Integer quantity = request.containsKey("quantity") ?
Integer.valueOf(request.get("quantity").toString()) : 1;
// 创建参与DTO
FlashSaleDTO.ParticipateDTO participateDTO = new FlashSaleDTO.ParticipateDTO();
participateDTO.setFlashSaleId(flashSaleId);
participateDTO.setQuantity(quantity);
// 调用秒杀服务
FlashSaleDTO.ResultDTO result = flashSaleService.participateFlashSale(userId, participateDTO);
response.put("success", result.getSuccess());
response.put("message", result.getMessage());
if (result.getOrderId() != null) {
response.put("orderId", result.getOrderId());
}
} catch (Exception e) {
log.error("参与秒杀失败", e);
response.put("success", false);
response.put("message", e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 获取商品列表
*/
@GetMapping("/products")
@Operation(summary = "获取商品列表")
public ResponseEntity<Map<String, Object>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "12") int size) {
Map<String, Object> response = new HashMap<>();
try {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id"));
Page<Product> productPage = productRepository.findAll(pageable);
List<Map<String, Object>> products = new ArrayList<>();
for (Product product : productPage.getContent()) {
Map<String, Object> item = new HashMap<>();
item.put("id", product.getId());
item.put("name", product.getName());
item.put("description", product.getDescription());
item.put("price", product.getPrice());
item.put("stock", product.getStock());
item.put("image", product.getImageUrl());
item.put("category", "");
products.add(item);
}
response.put("success", true);
response.put("list", products);
response.put("total", productPage.getTotalElements());
} catch (Exception e) {
log.error("获取商品列表失败", e);
response.put("success", false);
response.put("message", "获取商品列表失败");
}
return ResponseEntity.ok(response);
}
/**
* 获取秒杀活动列表
*/
@GetMapping("/flashsales")
@Operation(summary = "获取秒杀活动列表")
public ResponseEntity<Map<String, Object>> getFlashSales() {
Map<String, Object> response = new HashMap<>();
try {
List<FlashSaleDTO> flashSales = flashSaleService.getActiveFlashSales();
response.put("success", true);
response.put("list", flashSales);
response.put("total", flashSales.size());
} catch (Exception e) {
log.error("获取秒杀活动列表失败", e);
response.put("success", false);
response.put("message", "获取秒杀活动列表失败");
}
return ResponseEntity.ok(response);
}
}

View File

@@ -81,4 +81,9 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
* 统计库存小于指定数量的商品数量
*/
long countByStockLessThan(Integer stock);
/**
* 根据名称模糊查询(忽略大小写)
*/
Page<Product> findByNameContainingIgnoreCase(String name, Pageable pageable);
}

View File

@@ -45,7 +45,7 @@ spring:
# nodes: 42.192.62.91:7000,42.192.62.91:7001,42.192.62.91:7002,42.192.62.91:7003,42.192.62.91:7004,42.192.62.91:7005
# 通用配置
password:
# password:
timeout: 5000
jedis:
pool:

View File

@@ -0,0 +1,515 @@
<%@ 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

@@ -0,0 +1,383 @@
<%@ 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

@@ -0,0 +1,493 @@
<%@ 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>