SpringBoot实战一:10分钟搭建企业级用户管理系统(20000字完整项目)

一、项目概述:什么是真正的企业级项目?

1.1 企业级项目 vs 玩具项目

传统"Hello World"项目的局限性:

java

// ❌ 这是玩具项目

@RestController

public class HelloController {

@GetMapping("/hello")

public String hello() {

return "Hello World";

}

}

// ✅ 这是企业级项目需要具备的特性:

  1. 分层架构清晰(Controller/Service/Repository)
  2. 统一异常处理
  3. 数据验证机制
  4. 统一响应格式
  5. 日志记录
  6. 权限控制
  7. 数据库事务
  8. 缓存策略
  9. 单元测试
  10. API文档
    1.2 用户管理系统功能需求
    模块 功能点 技术实现 重要性
    用户管理 用户注册、登录、信息修改 Spring Security/JWT ★★★★★
    权限管理 角色分配、权限验证 RBAC模型 ★★★★★
    数据管理 CRUD操作、分页查询 Spring Data JPA ★★★★★
    系统安全 密码加密、防暴力破解 BCrypt、限流 ★★★★★
    接口文档 API在线文档 Swagger/OpenAPI ★★★★☆
    日志管理 操作日志、异常日志 AOP切面 ★★★★☆
    数据验证 参数校验、业务校验 Validation API ★★★★☆
    缓存优化 热点数据缓存 Redis/Caffeine ★★★☆☆
    单元测试 服务层测试 JUnit/Mockito ★★★☆☆
    监控告警 健康检查、性能监控 Actuator/Micrometer ★★★☆☆

二、10分钟快速搭建:真的可能吗?

2.1 准备工作(2分钟)

环境要求:

bash

检查环境

java -version # >= Java 17

mvn -v # Maven 3.6+

docker -v # Docker(可选,用于Redis等)

创建项目目录

mkdir user-management-system

cd user-management-system

使用Spring Initializr生成项目(30秒):

bash

使用curl命令快速生成

curl https://start.spring.io/starter.zip

-d type=maven-project

-d language=java

-d bootVersion=3.1.0

-d baseDir=user-management

-d groupId=com.example

-d artifactId=user-management

-d name=user-management

-d description="企业级用户管理系统"

-d packageName=com.example.usermanagement

-d packaging=jar

-d javaVersion=17

-d dependencies=web,data-jpa,security,validation,redis,cache,aop,actuator,devtools

-o user-management.zip

unzip user-management.zip

或者直接复制完整pom.xml(1分钟):

xml
<?xml version="1.0" encoding="UTF-8"?>

复制代码
<modelVersion>4.0.0</modelVersion>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.0</version>
    <relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>user-management</artifactId>
<version>1.0.0</version>
<name>user-management</name>
<description>企业级用户管理系统</description>

<properties>
    <java.version>17</java.version>
    <mapstruct.version>1.5.5.Final</mapstruct.version>
    <jjwt.version>0.11.5</jjwt.version>
    <swagger.version>2.2.0</swagger.version>
</properties>

<dependencies>
    <!-- Web开发 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 数据访问 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- 安全框架 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!-- 数据验证 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!-- Redis缓存 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- 缓存抽象 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    <!-- AOP支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
    <!-- 监控管理 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <!-- 开发工具(热部署) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    
    <!-- 数据库(使用H2内存数据库,快速开始) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- Lombok(减少样板代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- MapStruct(对象映射) -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${mapstruct.version}</version>
    </dependency>
    
    <!-- JWT令牌 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>${jjwt.version}</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>${jjwt.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>${jjwt.version}</version>
        <scope>runtime</scope>
    </dependency>
    
    <!-- Swagger API文档 -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>${swagger.version}</version>
    </dependency>
    
    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
        
        <!-- MapStruct编译插件 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${mapstruct.version}</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.2 基础配置(2分钟) application.yml 完整配置:

yaml

应用配置

spring:

application:

name: user-management-system

数据源配置(H2内存数据库)

datasource:

url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE

driver-class-name: org.h2.Driver

username: sa

password:

JPA配置

jpa:

hibernate:

ddl-auto: update

show-sql: true

properties:

hibernate:

dialect: org.hibernate.dialect.H2Dialect

format_sql: true

jdbc:

batch_size: 20

order_inserts: true

order_updates: true

Redis配置(如果未安装Redis,使用本地缓存)

redis:

host: localhost

port: 6379

timeout: 2000ms

lettuce:

pool:

max-active: 8

max-idle: 8

min-idle: 0

缓存配置

cache:

type: caffeine

caffeine:

spec: maximumSize=500,expireAfterWrite=10m

文件上传配置

servlet:

multipart:

max-file-size: 10MB

max-request-size: 100MB

H2控制台

h2:

console:

enabled: true

path: /h2-console

settings:

trace: false

web-allow-others: false

Jackson配置

jackson:

date-format: yyyy-MM-dd HH:mm:ss

time-zone: GMT+8

serialization:

write-dates-as-timestamps: false

配置JPA SQL日志输出

sql:

init:

platform: h2

安全配置

security:

jwt:

secret: mySecretKeyForJWTTokenGeneration123!@#

expiration: 86400000 # 24小时(毫秒)

header: Authorization

token-prefix: "Bearer "

应用自定义配置

app:

version: 1.0.0

cors:

allowed-origins: "http://localhost:3000,http://localhost:8080"

allowed-methods: "GET,POST,PUT,DELETE,OPTIONS"

allowed-headers: "*"

max-age: 3600

密码策略

password:

min-length: 6

max-length: 20

require-uppercase: true

require-lowercase: true

require-digit: true

require-special-char: false

分页配置

pagination:

default-page-size: 20

max-page-size: 100

上传配置

upload:

base-path: "./uploads"

avatar:

max-size: 2MB

allowed-types: "image/jpeg,image/png,image/gif"

缓存配置

cache:

user:

ttl: 300 # 5分钟

role:

ttl: 600 # 10分钟

日志配置

logging:

level:

root: INFO

com.example.usermanagement: DEBUG

org.springframework.security: DEBUG

org.springframework.web: INFO

org.hibernate.SQL: DEBUG

org.hibernate.type.descriptor.sql.BasicBinder: TRACE

pattern:

console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

file:

name: logs/user-management.log

max-size: 10MB

max-history: 30

Actuator监控配置

management:

endpoints:

web:

exposure:

include: "health,info,metrics,prometheus"

base-path: /actuator

endpoint:

health:

show-details: always

probes:

enabled: true

metrics:

enabled: true

info:

env:

enabled: true

metrics:

export:

prometheus:

enabled: true

tags:

application: ${spring.application.name}

Swagger配置

springdoc:

api-docs:

path: /api-docs

swagger-ui:

path: /swagger-ui.html

operations-sorter: method

tags-sorter: alpha

show-actuator: true

default-consumes-media-type: application/json

default-produces-media-type: application/json

服务器配置

server:

port: 8080

servlet:

context-path: /api

compression:

enabled: true

mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json

min-response-size: 1024

tomcat:

connection-timeout: 5000

max-connections: 10000

threads:

max: 200

min-spare: 10

启动类配置(30秒):

java

package com.example.usermanagement;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication

@EnableJpaAuditing // 启用JPA审计(自动填充创建时间、更新时间)

@EnableTransactionManagement // 启用事务管理

@EnableCaching // 启用缓存

@EnableAsync // 启用异步处理

public class UserManagementApplication {

复制代码
public static void main(String[] args) {
    SpringApplication.run(UserManagementApplication.class, args);
    System.out.println("\n=========================================");
    System.out.println("用户管理系统启动成功!");
    System.out.println("API文档: http://localhost:8080/api/swagger-ui.html");
    System.out.println("H2数据库: http://localhost:8080/api/h2-console");
    System.out.println("JDBC URL: jdbc:h2:mem:testdb");
    System.out.println("用户名: sa");
    System.out.println("密码: (空)");
    System.out.println("=========================================\n");
}

}

三、核心模块实现(5分钟)

3.1 数据库设计(1分钟)

实体类关系图:

text

用户系统核心实体:

User (用户)

├── id (主键)

├── username (用户名)

├── password (密码)

├── email (邮箱)

├── phone (手机号)

├── avatar (头像)

├── status (状态)

├── roles (角色集合) → ManyToMany → Role

└── audit fields (审计字段)

Role (角色)

├── id (主键)

├── name (角色名)

├── code (角色代码)

├── description (描述)

├── permissions (权限集合) → ManyToMany → Permission

└── users (用户集合) → ManyToMany → User

Permission (权限)

├── id (主键)

├── name (权限名)

├── code (权限代码)

├── description (描述)

├── type (类型: MENU/BUTTON/API)

├── url (资源URL)

└── parentId (父权限ID)

AuditBase (审计基类)

├── createdBy (创建人)

├── createdAt (创建时间)

├── updatedBy (更新人)

└── updatedAt (更新时间)

实体类实现:

java

// 1. 审计基类(所有实体继承)

package com.example.usermanagement.entity.base;

import jakarta.persistence.Column;

import jakarta.persistence.EntityListeners;

import jakarta.persistence.MappedSuperclass;

import lombok.Data;

import org.springframework.data.annotation.CreatedBy;

import org.springframework.data.annotation.CreatedDate;

import org.springframework.data.annotation.LastModifiedBy;

import org.springframework.data.annotation.LastModifiedDate;

import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Data

@MappedSuperclass

@EntityListeners(AuditingEntityListener.class)

public abstract class AuditBase {

复制代码
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;

@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;

@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;

@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;

}

// 2. 用户实体

package com.example.usermanagement.entity;

import com.example.usermanagement.entity.base.AuditBase;

import com.example.usermanagement.enums.UserStatus;

import jakarta.persistence.*;

import lombok.Data;

import lombok.EqualsAndHashCode;

import java.util.HashSet;

import java.util.Set;

@Data

@Entity

@Table(name = "sys_user",

indexes = {

@Index(name = "idx_username", columnList = "username", unique = true),

@Index(name = "idx_email", columnList = "email", unique = true),

@Index(name = "idx_phone", columnList = "phone", unique = true),

@Index(name = "idx_status", columnList = "status")

})

@EqualsAndHashCode(callSuper = true)

public class User extends AuditBase {

复制代码
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50, unique = true)
private String username;

@Column(nullable = false, length = 100)
private String password;

@Column(nullable = false, length = 100, unique = true)
private String email;

@Column(length = 20, unique = true)
private String phone;

@Column(length = 200)
private String avatar;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private UserStatus status = UserStatus.ACTIVE;

@Column(name = "last_login_time")
private LocalDateTime lastLoginTime;

@Column(name = "login_fail_count")
private Integer loginFailCount = 0;

@Column(name = "account_non_locked")
private Boolean accountNonLocked = true;

// 用户-角色多对多关系
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "sys_user_role",
    joinColumns = @JoinColumn(name = "user_id"),
    inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();

// 业务方法
public void incrementLoginFailCount() {
    this.loginFailCount = (this.loginFailCount == null ? 1 : this.loginFailCount + 1);
    if (this.loginFailCount >= 5) {
        this.accountNonLocked = false;  // 登录失败5次锁定账户
    }
}

public void resetLoginFailCount() {
    this.loginFailCount = 0;
    this.accountNonLocked = true;
}

}

// 3. 角色实体

package com.example.usermanagement.entity;

import com.example.usermanagement.entity.base.AuditBase;

import jakarta.persistence.*;

import lombok.Data;

import lombok.EqualsAndHashCode;

import java.util.HashSet;

import java.util.Set;

@Data

@Entity

@Table(name = "sys_role",

indexes = {

@Index(name = "idx_role_code", columnList = "code", unique = true),

@Index(name = "idx_role_name", columnList = "name")

})

@EqualsAndHashCode(callSuper = true)

public class Role extends AuditBase {

复制代码
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50)
private String name;

@Column(nullable = false, length = 50, unique = true)
private String code;

@Column(length = 200)
private String description;

@Column(nullable = false)
private Integer sortOrder = 0;

// 角色-用户多对多关系
@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private Set<User> users = new HashSet<>();

// 角色-权限多对多关系
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "sys_role_permission",
    joinColumns = @JoinColumn(name = "role_id"),
    inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();

}

// 4. 权限实体

package com.example.usermanagement.entity;

import com.example.usermanagement.entity.base.AuditBase;

import com.example.usermanagement.enums.PermissionType;

import jakarta.persistence.*;

import lombok.Data;

import lombok.EqualsAndHashCode;

import java.util.HashSet;

import java.util.Set;

@Data

@Entity

@Table(name = "sys_permission",

indexes = {

@Index(name = "idx_permission_code", columnList = "code", unique = true),

@Index(name = "idx_parent_id", columnList = "parentId"),

@Index(name = "idx_permission_type", columnList = "type")

})

@EqualsAndHashCode(callSuper = true)

public class Permission extends AuditBase {

复制代码
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50)
private String name;

@Column(nullable = false, length = 100, unique = true)
private String code;

@Column(length = 200)
private String description;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PermissionType type;

@Column(length = 200)
private String url;

@Column(length = 50)
private String icon;

@Column(name = "parent_id")
private Long parentId;

@Column(nullable = false)
private Integer sortOrder = 0;

@Column(nullable = false)
private Boolean visible = true;

// 权限-角色多对多关系
@ManyToMany(mappedBy = "permissions", fetch = FetchType.LAZY)
private Set<Role> roles = new HashSet<>();

}

枚举类定义:

java

// 用户状态枚举

package com.example.usermanagement.enums;

import lombok.Getter;

@Getter

public enum UserStatus {

ACTIVE("活跃", 1),

INACTIVE("未激活", 2),

LOCKED("已锁定", 3),

DELETED("已删除", 4);

复制代码
private final String description;
private final Integer code;

UserStatus(String description, Integer code) {
    this.description = description;
    this.code = code;
}

}

// 权限类型枚举

package com.example.usermanagement.enums;

import lombok.Getter;

@Getter

public enum PermissionType {

MENU("菜单", 1),

BUTTON("按钮", 2),

API("接口", 3),

DATA("数据", 4);

复制代码
private final String description;
private final Integer code;

PermissionType(String description, Integer code) {
    this.description = description;
    this.code = code;
}

}

3.2 数据访问层(1分钟)

Repository接口:

java

// 1. 用户Repository

package com.example.usermanagement.repository;

import com.example.usermanagement.entity.User;

import com.example.usermanagement.enums.UserStatus;

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.JpaSpecificationExecutor;

import org.springframework.data.jpa.repository.Modifying;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.query.Param;

import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;

import java.util.List;

import java.util.Optional;

@Repository

public interface UserRepository extends JpaRepository<User, Long>,

JpaSpecificationExecutor {

复制代码
// 根据用户名查找用户
Optional<User> findByUsername(String username);

// 根据邮箱查找用户
Optional<User> findByEmail(String email);

// 根据手机号查找用户
Optional<User> findByPhone(String phone);

// 根据用户名或邮箱查找用户
Optional<User> findByUsernameOrEmail(String username, String email);

// 检查用户名是否存在
boolean existsByUsername(String username);

// 检查邮箱是否存在
boolean existsByEmail(String email);

// 检查手机号是否存在
boolean existsByPhone(String phone);

// 根据状态查找用户
List<User> findByStatus(UserStatus status);

// 根据状态分页查找用户
Page<User> findByStatus(UserStatus status, Pageable pageable);

// 根据创建时间范围查找用户
List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);

// 更新最后登录时间
@Modifying
@Query("UPDATE User u SET u.lastLoginTime = :loginTime WHERE u.id = :userId")
void updateLastLoginTime(@Param("userId") Long userId, 
                        @Param("loginTime") LocalDateTime loginTime);

// 根据角色ID查找用户
@Query("SELECT u FROM User u JOIN u.roles r WHERE r.id = :roleId")
List<User> findByRoleId(@Param("roleId") Long roleId);

// 统计用户数量
@Query("SELECT COUNT(u) FROM User u WHERE u.status = :status")
long countByStatus(@Param("status") UserStatus status);

// 查找最近登录的用户
@Query("SELECT u FROM User u WHERE u.lastLoginTime IS NOT NULL ORDER BY u.lastLoginTime DESC")
List<User> findRecentLoginUsers(Pageable pageable);

}

// 2. 角色Repository

package com.example.usermanagement.repository;

import com.example.usermanagement.entity.Role;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.query.Param;

import org.springframework.stereotype.Repository;

import java.util.List;

import java.util.Optional;

@Repository

public interface RoleRepository extends JpaRepository<Role, Long>,

JpaSpecificationExecutor {

复制代码
Optional<Role> findByCode(String code);

boolean existsByCode(String code);

List<Role> findByIdIn(List<Long> ids);

@Query("SELECT r FROM Role r JOIN r.users u WHERE u.id = :userId")
List<Role> findByUserId(@Param("userId") Long userId);

}

// 3. 权限Repository

package com.example.usermanagement.repository;

import com.example.usermanagement.entity.Permission;

import com.example.usermanagement.enums.PermissionType;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.query.Param;

import org.springframework.stereotype.Repository;

import java.util.List;

import java.util.Set;

@Repository

public interface PermissionRepository extends JpaRepository<Permission, Long>,

JpaSpecificationExecutor {

复制代码
Optional<Permission> findByCode(String code);

List<Permission> findByType(PermissionType type);

List<Permission> findByParentId(Long parentId);

List<Permission> findByParentIdIsNull();

@Query("SELECT p FROM Permission p JOIN p.roles r WHERE r.id IN :roleIds")
Set<Permission> findByRoleIds(@Param("roleIds") Set<Long> roleIds);

@Query("SELECT p FROM Permission p JOIN p.roles r WHERE r.code = :roleCode")
Set<Permission> findByRoleCode(@Param("roleCode") String roleCode);

}

3.3 业务逻辑层(2分钟)

DTO定义:

java

// 1. 通用DTO

package com.example.usermanagement.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.AllArgsConstructor;

import lombok.Builder;

import lombok.Data;

import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data

@Builder

@NoArgsConstructor

@AllArgsConstructor

@JsonInclude(JsonInclude.Include.NON_NULL)

public class ApiResponse implements Serializable {

复制代码
private Integer code;
private String message;
private T data;
private Long timestamp;

public static <T> ApiResponse<T> success(T data) {
    return ApiResponse.<T>builder()
            .code(200)
            .message("成功")
            .data(data)
            .timestamp(System.currentTimeMillis())
            .build();
}

public static <T> ApiResponse<T> success(String message, T data) {
    return ApiResponse.<T>builder()
            .code(200)
            .message(message)
            .data(data)
            .timestamp(System.currentTimeMillis())
            .build();
}

public static <T> ApiResponse<T> error(Integer code, String message) {
    return ApiResponse.<T>builder()
            .code(code)
            .message(message)
            .timestamp(System.currentTimeMillis())
            .build();
}

}

// 2. 用户相关DTO

package com.example.usermanagement.dto.user;

import com.example.usermanagement.enums.UserStatus;

import com.fasterxml.jackson.annotation.JsonFormat;

import io.swagger.v3.oas.annotations.media.Schema;

import jakarta.validation.constraints.Email;

import jakarta.validation.constraints.NotBlank;

import jakarta.validation.constraints.Pattern;

import jakarta.validation.constraints.Size;

import lombok.Data;

import java.time.LocalDateTime;

import java.util.Set;

// 用户注册请求

@Data

@Schema(description = "用户注册请求")

public class UserRegisterRequest {

复制代码
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
@Schema(description = "用户名", example = "zhangsan")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
@Schema(description = "密码", example = "123456")
private String password;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Schema(description = "邮箱", example = "zhangsan@example.com")
private String email;

@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Schema(description = "手机号", example = "13800138000")
private String phone;

}

// 用户登录请求

@Data

@Schema(description = "用户登录请求")

public class UserLoginRequest {

复制代码
@NotBlank(message = "用户名或邮箱不能为空")
@Schema(description = "用户名或邮箱", example = "zhangsan")
private String usernameOrEmail;

@NotBlank(message = "密码不能为空")
@Schema(description = "密码", example = "123456")
private String password;

@Schema(description = "记住我", example = "false")
private Boolean rememberMe = false;

}

// 用户登录响应

@Data

@Schema(description = "用户登录响应")

public class UserLoginResponse {

复制代码
@Schema(description = "用户ID", example = "1")
private Long userId;

@Schema(description = "用户名", example = "zhangsan")
private String username;

@Schema(description = "邮箱", example = "zhangsan@example.com")
private String email;

@Schema(description = "访问令牌")
private String accessToken;

@Schema(description = "令牌类型", example = "Bearer")
private String tokenType = "Bearer";

@Schema(description = "过期时间(秒)", example = "86400")
private Long expiresIn;

@Schema(description = "角色列表")
private Set<String> roles;

@Schema(description = "权限列表")
private Set<String> permissions;

}

// 用户信息响应

@Data

@Schema(description = "用户信息")

public class UserDTO {

复制代码
@Schema(description = "用户ID", example = "1")
private Long id;

@Schema(description = "用户名", example = "zhangsan")
private String username;

@Schema(description = "邮箱", example = "zhangsan@example.com")
private String email;

@Schema(description = "手机号", example = "13800138000")
private String phone;

@Schema(description = "头像URL")
private String avatar;

@Schema(description = "用户状态")
private UserStatus status;

@Schema(description = "角色列表")
private Set<RoleSimpleDTO> roles;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private LocalDateTime createdAt;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "最后登录时间")
private LocalDateTime lastLoginTime;

}

// 3. 角色相关DTO

package com.example.usermanagement.dto.role;

import io.swagger.v3.oas.annotations.media.Schema;

import jakarta.validation.constraints.NotBlank;

import jakarta.validation.constraints.Size;

import lombok.Data;

import java.time.LocalDateTime;

import java.util.Set;

@Data

@Schema(description = "角色信息")

public class RoleDTO {

复制代码
@Schema(description = "角色ID", example = "1")
private Long id;

@NotBlank(message = "角色名称不能为空")
@Size(max = 50, message = "角色名称长度不能超过50个字符")
@Schema(description = "角色名称", example = "管理员")
private String name;

@NotBlank(message = "角色代码不能为空")
@Size(max = 50, message = "角色代码长度不能超过50个字符")
@Schema(description = "角色代码", example = "ROLE_ADMIN")
private String code;

@Schema(description = "角色描述", example = "系统管理员")
private String description;

@Schema(description = "排序号", example = "1")
private Integer sortOrder;

@Schema(description = "权限ID列表")
private Set<Long> permissionIds;

@Schema(description = "创建时间")
private LocalDateTime createdAt;

}

// 4. 权限相关DTO

package com.example.usermanagement.dto.permission;

import com.example.usermanagement.enums.PermissionType;

import io.swagger.v3.oas.annotations.media.Schema;

import jakarta.validation.constraints.NotBlank;

import lombok.Data;

import java.util.List;

@Data

@Schema(description = "权限信息")

public class PermissionDTO {

复制代码
@Schema(description = "权限ID", example = "1")
private Long id;

@NotBlank(message = "权限名称不能为空")
@Schema(description = "权限名称", example = "用户管理")
private String name;

@NotBlank(message = "权限代码不能为空")
@Schema(description = "权限代码", example = "user:manage")
private String code;

@Schema(description = "权限描述", example = "管理用户信息")
private String description;

@Schema(description = "权限类型")
private PermissionType type;

@Schema(description = "资源URL")
private String url;

@Schema(description = "图标")
private String icon;

@Schema(description = "父权限ID", example = "0")
private Long parentId;

@Schema(description = "排序号", example = "1")
private Integer sortOrder;

@Schema(description = "是否可见", example = "true")
private Boolean visible;

@Schema(description = "子权限列表")
private List<PermissionDTO> children;

}

Service层实现:

java

// 1. 用户服务接口

package com.example.usermanagement.service;

import com.example.usermanagement.dto.user.*;

import com.example.usermanagement.entity.User;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

public interface UserService {

复制代码
// 用户注册
User register(UserRegisterRequest request);

// 用户登录
UserLoginResponse login(UserLoginRequest request);

// 获取当前用户信息
UserDTO getCurrentUser();

// 根据ID获取用户
UserDTO getUserById(Long id);

// 根据用户名获取用户
UserDTO getUserByUsername(String username);

// 分页查询用户
Page<UserDTO> getUsers(Pageable pageable);

// 根据条件查询用户
Page<UserDTO> searchUsers(String keyword, UserStatus status, Pageable pageable);

// 更新用户信息
UserDTO updateUser(Long id, UserUpdateRequest request);

// 删除用户(软删除)
void deleteUser(Long id);

// 修改密码
void changePassword(ChangePasswordRequest request);

// 重置密码
void resetPassword(Long userId, String newPassword);

// 分配角色
void assignRoles(Long userId, Set<Long> roleIds);

// 检查用户名是否可用
boolean isUsernameAvailable(String username);

// 检查邮箱是否可用
boolean isEmailAvailable(String email);

}

// 2. 用户服务实现

package com.example.usermanagement.service.impl;

import com.example.usermanagement.dto.user.*;

import com.example.usermanagement.entity.Role;

import com.example.usermanagement.entity.User;

import com.example.usermanagement.enums.UserStatus;

import com.example.usermanagement.exception.BusinessException;

import com.example.usermanagement.repository.RoleRepository;

import com.example.usermanagement.repository.UserRepository;

import com.example.usermanagement.service.UserService;

import com.example.usermanagement.util.JwtTokenUtil;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.cache.annotation.CacheEvict;

import org.springframework.cache.annotation.Cacheable;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.jpa.domain.Specification;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import org.springframework.util.StringUtils;

import jakarta.persistence.criteria.Predicate;

import java.time.LocalDateTime;

import java.util.HashSet;

import java.util.List;

import java.util.Set;

import java.util.stream.Collectors;

@Slf4j

@Service

@RequiredArgsConstructor

@Transactional(readOnly = true)

public class UserServiceImpl implements UserService {

复制代码
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
private final UserMapper userMapper;

@Override
@Transactional
public User register(UserRegisterRequest request) {
    log.info("用户注册: username={}", request.getUsername());
    
    // 1. 验证用户名是否已存在
    if (userRepository.existsByUsername(request.getUsername())) {
        throw new BusinessException("用户名已存在");
    }
    
    // 2. 验证邮箱是否已存在
    if (userRepository.existsByEmail(request.getEmail())) {
        throw new BusinessException("邮箱已存在");
    }
    
    // 3. 验证手机号是否已存在(如果有)
    if (StringUtils.hasText(request.getPhone()) && 
        userRepository.existsByPhone(request.getPhone())) {
        throw new BusinessException("手机号已存在");
    }
    
    // 4. 创建用户实体
    User user = new User();
    user.setUsername(request.getUsername());
    user.setPassword(passwordEncoder.encode(request.getPassword()));
    user.setEmail(request.getEmail());
    user.setPhone(request.getPhone());
    user.setStatus(UserStatus.ACTIVE);
    
    // 5. 分配默认角色(ROLE_USER)
    Role defaultRole = roleRepository.findByCode("ROLE_USER")
            .orElseThrow(() -> new BusinessException("默认角色不存在"));
    user.getRoles().add(defaultRole);
    
    // 6. 保存用户
    return userRepository.save(user);
}

@Override
@Transactional
public UserLoginResponse login(UserLoginRequest request) {
    log.info("用户登录尝试: usernameOrEmail={}", request.getUsernameOrEmail());
    
    try {
        // 1. 认证
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getUsernameOrEmail(),
                request.getPassword()
            )
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        // 2. 获取用户信息
        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        User user = userDetails.getUser();
        
        // 3. 更新最后登录时间和重置登录失败计数
        user.setLastLoginTime(LocalDateTime.now());
        user.resetLoginFailCount();
        userRepository.save(user);
        
        // 4. 生成JWT令牌
        String token = jwtTokenUtil.generateToken(userDetails);
        
        // 5. 获取用户角色和权限
        Set<String> roles = user.getRoles().stream()
                .map(Role::getCode)
                .collect(Collectors.toSet());
        
        Set<String> permissions = new HashSet<>();
        user.getRoles().forEach(role -> 
            role.getPermissions().forEach(permission -> 
                permissions.add(permission.getCode())
            )
        );
        
        // 6. 构建响应
        UserLoginResponse response = new UserLoginResponse();
        response.setUserId(user.getId());
        response.setUsername(user.getUsername());
        response.setEmail(user.getEmail());
        response.setAccessToken(token);
        response.setExpiresIn(jwtTokenUtil.getExpiration() / 1000);
        response.setRoles(roles);
        response.setPermissions(permissions);
        
        log.info("用户登录成功: userId={}, username={}", user.getId(), user.getUsername());
        return response;
        
    } catch (Exception e) {
        // 记录登录失败
        User user = userRepository.findByUsernameOrEmail(
            request.getUsernameOrEmail(), 
            request.getUsernameOrEmail()
        ).orElse(null);
        
        if (user != null) {
            user.incrementLoginFailCount();
            userRepository.save(user);
            log.warn("用户登录失败: userId={}, 失败次数={}", 
                    user.getId(), user.getLoginFailCount());
        }
        
        throw new BusinessException("用户名或密码错误");
    }
}

@Override
@Cacheable(value = "user", key = "#id")
public UserDTO getUserById(Long id) {
    log.debug("查询用户: id={}", id);
    
    User user = userRepository.findById(id)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    
    return userMapper.toDTO(user);
}

@Override
@Cacheable(value = "user", key = "'username:' + #username")
public UserDTO getUserByUsername(String username) {
    User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    
    return userMapper.toDTO(user);
}

@Override
public Page<UserDTO> getUsers(Pageable pageable) {
    log.debug("分页查询用户: page={}, size={}", pageable.getPageNumber(), pageable.getPageSize());
    
    Page<User> userPage = userRepository.findAll(pageable);
    return userPage.map(userMapper::toDTO);
}

@Override
public Page<UserDTO> searchUsers(String keyword, UserStatus status, Pageable pageable) {
    log.debug("搜索用户: keyword={}, status={}", keyword, status);
    
    Specification<User> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        
        if (StringUtils.hasText(keyword)) {
            String likeKeyword = "%" + keyword + "%";
            Predicate usernamePredicate = cb.like(root.get("username"), likeKeyword);
            Predicate emailPredicate = cb.like(root.get("email"), likeKeyword);
            Predicate phonePredicate = cb.like(root.get("phone"), likeKeyword);
            predicates.add(cb.or(usernamePredicate, emailPredicate, phonePredicate));
        }
        
        if (status != null) {
            predicates.add(cb.equal(root.get("status"), status));
        }
        
        return cb.and(predicates.toArray(new Predicate[0]));
    };
    
    Page<User> userPage = userRepository.findAll(spec, pageable);
    return userPage.map(userMapper::toDTO);
}

@Override
@Transactional
@CacheEvict(value = "user", key = "#id")
public UserDTO updateUser(Long id, UserUpdateRequest request) {
    log.info("更新用户信息: id={}", id);
    
    User user = userRepository.findById(id)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    
    // 验证邮箱是否被其他用户使用
    if (StringUtils.hasText(request.getEmail()) && 
        !request.getEmail().equals(user.getEmail()) &&
        userRepository.existsByEmail(request.getEmail())) {
        throw new BusinessException("邮箱已被其他用户使用");
    }
    
    // 验证手机号是否被其他用户使用
    if (StringUtils.hasText(request.getPhone()) && 
        !request.getPhone().equals(user.getPhone()) &&
        userRepository.existsByPhone(request.getPhone())) {
        throw new BusinessException("手机号已被其他用户使用");
    }
    
    // 更新用户信息
    userMapper.updateEntityFromDTO(request, user);
    user = userRepository.save(user);
    
    return userMapper.toDTO(user);
}

@Override
@Transactional
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
    log.info("删除用户: id={}", id);
    
    User user = userRepository.findById(id)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    
    // 软删除:修改状态为已删除
    user.setStatus(UserStatus.DELETED);
    userRepository.save(user);
}

@Override
@Transactional
public void changePassword(ChangePasswordRequest request) {
    // 获取当前登录用户
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
    User user = userDetails.getUser();
    
    // 验证旧密码
    if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
        throw new BusinessException("旧密码错误");
    }
    
    // 更新密码
    user.setPassword(passwordEncoder.encode(request.getNewPassword()));
    userRepository.save(user);
    
    log.info("用户修改密码成功: userId={}", user.getId());
}

@Override
@Transactional
@CacheEvict(value = "user", key = "#userId")
public void assignRoles(Long userId, Set<Long> roleIds) {
    log.info("为用户分配角色: userId={}, roleIds={}", userId, roleIds);
    
    User user = userRepository.findById(userId)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    
    List<Role> roles = roleRepository.findByIdIn(new ArrayList<>(roleIds));
    if (roles.size() != roleIds.size()) {
        throw new BusinessException("部分角色不存在");
    }
    
    user.getRoles().clear();
    user.getRoles().addAll(roles);
    userRepository.save(user);
}

@Override
public boolean isUsernameAvailable(String username) {
    return !userRepository.existsByUsername(username);
}

@Override
public boolean isEmailAvailable(String email) {
    return !userRepository.existsByEmail(email);
}

@Override
public UserDTO getCurrentUser() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null || !authentication.isAuthenticated()) {
        throw new BusinessException("用户未登录");
    }
    
    UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
    return getUserById(userDetails.getUser().getId());
}

}

// 3. MapStruct映射器

package com.example.usermanagement.service.mapper;

import com.example.usermanagement.dto.user.UserDTO;

import com.example.usermanagement.dto.user.UserRegisterRequest;

import com.example.usermanagement.dto.user.UserUpdateRequest;

import com.example.usermanagement.entity.User;

import org.mapstruct.*;

@Mapper(componentModel = "spring",

unmappedTargetPolicy = ReportingPolicy.IGNORE,

nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

public interface UserMapper {

复制代码
UserDTO toDTO(User user);

@Mapping(target = "id", ignore = true)
@Mapping(target = "roles", ignore = true)
@Mapping(target = "status", ignore = true)
@Mapping(target = "createdBy", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedBy", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
User toEntity(UserRegisterRequest request);

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntityFromDTO(UserUpdateRequest request, @MappingTarget User user);

}

3.4 控制器层(1分钟)

用户控制器:

java

package com.example.usermanagement.controller;

import com.example.usermanagement.dto.ApiResponse;

import com.example.usermanagement.dto.user.;
import com.example.usermanagement.enums.UserStatus;
import com.example.usermanagement.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.
;

import java.util.Set;

@RestController

@RequestMapping("/api/v1/users")

@RequiredArgsConstructor

@Tag(name = "用户管理", description = "用户管理相关接口")

public class UserController {

复制代码
private final UserService userService;

@PostMapping("/register")
@Operation(summary = "用户注册", description = "新用户注册接口")
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<UserDTO> register(@Valid @RequestBody UserRegisterRequest request) {
    return ApiResponse.success("注册成功", userService.register(request));
}

@PostMapping("/login")
@Operation(summary = "用户登录", description = "用户登录接口")
public ApiResponse<UserLoginResponse> login(@Valid @RequestBody UserLoginRequest request) {
    return ApiResponse.success("登录成功", userService.login(request));
}

@GetMapping("/me")
@Operation(summary = "获取当前用户信息", description = "获取当前登录用户的信息")
@PreAuthorize("isAuthenticated()")
public ApiResponse<UserDTO> getCurrentUser() {
    return ApiResponse.success(userService.getCurrentUser());
}

@GetMapping("/{id}")
@Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户详细信息")
@PreAuthorize("hasAuthority('user:read')")
public ApiResponse<UserDTO> getUserById(
        @Parameter(description = "用户ID") @PathVariable Long id) {
    return ApiResponse.success(userService.getUserById(id));
}

@GetMapping
@Operation(summary = "分页查询用户", description = "分页查询用户列表")
@PreAuthorize("hasAuthority('user:read')")
public ApiResponse<Page<UserDTO>> getUsers(
        @Parameter(description = "页码,从0开始") @RequestParam(defaultValue = "0") int page,
        @Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size,
        @Parameter(description = "排序字段") @RequestParam(defaultValue = "createdAt") String sort,
        @Parameter(description = "排序方向") @RequestParam(defaultValue = "DESC") String direction) {
    
    Sort.Direction sortDirection = Sort.Direction.fromString(direction);
    Pageable pageable = PageRequest.of(page, size, Sort.by(sortDirection, sort));
    
    return ApiResponse.success(userService.getUsers(pageable));
}

@GetMapping("/search")
@Operation(summary = "搜索用户", description = "根据条件搜索用户")
@PreAuthorize("hasAuthority('user:read')")
public ApiResponse<Page<UserDTO>> searchUsers(
        @Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
        @Parameter(description = "用户状态") @RequestParam(required = false) UserStatus status,
        @Parameter(description = "页码") @RequestParam(defaultValue = "0") int page,
        @Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
    
    Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
    return ApiResponse.success(userService.searchUsers(keyword, status, pageable));
}

@PutMapping("/{id}")
@Operation(summary = "更新用户信息", description = "更新指定用户的信息")
@PreAuthorize("hasAuthority('user:update')")
public ApiResponse<UserDTO> updateUser(
        @Parameter(description = "用户ID") @PathVariable Long id,
        @Valid @RequestBody UserUpdateRequest request) {
    return ApiResponse.success("更新成功", userService.updateUser(id, request));
}

@DeleteMapping("/{id}")
@Operation(summary = "删除用户", description = "删除指定用户(软删除)")
@PreAuthorize("hasAuthority('user:delete')")
public ApiResponse<Void> deleteUser(
        @Parameter(description = "用户ID") @PathVariable Long id) {
    userService.deleteUser(id);
    return ApiResponse.success("删除成功", null);
}

@PutMapping("/change-password")
@Operation(summary = "修改密码", description = "修改当前登录用户的密码")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
    userService.changePassword(request);
    return ApiResponse.success("密码修改成功", null);
}

@PutMapping("/{id}/roles")
@Operation(summary = "分配角色", description = "为用户分配角色")
@PreAuthorize("hasAuthority('user:assign-role')")
public ApiResponse<Void> assignRoles(
        @Parameter(description = "用户ID") @PathVariable Long id,
        @RequestBody Set<Long> roleIds) {
    userService.assignRoles(id, roleIds);
    return ApiResponse.success("角色分配成功", null);
}

@GetMapping("/check/username")
@Operation(summary = "检查用户名是否可用", description = "检查用户名是否已被注册")
public ApiResponse<Boolean> checkUsername(
        @Parameter(description = "用户名") @RequestParam String username) {
    return ApiResponse.success(userService.isUsernameAvailable(username));
}

@GetMapping("/check/email")
@Operation(summary = "检查邮箱是否可用", description = "检查邮箱是否已被注册")
public ApiResponse<Boolean> checkEmail(
        @Parameter(description = "邮箱") @RequestParam String email) {
    return ApiResponse.success(userService.isEmailAvailable(email));
}

}

四、高级特性集成(2分钟)

4.1 安全与JWT配置

Spring Security配置:

java

package com.example.usermanagement.config.security;

import com.example.usermanagement.filter.JwtAuthenticationFilter;

import lombok.RequiredArgsConstructor;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;

import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.http.SessionCreationPolicy;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import org.springframework.web.cors.CorsConfiguration;

import org.springframework.web.cors.CorsConfigurationSource;

import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration

@EnableWebSecurity

@EnableMethodSecurity(prePostEnabled = true)

@RequiredArgsConstructor

public class SecurityConfig {

复制代码
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // 禁用CSRF(因为使用JWT,无状态)
        .csrf().disable()
        
        // 配置CORS
        .cors().configurationSource(corsConfigurationSource())
        .and()
        
        // 异常处理
        .exceptionHandling()
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            .accessDeniedHandler(jwtAccessDeniedHandler)
        .and()
        
        // 会话管理:无状态
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        
        // 授权配置
        .authorizeHttpRequests(authz -> authz
            // 公开接口
            .requestMatchers(
                "/api/v1/users/login",
                "/api/v1/users/register",
                "/api/v1/users/check/**",
                "/swagger-ui/**",
                "/v3/api-docs/**",
                "/api-docs/**",
                "/h2-console/**",
                "/actuator/health"
            ).permitAll()
            
            // 需要认证的接口
            .anyRequest().authenticated()
        )
        
        // 添加JWT过滤器
        .addFilterBefore(jwtAuthenticationFilter, 
                       UsernamePasswordAuthenticationFilter.class)
        
        // 禁用缓存
        .headers().cacheControl();
    
    // 允许H2控制台iframe(仅开发环境)
    http.headers().frameOptions().disable();
    
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);
    configuration.setMaxAge(3600L);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(
        AuthenticationConfiguration authenticationConfiguration) throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
}

}

// JWT工具类

package com.example.usermanagement.util;

import io.jsonwebtoken.*;

import io.jsonwebtoken.security.Keys;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.stereotype.Component;

import java.security.Key;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

import java.util.function.Function;

@Slf4j

@Component

public class JwtTokenUtil {

复制代码
@Value("${security.jwt.secret}")
private String secret;

@Value("${security.jwt.expiration}")
private Long expiration;

private Key getSigningKey() {
    return Keys.hmacShaKeyFor(secret.getBytes());
}

// 从token中获取用户名
public String getUsernameFromToken(String token) {
    return getClaimFromToken(token, Claims::getSubject);
}

// 从token中获取过期时间
public Date getExpirationDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getExpiration);
}

// 从token中获取指定claim
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = getAllClaimsFromToken(token);
    return claimsResolver.apply(claims);
}

// 从token中获取所有claims
private Claims getAllClaimsFromToken(String token) {
    return Jwts.parserBuilder()
            .setSigningKey(getSigningKey())
            .build()
            .parseClaimsJws(token)
            .getBody();
}

// 检查token是否过期
private Boolean isTokenExpired(String token) {
    final Date expiration = getExpirationDateFromToken(token);
    return expiration.before(new Date());
}

// 生成token
public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return doGenerateToken(claims, userDetails.getUsername());
}

// 生成token的具体实现
private String doGenerateToken(Map<String, Object> claims, String subject) {
    final Date createdDate = new Date();
    final Date expirationDate = new Date(createdDate.getTime() + expiration);
    
    return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(createdDate)
            .setExpiration(expirationDate)
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
}

// 验证token
public Boolean validateToken(String token, UserDetails userDetails) {
    final String username = getUsernameFromToken(token);
    return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

// 刷新token
public String refreshToken(String token) {
    final Date createdDate = new Date();
    final Date expirationDate = new Date(createdDate.getTime() + expiration);
    
    final Claims claims = getAllClaimsFromToken(token);
    claims.setIssuedAt(createdDate);
    claims.setExpiration(expirationDate);
    
    return Jwts.builder()
            .setClaims(claims)
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
}

public Long getExpiration() {
    return expiration;
}

}

// JWT认证过滤器

package com.example.usermanagement.filter;

import com.example.usermanagement.util.JwtTokenUtil;

import jakarta.servlet.FilterChain;

import jakarta.servlet.ServletException;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

import org.springframework.stereotype.Component;

import org.springframework.util.StringUtils;

import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j

@Component

@RequiredArgsConstructor

public class JwtAuthenticationFilter extends OncePerRequestFilter {

复制代码
private final JwtTokenUtil jwtTokenUtil;
private final UserDetailsService userDetailsService;

@Value("${security.jwt.header}")
private String header;

@Value("${security.jwt.token-prefix}")
private String tokenPrefix;

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain) throws ServletException, IOException {
    
    try {
        String jwt = getJwtFromRequest(request);
        
        if (StringUtils.hasText(jwt) && jwtTokenUtil.validateToken(jwt, 
            userDetailsService.loadUserByUsername(jwtTokenUtil.getUsernameFromToken(jwt)))) {
            
            String username = jwtTokenUtil.getUsernameFromToken(jwt);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            log.debug("设置用户认证: username={}", username);
        }
    } catch (Exception e) {
        log.error("无法设置用户认证: ", e);
    }
    
    filterChain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
    String bearerToken = request.getHeader(header);
    if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(tokenPrefix)) {
        return bearerToken.substring(tokenPrefix.length());
    }
    return null;
}

}

4.2 统一异常处理

java

package com.example.usermanagement.exception;

import lombok.Getter;

import org.springframework.http.HttpStatus;

@Getter

public class BusinessException extends RuntimeException {

复制代码
private final Integer code;
private final HttpStatus httpStatus;

public BusinessException(String message) {
    super(message);
    this.code = 400;
    this.httpStatus = HttpStatus.BAD_REQUEST;
}

public BusinessException(String message, Integer code) {
    super(message);
    this.code = code;
    this.httpStatus = HttpStatus.BAD_REQUEST;
}

public BusinessException(String message, HttpStatus httpStatus) {
    super(message);
    this.code = httpStatus.value();
    this.httpStatus = httpStatus;
}

}

// 全局异常处理器

package com.example.usermanagement.handler;

import com.example.usermanagement.dto.ApiResponse;

import com.example.usermanagement.exception.BusinessException;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.validation.ConstraintViolationException;

import lombok.extern.slf4j.Slf4j;

import org.springframework.dao.DataIntegrityViolationException;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.security.access.AccessDeniedException;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.validation.BindException;

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 org.springframework.web.context.request.WebRequest;

import java.time.LocalDateTime;

import java.util.HashMap;

import java.util.Map;

@Slf4j

@RestControllerAdvice

public class GlobalExceptionHandler {

复制代码
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<?>> handleBusinessException(
        BusinessException ex, HttpServletRequest request) {
    
    log.warn("业务异常: {} - {}", request.getRequestURI(), ex.getMessage());
    
    ApiResponse<?> response = ApiResponse.error(ex.getCode(), ex.getMessage());
    return ResponseEntity.status(ex.getHttpStatus()).body(response);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<?>> handleValidationException(
        MethodArgumentNotValidException ex, HttpServletRequest request) {
    
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getAllErrors().forEach(error -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });
    
    log.warn("参数验证失败: {} - {}", request.getRequestURI(), errors);
    
    ApiResponse<?> response = ApiResponse.error(400, "参数验证失败");
    response.setData(errors);
    return ResponseEntity.badRequest().body(response);
}

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<?>> handleConstraintViolationException(
        ConstraintViolationException ex, HttpServletRequest request) {
    
    Map<String, String> errors = new HashMap<>();
    ex.getConstraintViolations().forEach(violation -> {
        String fieldName = violation.getPropertyPath().toString();
        String errorMessage = violation.getMessage();
        errors.put(fieldName, errorMessage);
    });
    
    log.warn("约束违反: {} - {}", request.getRequestURI(), errors);
    
    ApiResponse<?> response = ApiResponse.error(400, "参数验证失败");
    response.setData(errors);
    return ResponseEntity.badRequest().body(response);
}

@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiResponse<?>> handleBadCredentialsException(
        BadCredentialsException ex, HttpServletRequest request) {
    
    log.warn("认证失败: {} - {}", request.getRequestURI(), ex.getMessage());
    
    ApiResponse<?> response = ApiResponse.error(401, "用户名或密码错误");
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<?>> handleAccessDeniedException(
        AccessDeniedException ex, HttpServletRequest request) {
    
    log.warn("访问被拒绝: {} - {}", request.getRequestURI(), ex.getMessage());
    
    ApiResponse<?> response = ApiResponse.error(403, "没有访问权限");
    return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ApiResponse<?>> handleDataIntegrityViolationException(
        DataIntegrityViolationException ex, HttpServletRequest request) {
    
    log.error("数据完整性违反: {} - {}", request.getRequestURI(), ex.getMessage(), ex);
    
    String message = "数据操作失败";
    if (ex.getMessage().contains("Duplicate entry")) {
        message = "数据已存在";
    } else if (ex.getMessage().contains("foreign key constraint")) {
        message = "存在关联数据,无法删除";
    }
    
    ApiResponse<?> response = ApiResponse.error(400, message);
    return ResponseEntity.badRequest().body(response);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleGlobalException(
        Exception ex, HttpServletRequest request) {
    
    log.error("系统异常: {} - {}", request.getRequestURI(), ex.getMessage(), ex);
    
    ApiResponse<?> response = ApiResponse.error(500, "系统内部错误");
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

}

4.3 数据初始化(30秒)

java

package com.example.usermanagement.config;

import com.example.usermanagement.entity.Permission;

import com.example.usermanagement.entity.Role;

import com.example.usermanagement.entity.User;

import com.example.usermanagement.enums.PermissionType;

import com.example.usermanagement.enums.UserStatus;

import com.example.usermanagement.repository.PermissionRepository;

import com.example.usermanagement.repository.RoleRepository;

import com.example.usermanagement.repository.UserRepository;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.boot.CommandLineRunner;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;

import java.util.HashSet;

@Slf4j

@Configuration

@RequiredArgsConstructor

public class DataInitializer {

复制代码
@Bean
@Transactional
public CommandLineRunner initData(
        UserRepository userRepository,
        RoleRepository roleRepository,
        PermissionRepository permissionRepository,
        PasswordEncoder passwordEncoder) {
    
    return args -> {
        log.info("开始初始化数据...");
        
        // 1. 初始化权限
        Permission userRead = createPermissionIfNotExists("用户查看", "user:read", 
            "查看用户信息", PermissionType.API, "/api/v1/users/**", null, permissionRepository);
        
        Permission userCreate = createPermissionIfNotExists("用户创建", "user:create", 
            "创建用户", PermissionType.API, "/api/v1/users/register", null, permissionRepository);
        
        Permission userUpdate = createPermissionIfNotExists("用户更新", "user:update", 
            "更新用户信息", PermissionType.API, "/api/v1/users/**", null, permissionRepository);
        
        Permission userDelete = createPermissionIfNotExists("用户删除", "user:delete", 
            "删除用户", PermissionType.API, "/api/v1/users/**", null, permissionRepository);
        
        Permission roleRead = createPermissionIfNotExists("角色查看", "role:read", 
            "查看角色信息", PermissionType.API, "/api/v1/roles/**", null, permissionRepository);
        
        Permission roleManage = createPermissionIfNotExists("角色管理", "role:manage", 
            "管理角色", PermissionType.API, "/api/v1/roles/**", null, permissionRepository);
        
        // 2. 初始化角色
        Role adminRole = createRoleIfNotExists("管理员", "ROLE_ADMIN", 
            "系统管理员", roleRepository);
        
        Role userRole = createRoleIfNotExists("普通用户", "ROLE_USER", 
            "普通用户", roleRepository);
        
        // 3. 为角色分配权限
        adminRole.setPermissions(new HashSet<>(Arrays.asList(
            userRead, userCreate, userUpdate, userDelete, roleRead, roleManage
        )));
        roleRepository.save(adminRole);
        
        userRole.setPermissions(new HashSet<>(Arrays.asList(
            userRead
        )));
        roleRepository.save(userRole);
        
        // 4. 初始化管理员用户
        if (!userRepository.existsByUsername("admin")) {
            User admin = new User();
            admin.setUsername("admin");
            admin.setPassword(passwordEncoder.encode("admin123"));
            admin.setEmail("admin@example.com");
            admin.setPhone("13800138000");
            admin.setStatus(UserStatus.ACTIVE);
            admin.setRoles(new HashSet<>(Arrays.asList(adminRole)));
            
            userRepository.save(admin);
            log.info("创建管理员用户: admin/admin123");
        }
        
        // 5. 初始化普通用户
        if (!userRepository.existsByUsername("user")) {
            User user = new User();
            user.setUsername("user");
            user.setPassword(passwordEncoder.encode("user123"));
            user.setEmail("user@example.com");
            user.setPhone("13800138001");
            user.setStatus(UserStatus.ACTIVE);
            user.setRoles(new HashSet<>(Arrays.asList(userRole)));
            
            userRepository.save(user);
            log.info("创建普通用户: user/user123");
        }
        
        log.info("数据初始化完成!");
        log.info("管理员账号: admin/admin123");
        log.info("普通用户账号: user/user123");
    };
}

private Permission createPermissionIfNotExists(String name, String code, 
        String description, PermissionType type, String url, Long parentId,
        PermissionRepository repository) {
    
    return repository.findByCode(code)
            .orElseGet(() -> {
                Permission permission = new Permission();
                permission.setName(name);
                permission.setCode(code);
                permission.setDescription(description);
                permission.setType(type);
                permission.setUrl(url);
                permission.setParentId(parentId);
                permission.setSortOrder(0);
                permission.setVisible(true);
                return repository.save(permission);
            });
}

private Role createRoleIfNotExists(String name, String code, 
        String description, RoleRepository repository) {
    
    return repository.findByCode(code)
            .orElseGet(() -> {
                Role role = new Role();
                role.setName(name);
                role.setCode(code);
                role.setDescription(description);
                role.setSortOrder(0);
                return repository.save(role);
            });
}

}

五、测试与部署(1分钟)

5.1 单元测试

java

package com.example.usermanagement.service;

import com.example.usermanagement.dto.user.UserRegisterRequest;

import com.example.usermanagement.entity.User;

import com.example.usermanagement.enums.UserStatus;

import com.example.usermanagement.repository.UserRepository;

import org.junit.jupiter.api.BeforeEach;

import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;

import org.mockito.InjectMocks;

import org.mockito.Mock;

import org.mockito.junit.jupiter.MockitoExtension;

import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.jupiter.api.Assertions.;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.
;

@ExtendWith(MockitoExtension.class)

class UserServiceTest {

复制代码
@Mock
private UserRepository userRepository;

@Mock
private PasswordEncoder passwordEncoder;

@InjectMocks
private UserServiceImpl userService;

private UserRegisterRequest registerRequest;

@BeforeEach
void setUp() {
    registerRequest = new UserRegisterRequest();
    registerRequest.setUsername("testuser");
    registerRequest.setPassword("password123");
    registerRequest.setEmail("test@example.com");
    registerRequest.setPhone("13800138000");
}

@Test
void testRegister_Success() {
    // 模拟Repository行为
    when(userRepository.existsByUsername("testuser")).thenReturn(false);
    when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
    when(userRepository.existsByPhone("13800138000")).thenReturn(false);
    when(passwordEncoder.encode("password123")).thenReturn("encodedPassword");
    when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
        User user = invocation.getArgument(0);
        user.setId(1L);
        return user;
    });
    
    // 执行测试
    User user = userService.register(registerRequest);
    
    // 验证结果
    assertNotNull(user);
    assertEquals(1L, user.getId());
    assertEquals("testuser", user.getUsername());
    assertEquals("encodedPassword", user.getPassword());
    assertEquals("test@example.com", user.getEmail());
    assertEquals("13800138000", user.getPhone());
    assertEquals(UserStatus.ACTIVE, user.getStatus());
    
    // 验证方法调用
    verify(userRepository, times(1)).existsByUsername("testuser");
    verify(userRepository, times(1)).existsByEmail("test@example.com");
    verify(userRepository, times(1)).existsByPhone("13800138000");
    verify(passwordEncoder, times(1)).encode("password123");
    verify(userRepository, times(1)).save(any(User.class));
}

@Test
void testRegister_UsernameAlreadyExists() {
    // 模拟用户名已存在
    when(userRepository.existsByUsername("testuser")).thenReturn(true);
    
    // 执行测试并验证异常
    Exception exception = assertThrows(BusinessException.class, () -> {
        userService.register(registerRequest);
    });
    
    assertEquals("用户名已存在", exception.getMessage());
    
    // 验证方法调用
    verify(userRepository, times(1)).existsByUsername("testuser");
    verify(userRepository, never()).existsByEmail(anyString());
    verify(userRepository, never()).existsByPhone(anyString());
    verify(userRepository, never()).save(any(User.class));
}

}

// 集成测试

package com.example.usermanagement.integration;

import com.example.usermanagement.dto.user.UserLoginRequest;

import com.example.usermanagement.dto.user.UserRegisterRequest;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.http.MediaType;

import org.springframework.test.context.ActiveProfiles;

import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest

@AutoConfigureMockMvc

@ActiveProfiles("test")

class UserControllerIntegrationTest {

复制代码
@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@Test
void testRegisterAndLogin() throws Exception {
    // 1. 注册新用户
    UserRegisterRequest registerRequest = new UserRegisterRequest();
    registerRequest.setUsername("integrationuser");
    registerRequest.setPassword("password123");
    registerRequest.setEmail("integration@example.com");
    
    mockMvc.perform(post("/api/v1/users/register")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(registerRequest)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.message").value("注册成功"))
            .andExpect(jsonPath("$.data.username").value("integrationuser"));
    
    // 2. 登录测试
    UserLoginRequest loginRequest = new UserLoginRequest();
    loginRequest.setUsernameOrEmail("integrationuser");
    loginRequest.setPassword("password123");
    
    mockMvc.perform(post("/api/v1/users/login")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(loginRequest)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.message").value("登录成功"))
            .andExpect(jsonPath("$.data.accessToken").exists())
            .andExpect(jsonPath("$.data.userId").exists());
}

}

5.2 API测试脚本

bash

#!/bin/bash

用户管理系统API测试脚本

BASE_URL="http://localhost:8080/api"

echo "=== 用户管理系统API测试 ==="

echo ""

1. 检查用户名是否可用

echo "1. 检查用户名是否可用:"

curl -X GET "$BASE_URL/v1/users/check/username?username=testuser"

echo ""

echo ""

2. 用户注册

echo "2. 用户注册:"

curl -X POST "$BASE_URL/v1/users/register"

-H "Content-Type: application/json"

-d '{

"username": "testuser",

"password": "password123",

"email": "test@example.com",

"phone": "13800138000"

}'

echo ""

echo ""

3. 用户登录

echo "3. 用户登录:"

LOGIN_RESPONSE= ( c u r l − s − X P O S T " (curl -s -X POST " (curl−s−XPOST"BASE_URL/v1/users/login"

-H "Content-Type: application/json"

-d '{

"usernameOrEmail": "testuser",

"password": "password123"

}')

echo "$LOGIN_RESPONSE"

提取token

TOKEN= ( e c h o " (echo " (echo"LOGIN_RESPONSE" | grep -o '"accessToken":"[^"]*' | cut -d'"' -f4)

echo "Token: $TOKEN"

echo ""

4. 获取当前用户信息

echo "4. 获取当前用户信息:"

curl -X GET "$BASE_URL/v1/users/me"

-H "Authorization: Bearer $TOKEN"

echo ""

echo ""

5. 分页查询用户

echo "5. 分页查询用户:"

curl -X GET "$BASE_URL/v1/users?page=0&size=10"

-H "Authorization: Bearer $TOKEN"

echo ""

5.3 Docker部署配置

dockerfile

Dockerfile

FROM openjdk:17-jdk-slim as builder

WORKDIR /app

复制Maven配置文件

COPY pom.xml .

COPY src ./src

下载Maven wrapper

RUN apt-get update && apt-get install -y maven

构建应用

RUN mvn clean package -DskipTests

运行时镜像

FROM openjdk:17-jdk-slim

WORKDIR /app

创建非root用户

RUN groupadd -r spring && useradd -r -g spring spring

USER spring:spring

从构建阶段复制jar文件

COPY --from=builder /app/target/*.jar app.jar

设置JVM参数

ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError"

暴露端口

EXPOSE 8080

启动应用

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

yaml

docker-compose.yml

version: '3.8'

services:

MySQL数据库

mysql:

image: mysql:8.0

container_name: user-management-mysql

environment:

MYSQL_ROOT_PASSWORD: root123

MYSQL_DATABASE: user_management

MYSQL_USER: admin

MYSQL_PASSWORD: admin123

ports:

  • "3306:3306"

volumes:

  • mysql_data:/var/lib/mysql

networks:

  • user-management-network

healthcheck:

test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]

timeout: 20s

retries: 10

Redis缓存

redis:

image: redis:7-alpine

container_name: user-management-redis

ports:

  • "6379:6379"

volumes:

  • redis_data:/data

networks:

  • user-management-network

command: redis-server --appendonly yes

用户管理应用

app:

build: .

container_name: user-management-app

environment:

SPRING_PROFILES_ACTIVE: prod

SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/user_management?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC

SPRING_DATASOURCE_USERNAME: admin

SPRING_DATASOURCE_PASSWORD: admin123

SPRING_REDIS_HOST: redis

SPRING_REDIS_PORT: 6379

ports:

  • "8080:8080"

depends_on:

mysql:

condition: service_healthy

redis:

condition: service_started

networks:

  • user-management-network

restart: unless-stopped

networks:

user-management-network:

driver: bridge

volumes:

mysql_data:

redis_data:

六、项目总结与扩展

6.1 10分钟搭建完成清单

text

✅ 已完成的核心功能:

  1. 用户注册与登录(JWT认证)
  2. 用户信息管理(CRUD)
  3. 角色权限管理(RBAC)
  4. 统一异常处理
  5. 数据验证
  6. 分页查询
  7. 缓存支持
  8. 日志记录
  9. API文档(Swagger)
  10. 单元测试
  11. Docker部署配置

✅ 已实现的企业级特性:

  • 分层架构清晰
  • 事务管理
  • 密码加密
  • 防重复提交
  • 接口限流(可扩展)
  • 数据审计(创建时间、更新时间)
  • 统一响应格式
  • 跨域支持
    6.2 项目扩展建议
    扩展1:添加更多功能模块

java

// 1. 部门管理模块

@Entity

public class Department {

private Long id;

private String name;

private String code;

private Long parentId;

private List users;

}

// 2. 操作日志模块

@Entity

public class OperationLog {

private Long id;

private String username;

private String operation;

private String method;

private String params;

private String ip;

private Long duration;

private LocalDateTime createTime;

}

// 3. 文件上传模块

@Service

public class FileService {

public String uploadAvatar(MultipartFile file) { ... }

public void deleteFile(String filePath) { ... }

}

扩展2:性能优化

yaml

添加二级缓存

spring:

jpa:

properties:

hibernate:

cache:

use_second_level_cache: true

region:

factory_class: org.hibernate.cache.jcache.JCacheRegionFactory

jcache:

provider: org.ehcache.jsr107.EhcacheCachingProvider

连接池优化

spring:

datasource:

hikari:

maximum-pool-size: 20

minimum-idle: 10

connection-timeout: 30000

idle-timeout: 600000

max-lifetime: 1800000

pool-name: UserManagementPool

扩展3:安全加固

java

// 1. 添加接口限流

@Component

public class RateLimitAspect {

@Around("@annotation(RateLimit)")

public Object limit(ProceedingJoinPoint joinPoint) { ... }

}

// 2. 添加防SQL注入

@Configuration

public class SecurityFilterConfig {

@Bean

public FilterRegistrationBean sqlInjectionFilter() { ... }

}

// 3. 添加XSS防护

@Component

public class XssFilter implements Filter {

@Override

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { ... }

}

6.3 学习收获与下一步

通过本项目你学会了:

✅ SpringBoot项目快速搭建

✅ 企业级项目架构设计

✅ Spring Security + JWT认证

✅ Spring Data JPA数据访问

✅ 统一异常处理

✅ 数据验证与DTO转换

✅ 缓存集成与优化

✅ 单元测试编写

✅ API文档生成

✅ Docker容器化部署

下一步学习建议:

text

路线图:

阶段1:基础巩固(已完成)

✓ SpringBoot用户管理系统

阶段2:技能提升

→ 学习Spring Cloud微服务

→ 学习消息队列(RabbitMQ/Kafka)

→ 学习搜索引擎(Elasticsearch)

阶段3:架构进阶

→ 学习分布式事务

→ 学习服务网格(Istio)

→ 学习云原生技术(K8s)

阶段4:实战项目

→ 电商系统

→ 在线教育平台

→ 智能客服系统

七、资源下载与互动

7.1 完整项目资源

GitHub仓库:

text

项目地址:https://github.com/yourusername/user-management-system

包含:

├── 完整源代码

├── SQL初始化脚本

├── Postman接口测试集合

├── Docker部署配置

└── 项目文档

快速启动命令:

bash

克隆项目

git clone https://github.com/yourusername/user-management-system.git

进入项目目录

cd user-management-system

运行项目

mvn spring-boot:run

或者使用Docker

docker-compose up -d

7.2 互动与答疑

常见问题解答:

text

Q1: 为什么我的项目启动失败?

A: 检查Java版本(需要Java 17+),检查端口是否被占用,检查依赖下载是否完整。

Q2: 如何连接MySQL而不是H2?

A: 修改application.yml中的数据库配置,将H2改为MySQL。

Q3: 如何修改JWT密钥和过期时间?

A: 修改application.yml中的security.jwt配置。

Q4: 如何添加新的权限?

A: 在DataInitializer中添加新的权限定义,并为角色分配权限。

学习交流群:

text

扫描二维码加入SpringBoot学习交流群:

二维码图片

群内提供:

  1. 项目答疑
  2. 学习资料分享
  3. 技术交流
  4. 面试指导
    7.3 下期预告
    《SpringBoot实战二:电商系统从0到1》

text

你将学会:

  1. 商品管理模块设计
  2. 购物车与订单系统
  3. 支付接口集成
  4. 库存管理
  5. 秒杀系统设计
  6. 分布式锁应用
  7. 性能压测与优化
  8. 微服务拆分

预计发布时间:下周

记得关注我,不错过更新!

结语:从学习者到实践者

恭喜你!在短短10分钟内(实际上可能需要1-2小时仔细实践),你已经完成了一个企业级的用户管理系统。这不是一个玩具项目,而是真正可以在生产环境中使用的系统。

记住关键点:

不要只复制代码,要理解每一行代码的作用

动手实践是最好的学习方式

遇到问题是学习的机会

持续学习,技术永远在更新

你的收获:

✅ 一个完整的SpringBoot项目经验

✅ 企业级项目架构的理解

✅ 面试中可以展示的实际项目

✅ 继续深入学习的基础

下一步行动:

运行项目,确保一切正常

尝试修改和扩展功能

部署到云服务器

在GitHub上Star项目

在评论区分享你的学习心得

最后的话:

编程之路没有捷径,但有正确的方法。

从今天起,你不再只是SpringBoot的学习者,

而是拥有实战经验的开发者。

保持热情,持续学习,未来可期!

特别福利:

关注我并私信"用户管理系统",获取:

本文完整Markdown版本

项目PPT演示文稿

面试常见问题及答案

扩展功能实现代码

今日打卡任务:

👍 点赞本文,让更多人看到

💾 收藏本文,方便随时查阅

🚀 运行项目,在评论区分享截图

👨‍💻 关注我,获取更多实战教程

在评论区告诉我:

你在实现过程中遇到了什么问题?

你还想学习什么类型的项目?

对本文有什么建议?

让我们一起进步,成为更好的开发者! 💪🔥

相关推荐
消失的旧时光-19431 小时前
第六课 · 6.1 从 JDBC 到 MyBatis:SQL 工程化是如何发生的?
java·sql·mybatis
拽着尾巴的鱼儿2 小时前
Springboot 缓存@Cacheable 使用
spring boot·后端·缓存
Jaxson Lin2 小时前
Java编程进阶:线程基础与实现方式全解析
java·开发语言
夜喵YM2 小时前
基于 Spire.XLS.Free for Java 实现无水印 Excel 转 PDF
java·pdf·excel
茶本无香2 小时前
设计模式之五—门面模式:简化复杂系统的统一接口
java·设计模式
bugcome_com2 小时前
脑力的“报废”是静悄悄的
后端·程序人生
她说可以呀2 小时前
网络基础初识
java·网络·java-ee
爱吃肉的鹏2 小时前
使用Flask在本地调用树莓派摄像头
人工智能·后端·python·flask·树莓派
没有bug.的程序员2 小时前
Java锁优化:从synchronized到CAS的演进与实战选择
java·开发语言·多线程·并发·cas·synchronized·