用户管理详解
本文详细讲解系统模块的用户管理功能,包括用户注册、登录、信息更新等核心流程。
📋 目录导航
📊 数据模型
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);
}
关键点解析:
-
@Transactional- 事务保护- 如果任何步骤失败,所有操作都会回滚
- 避免出现"用户创建了但角色没分配"的情况
-
密码加密 -
passwordEncoder.encode()- 使用 BCrypt 算法
- 每次加密结果都不同(即使密码相同)
- 示例:
password123→$2a$10$N9qo8...
-
雪花算法 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: 用户注册后无法登录?
检查清单:
- 用户
status是否为 1(启用) - 密码是否正确(注意大小写)
- 是否分配了角色
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 失效(可选)
}