后端功能增强:全局异常处理、API控制器、JSP视图和单元测试
- 添加 GlobalExceptionHandler 全局异常处理 - 添加 ApiController REST API 控制器 - 更新 WebConfig 跨域配置和 ProductRepository 查询方法 - 新增 monitor/product-detail/profile JSP 视图页面 - 添加 FlashSaleServiceTest 秒杀服务单元测试 - 更新 application.yml 配置
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -81,4 +81,9 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
* 统计库存小于指定数量的商品数量
|
||||
*/
|
||||
long countByStockLessThan(Integer stock);
|
||||
|
||||
/**
|
||||
* 根据名称模糊查询(忽略大小写)
|
||||
*/
|
||||
Page<Product> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
515
src/main/webapp/WEB-INF/views/admin/monitor.jsp
Normal file
515
src/main/webapp/WEB-INF/views/admin/monitor.jsp
Normal 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>
|
||||
383
src/main/webapp/WEB-INF/views/product-detail.jsp
Normal file
383
src/main/webapp/WEB-INF/views/product-detail.jsp
Normal 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>
|
||||
493
src/main/webapp/WEB-INF/views/profile.jsp
Normal file
493
src/main/webapp/WEB-INF/views/profile.jsp
Normal 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>
|
||||
Reference in New Issue
Block a user