Spring Boot 用户管理系统详细技术文档
一、项目整体架构概述
1.1 技术栈
- 框架: Spring Boot
- 持久层: MyBatis
- 缓存: Redis
- 数据库: MySQL(推测)
- 工具: Lombok
- API规范: RESTful
1.2 项目结构
com.example.usermanagement
├── common # 公共类
│ └── Result.java # 统一响应格式
├── config # 配置类
│ ├── CorsConfig.java # 跨域配置
│ └── RedisConfig.java # Redis配置
├── controller # 控制层
│ ├── AuthController.java # 认证控制器
│ ├── CsvController.java # CSV导入导出
│ └── UserController.java # 用户管理
├── entity # 实体类
│ └── User.java # 用户实体
├── mapper # 数据访问层
│ └── UserMapper.java # 用户Mapper
├── service # 业务层
│ ├── UserService.java # 用户业务接口
│ └── impl
│ └── UserServiceImpl.java # 用户业务实现
└── util # 工具类
└── PasswordUtils.java # 密码工具
1.3 架构模式
采用经典的分层架构模式(MVC):
- Controller层: 处理HTTP请求,调用Service层
- Service层: 处理业务逻辑,调用Mapper层
- Mapper层: 数据访问,与数据库交互
- Entity层: 数据实体定义
- Common层: 公共组件
- Config层: 系统配置
二、详细代码解析
2.1 Result.java - 统一响应结果类
package com.example.usermanagement.common;
import lombok.Data;
/**
* 统一响应结果类
* 用于规范化API返回格式
*/
@Data // Lombok注解:自动生成getter/setter/toString/equals/hashCode方法
public class Result<T> { // 泛型类,T表示响应数据的类型
private Integer code; // 响应码:200成功,其他失败
private String message; // 响应消息
private T data; // 响应数据,使用泛型支持任意类型
// 成功响应(带数据)
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>(); // 创建Result实例
result.setCode(200); // 设置成功状态码
result.setMessage("操作成功"); // 设置成功消息
result.setData(data); // 设置返回数据
return result;
}
// 成功响应(不带数据)
public static Result<Void> success() {
return success(null); // 调用带参数的success方法,传入null
}
// 失败响应(支持泛型)
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500); // 设置错误状态码
result.setMessage(message); // 设置错误消息
result.setData(null); // 错误时数据为null
return result;
}
/**
* 注释掉的代码:手动实现getter/setter
* 因为使用了@Data注解,这些方法会自动生成
* 保留这些代码可能是为了展示或备用
*/
}
核心设计理念:
- 统一API响应格式,便于前端处理
- 使用泛型支持不同类型的响应数据
- 使用静态工厂方法简化对象创建
2.2 CorsConfig.java - 跨域配置
package com.example.usermanagement.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置
*/
@Configuration // 标识为Spring配置类,会被Spring容器管理
public class CorsConfig implements WebMvcConfigurer { // 实现WebMvc配置器接口
@Override
public void addCorsMappings(CorsRegistry registry) { // 重写跨域映射配置方法
registry.addMapping("/**") // 对所有路径生效
.allowedOriginPatterns( // 允许的源地址模式
"http://localhost:*", // 允许所有localhost端口
"http://127.0.0.1:*", // 允许所有127.0.0.1端口
"http://192.168.*:*", // 允许所有192.168段的IP(局域网)
"http://10.*:*" // 允许所有10段的IP(内网)
)
// Vue开发服务器地址(注释说明用途)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的HTTP方法
.allowedHeaders("*") // 允许所有请求头
.allowCredentials(true) // 允许携带认证信息(如Cookie)
.maxAge(3600); // 预检请求的缓存时间(秒)
}
}
关键点:
- 解决前后端分离的跨域问题
- 支持开发环境的多种访问方式
- OPTIONS预检请求缓存1小时,减少请求次数
2.3 RedisConfig.java - Redis配置
package com.example.usermanagement.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
* 配置Redis序列化方式,使存储的数据更易读
*/
@Configuration
public class RedisConfig {
@Bean // 声明为Spring Bean,会被Spring容器管理
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory); // 设置连接工厂
// 方案1:使用GenericJackson2JsonRedisSerializer(推荐)
// 这个序列化器会在JSON中包含类型信息,支持反序列化
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// String序列化器,用于key的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key使用String序列化
template.setKeySerializer(stringRedisSerializer);
// hash的key也使用String序列化
template.setHashKeySerializer(stringRedisSerializer);
// value使用Jackson序列化(JSON格式)
template.setValueSerializer(genericJackson2JsonRedisSerializer);
// hash的value使用Jackson序列化
template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
template.afterPropertiesSet(); // 初始化设置生效
return template;
}
/*
* 方案2:自定义ObjectMapper配置(注释掉的备选方案)
* 提供了更细粒度的控制,但通常不需要
*/
}
设计考虑:
- key使用String序列化,便于在Redis客户端查看
- value使用JSON序列化,便于调试和跨语言使用
- 保留了类型信息,支持自动反序列化
2.4 AuthController.java - 认证控制器
package com.example.usermanagement.controller;
import com.example.usermanagement.common.Result;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 认证控制器
* 处理用户登录、登出等认证相关请求
*
* 注意:这是一个简化的示例,实际项目应该使用:
* - Spring Security + JWT进行认证授权
* - 密码应该使用BCrypt等算法加密存储
* - Token应该有过期时间和刷新机制
*/
@RestController // @Controller + @ResponseBody,返回JSON
@RequestMapping("/api/auth") // 基础路径映射
@CrossOrigin // 允许跨域(冗余,已有全局配置)
public class AuthController {
// 创建日志对象,用于记录运行信息
private static final Logger log = LoggerFactory.getLogger(AuthController.class);
@Autowired // 自动注入UserService
private UserService userService;
/**
* 用户登录
* POST /api/auth/login
*
* 请求体示例:
* {
* "username": "admin",
* "password": "123456"
* }
*
* 响应示例:
* {
* "code": 200,
* "message": "操作成功",
* "data": {
* "token": "uuid-token-string",
* "user": { ... },
* "role": "admin"
* }
* }
*/
@PostMapping("/login") // 处理POST请求
public Result<Map<String, Object>> login(@RequestBody Map<String, String> loginData) {
try {
// 从请求体中提取用户名和密码
String username = loginData.get("username");
String password = loginData.get("password");
log.info("用户登录请求:username={}", username);
// 查询用户是否存在
User user = userService.getUserByUsername(username);
if (user == null) {
return Result.<Map<String, Object>>error("用户不存在");
}
// 验证密码(注意:实际项目中应该加密比较)
if (!password.equals(user.getPassword())) {
return Result.<Map<String, Object>>error("密码错误");
}
// 检查用户状态
if (user.getStatus() == 0) {
return Result.<Map<String, Object>>error("账号已被禁用");
}
// 生成token(简单示例,实际项目应使用JWT)
String token = UUID.randomUUID().toString();
// 构建返回数据
Map<String, Object> data = new HashMap<>();
data.put("token", token); // 认证令牌
data.put("user", user); // 用户信息
// 设置用户权限(示例:admin用户名的用户为管理员)
String role = "admin".equals(username) ? "admin" : "user";
data.put("role", role);
return Result.success(data);
} catch (Exception e) {
log.error("登录失败", e);
return Result.<Map<String, Object>>error("登录失败:" + e.getMessage());
}
}
/**
* 退出登录
* POST /api/auth/logout
*
* 实际项目中应该:
* 1. 从请求头获取token
* 2. 验证token有效性
* 3. 从Redis删除token
* 4. 记录登出日志
*/
@PostMapping("/logout")
public Result<Void> logout() {
try {
log.info("用户退出登录");
// 实际项目中应该清除服务端的token等操作
// 这里只是返回成功,前端会清除本地存储的token
return Result.success();
} catch (Exception e) {
log.error("退出登录失败", e);
return Result.error("退出登录失败");
}
}
/**
* 获取当前用户信息
* GET /api/auth/info
*
* 实际项目中应该:
* 1. 从请求头获取token
* 2. 解析token获取用户ID
* 3. 查询并返回用户信息
*
* 这里仅作为示例,需要传入用户ID
*/
@GetMapping("/info")
public Result<Map<String, Object>> getUserInfo(@RequestParam Long userId) {
try {
log.info("获取用户信息:userId={}", userId);
User user = userService.getUserById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 构建返回数据
Map<String, Object> data = new HashMap<>();
data.put("user", user);
data.put("role", "admin".equals(user.getUsername()) ? "admin" : "user");
return Result.success(data);
} catch (Exception e) {
log.error("获取用户信息失败", e);
return Result.error("获取用户信息失败");
}
}
}
2.5 CsvController.java - CSV导入导出控制器
package com.example.usermanagement.controller;
import com.example.usermanagement.common.Result;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
/**
* CSV导入导出控制器
*/
@RestController
@RequestMapping("/api/csv")
@CrossOrigin
public class CsvController {
private static final Logger log = LoggerFactory.getLogger(CsvController.class);
@Autowired
private UserService userService;
/**
* 导出用户数据为CSV
* GET /api/csv/export
*/
@GetMapping("/export")
public void exportUsers(HttpServletResponse response) {
try {
// 设置响应头,告诉浏览器这是一个CSV文件下载
response.setContentType("text/csv;charset=UTF-8");
// 生成文件名:用户数据_20240101.csv
String fileName = URLEncoder.encode("用户数据_" +
new SimpleDateFormat("yyyyMMdd").format(new Date()) + ".csv", "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
// 添加BOM(字节顺序标记)以支持Excel正确识别UTF-8编码
// BOM: 0xEF 0xBB 0xBF
response.getOutputStream().write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});
// 创建CSV写入器
try (PrintWriter writer = new PrintWriter(
new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8))) {
// 写入表头(注意:文件中有乱码,应该是"用户名,密码,邮箱...")
writer.println("ID,用户名,密码,邮箱,手机号,状态,分数,创建时间,更新时间");
// 获取所有用户数据
List<User> users = userService.getAllUsers();
// 写入数据
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (User user : users) {
// 使用printf格式化输出,处理null值
writer.printf("%d,%s,%s,%s,%s,%d,%d,%s,%s%n",
user.getId(),
user.getUsername(),
user.getPassword(),
user.getEmail() != null ? user.getEmail() : "",
user.getPhone() != null ? user.getPhone() : "",
user.getStatus(),
user.getScore() != null ? user.getScore() : 0,
user.getCreateTime() != null ?
dateFormat.format(user.getCreateTime()) : "",
user.getUpdateTime() != null ?
dateFormat.format(user.getUpdateTime()) : ""
);
}
writer.flush(); // 刷新缓冲区,确保数据写入
}
} catch (Exception e) {
log.error("导出CSV失败", e);
}
}
/**
* 导入CSV更新用户数据
* POST /api/csv/import
*/
@PostMapping("/import")
public Result<Map<String, Object>> importUsers(@RequestParam("file") MultipartFile file) {
try {
// 检查文件是否为空
if (file.isEmpty()) {
return Result.<Map<String, Object>>error("请选择文件");
}
List<String> errors = new ArrayList<>(); // 记录错误信息
int successCount = 0; // 成功计数
int errorCount = 0; // 失败计数
// 读取CSV文件
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
// 跳过表头(第一行)和空行
if (lineNumber == 1 || line.trim().isEmpty()) {
continue;
}
try {
// 解析CSV行(简单分割,实际应考虑字段中包含逗号的情况)
String[] fields = line.split(",");
if (fields.length < 7) {
errors.add("第" + lineNumber + "行:字段不足");
errorCount++;
continue;
}
// 创建用户对象
User user = new User();
user.setUsername(fields[1].trim());
user.setPassword(fields[2].trim());
user.setEmail(fields[3].trim());
user.setPhone(fields[4].trim());
user.setStatus(Integer.parseInt(fields[5].trim()));
user.setScore(Integer.parseInt(fields[6].trim()));
// 检查是更新还是新增
User existUser = userService.getUserByUsername(user.getUsername());
if (existUser != null) {
// 更新现有用户
user.setId(existUser.getId());
userService.updateUser(user);
} else {
// 创建新用户
userService.createUser(user);
}
successCount++;
} catch (Exception e) {
errors.add("第" + lineNumber + "行:" + e.getMessage());
errorCount++;
}
}
}
// 构建返回结果
Map<String, Object> result = new HashMap<>();
result.put("successCount", successCount);
result.put("errorCount", errorCount);
result.put("errors", errors);
return Result.success(result);
} catch (Exception e) {
log.error("导入CSV失败", e);
return Result.<Map<String, Object>>error("导入失败:" + e.getMessage());
}
}
/**
* 下载CSV模板
* GET /api/csv/template
*
* 提供一个CSV模板供用户下载参考
*/
@GetMapping("/template")
public void downloadTemplate(HttpServletResponse response) {
try {
log.info("下载CSV模板");
// 设置响应头
response.setContentType("text/csv;charset=UTF-8");
String fileName = URLEncoder.encode("用户导入模板.csv", "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
// 添加BOM
response.getOutputStream().write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});
// 写入模板内容
try (PrintWriter writer = new PrintWriter(
new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8))) {
// 表头
writer.println("ID,用户名,密码,邮箱,手机号,状态,分数,创建时间,更新时间");
// 示例数据
writer.println("1,zhangsan,123456,zhangsan@example.com,13800138001,1,85,2024-01-01 10:00:00,2024-01-01 10:00:00");
writer.println("2,lisi,123456,lisi@example.com,13800138002,1,90,2024-01-01 10:00:00,2024-01-01 10:00:00");
writer.println("3,wangwu,123456,wangwu@example.com,13800138003,0,75,2024-01-01 10:00:00,2024-01-01 10:00:00");
writer.flush();
}
} catch (Exception e) {
log.error("下载模板失败", e);
}
}
}
2.6 UserController.java - 用户控制器
package com.example.usermanagement.controller;
import com.example.usermanagement.common.Result;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 用户控制器
* @RestController: 相当于@Controller + @ResponseBody,返回JSON数据
* @RequestMapping: 定义请求路径前缀
*/
@RestController
@RequestMapping("/api/users")
public class UserController {
// 手动创建日志对象
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
/**
* 创建用户
* POST /api/users
*
* 请求体示例:
* {
* "username": "testuser",
* "password": "123456",
* "email": "test@example.com",
* "phone": "13800138000",
* "score": 85
* }
*
* @param user 用户对象(从请求体自动映射)
* @return 创建成功的用户信息
*/
@PostMapping
public Result<User> createUser(@RequestBody User user) {
try {
log.info("接收创建用户请求:username={}", user.getUsername());
User createdUser = userService.createUser(user);
return Result.success(createdUser);
} catch (Exception e) {
log.error("创建用户失败", e);
return Result.<User>error(e.getMessage());
}
}
/**
* 删除用户
* DELETE /api/users/{id}
*
* @param id 用户ID(从URL路径获取)
* @return 操作结果
*/
@DeleteMapping("/{id}")
public Result<Void> deleteUser(@PathVariable Long id) {
try {
log.info("接收删除用户请求:ID={}", id);
boolean success = userService.deleteUser(id);
if (success) {
return Result.success();
} else {
return Result.<Void>error("用户不存在");
}
} catch (Exception e) {
log.error("删除用户失败", e);
return Result.<Void>error(e.getMessage());
}
}
/**
* 更新用户信息
* PUT /api/users/{id}
*
* 请求体示例:
* {
* "email": "newemail@example.com",
* "phone": "13900139000",
* "status": 1,
* "score": 90
* }
*
* @param id 用户ID
* @param user 要更新的用户信息
* @return 更新后的用户信息
*/
@PutMapping("/{id}")
public Result<User> updateUser(@PathVariable Long id, @RequestBody User user) {
try {
log.info("接收更新用户请求:ID={}", id);
user.setId(id); // 设置ID,确保更新正确的用户
User updatedUser = userService.updateUser(user);
return Result.success(updatedUser);
} catch (Exception e) {
log.error("更新用户失败", e);
return Result.<User>error(e.getMessage());
}
}
/**
* 根据ID查询用户
* GET /api/users/{id}
*
* @param id 用户ID
* @return 用户信息
*/
@GetMapping("/{id}")
public Result<User> getUserById(@PathVariable Long id) {
try {
log.info("接收查询用户请求:ID={}", id);
User user = userService.getUserById(id);
if (user != null) {
return Result.success(user);
} else {
return Result.<User>error("用户不存在");
}
} catch (Exception e) {
log.error("查询用户失败", e);
return Result.<User>error(e.getMessage());
}
}
/**
* 查询所有用户
* GET /api/users
*
* @return 所有用户列表
*/
@GetMapping
public Result<List<User>> getAllUsers() {
try {
log.info("接收查询所有用户请求");
List<User> users = userService.getAllUsers();
return Result.success(users);
} catch (Exception e) {
log.error("查询所有用户失败", e);
return Result.<List<User>>error(e.getMessage());
}
}
/**
* 分页查询用户(支持搜索)
* GET /api/users/page?pageNum=1&pageSize=10&username=zhang
*
* @param pageNum 页码,默认1
* @param pageSize 每页数量,默认10
* @param username 用户名搜索关键字(可选)
* @return 分页数据
*/
@GetMapping("/page")
public Result<Map<String, Object>> getUsersByPage(
@RequestParam(defaultValue = "1") Integer pageNum, // 默认第1页
@RequestParam(defaultValue = "10") Integer pageSize, // 默认每页10条
@RequestParam(required = false) String username) { // 可选的搜索参数
try {
log.info("接收分页查询请求:pageNum={}, pageSize={}, username={}",
pageNum, pageSize, username);
List<User> users;
int total;
// 根据是否有username参数决定调用哪个方法
if (username != null && !username.trim().isEmpty()) {
// 带搜索条件的查询
users = userService.searchUsersByUsername(username, pageNum, pageSize);
total = userService.getSearchTotalCount(username);
} else {
// 普通分页查询
users = userService.getUsersByPage(pageNum, pageSize);
total = userService.getTotalCount();
}
// 构建返回数据
Map<String, Object> data = new HashMap<>();
data.put("list", users); // 用户列表
data.put("total", total); // 总记录数
data.put("pageNum", pageNum); // 当前页码
data.put("pageSize", pageSize); // 每页大小
data.put("pages", (total + pageSize - 1) / pageSize); // 总页数(向上取整)
return Result.success(data);
} catch (Exception e) {
log.error("分页查询用户失败", e);
return Result.<Map<String, Object>>error(e.getMessage());
}
}
/**
* 根据用户名查询用户
* GET /api/users/username/{username}
*
* @param username 用户名
* @return 用户信息
*/
@GetMapping("/username/{username}")
public Result<User> getUserByUsername(@PathVariable String username) {
try {
log.info("接收根据用户名查询请求:username={}", username);
User user = userService.getUserByUsername(username);
if (user != null) {
return Result.success(user);
} else {
return Result.<User>error("用户不存在");
}
} catch (Exception e) {
log.error("根据用户名查询用户失败", e);
return Result.<User>error(e.getMessage());
}
}
/**
* 根据邮箱查询用户
* GET /api/users/email/{email}
*
* @param email 邮箱地址
* @return 用户信息
*/
@GetMapping("/email/{email}")
public Result<User> getUserByEmail(@PathVariable String email) {
try {
log.info("接收根据邮箱查询请求:email={}", email);
User user = userService.getUserByEmail(email);
if (user != null) {
return Result.success(user);
} else {
return Result.<User>error("该邮箱未注册");
}
} catch (Exception e) {
log.error("根据邮箱查询用户失败", e);
return Result.<User>error(e.getMessage());
}
}
/**
* 修改密码
* PUT /api/users/{id}/password
*
* 请求体示例:
* {
* "oldPassword": "123456",
* "newPassword": "newpass123"
* }
*
* @param id 用户ID
* @param passwordData 包含新旧密码的Map
* @return 操作结果
*/
@PutMapping("/{id}/password")
public Result<Void> changePassword(@PathVariable Long id,
@RequestBody Map<String, String> passwordData) {
try {
log.info("接收修改密码请求:ID={}", id);
String oldPassword = passwordData.get("oldPassword");
String newPassword = passwordData.get("newPassword");
// 参数验证
if (oldPassword == null || newPassword == null) {
return Result.<Void>error("密码不能为空");
}
// 获取用户信息
User user = userService.getUserById(id);
if (user == null) {
return Result.<Void>error("用户不存在");
}
// 验证原密码(实际项目中密码应该是加密存储的)
if (!oldPassword.equals(user.getPassword())) {
return Result.<Void>error("原密码错误");
}
// 更新密码
User updateUser = new User();
updateUser.setId(id);
updateUser.setPassword(newPassword);
userService.updateUser(updateUser);
return Result.success();
} catch (Exception e) {
log.error("修改密码失败", e);
return Result.<Void>error(e.getMessage());
}
}
}
2.7 User.java - 用户实体类
package com.example.usermanagement.entity;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 用户实体类
* @Data: Lombok注解,自动生成getter/setter/toString等方法
* implements Serializable: 实现序列化接口,用于Redis存储
*/
@Data
public class User implements Serializable {
// 序列化版本ID,用于版本控制
private static final long serialVersionUID = 1L;
private Long id; // 用户ID(主键)
private String username; // 用户名(唯一)
private String password; // 密码(应加密存储)
private String email; // 邮箱
private String phone; // 手机号
private Integer status; // 状态:0-禁用,1-启用
private Date createTime; // 创建时间
private Date updateTime; // 更新时间
private Integer score; // 用户分数
/**
* 注释掉的getter/setter方法
* 因为使用了@Data注解,这些方法会自动生成
* 保留这些代码可能是为了:
* 1. 展示传统的JavaBean写法
* 2. 在某些IDE中提供更好的代码提示
* 3. 作为备用方案(如果Lombok出现问题)
*/
}
2.8 UserMapper.java - 用户Mapper接口
package com.example.usermanagement.mapper;
import com.example.usermanagement.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户Mapper接口
* @Mapper: 标识这是MyBatis的Mapper接口
*
* MyBatis会根据对应的XML文件或注解生成实现类
* 负责与数据库的直接交互
*/
@Mapper
public interface UserMapper {
/**
* 插入用户
* @param user 用户对象
* @return 影响的行数
*/
int insert(User user);
/**
* 根据ID删除用户
* @param id 用户ID
* @return 影响的行数
*/
int deleteById(Long id);
/**
* 更新用户信息
* @param user 用户对象(必须包含ID)
* @return 影响的行数
*/
int update(User user);
/**
* 根据ID查询用户
* @param id 用户ID
* @return 用户对象,不存在返回null
*/
User selectById(Long id);
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户对象,不存在返回null
*/
User selectByUsername(String username);
/**
* 查询所有用户
* @return 用户列表
*/
List<User> selectAll();
/**
* 分页查询用户
* @Param注解:指定参数名称,用于XML中引用
* @param offset 偏移量(跳过的记录数)
* @param limit 限制数量(每页大小)
* @return 用户列表
*/
List<User> selectByPage(@Param("offset") Integer offset,
@Param("limit") Integer limit);
/**
* 统计用户总数
* @return 用户总数
*/
int count();
/**
* 根据邮箱查询用户
* @param email 邮箱地址
* @return 用户信息
*/
User selectByEmail(String email);
/**
* 根据用户名模糊搜索(分页)
* @param username 用户名关键字
* @param offset 偏移量
* @param limit 限制数量
* @return 用户列表
*/
List<User> searchByUsername(@Param("username") String username,
@Param("offset") int offset,
@Param("limit") int limit);
/**
* 统计搜索结果数量
* @param username 用户名关键字
* @return 符合条件的用户数量
*/
int countByUsername(@Param("username") String username);
}
2.9 UserService.java - 用户业务接口
package com.example.usermanagement.service;
import com.example.usermanagement.entity.User;
import java.util.List;
/**
* 用户业务接口
* 定义用户相关的业务操作方法
*
* Service层负责:
* 1. 业务逻辑处理
* 2. 事务管理
* 3. 缓存处理
* 4. 参数校验
* 5. 异常处理
*/
public interface UserService {
/**
* 创建用户
* 1. 检查用户名是否已存在
* 2. 设置默认值
* 3. 插入数据库
* 4. 缓存到Redis
*
* @param user 用户对象
* @return 创建成功的用户对象(包含生成的ID)
* @throws RuntimeException 用户名已存在时抛出
*/
User createUser(User user);
/**
* 删除用户
* 1. 检查用户是否存在
* 2. 从数据库删除
* 3. 清除Redis缓存
*
* @param id 用户ID
* @return 是否删除成功
*/
boolean deleteUser(Long id);
/**
* 更新用户信息
* 1. 检查用户是否存在
* 2. 如果修改用户名,检查新用户名是否已被使用
* 3. 更新数据库
* 4. 清除Redis缓存
*
* @param user 用户对象(必须包含ID)
* @return 更新后的用户对象
* @throws RuntimeException 用户不存在或用户名已被使用时抛出
*/
User updateUser(User user);
/**
* 根据ID获取用户
* 1. 先从Redis缓存查询
* 2. 缓存未命中则查询数据库
* 3. 查询结果存入Redis
*
* @param id 用户ID
* @return 用户对象,不存在返回null
*/
User getUserById(Long id);
/**
* 根据用户名获取用户
* 直接查询数据库(不使用缓存,因为用户名可能变更)
*
* @param username 用户名
* @return 用户对象,不存在返回null
*/
User getUserByUsername(String username);
/**
* 获取所有用户
* 用于导出数据等场景
*
* @return 所有用户列表
*/
List<User> getAllUsers();
/**
* 分页获取用户
*
* @param pageNum 页码(从1开始)
* @param pageSize 每页数量
* @return 用户列表
*/
List<User> getUsersByPage(Integer pageNum, Integer pageSize);
/**
* 获取用户总数
* 用于分页显示
*
* @return 用户总数
*/
int getTotalCount();
/**
* 根据邮箱查询用户
* @param email 邮箱地址
* @return 用户信息,如果不存在返回null
*/
User getUserByEmail(String email);
/**
* 根据用户名搜索用户(分页)
* @param username 用户名关键字
* @param pageNum 页码
* @param pageSize 每页大小
* @return 用户列表
*/
List<User> searchUsersByUsername(String username, Integer pageNum, Integer pageSize);
/**
* 获取搜索结果总数
*
* @param username 用户名关键字
* @return 符合条件的用户数量
*/
int getSearchTotalCount(String username);
}
2.10 UserServiceImpl.java - 用户业务实现类
package com.example.usermanagement.service.impl;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.mapper.UserMapper;
import com.example.usermanagement.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 用户业务实现类
*
* @Service: 标识这是Service层组件
* @Transactional: 开启事务管理,确保数据一致性
*/
@Service
@Transactional // 类级别事务,所有public方法都在事务中执行
public class UserServiceImpl implements UserService {
// 手动创建日志对象
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// Redis键前缀
private static final String USER_KEY_PREFIX = "user:";
// 缓存过期时间(小时)
private static final long CACHE_EXPIRE_HOURS = 2;
@Override
public User createUser(User user) {
// 检查用户名是否已存在(业务校验)
User existUser = userMapper.selectByUsername(user.getUsername());
if (existUser != null) {
throw new RuntimeException("用户名已存在");
}
// 设置默认状态(如果未指定)
if (user.getStatus() == null) {
user.setStatus(1); // 默认启用
}
// 插入数据库
userMapper.insert(user);
// 缓存到Redis
String key = USER_KEY_PREFIX + user.getId();
redisTemplate.opsForValue().set(key, user, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
log.info("创建用户成功:{}", user.getUsername());
return user;
}
@Override
public boolean deleteUser(Long id) {
// 先查询用户是否存在
User user = getUserById(id);
if (user == null) {
return false;
}
// 从数据库删除
int result = userMapper.deleteById(id);
// 从Redis删除缓存
String key = USER_KEY_PREFIX + id;
redisTemplate.delete(key);
log.info("删除用户成功:ID={}", id);
return result > 0;
}
@Override
public User updateUser(User user) {
// 检查用户是否存在
User existUser = getUserById(user.getId());
if (existUser == null) {
throw new RuntimeException("用户不存在");
}
// 如果修改了用户名,检查新用户名是否已被使用
if (user.getUsername() != null && !user.getUsername().equals(existUser.getUsername())) {
User checkUser = userMapper.selectByUsername(user.getUsername());
if (checkUser != null) {
throw new RuntimeException("用户名已被使用");
}
}
// 更新数据库
userMapper.update(user);
// 更新缓存(先删除,下次查询时再缓存)
String key = USER_KEY_PREFIX + user.getId();
redisTemplate.delete(key);
log.info("更新用户成功:ID={}", user.getId());
return getUserById(user.getId()); // 返回最新的用户信息
}
@Override
public User getUserById(Long id) {
String key = USER_KEY_PREFIX + id;
// 缓存策略:Cache-Aside Pattern
try {
// 1. 先从Redis查询
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
log.debug("从Redis获取用户:ID={}", id);
return user;
}
} catch (Exception e) {
// Redis异常时降级到数据库查询
log.warn("Redis查询失败,降级到数据库查询:{}", e.getMessage());
}
// 2. Redis没有,从数据库查询
User user = userMapper.selectById(id);
if (user != null) {
try {
// 3. 存入Redis缓存
redisTemplate.opsForValue().set(key, user, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
log.debug("从数据库获取用户并缓存:ID={}", id);
} catch (Exception e) {
// Redis存储失败不影响业务
log.warn("Redis存储失败,但数据查询正常:{}", e.getMessage());
}
}
return user;
}
@Override
public User getUserByUsername(String username) {
// 用户名查询不使用缓存(因为用户名可能变更)
return userMapper.selectByUsername(username);
}
@Override
public List<User> getAllUsers() {
// 查询所有用户(通常用于导出,不缓存)
return userMapper.selectAll();
}
@Override
public List<User> getUsersByPage(Integer pageNum, Integer pageSize) {
// 计算偏移量:(页码-1) * 每页大小
Integer offset = (pageNum - 1) * pageSize;
return userMapper.selectByPage(offset, pageSize);
}
@Override
public int getTotalCount() {
return userMapper.count();
}
@Override
public User getUserByEmail(String email) {
// 参数校验
if (email == null || email.trim().isEmpty()) {
log.warn("邮箱参数为空");
return null;
}
// 直接查询数据库(邮箱查询频率较低,不使用缓存)
User user = userMapper.selectByEmail(email.trim());
if (user != null) {
log.info("根据邮箱查询用户成功:{}", email);
} else {
log.info("未找到邮箱对应的用户:{}", email);
}
return user;
}
@Override
public List<User> searchUsersByUsername(String username, Integer pageNum, Integer pageSize) {
int offset = (pageNum - 1) * pageSize;
return userMapper.searchByUsername(username, offset, pageSize);
}
@Override
public int getSearchTotalCount(String username) {
return userMapper.countByUsername(username);
}
}
2.11 PasswordUtils.java - 密码工具类
package com.example.usermanagement.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 密码工具类
* 提供密码加密和验证功能
*/
public class PasswordUtils {
/**
* MD5加密
* 注意:MD5已不够安全,实际项目建议使用BCrypt或PBKDF2
*
* @param password 原始密码
* @return 加密后的密码(32位16进制字符串)
*/
public static String encryptMD5(String password) {
try {
// 获取MD5消息摘要实例
MessageDigest md = MessageDigest.getInstance("MD5");
// 更新要计算的数据
md.update(password.getBytes());
// 计算摘要
byte[] digest = md.digest();
// 转换为16进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
// 每个字节转换为2位16进制
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
// MD5算法不可用(几乎不可能发生)
throw new RuntimeException("MD5加密失败", e);
}
}
/**
* 验证密码
*
* @param inputPassword 输入的密码(明文)
* @param storedPassword 存储的加密密码
* @return 是否匹配
*/
public static boolean verify(String inputPassword, String storedPassword) {
// 将输入密码加密后与存储的密码比较
return encryptMD5(inputPassword).equals(storedPassword);
}
}
三、核心功能流程
3.1 用户注册流程
- 前端发送POST请求到
/api/users
- UserController接收请求,调用UserService
- UserService执行业务逻辑:
- 检查用户名是否存在
- 设置默认值
- 调用Mapper插入数据库
- 缓存到Redis
- 返回创建成功的用户信息
3.2 用户登录流程
- 前端发送POST请求到
/api/auth/login
- AuthController验证用户名密码
- 生成Token(示例中使用UUID)
- 返回Token和用户信息
- 前端保存Token用于后续请求
3.3 缓存策略
采用Cache-Aside Pattern:
- 读取时:先查缓存,未命中则查数据库并缓存
- 更新时:更新数据库后删除缓存
- 删除时:删除数据库记录和缓存
3.4 分页查询流程
- 接收页码和每页大小参数
- 计算偏移量:(pageNum - 1) * pageSize
- 调用Mapper查询指定范围的数据
- 查询总记录数
- 计算总页数并返回
四、安全性考虑
4.1 当前实现的问题
- 密码明文存储:应使用BCrypt等算法加密
- 简单Token:应使用JWT并设置过期时间
- 缺少认证拦截:应添加拦截器或使用Spring Security
- SQL注入风险:MyBatis参数化查询已规避
- 缺少参数验证:应使用@Valid注解验证
4.2 改进建议
- 集成Spring Security
- 使用JWT替代UUID Token
- 密码使用BCrypt加密
- 添加请求限流
- 实现权限控制(RBAC)
- 添加操作日志审计
五、性能优化
5.1 已实现的优化
- Redis缓存:减少数据库访问
- 分页查询:避免一次加载大量数据
- 索引优化:username、email字段应建立索引
5.2 可优化方向
- 数据库连接池:配置HikariCP
- 批量操作:CSV导入使用批量插入
- 异步处理:导出操作可异步执行
- 缓存预热:系统启动时加载热点数据
六、总结
这是一个典型的Spring Boot用户管理系统,展示了:
- 标准的MVC分层架构
- RESTful API设计
- Redis缓存集成
- MyBatis持久层框架使用
- 统一的响应格式和异常处理
- CSV数据导入导出功能
系统具备基本的用户CRUD功能,适合作为学习项目或小型应用的基础框架。通过添加认证授权、参数验证、加密存储等功能,可以演进为生产级应用。