一、控制器层(Controller)

Article detail

后端

2026/4/19 · 65 分钟阅读

一、控制器层(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 URLhttp://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 ConflictEmail already exists
指定无效角色"role": "SUPERVISOR"400 Bad RequestInvalid role: SUPERVISOR
非管理员访问使用普通用户 Token403 ForbiddenAccess Denied
删除自己尝试删除当前登录的管理员500 Internal Server ErrorYou cannot delete your own account
删除最后一个管理员系统中仅有一个管理员时删除他500 Internal Server ErrorCannot delete the last admin user.

6.5 Postman 环境变量建议

变量名示例值
baseUrlhttp://localhost:8080
adminToken管理员登录后获取的 JWT

在请求 URL 中使用 {{baseUrl}}/api/users,在 Authorization 标签页选择 Bearer Token 并填入 {{adminToken}}

评论

动作测试