跳到主要内容

用户管理详解

本文详细讲解系统模块的用户管理功能,包括用户注册、登录、信息更新等核心流程。

📋 目录导航


📊 数据模型

SysUser 实体类

@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.ASSIGN_ID) // 雪花算法生成 ID
private Long id;

private String username; // 用户名 (唯一)
private String nickname; // 昵称
private String password; // 加密后的密码
private String email; // 邮箱 (唯一)
private String avatar; // 头像 URL
private Integer status; // 状态: 0=禁用, 1=启用

// 公共字段 (由 BaseEntity 提供)
private Integer version; // 乐观锁版本号
private Long createBy; // 创建人 ID
private LocalDateTime createTime; // 创建时间
private Long updateBy; // 更新人 ID
private LocalDateTime updateTime; // 更新时间
private Integer isDeleted; // 逻辑删除: 0=未删除, 1=已删除
}

数据库表结构

CREATE TABLE `sys_user` (
`id` BIGINT NOT NULL COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
`password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
`status` TINYINT DEFAULT 1 COMMENT '状态: 0=禁用, 1=启用',
`version` INT DEFAULT 0 COMMENT '乐观锁版本',
`create_by` BIGINT DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_by` BIGINT DEFAULT NULL,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_email` (`email`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

重要字段说明

字段说明新手注意
id主键使用雪花算法自动生成,无需手动设置
username用户名唯一约束,登录时使用
password密码永远不保存明文,必须 BCrypt 加密
email邮箱唯一约束,可用于登录和找回密码
status状态0=禁用(无法登录),1=启用
is_deleted逻辑删除0=正常,1=已删除(软删除,不真正删除数据)

🔐 用户注册流程

完整流程图

sequenceDiagram
participant Client as 前端
participant Controller as AuthController
participant Service as UserServiceImpl
participant Mapper as UserMapper
participant DB as MySQL

Client->>Controller: POST /api/v1/auth/register
Note over Client: {username, password, email}

Controller->>Service: register(RegisterDTO)

Service->>Service: 1. 检查用户名是否存在
Service->>Mapper: selectOne(username)
Mapper->>DB: SELECT * WHERE username=?
DB-->>Mapper: null (不存在)
Mapper-->>Service: null

Service->>Service: 2. 检查邮箱是否存在
Service->>Mapper: selectOne(email)
Mapper->>DB: SELECT * WHERE email=?
DB-->>Mapper: null
Mapper-->>Service: null

Service->>Service: 3. 密码加密 BCrypt
Note over Service: $2a$10$encrypted...

Service->>Service: 4. 查询默认角色 (USER)
Service->>Mapper: roleMapper.selectOne

Service->>Mapper: 5. 插入用户
Mapper->>DB: INSERT INTO sys_user
DB-->>Mapper: OK (id=100)

Service->>Mapper: 6. 分配默认角色
Mapper->>DB: INSERT INTO sys_user_role

Service-->>Controller: UserVO (含用户信息)
Controller-->>Client: Result<UserVO>
Note over Client: {code:0, data:{id:100,...}}

代码实现详解

@Override
@Transactional // ⭐ 事务注解:确保所有操作成功或全部回滚
public UserVO register(RegisterDTO registerDTO) {
// ===== 1. 检查用户名是否已存在 =====
SysUser existByUsername = userMapper.selectOne(
new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, registerDTO.getUsername())
);
if (existByUsername != null) {
throw new BusinessException(SystemErrorCode.USER_ALREADY_EXISTS);
}

// ===== 2. 检查邮箱是否已存在 =====
if (registerDTO.getEmail() != null) {
SysUser existByEmail = userMapper.selectOne(
new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getEmail, registerDTO.getEmail())
);
if (existByEmail != null) {
throw new BusinessException(SystemErrorCode.EMAIL_ALREADY_EXISTS);
}
}

// ===== 3. 创建用户实体 =====
SysUser user = new SysUser();
user.setUsername(registerDTO.getUsername());
user.setNickname(registerDTO.getNickname());
user.setEmail(registerDTO.getEmail());

// ⭐ 密码加密(永远不保存明文密码)
String encodedPassword = passwordEncoder.encode(registerDTO.getPassword());
user.setPassword(encodedPassword);

user.setStatus(1); // 默认启用

// ===== 4. 保存用户到数据库 =====
userMapper.insert(user); // MyBatis-Plus 自动生成 ID

// ===== 5. 分配默认角色 =====
SysRole defaultRole = roleMapper.selectOne(
new LambdaQueryWrapper<SysRole>()
.eq(SysRole::getRoleKey, RoleConstants.DEFAULT_USER_ROLE)
);
if (defaultRole != null) {
userMapper.assignRole(user.getId(), defaultRole.getId());
}

// ===== 6. 转换为 VO 返回 =====
return userConverter.entityToVo(user);
}

关键点解析

  1. @Transactional - 事务保护

    • 如果任何步骤失败,所有操作都会回滚
    • 避免出现"用户创建了但角色没分配"的情况
  2. 密码加密 - passwordEncoder.encode()

    • 使用 BCrypt 算法
    • 每次加密结果都不同(即使密码相同)
    • 示例:password123$2a$10$N9qo8...
  3. 雪花算法 ID - @TableId(type = IdType.ASSIGN_ID)

    • MyBatis-Plus 自动生成分布式唯一 ID
    • 无需手动设置,insert 后自动回填到 user.getId()

🔑 登录认证流程

完整流程图

sequenceDiagram
participant Client as 前端
participant Controller as AuthController
participant Service as UserServiceImpl
participant Security as Spring Security
participant JWT as JwtTokenProvider

Client->>Controller: POST /api/v1/auth/login
Note over Client: {username, password}

Controller->>Service: login(LoginDTO)

Service->>Service: 1. 根据用户名查询用户
Note over Service: SELECT * WHERE username=?

Service->>Service: 2. 检查用户状态
alt 用户不存在
Service-->>Controller: 抛出异常:用户不存在
else 用户已禁用
Service-->>Controller: 抛出异常:用户已被禁用
end

Service->>Security: 3. 验证密码
Security->>Security: matches(raw, encoded)
Note over Security: BCrypt 比对

alt 密码错误
Service-->>Controller: 抛出异常:密码错误
end

Service->>Service: 4. 查询用户角色
Note over Service: 从缓存或数据库获取

Service->>JWT: 5. 生成 JWT Token
JWT->>JWT: 构建 Claims
Note over JWT: {userId, username, roles}
JWT-->>Service: 返回 Token

Service-->>Controller: LoginVO {token, user}
Controller-->>Client: Result<LoginVO>
Note over Client: 保存 Token 到 localStorage

代码实现详解

@Override
public LoginVO login(LoginDTO loginDTO) {
// ===== 1. 查询用户 =====
SysUser user = userMapper.selectOne(
new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, loginDTO.getUsername())
);

if (user == null) {
throw new BusinessException(SystemErrorCode.USER_NOT_FOUND);
}

// ===== 2. 检查用户状态 =====
if (user.getStatus() == 0) {
throw new BusinessException(SystemErrorCode.USER_DISABLED);
}

// ===== 3. 验证密码 =====
boolean matches = passwordEncoder.matches(
loginDTO.getPassword(), // 用户输入的明文密码
user.getPassword() // 数据库中的加密密码
);

if (!matches) {
throw new BusinessException(SystemErrorCode.INVALID_CREDENTIALS);
}

// ===== 4. 获取用户角色 =====
List<String> roleKeys = getUserRoleKeys(user.getId()); // 缓存优化

// 转换为 Spring Security 权限格式
List<SimpleGrantedAuthority> authorities = roleKeys.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());

// ===== 5. 构建 UserDetails =====
UserDetails userDetails = User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.build();

// ===== 6. 生成 JWT Token =====
String token = jwtTokenProvider.generateToken(userDetails, user.getId());

// ===== 7. 构建返回对象 =====
LoginVO loginVO = new LoginVO();
loginVO.setToken(token);

UserVO userVO = userConverter.entityToVo(user);
userVO.setRoles(roleKeys);
loginVO.setUser(userVO);

return loginVO;
}

JWT Token 示例

eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6MSw...
├─ Header: {"alg":"HS384"}
├─ Payload: {"sub":"admin","userId":1,"roles":["ADMIN"],"exp":1702345678}
└─ Signature: HMACSHA384(base64(header) + "." + base64(payload), secret)

客户端使用方式

// 保存 Token
localStorage.setItem('token', response.data.token);

// 后续请求携带 Token
fetch('/api/v1/users/me', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});

👤 用户管理 API

1. 获取当前用户信息

GET /api/v1/users/me
Authorization: Bearer {token}

响应示例

{
"code": 0,
"data": {
"id": 1,
"username": "admin",
"nickname": "管理员",
"email": "admin@example.com",
"avatar": "https://example.com/avatar.jpg",
"status": 1,
"roles": ["ADMIN", "USER"]
}
}

代码实现

@GetMapping("/me")
public Result<UserVO> getCurrentUser() {
// SecurityUtils 从 JWT Token 中提取用户 ID
Long userId = SecurityUtils.getCurrentUserId();

Optional<UserVO> userVO = userService.getVoById(userId);
return userVO.map(Result::success)
.orElseGet(() -> Result.error(404, "用户不存在"));
}

2. 更新个人资料

PUT /api/v1/users/me
Authorization: Bearer {token}
Content-Type: application/json

{
"nickname": "新昵称",
"email": "new@example.com",
"avatar": "https://new-avatar.jpg"
}

关键点

  • ✅ 用户只能更新自己的信息
  • ✅ 用户名和密码不能通过此接口修改(需要专门的接口)
  • ✅ 更新后会自动清除角色缓存

3. 管理员操作(需要 ADMIN 权限)

@PreAuthorize("hasRole('ADMIN')")  // ⭐ 权限注解
@GetMapping("/{id}")
public Result<UserVO> getUserById(@PathVariable Long id) {
// 只有管理员能访问此接口
}

🔒 权限控制

角色管理

// 用户拥有的角色存储在 sys_user_role 表
// 格式:user_id | role_id
// 1 | 1 (用户1拥有角色1)
// 1 | 2 (用户1拥有角色2)

// 角色信息存储在 sys_role 表
// role_id | role_key | role_name
// 1 | ADMIN | 管理员
// 2 | USER | 普通用户

缓存策略

@Cacheable(value = "user:roles", key = "#userId")  // ⭐ 缓存注解
public List<String> getUserRoleKeys(Long userId) {
// 第一次查询数据库,后续从 Redis 缓存读取
// 缓存 30 分钟自动过期
}

@CacheEvict(value = "user:roles", key = "#userId") // ⭐ 清除缓存
public void evictUserRolesCache(Long userId) {
// 角色变更时调用,确保数据一致性
}

🎯 常见问题

Q1: 用户注册后无法登录?

检查清单

  1. 用户 status 是否为 1(启用)
  2. 密码是否正确(注意大小写)
  3. 是否分配了角色

Q2: 密码如何验证?

A: 使用 PasswordEncoder.matches()

// ❌ 错误:直接比对会永远失败
if (user.getPassword().equals(loginDTO.getPassword())) { ... }

// ✅ 正确:使用 BCrypt 验证
if (passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { ... }

Q3: 如何修改密码?

A: 需要专门的修改密码接口(TODO)

public void changePassword(Long userId, String oldPassword, String newPassword) {
// 1. 验证旧密码
// 2. 加密新密码
// 3. 更新数据库
// 4. 使所有旧 Token 失效(可选)
}

📚 相关文档