一、控制器层(Controller)
本文档包含管理员用户管理功能的完整代码实现、功能描述以及 Postman API 测试指南。
一、控制器层(Controller)
1.1 UserController - 用户管理控制器
路径:com.netflix.clone.demo.controller.UserController
功能:提供管理员对用户进行 CRUD 管理的 RESTful API,所有接口均需要 ADMIN 角色权限。
package com.netflix.clone.demo.controller;
import com.netflix.clone.demo.dto.request.UserRequest;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.dto.response.PageResponse;
import com.netflix.clone.demo.dto.response.UserResponse;
import com.netflix.clone.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@PreAuthorize("hasRole('ADMIN')") // 确保所有接口仅 ADMIN 可访问
public class UserController {
@Autowired
private UserService userService;
/**
* 创建新用户(管理员操作)
* <p>允许管理员直接创建用户,可指定角色,并自动发送邮箱验证邮件。</p>
*
* @param userRequest 包含邮箱、密码、全名、角色的请求体
* @return 成功消息
*/
@PostMapping
public ResponseEntity<MessageResponse> createUser(@Valid @RequestBody UserRequest userRequest) {
return ResponseEntity.ok(userService.createUser(userRequest));
}
/**
* 更新用户信息
* <p>更新指定ID用户的姓名和角色,会校验角色合法性并防止移除最后一个管理员。</p>
*
* @param id 用户ID
* @param userRequest 包含要更新的姓名和角色
* @return 成功消息
*/
@PutMapping("/{id}")
public ResponseEntity<MessageResponse> updateUser(@PathVariable Long id, @RequestBody UserRequest userRequest) {
return ResponseEntity.ok(userService.updateUser(id, userRequest));
}
/**
* 分页获取用户列表(支持搜索)
*
* @param page 页码(从0开始),默认0
* @param size 每页条数,默认10
* @param search 可选搜索关键词,匹配邮箱或姓名
* @return 分页的用户响应数据
*/
@GetMapping()
public ResponseEntity<PageResponse<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String search
) {
return ResponseEntity.ok(userService.getUsers(page, size, search));
}
/**
* 删除用户
* <p>不能删除自己,且不能删除最后一个管理员。</p>
*
* @param id 用户ID
* @param authentication 当前登录用户认证信息
* @return 成功消息
*/
@DeleteMapping("/{id}")
public ResponseEntity<MessageResponse> deleteUser(@PathVariable Long id, Authentication authentication) {
String currentUserEmail = authentication.getName();
return ResponseEntity.ok(userService.deleteUser(id, currentUserEmail));
}
/**
* 切换用户激活状态(启用/禁用)
* <p>不能禁用自己,且不能禁用最后一个激活的管理员。</p>
*
* @param id 用户ID
* @param authentication 当前登录用户认证信息
* @return 成功消息
*/
@PutMapping("/{id}/toggle-status")
public ResponseEntity<MessageResponse> toggleUserStatus(
@PathVariable Long id, Authentication authentication
) {
String currentUserEmail = authentication.getName();
return ResponseEntity.ok(userService.toggleUserStatus(id, currentUserEmail));
}
/**
* 修改用户角色
*
* @param id 用户ID
* @param userRequest 包含新角色的请求体
* @return 成功消息
*/
@PutMapping("/{id}/change-role")
public ResponseEntity<MessageResponse> changeUserRole(
@PathVariable Long id, @RequestBody UserRequest userRequest
) {
return ResponseEntity.ok(userService.changeUserRole(id, userRequest));
}
}
二、业务服务接口层(Service)
2.1 UserService - 用户服务接口
路径:com.netflix.clone.demo.service.UserService
功能:定义管理员用户管理相关的业务方法契约。
package com.netflix.clone.demo.service;
import com.netflix.clone.demo.dto.request.UserRequest;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.dto.response.PageResponse;
import com.netflix.clone.demo.dto.response.UserResponse;
import jakarta.validation.Valid;
public interface UserService {
MessageResponse createUser(@Valid UserRequest userRequest);
MessageResponse updateUser(Long id, UserRequest userRequest);
PageResponse<UserResponse> getUsers(int page, int size, String search);
MessageResponse deleteUser(Long id, String currentUserEmail);
MessageResponse toggleUserStatus(Long id, String currentUserEmail);
MessageResponse changeUserRole(Long id, UserRequest userRequest);
}
三、业务服务实现层(Service.Impl)
3.1 UserServiceImpl - 用户服务实现类
路径:com.netflix.clone.demo.service.Impl.UserServiceImpl
功能:实现 UserService 接口,处理用户 CRUD 的具体业务逻辑,包含管理员保护机制。
package com.netflix.clone.demo.service.Impl;
import com.netflix.clone.demo.dao.UserRepository;
import com.netflix.clone.demo.dto.request.UserRequest;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.dto.response.PageResponse;
import com.netflix.clone.demo.dto.response.UserResponse;
import com.netflix.clone.demo.entity.User;
import com.netflix.clone.demo.enums.Role;
import com.netflix.clone.demo.exception.EmailAlreadyExistsException;
import com.netflix.clone.demo.exception.InvalidRoleException;
import com.netflix.clone.demo.service.EmailService;
import com.netflix.clone.demo.service.UserService;
import com.netflix.clone.demo.util.PaginationUtils;
import com.netflix.clone.demo.util.ServiceUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Arrays;
import java.util.UUID;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private ServiceUtils serviceUtils;
@Autowired
private EmailService emailService;
/**
* 管理员创建新用户
*/
@Override
public MessageResponse createUser(UserRequest userRequest) {
if (userRepository.findByEmail(userRequest.getEmail()).isPresent()) {
throw new EmailAlreadyExistsException("Email already exists");
}
validateRole(userRequest.getRole());
User user = new User();
user.setEmail(userRequest.getEmail());
user.setPassword(passwordEncoder.encode(userRequest.getPassword()));
user.setFullName(userRequest.getFullName());
user.setRole(Role.valueOf(userRequest.getRole().toUpperCase()));
user.setActive(true);
String verificationToken = UUID.randomUUID().toString();
user.setVerificationToken(verificationToken);
user.setVerificationTokenExpiry(Instant.now().plusSeconds(86400));
userRepository.save(user);
emailService.sendVerificationEmail(userRequest.getEmail(), verificationToken);
return new MessageResponse("User created successfully.");
}
private void validateRole(String role) {
if (Arrays.stream(Role.values()).noneMatch(r -> r.name().equalsIgnoreCase(role))) {
throw new InvalidRoleException("Invalid role:" + role);
}
}
/**
* 更新用户信息(姓名、角色)
*/
@Override
public MessageResponse updateUser(Long id, UserRequest userRequest) {
User user = serviceUtils.getUserByIdOrThrow(id);
ensureNotLastActiveAdmin(user);
validateRole(userRequest.getRole());
user.setFullName(userRequest.getFullName());
user.setRole(Role.valueOf(userRequest.getRole().toUpperCase()));
userRepository.save(user);
return new MessageResponse("User updated successfully.");
}
private void ensureNotLastActiveAdmin(User user) {
if (user.isActive() && user.getRole() == Role.ADMIN) {
long activeAdminCount = userRepository.countByRoleAndActive(Role.ADMIN, true);
if (activeAdminCount <= 1) {
throw new RuntimeException("Cannot deactivate the last active admin user");
}
}
}
/**
* 分页获取用户列表,支持搜索
*/
@Override
public PageResponse<UserResponse> getUsers(int page, int size, String search) {
Pageable pageable = PaginationUtils.createPageRequest(page, size, "id");
Page<User> userPage;
if (search != null && !search.trim().isEmpty()) {
userPage = userRepository.searchUsers(search.trim(), pageable);
} else {
userPage = userRepository.findAll(pageable);
}
return PaginationUtils.toPageResponse(userPage, UserResponse::fromEntity);
}
/**
* 删除用户,包含自删除保护和最后管理员保护
*/
@Override
public MessageResponse deleteUser(Long id, String currentUserEmail) {
User user = serviceUtils.getUserByIdOrThrow(id);
if (user.getEmail().equals(currentUserEmail)) {
throw new RuntimeException("You cannot delete your own account");
}
ensureNotLastAdmin(user, "delete");
userRepository.deleteById(id);
return new MessageResponse("User deleted successfully.");
}
private void ensureNotLastAdmin(User user, String operation) {
if (user.getRole() == Role.ADMIN) {
long adminCount = userRepository.countByRole(Role.ADMIN);
if (adminCount <= 1) {
throw new RuntimeException("Cannot " + operation + " the last admin user.");
}
}
}
/**
* 切换用户激活状态
*/
@Override
public MessageResponse toggleUserStatus(Long id, String currentUserEmail) {
User user = serviceUtils.getUserByIdOrThrow(id);
if (user.getEmail().equals(currentUserEmail)) {
throw new RuntimeException("You cannot deactivate your own account");
}
ensureNotLastActiveAdmin(user);
user.setActive(!user.isActive());
userRepository.save(user);
return new MessageResponse("User status updated successfully.");
}
/**
* 修改用户角色
*/
@Override
public MessageResponse changeUserRole(Long id, UserRequest userRequest) {
User user = serviceUtils.getUserByIdOrThrow(id);
validateRole(userRequest.getRole());
Role newRole = Role.valueOf(userRequest.getRole().toUpperCase());
if (user.getRole() == Role.ADMIN && newRole == Role.USER) {
ensureNotLastAdmin(user, "change the role of");
}
user.setRole(newRole);
userRepository.save(user);
return new MessageResponse("User role updated successfully.");
}
}
四、工具类(Util)
4.1 PaginationUtils - 分页工具类
路径:com.netflix.clone.demo.util.PaginationUtils
功能:提供创建 Pageable 对象和将 Spring Data 的 Page 转换为自定义 PageResponse 的静态方法。
package com.netflix.clone.demo.util;
import com.netflix.clone.demo.dto.response.PageResponse;
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.security.core.parameters.P;
import java.util.List;
import java.util.function.Function;
public class PaginationUtils {
private PaginationUtils() {
}
public static Pageable createPageRequest(int page, int size, String sortBy) {
return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, sortBy));
}
public static Pageable createPageRequest(int page, int size) {
return PageRequest.of(page, size);
}
public static <T, R> PageResponse<R> toPageResponse(Page<T> page, Function<T, R> mapper) {
List<R> content = page.getContent().stream().map(mapper).toList();
return new PageResponse<>(content, page.getTotalElements(), page.getTotalPages(), page.getNumber(), page.getSize());
}
public static <R> PageResponse<R> toPageResponse(Page<?> page, List<R> mappedContent) {
return new PageResponse<>(
mappedContent,
page.getTotalElements(),
page.getTotalPages(),
page.getNumber(),
page.getSize()
);
}
}
五、依赖说明
为使 UserServiceImpl 正常编译运行,UserRepository 接口需补充以下方法声明:
package com.netflix.clone.demo.dao;
import com.netflix.clone.demo.entity.User;
import com.netflix.clone.demo.enums.Role;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
long countByRoleAndActive(Role role, boolean active);
long countByRole(Role role);
@Query("SELECT u FROM User u WHERE LOWER(u.email) LIKE LOWER(CONCAT('%', :search, '%')) " +
"OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :search, '%'))")
Page<User> searchUsers(@Param("search") String search, Pageable pageable);
}
⚠️ 注意:以上
UserRepository代码为补充说明,不在本次提供的文件列表中,但项目运行时必须存在。
六、Postman API 测试指南
6.1 基础配置
- Base URL:
http://localhost:8080 - 所有接口均需要 ADMIN 权限:需先以管理员账号登录获取 JWT Token,并在请求头中携带
Authorization: Bearer <token>。
6.2 管理员登录获取 Token
请求:
- Method:
POST - URL:
http://localhost:8080/api/auth/login - Body (JSON):
{ "email": "admin@example.com", "password": "adminPassword" }
响应:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"email": "admin@example.com",
"fullName": "Admin User",
"role": "ADMIN"
}
复制 token 值,后续所有请求均需在 Headers 中添加:
Authorization: Bearer <token>
6.3 接口测试详情
① 创建用户 – POST /api/users
请求体:
{
"email": "newuser@mailinator.com",
"password": "securePass123",
"fullName": "New User",
"role": "USER"
}
预期响应:
{
"message": "User created successfully."
}
② 分页获取用户列表 – GET /api/users
请求示例:
- 无搜索:
GET /api/users?page=0&size=10 - 带搜索:
GET /api/users?page=0&size=10&search=admin
预期响应:
{
"content": [
{
"id": 1,
"email": "admin@example.com",
"fullName": "Admin User",
"role": "ADMIN",
"active": true,
"createAt": "2026-04-19T10:00:00Z",
"updateAt": "2026-04-19T10:00:00Z"
},
{ ... }
],
"totalElements": 25,
"totalPages": 3,
"number": 0,
"size": 10
}
③ 更新用户信息 – PUT /api/users/{id}
请求体:
{
"fullName": "Updated Name",
"role": "ADMIN"
}
预期响应:
{
"message": "User updated successfully."
}
④ 切换用户激活状态 – PUT /api/users/{id}/toggle-status
无需请求体。
预期响应:
{
"message": "User status updated successfully."
}
⑤ 修改用户角色 – PUT /api/users/{id}/change-role
请求体:
{
"role": "USER"
}
预期响应:
{
"message": "User role updated successfully."
}
⑥ 删除用户 – DELETE /api/users/{id}
无需请求体。
预期响应:
{
"message": "User deleted successfully."
}
6.4 异常场景测试
| 场景 | 请求体/操作 | 预期 HTTP 状态 | 错误信息示例 |
|---|---|---|---|
| 创建用户时邮箱已存在 | 使用已注册邮箱 | 409 Conflict | Email already exists |
| 指定无效角色 | "role": "SUPERVISOR" | 400 Bad Request | Invalid role: SUPERVISOR |
| 非管理员访问 | 使用普通用户 Token | 403 Forbidden | Access Denied |
| 删除自己 | 尝试删除当前登录的管理员 | 500 Internal Server Error | You cannot delete your own account |
| 删除最后一个管理员 | 系统中仅有一个管理员时删除他 | 500 Internal Server Error | Cannot delete the last admin user. |
6.5 Postman 环境变量建议
| 变量名 | 示例值 |
|---|---|
baseUrl | http://localhost:8080 |
adminToken | 管理员登录后获取的 JWT |
在请求 URL 中使用 {{baseUrl}}/api/users,在 Authorization 标签页选择 Bearer Token 并填入 {{adminToken}}。
评论