From 00306082fb7bc127605b8312e360397cc4844258 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Mar 2026 16:43:04 +0800 Subject: [PATCH] feat: upgrade user management demo --- .../demo/controller/UserController.java | 32 +- .../com/example/demo/dto/UserRequest.java | 17 +- .../example/demo/dto/UserStatsResponse.java | 9 + .../exception/DuplicateEmailException.java | 7 + .../exception/GlobalExceptionHandler.java | 22 +- .../com/example/demo/service/UserService.java | 89 ++- src/main/resources/static/index.html | 364 +++++----- src/main/resources/static/users.html | 630 ++++++++++++------ .../demo/controller/UserControllerTest.java | 47 +- 9 files changed, 753 insertions(+), 464 deletions(-) create mode 100644 src/main/java/com/example/demo/dto/UserStatsResponse.java create mode 100644 src/main/java/com/example/demo/exception/DuplicateEmailException.java diff --git a/src/main/java/com/example/demo/controller/UserController.java b/src/main/java/com/example/demo/controller/UserController.java index e1e7461..6bec44f 100644 --- a/src/main/java/com/example/demo/controller/UserController.java +++ b/src/main/java/com/example/demo/controller/UserController.java @@ -2,6 +2,7 @@ package com.example.demo.controller; import com.example.demo.common.ApiResponse; import com.example.demo.dto.UserRequest; +import com.example.demo.dto.UserStatsResponse; import com.example.demo.model.User; import com.example.demo.service.UserService; import jakarta.validation.Valid; @@ -24,31 +25,38 @@ public class UserController { return ApiResponse.ok(userService.findAll()); } + @GetMapping("/stats") + public ApiResponse getUserStats() { + return ApiResponse.ok(userService.getStats()); + } + + @GetMapping("/search") + public ApiResponse> searchUsers(@RequestParam(required = false) String keyword, + @RequestParam(required = false) String name) { + String term = keyword != null && !keyword.isBlank() ? keyword : name; + return ApiResponse.ok(userService.search(term)); + } + @GetMapping("/{id}") public ApiResponse getUserById(@PathVariable Long id) { return ApiResponse.ok(userService.findById(id)); } @PostMapping - public ApiResponse createUser(@Valid @RequestBody UserRequest req) { - User user = new User(null, req.name(), req.email(), req.age()); - return ApiResponse.ok("创建成功", userService.create(user)); + public ApiResponse createUser(@Valid @RequestBody UserRequest request) { + User user = new User(null, request.name(), request.email(), request.age()); + return ApiResponse.ok("User created successfully", userService.create(user)); } @PutMapping("/{id}") - public ApiResponse updateUser(@PathVariable Long id, @Valid @RequestBody UserRequest req) { - User user = new User(id, req.name(), req.email(), req.age()); - return ApiResponse.ok("更新成功", userService.update(id, user)); + public ApiResponse updateUser(@PathVariable Long id, @Valid @RequestBody UserRequest request) { + User user = new User(id, request.name(), request.email(), request.age()); + return ApiResponse.ok("User updated successfully", userService.update(id, user)); } @DeleteMapping("/{id}") public ApiResponse deleteUser(@PathVariable Long id) { userService.delete(id); - return ApiResponse.ok("删除成功", null); - } - - @GetMapping("/search") - public ApiResponse> searchUsers(@RequestParam String name) { - return ApiResponse.ok(userService.findByName(name)); + return ApiResponse.ok("User deleted successfully", null); } } diff --git a/src/main/java/com/example/demo/dto/UserRequest.java b/src/main/java/com/example/demo/dto/UserRequest.java index 1c3fdf8..9887cfe 100644 --- a/src/main/java/com/example/demo/dto/UserRequest.java +++ b/src/main/java/com/example/demo/dto/UserRequest.java @@ -4,16 +4,21 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record UserRequest( - @NotBlank(message = "姓名不能为空") + @NotBlank(message = "Name is required") + @Size(max = 40, message = "Name must be at most 40 characters") String name, - @NotBlank(message = "邮箱不能为空") - @Email(message = "邮箱格式不正确") + @NotBlank(message = "Email is required") + @Email(message = "Email format is invalid") String email, - @Min(value = 1, message = "年龄最小为 1") - @Max(value = 120, message = "年龄最大为 120") + @NotNull(message = "Age is required") + @Min(value = 1, message = "Age must be at least 1") + @Max(value = 120, message = "Age must be at most 120") Integer age -) {} +) { +} diff --git a/src/main/java/com/example/demo/dto/UserStatsResponse.java b/src/main/java/com/example/demo/dto/UserStatsResponse.java new file mode 100644 index 0000000..f6db572 --- /dev/null +++ b/src/main/java/com/example/demo/dto/UserStatsResponse.java @@ -0,0 +1,9 @@ +package com.example.demo.dto; + +public record UserStatsResponse( + long totalUsers, + long adults, + long underThirty, + double averageAge +) { +} diff --git a/src/main/java/com/example/demo/exception/DuplicateEmailException.java b/src/main/java/com/example/demo/exception/DuplicateEmailException.java new file mode 100644 index 0000000..e533735 --- /dev/null +++ b/src/main/java/com/example/demo/exception/DuplicateEmailException.java @@ -0,0 +1,7 @@ +package com.example.demo.exception; + +public class DuplicateEmailException extends RuntimeException { + public DuplicateEmailException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java index 2e54f79..af09c46 100644 --- a/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -15,24 +16,31 @@ import java.util.Map; public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) - public ResponseEntity> handleNotFound(ResourceNotFoundException e) { + public ResponseEntity> handleNotFound(ResourceNotFoundException exception) { return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.fail(404, e.getMessage())); + .body(ApiResponse.fail(404, exception.getMessage())); + } + + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity> handleDuplicateEmail(DuplicateEmailException exception) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(ApiResponse.fail(409, exception.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity>> handleValidation(MethodArgumentNotValidException e) { + public ResponseEntity>> handleValidation(MethodArgumentNotValidException exception) { Map errors = new HashMap<>(); - for (FieldError error : e.getBindingResult().getFieldErrors()) { + for (FieldError error : exception.getBindingResult().getFieldErrors()) { errors.put(error.getField(), error.getDefaultMessage()); } + return ResponseEntity.badRequest() - .body(new ApiResponse<>(400, "参数校验失败", errors, java.time.Instant.now())); + .body(new ApiResponse<>(400, "Validation failed", errors, Instant.now())); } @ExceptionHandler(Exception.class) - public ResponseEntity> handleAny(Exception e) { + public ResponseEntity> handleAny(Exception exception) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.fail(500, "服务器内部错误: " + e.getMessage())); + .body(ApiResponse.fail(500, "Unexpected server error")); } } diff --git a/src/main/java/com/example/demo/service/UserService.java b/src/main/java/com/example/demo/service/UserService.java index 7009f12..3635e7e 100644 --- a/src/main/java/com/example/demo/service/UserService.java +++ b/src/main/java/com/example/demo/service/UserService.java @@ -1,65 +1,112 @@ package com.example.demo.service; +import com.example.demo.dto.UserStatsResponse; +import com.example.demo.exception.DuplicateEmailException; import com.example.demo.exception.ResourceNotFoundException; import com.example.demo.model.User; import org.springframework.stereotype.Service; -import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @Service public class UserService { - private final List users = new ArrayList<>(); + private final List users = new CopyOnWriteArrayList<>(); private final AtomicLong idGenerator = new AtomicLong(1); public UserService() { - users.add(new User(idGenerator.getAndIncrement(), "张三", "zhangsan@example.com", 25)); - users.add(new User(idGenerator.getAndIncrement(), "李四", "lisi@example.com", 30)); - users.add(new User(idGenerator.getAndIncrement(), "王五", "wangwu@example.com", 28)); + users.add(new User(idGenerator.getAndIncrement(), "Alice Chen", "alice@example.com", 25)); + users.add(new User(idGenerator.getAndIncrement(), "Brandon Li", "brandon@example.com", 30)); + users.add(new User(idGenerator.getAndIncrement(), "Carol Wang", "carol@example.com", 28)); } public List findAll() { - return new ArrayList<>(users); + return users.stream() + .sorted((left, right) -> Long.compare(left.getId(), right.getId())) + .collect(Collectors.toList()); } public User findById(Long id) { return users.stream() - .filter(u -> u.getId().equals(id)) + .filter(user -> user.getId().equals(id)) .findFirst() - .orElseThrow(() -> new ResourceNotFoundException("用户不存在: id=" + id)); + .orElseThrow(() -> new ResourceNotFoundException("User not found: id=" + id)); } - public List findByName(String name) { + public List search(String keyword) { + if (keyword == null || keyword.isBlank()) { + return findAll(); + } + + String normalizedKeyword = keyword.trim().toLowerCase(); return users.stream() - .filter(u -> u.getName().contains(name)) + .filter(user -> user.getName().toLowerCase().contains(normalizedKeyword) + || user.getEmail().toLowerCase().contains(normalizedKeyword)) + .sorted((left, right) -> Long.compare(left.getId(), right.getId())) .collect(Collectors.toList()); } public User create(User user) { - user.setId(idGenerator.getAndIncrement()); - users.add(user); - return user; + ensureEmailAvailable(user.getEmail(), null); + User normalized = normalize(user, idGenerator.getAndIncrement()); + users.add(normalized); + return normalized; } public User update(Long id, User user) { findById(id); - user.setId(id); - for (int i = 0; i < users.size(); i++) { - if (users.get(i).getId().equals(id)) { - users.set(i, user); - return user; + ensureEmailAvailable(user.getEmail(), id); + + User normalized = normalize(user, id); + for (int index = 0; index < users.size(); index++) { + if (users.get(index).getId().equals(id)) { + users.set(index, normalized); + return normalized; } } - throw new ResourceNotFoundException("用户不存在: id=" + id); + + throw new ResourceNotFoundException("User not found: id=" + id); } public void delete(Long id) { - boolean removed = users.removeIf(u -> u.getId().equals(id)); + boolean removed = users.removeIf(user -> user.getId().equals(id)); if (!removed) { - throw new ResourceNotFoundException("用户不存在: id=" + id); + throw new ResourceNotFoundException("User not found: id=" + id); } } + + public UserStatsResponse getStats() { + long totalUsers = users.size(); + long adults = users.stream().filter(user -> user.getAge() >= 18).count(); + long underThirty = users.stream().filter(user -> user.getAge() < 30).count(); + double averageAge = users.stream() + .mapToInt(User::getAge) + .average() + .orElse(0.0); + + return new UserStatsResponse(totalUsers, adults, underThirty, averageAge); + } + + private void ensureEmailAvailable(String email, Long currentUserId) { + String normalizedEmail = email == null ? "" : email.trim().toLowerCase(); + boolean exists = users.stream() + .anyMatch(user -> user.getEmail().equalsIgnoreCase(normalizedEmail) + && (currentUserId == null || !user.getId().equals(currentUserId))); + + if (exists) { + throw new DuplicateEmailException("A user with email " + normalizedEmail + " already exists."); + } + } + + private User normalize(User user, Long id) { + return new User( + id, + user.getName().trim(), + user.getEmail().trim().toLowerCase(), + user.getAge() + ); + } } diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index e5e1b38..0211cf3 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1,225 +1,161 @@ - + - Spring Boot 学习中心 + Spring Boot Learning Hub -

🍃 Spring Boot 学习中心

- - - -
-

📚 学习模块

-
- -

👥 用户管理

-

RESTful API 设计、CRUD 操作、参数绑定

-
- -

🔪 AOP 切面编程

-

日志记录、性能监控、限流控制

-
- -

📡 事件机制

-

发布/订阅模式、解耦业务逻辑

-
- -

🔐 鉴权演示(学习用)

-

最小 JWT 流程:登录、携带 Token、访问受保护接口

-
+
+
+ Spring Boot practice +

Explore web, validation, security, and AOP from one clean hub.

+

+ This project now highlights the most useful demos directly from the homepage so you can jump into the + user management flow, auth example, AOP pages, and health endpoints without decoding broken text first. +

+ -
- -

🔗 快速链接

- - -

🧪 接口测试

- -
-

GET 参数示例

-
- - - -
-
-

GET /learn/params?name=xxx&age=18

-
- -
-

路径变量示例

-
- - -
-
-

GET /learn/path/{id}

-
- -
-

POST JSON 示例

-
- - -
-
-

POST /learn/body

-
- -

📖 学习路径

- -
-

1. IOC 容器

-
    -
  • @Component, @Service, @Repository, @Controller
  • -
  • @Autowired 依赖注入
  • -
  • @Configuration + @Bean 配置类
  • -
-
- -
-

2. Web 开发

-
    -
  • @RestController = @Controller + @ResponseBody
  • -
  • @RequestMapping, @GetMapping, @PostMapping
  • -
  • @PathVariable, @RequestParam, @RequestBody
  • -
-
- -
-

3. AOP 切面编程

-
@Aspect
-@Component
-public class LoggingAspect {
-    @Before("execution(* com.example.*.*(..))")
-    public void logBefore(JoinPoint jp) {
-        System.out.println("方法调用: " + jp.getSignature());
-    }
-}
-
- -
-

4. 事件机制

-
// 发布事件
-@Autowired
-ApplicationEventPublisher publisher;
-publisher.publishEvent(new UserEvent(...));
+    
 
-// 监听事件
-@EventListener
-public void onEvent(UserEvent event) {
-    // 处理事件
-}
-
- -

📁 项目结构

-
-
├── src/main/java/com/example/demo/
-│   ├── DemoApplication.java       # 启动类
-│   ├── controller/                # 控制器层
-│   │   ├── LearnController.java   # 学习示例
-│   │   ├── UserController.java    # 用户 API
-│   │   └── AopEventController.java
-│   ├── service/                   # 业务逻辑层
-│   ├── model/                     # 实体类
-│   ├── aop/                       # AOP 切面
-│   │   ├── LoggingAspect.java
-│   │   ├── PerformanceAspect.java
-│   │   └── RateLimitAspect.java
-│   └── event/                     # 事件机制
-│       ├── UserEventPublisher.java
-│       └── UserEventListener.java
-├── src/main/resources/
-│   ├── static/                    # 静态资源
-│   │   ├── index.html
-│   │   ├── users.html
-│   │   ├── aop.html
-│   │   └── events.html
-│   └── application.properties     # 配置文件
-└── pom.xml                        # Maven 配置
-
- - - - +
+
+

User Management

+

CRUD, validation, duplicate email protection, search, and stats cards in one interactive page.

+ /users.html +
+
+

AOP Showcase

+

Review logging, performance tracing, and rate limiting demonstrations built with Spring AOP.

+ /aop.html +
+
+

Event Flow

+

See how controller actions can publish events and decouple downstream reactions.

+ /events.html +
+ +
+
- \ No newline at end of file + diff --git a/src/main/resources/static/users.html b/src/main/resources/static/users.html index 8a2a40c..63e1452 100644 --- a/src/main/resources/static/users.html +++ b/src/main/resources/static/users.html @@ -1,226 +1,466 @@ - + - 用户管理 - Spring Boot + User Management Lab -

👥 用户管理 - RESTful API 示例

- -
-
-

用户列表

- -
- - - - - - - - - - - -
ID姓名邮箱年龄操作
-
- -
-

📖 学习要点

-
- RESTful API 设计: -
    -
  • GET /api/users - 获取所有用户
  • -
  • GET /api/users/{id} - 获取单个用户
  • -
  • POST /api/users - 创建用户
  • -
  • PUT /api/users/{id} - 更新用户
  • -
  • DELETE /api/users/{id} - 删除用户
  • -
-
- -

Controller 代码示例

-
@RestController
-@RequestMapping("/api/users")
-public class UserController {
-    
-    @GetMapping
-    public List<User> getAllUsers() { ... }
-    
-    @GetMapping("/{id}")
-    public User getUserById(@PathVariable Long id) { ... }
-    
-    @PostMapping
-    public User createUser(@RequestBody User user) { ... }
-    
-    @PutMapping("/{id}")
-    public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
-    
-    @DeleteMapping("/{id}")
-    public String deleteUser(@PathVariable Long id) { ... }
-}
-
- -
-

🔧 Spring 注解说明

- - - - - - - - - -
注解说明
@RestController= @Controller + @ResponseBody
@RequestMapping定义路由映射
@GetMappingGET 请求映射
@PostMappingPOST 请求映射
@PathVariable获取路径变量
@RequestBody获取请求体 JSON
@RequestParam获取查询参数
-
- -

← 返回学习中心

- - -