Spring Boot 用户管理系统

发布于:2025-09-10 ⋅ 阅读:(23) ⋅ 点赞:(0)

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 用户注册流程

  1. 前端发送POST请求到 /api/users
  2. UserController接收请求,调用UserService
  3. UserService执行业务逻辑:
    • 检查用户名是否存在
    • 设置默认值
    • 调用Mapper插入数据库
    • 缓存到Redis
  4. 返回创建成功的用户信息

3.2 用户登录流程

  1. 前端发送POST请求到 /api/auth/login
  2. AuthController验证用户名密码
  3. 生成Token(示例中使用UUID)
  4. 返回Token和用户信息
  5. 前端保存Token用于后续请求

3.3 缓存策略

采用Cache-Aside Pattern

  1. 读取时:先查缓存,未命中则查数据库并缓存
  2. 更新时:更新数据库后删除缓存
  3. 删除时:删除数据库记录和缓存

3.4 分页查询流程

  1. 接收页码和每页大小参数
  2. 计算偏移量:(pageNum - 1) * pageSize
  3. 调用Mapper查询指定范围的数据
  4. 查询总记录数
  5. 计算总页数并返回

四、安全性考虑

4.1 当前实现的问题

  1. 密码明文存储:应使用BCrypt等算法加密
  2. 简单Token:应使用JWT并设置过期时间
  3. 缺少认证拦截:应添加拦截器或使用Spring Security
  4. SQL注入风险:MyBatis参数化查询已规避
  5. 缺少参数验证:应使用@Valid注解验证

4.2 改进建议

  1. 集成Spring Security
  2. 使用JWT替代UUID Token
  3. 密码使用BCrypt加密
  4. 添加请求限流
  5. 实现权限控制(RBAC)
  6. 添加操作日志审计

五、性能优化

5.1 已实现的优化

  1. Redis缓存:减少数据库访问
  2. 分页查询:避免一次加载大量数据
  3. 索引优化:username、email字段应建立索引

5.2 可优化方向

  1. 数据库连接池:配置HikariCP
  2. 批量操作:CSV导入使用批量插入
  3. 异步处理:导出操作可异步执行
  4. 缓存预热:系统启动时加载热点数据

六、总结

这是一个典型的Spring Boot用户管理系统,展示了:

  • 标准的MVC分层架构
  • RESTful API设计
  • Redis缓存集成
  • MyBatis持久层框架使用
  • 统一的响应格式和异常处理
  • CSV数据导入导出功能

系统具备基本的用户CRUD功能,适合作为学习项目或小型应用的基础框架。通过添加认证授权、参数验证、加密存储等功能,可以演进为生产级应用。


网站公告

今日签到

点亮在社区的每一天
去签到