后端功能增强:全局异常处理、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);
}