7. Spring Boot CRUD 项目实战

0 阅读9分钟

一、项目概述

1. 项目目标

构建一个完整的用户管理系统,整合所有学习过的知识点:

  • Spring Boot 快速启动
  • JPA 数据持久化
  • RESTful API 开发
  • 数据验证与异常处理
  • 前后端联调与跨域配置

2. 功能清单

功能HTTP 方法URL说明
获取所有用户GET/api/users分页查询用户列表
获取单个用户GET/api/users/{id}根据 ID 查询用户
创建用户POST/api/users创建新用户
更新用户PUT/api/users/{id}更新用户信息
删除用户DELETE/api/users/{id}删除用户
搜索用户GET/api/users/search根据姓名搜索用户

二、项目结构

my-app/
├── pom.xml
└── src/
    └── main/
        ├── java/com/example/myapp/
        │   ├── MyApplication.java              # 启动类
        │   ├── config/
        │   │   └── CorsConfig.java             # CORS 配置
        │   ├── controller/
        │   │   └── UserController.java         # 用户控制器
        │   ├── dto/
        │   │   ├── CreateUserDTO.java         # 创建用户 DTO
        │   │   ├── UpdateUserDTO.java         # 更新用户 DTO
        │   │   └── UserDTO.java               # 用户响应 DTO
        │   ├── exception/
        │   │   ├── GlobalExceptionHandler.java  # 全局异常处理
        │   │   └── ResourceNotFoundException.java # 资源未找到异常
        │   ├── model/
        │   │   └── User.java                  # 用户实体
        │   ├── repository/
        │   │   └── UserRepository.java        # 用户仓库
        │   └── service/
        │       └── UserService.java           # 用户服务
        └── resources/
            └── application.properties        # 配置文件

三、完整代码实现

1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    <name>my-app</name>
    <description>Spring Boot CRUD 实战项目</description>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- MySQL Driver -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. application.properties

# 服务器配置
server.port=8080

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA 配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# 日志配置
logging.level.org.springframework.web=INFO
logging.level.com.example.myapp=DEBUG

3. MyApplication.java(启动类)

package com.example.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

4. CorsConfig.java(CORS 配置)

package com.example.myapp.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins(
                        "http://localhost:3000",   // React
                        "http://localhost:5173"    // Vue
                )
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

5. User.java(实体类)

package com.example.myapp.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "姓名不能为空")
    @Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
    @Column(nullable = false)
    private String name;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @Column(nullable = false, unique = true)
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 100, message = "密码长度必须在 6-100 个字符之间")
    @Column(nullable = false)
    private String password;
    
    @Size(max = 20, message = "电话号码长度不能超过 20 个字符")
    private String phone;
    
    @Column(nullable = false)
    private Boolean active = true;
    
    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    @Column(nullable = false)
    private LocalDateTime updatedAt;
}

6. UserRepository.java(仓库接口)

package com.example.myapp.repository;

import com.example.myapp.model.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // 根据邮箱查询用户
    Optional<User> findByEmail(String email);
    
    // 检查邮箱是否存在
    boolean existsByEmail(String email);
    
    // 根据姓名模糊查询用户
    List<User> findByNameContainingIgnoreCase(String name);
    
    // 根据姓名模糊查询(分页)
    Page<User> findByNameContainingIgnoreCase(String name, Pageable pageable);
    
    // 查询活跃用户
    List<User> findByActiveTrue();
}

7. UserDTO.java(响应 DTO)

package com.example.myapp.dto;

import com.example.myapp.model.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private Long id;
    private String name;
    private String email;
    private String phone;
    private Boolean active;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // 从 User 转换为 UserDTO
    public static UserDTO fromEntity(User user) {
        return new UserDTO(
                user.getId(),
                user.getName(),
                user.getEmail(),
                user.getPhone(),
                user.getActive(),
                user.getCreatedAt(),
                user.getUpdatedAt()
        );
    }
}

8. CreateUserDTO.java(创建用户 DTO)

package com.example.myapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserDTO {
    
    @NotBlank(message = "姓名不能为空")
    @Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
    private String name;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 100, message = "密码长度必须在 6-100 个字符之间")
    private String password;
    
    @Size(max = 20, message = "电话号码长度不能超过 20 个字符")
    private String phone;
}

9. UpdateUserDTO.java(更新用户 DTO)

package com.example.myapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateUserDTO {
    
    @Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
    private String name;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Size(max = 20, message = "电话号码长度不能超过 20 个字符")
    private String phone;
    
    private Boolean active;
}

10. ResourceNotFoundException.java(资源未找到异常)

package com.example.myapp.exception;

public class ResourceNotFoundException extends RuntimeException {
    
    public ResourceNotFoundException(String message) {
        super(message);
    }
    
    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s not found with id: %d", resource, id));
    }
}

11. GlobalExceptionHandler.java(全局异常处理)

package com.example.myapp.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    // 处理资源未找到异常
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                ex.getMessage(),
                LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
    
    // 处理数据验证异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        
        ValidationErrorResponse error = new ValidationErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                "数据验证失败",
                LocalDateTime.now(),
                errors
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
    
    // 处理其他异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                ex.getMessage(),
                LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
    // 错误响应 DTO
    record ErrorResponse(int status, String message, LocalDateTime timestamp) {}
    
    // 验证错误响应 DTO
    record ValidationErrorResponse(
            int status,
            String message,
            LocalDateTime timestamp,
            Map<String, String> errors
    ) {}
}

12. UserService.java(用户服务)

package com.example.myapp.service;

import com.example.myapp.dto.CreateUserDTO;
import com.example.myapp.dto.UpdateUserDTO;
import com.example.myapp.dto.UserDTO;
import com.example.myapp.exception.ResourceNotFoundException;
import com.example.myapp.model.User;
import com.example.myapp.repository.UserRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Transactional
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    // 获取所有用户(分页)
    public Page<UserDTO> findAll(Pageable pageable) {
        return userRepository.findAll(pageable)
                .map(UserDTO::fromEntity);
    }
    
    // 获取所有用户(列表)
    public List<UserDTO> findAll() {
        return userRepository.findAll().stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }
    
    // 根据 ID 获取用户
    public UserDTO findById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        return UserDTO.fromEntity(user);
    }
    
    // 创建用户
    public UserDTO create(CreateUserDTO userDTO) {
        // 检查邮箱是否已存在
        if (userRepository.existsByEmail(userDTO.getEmail())) {
            throw new IllegalArgumentException("邮箱已被使用: " + userDTO.getEmail());
        }
        
        // 转换 DTO 为 Entity
        User user = new User();
        user.setName(userDTO.getName());
        user.setEmail(userDTO.getEmail());
        user.setPassword(userDTO.getPassword());
        user.setPhone(userDTO.getPhone());
        
        // 保存用户
        User savedUser = userRepository.save(user);
        
        return UserDTO.fromEntity(savedUser);
    }
    
    // 更新用户
    public UserDTO update(Long id, UpdateUserDTO userDTO) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        
        // 检查邮箱是否被其他用户使用
        if (userDTO.getEmail() != null && 
            !userDTO.getEmail().equals(user.getEmail()) && 
            userRepository.existsByEmail(userDTO.getEmail())) {
            throw new IllegalArgumentException("邮箱已被使用: " + userDTO.getEmail());
        }
        
        // 更新用户信息
        if (userDTO.getName() != null) {
            user.setName(userDTO.getName());
        }
        if (userDTO.getEmail() != null) {
            user.setEmail(userDTO.getEmail());
        }
        if (userDTO.getPhone() != null) {
            user.setPhone(userDTO.getPhone());
        }
        if (userDTO.getActive() != null) {
            user.setActive(userDTO.getActive());
        }
        
        User updatedUser = userRepository.save(user);
        return UserDTO.fromEntity(updatedUser);
    }
    
    // 删除用户
    public void delete(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        userRepository.delete(user);
    }
    
    // 根据姓名搜索用户
    public List<UserDTO> searchByName(String name) {
        return userRepository.findByNameContainingIgnoreCase(name).stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }
}

13. UserController.java(用户控制器)

package com.example.myapp.controller;

import com.example.myapp.dto.CreateUserDTO;
import com.example.myapp.dto.UpdateUserDTO;
import com.example.myapp.dto.UserDTO;
import com.example.myapp.service.UserService;
import jakarta.validation.Valid;
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.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    // 获取所有用户(分页)
    @GetMapping
    public ResponseEntity<Page<UserDTO>> findAll(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir
    ) {
        Sort sort = sortDir.equalsIgnoreCase("desc") 
                ? Sort.by(sortBy).descending() 
                : Sort.by(sortBy).ascending();
        Pageable pageable = PageRequest.of(page, size, sort);
        Page<UserDTO> users = userService.findAll(pageable);
        return ResponseEntity.ok(users);
    }
    
    // 根据 ID 获取用户
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> findById(@PathVariable Long id) {
        UserDTO user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    // 创建用户
    @PostMapping
    public ResponseEntity<UserDTO> create(@Valid @RequestBody CreateUserDTO userDTO) {
        UserDTO createdUser = userService.create(userDTO);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
    }
    
    // 更新用户
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> update(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserDTO userDTO
    ) {
        UserDTO updatedUser = userService.update(id, userDTO);
        return ResponseEntity.ok(updatedUser);
    }
    
    // 删除用户
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
    
    // 根据姓名搜索用户
    @GetMapping("/search")
    public ResponseEntity<List<UserDTO>> searchByName(@RequestParam String name) {
        List<UserDTO> users = userService.searchByName(name);
        return ResponseEntity.ok(users);
    }
}

四、API 测试

1. 创建用户

请求:

POST http://localhost:8080/api/users
Content-Type: application/json

{
  "name": "张三",
  "email": "zhangsan@example.com",
  "password": "123456",
  "phone": "13800138000"
}

响应:

{
  "id": 1,
  "name": "张三",
  "email": "zhangsan@example.com",
  "phone": "13800138000",
  "active": true,
  "createdAt": "2024-03-26T15:30:00",
  "updatedAt": "2024-03-26T15:30:00"
}

2. 获取所有用户(分页)

请求:

GET http://localhost:8080/api/users?page=0&size=10&sortBy=id&sortDir=desc

响应:

{
  "content": [
    {
      "id": 5,
      "name": "王五",
      "email": "wangwu@example.com",
      "phone": "13800338000",
      "active": true,
      "createdAt": "2024-03-26T15:35:00",
      "updatedAt": "2024-03-26T15:35:00"
    }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 10,
    "sort": {
      "empty": false,
      "sorted": true,
      "unsorted": false
    }
  },
  "totalPages": 1,
  "totalElements": 1,
  "last": true,
  "numberOfElements": 1,
  "first": true,
  "empty": false
}

3. 根据 ID 获取用户

请求:

GET http://localhost:8080/api/users/1

响应:

{
  "id": 1,
  "name": "张三",
  "email": "zhangsan@example.com",
  "phone": "13800138000",
  "active": true,
  "createdAt": "2024-03-26T15:30:00",
  "updatedAt": "2024-03-26T15:30:00"
}

4. 更新用户

请求:

PUT http://localhost:8080/api/users/1
Content-Type: application/json

{
  "name": "张三(已更新)",
  "phone": "13900139000"
}

响应:

{
  "id": 1,
  "name": "张三(已更新)",
  "email": "zhangsan@example.com",
  "phone": "13900139000",
  "active": true,
  "createdAt": "2024-03-26T15:30:00",
  "updatedAt": "2024-03-26T15:40:00"
}

5. 删除用户

请求:

DELETE http://localhost:8080/api/users/1

响应:

HTTP 204 No Content

6. 搜索用户

请求:

GET http://localhost:8080/api/users/search?name=张

响应:

[
  {
    "id": 2,
    "name": "张三",
    "email": "zhangsan@example.com",
    "phone": "13800138000",
    "active": true,
    "createdAt": "2024-03-26T15:30:00",
    "updatedAt": "2024-03-26T15:30:00"
  }
]

7. 数据验证失败

请求:

POST http://localhost:8080/api/users
Content-Type: application/json

{
  "name": "张",
  "email": "invalid-email",
  "password": "123"
}

响应:

{
  "status": 400,
  "message": "数据验证失败",
  "timestamp": "2024-03-26T15:45:00",
  "errors": {
    "name": "姓名长度必须在 2-50 个字符之间",
    "email": "邮箱格式不正确",
    "password": "密码长度必须在 6-100 个字符之间"
  }
}

8. 资源未找到

请求:

GET http://localhost:8080/api/users/999

响应:

{
  "status": 404,
  "message": "User not found with id: 999",
  "timestamp": "2024-03-26T15:50:00"
}

五、前端对接示例

React 示例

UserService.js:

import axios from 'axios';

const API_BASE_URL = 'http://localhost:8080/api/users';

export default {
    // 获取所有用户
    async findAll(page = 0, size = 10, sortBy = 'id', sortDir = 'asc') {
        const response = await axios.get(API_BASE_URL, {
            params: { page, size, sortBy, sortDir }
        });
        return response.data;
    },
    
    // 根据 ID 获取用户
    async findById(id) {
        const response = await axios.get(`${API_BASE_URL}/${id}`);
        return response.data;
    },
    
    // 创建用户
    async create(userData) {
        const response = await axios.post(API_BASE_URL, userData);
        return response.data;
    },
    
    // 更新用户
    async update(id, userData) {
        const response = await axios.put(`${API_BASE_URL}/${id}`, userData);
        return response.data;
    },
    
    // 删除用户
    async delete(id) {
        await axios.delete(`${API_BASE_URL}/${id}`);
    },
    
    // 搜索用户
    async searchByName(name) {
        const response = await axios.get(`${API_BASE_URL}/search`, {
            params: { name }
        });
        return response.data;
    }
};

UserList.js:

import { useState, useEffect } from 'react';
import userService from './UserService';

function UserList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [page, setPage] = useState(0);
    const [totalPages, setTotalPages] = useState(0);

    const fetchUsers = async () => {
        try {
            setLoading(true);
            const data = await userService.findAll(page, 10);
            setUsers(data.content);
            setTotalPages(data.totalPages);
            setError(null);
        } catch (err) {
            setError('获取用户列表失败');
            console.error(err);
        } finally {
            setLoading(false);
        }
    };

    const handleDelete = async (id) => {
        if (window.confirm('确定要删除该用户吗?')) {
            try {
                await userService.delete(id);
                fetchUsers();
                alert('删除成功!');
            } catch (err) {
                alert('删除失败!');
                console.error(err);
            }
        }
    };

    useEffect(() => {
        fetchUsers();
    }, [page]);

    if (loading) return <div>加载中...</div>;
    if (error) return <div>{error}</div>;

    return (
        <div>
            <h2>用户列表</h2>
            <table border="1">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>姓名</th>
                        <th>邮箱</th>
                        <th>电话</th>
                        <th>状态</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    {users.map(user => (
                        <tr key={user.id}>
                            <td>{user.id}</td>
                            <td>{user.name}</td>
                            <td>{user.email}</td>
                            <td>{user.phone}</td>
                            <td>{user.active ? '活跃' : '禁用'}</td>
                            <td>
                                <button onClick={() => handleDelete(user.id)}>
                                    删除
                                </button>
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
            
            <div>
                <button 
                    disabled={page === 0} 
                    onClick={() => setPage(page - 1)}
                >
                    上一页
                </button>
                <span>第 {page + 1} / {totalPages} 页</span>
                <button 
                    disabled={page >= totalPages - 1} 
                    onClick={() => setPage(page + 1)}
                >
                    下一页
                </button>
            </div>
        </div>
    );
}

export default UserList;

六、运行项目

1. 创建数据库

CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

2. 启动项目

mvn spring-boot:run

3. 访问 API

使用 Postman、Apifox 或浏览器测试 API。


七、总结

组件作用
Controller接收 HTTP 请求,返回响应
Service业务逻辑处理
Repository数据访问层
Entity数据库表映射
DTO数据传输对象
Exception异常处理
CorsConfig跨域配置