Spring Boot + Cursor 实战:从零到一搭建一个生产级用户中心

文章目录

理论讲再多,不如动手做一个项目。这篇文章,我将带你用 Cursor 从零开始搭建一个生产级的用户中心------包含注册、登录、JWT 鉴权、Redis 缓存、全局异常处理、单元测试等完整功能。全程使用 Cursor 辅助开发,让你直观感受 AI 编程的真实效率。

一、项目概述

1.1 项目目标

构建一个用户中心微服务,包含以下功能:

yaml 复制代码
核心功能:
  - 用户注册(手机号 + 密码)
  - 用户登录(JWT Token)
  - 用户信息查询
  - 用户信息更新

技术特性:
  - 密码 BCrypt 加密
  - Redis 存储 Token 黑名单
  - 全局异常处理
  - 参数校验
  - 单元测试覆盖

非功能要求:
  - 响应时间 < 100ms
  - 支持并发 1000 QPS

1.2 技术栈

组件 技术选型 版本
基础框架 Spring Boot 3.2.0
数据库 MySQL 8.0
缓存 Redis 7.0
ORM MyBatis-Plus 3.5.5
JWT JJWT 0.12.3
连接池 HikariCP 5.0.1
测试 JUnit 5 + Mockito -

1.3 项目结构

复制代码
user-center/
├── src/main/java/com/example/usercenter/
│   ├── controller/          # API 接口层
│   ├── service/             # 业务逻辑层
│   ├── mapper/              # 数据访问层
│   ├── entity/              # 数据库实体
│   ├── dto/                 # 数据传输对象
│   │   ├── request/         # 请求 DTO
│   │   └── response/        # 响应 DTO
│   ├── config/              # 配置类
│   ├── exception/           # 异常处理
│   ├── interceptor/         # 拦截器
│   └── util/                # 工具类
├── src/main/resources/
│   ├── application.yml
│   └── mapper/              # MyBatis XML 文件
└── pom.xml

二、项目初始化

2.1 让 Cursor 生成项目骨架

指令

复制代码
创建一个 Spring Boot 3.2 项目,项目名 user-center,使用 Java 17。
依赖包含:Spring Web、MyBatis-Plus、MySQL Driver、Lombok、Spring Validation、Spring Data Redis、JJWT。

请生成完整的 pom.xml 文件。

Cursor 输出(核心部分):

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

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

    <groupId>com.example</groupId>
    <artifactId>user-center</artifactId>
    <version>1.0.0</version>
    <name>user-center</name>
    <description>User Center Service</description>

    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <jjwt.version>0.12.3</jjwt.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- Database -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</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>

        <!-- Tools -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

三、数据库设计与实体类

3.1 数据库表设计

指令

复制代码
设计用户表,表名 `user`,包含以下字段:
- id(主键,自增)
- phone(手机号,唯一索引)
- password(密码,BCrypt 加密存储)
- nickname(昵称)
- avatar(头像 URL)
- status(状态:0-禁用,1-正常)
- last_login_time(最后登录时间)
- create_time(创建时间)
- update_time(更新时间)

请生成建表 SQL。

Cursor 输出

sql 复制代码
CREATE TABLE `user` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `phone` VARCHAR(11) NOT NULL COMMENT '手机号',
    `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
    `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
    `status` TINYINT(4) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
    `last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

3.2 生成实体类

指令

复制代码
根据上面的 user 表,生成 MyBatis-Plus 的实体类 User。
要求:
1. 使用 Lombok 注解
2. 添加 @TableName 注解
3. 添加 @TableId 指定主键策略
4. 添加必要的 JavaDoc

Cursor 输出

java 复制代码
package com.example.usercenter.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 用户实体类
 *
 * @author AI Assistant
 * @date 2026-01-15
 */
@Data
@TableName("user")
public class User {

    /**
     * 用户ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 密码(BCrypt加密)
     */
    private String password;

    /**
     * 昵称
     */
    private String nickname;

    /**
     * 头像URL
     */
    private String avatar;

    /**
     * 状态:0-禁用,1-正常
     */
    private Integer status;

    /**
     * 最后登录时间
     */
    private LocalDateTime lastLoginTime;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

四、DTO 与 Mapper 层

4.1 生成请求/响应 DTO

指令

复制代码
生成用户模块的 DTO:

请求 DTO:
1. UserRegisterRequest:手机号、密码、昵称
2. UserLoginRequest:手机号、密码
3. UserUpdateRequest:昵称、头像

响应 DTO:
1. UserVO:用户ID、手机号、昵称、头像、状态
2. LoginVO:用户ID、昵称、Token

要求:
1. 使用 Jakarta Validation 注解做参数校验
2. 使用 Lombok 注解

Cursor 输出

java 复制代码
// UserRegisterRequest.java
package com.example.usercenter.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;

/**
 * 用户注册请求 DTO
 */
@Data
public class UserRegisterRequest {

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

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
    private String password;

    @Size(max = 50, message = "昵称长度不能超过50位")
    private String nickname;
}

// LoginVO.java
package com.example.usercenter.dto.response;

import lombok.Builder;
import lombok.Data;

/**
 * 登录响应 VO
 */
@Data
@Builder
public class LoginVO {

    private Long userId;

    private String nickname;

    private String token;
}

4.2 生成 Mapper

指令

复制代码
生成 UserMapper,继承 MyBatis-Plus 的 BaseMapper。
添加根据手机号查询用户的方法:selectByPhone

Cursor 输出

java 复制代码
package com.example.usercenter.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.usercenter.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

/**
 * 用户 Mapper 接口
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {

    /**
     * 根据手机号查询用户
     *
     * @param phone 手机号
     * @return 用户信息
     */
    @Select("SELECT * FROM user WHERE phone = #{phone}")
    User selectByPhone(String phone);
}

五、工具类与配置

5.1 JWT 工具类

指令

复制代码
生成 JWT 工具类 JwtUtil,包含:
1. 生成 Token(用户ID作为 subject,过期时间 7 天)
2. 解析 Token 获取用户ID
3. 验证 Token 有效性
4. Token 黑名单管理(配合 Redis)

使用 JJWT 0.12.3 版本。

Cursor 输出

java 复制代码
package com.example.usercenter.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * JWT 工具类
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {

    private final StringRedisTemplate redisTemplate;

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

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

    private static final String TOKEN_BLACKLIST_PREFIX = "token:blacklist:";

    /**
     * 生成 Token
     */
    public String generateToken(Long userId) {
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + expiration);

        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));

        return Jwts.builder()
                .subject(String.valueOf(userId))
                .issuedAt(now)
                .expiration(expireDate)
                .signWith(key)
                .compact();
    }

    /**
     * 从 Token 获取用户ID
     */
    public Long getUserIdFromToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
            Claims claims = Jwts.parser()
                    .verifyWith(key)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload();
            return Long.parseLong(claims.getSubject());
        } catch (Exception e) {
            log.error("解析 Token 失败: {}", e.getMessage());
            return null;
        }
    }

    /**
     * 验证 Token 有效性
     */
    public boolean validateToken(String token) {
        // 检查是否在黑名单中
        String blacklistKey = TOKEN_BLACKLIST_PREFIX + token;
        Boolean isBlacklisted = redisTemplate.hasKey(blacklistKey);
        if (Boolean.TRUE.equals(isBlacklisted)) {
            return false;
        }

        // 检查是否过期
        Long userId = getUserIdFromToken(token);
        return userId != null;
    }

    /**
     * 将 Token 加入黑名单(登出时使用)
     */
    public void addToBlacklist(String token) {
        Long userId = getUserIdFromToken(token);
        if (userId != null) {
            // 获取剩余过期时间
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
            Claims claims = Jwts.parser()
                    .verifyWith(key)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload();
            long ttl = claims.getExpiration().getTime() - System.currentTimeMillis();
            
            if (ttl > 0) {
                String blacklistKey = TOKEN_BLACKLIST_PREFIX + token;
                redisTemplate.opsForValue().set(blacklistKey, "1", ttl, TimeUnit.MILLISECONDS);
            }
        }
    }
}

5.2 配置文件

指令

复制代码
生成 application.yml 配置文件,包含:
1. 数据库连接配置
2. Redis 配置
3. MyBatis-Plus 配置
4. JWT 配置(secret、expiration)
5. 日志配置

Cursor 输出

yaml 复制代码
spring:
  application:
    name: user-center

  datasource:
    url: jdbc:mysql://localhost:3306/user_center?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5

  data:
    redis:
      host: localhost
      port: 6379
      password:
      database: 0
      timeout: 5000ms

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.usercenter.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted

jwt:
  secret: "your-256-bit-secret-key-for-jwt-signature-please-change-in-production"
  expiration: 604800000  # 7天(毫秒)

logging:
  level:
    com.example.usercenter: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"

六、业务逻辑层(Service)

6.1 生成 Service 接口和实现

指令

复制代码
生成 UserService 接口和实现类,包含以下方法:
1. register(registerRequest) - 用户注册
2. login(loginRequest) - 用户登录
3. getUserInfo(userId) - 获取用户信息
4. updateUser(userId, updateRequest) - 更新用户信息
5. logout(token) - 用户登出

要求:
- 注册时检查手机号是否已存在
- 密码使用 BCrypt 加密
- 登录失败时记录失败次数
- 使用 @Transactional 保证事务
- 添加详细日志

Cursor 输出

java 复制代码
// UserService.java
package com.example.usercenter.service;

import com.example.usercenter.dto.request.UserLoginRequest;
import com.example.usercenter.dto.request.UserRegisterRequest;
import com.example.usercenter.dto.request.UserUpdateRequest;
import com.example.usercenter.dto.response.LoginVO;
import com.example.usercenter.dto.response.UserVO;

public interface UserService {

    /**
     * 用户注册
     */
    void register(UserRegisterRequest request);

    /**
     * 用户登录
     */
    LoginVO login(UserLoginRequest request);

    /**
     * 获取用户信息
     */
    UserVO getUserInfo(Long userId);

    /**
     * 更新用户信息
     */
    void updateUser(Long userId, UserUpdateRequest request);

    /**
     * 用户登出
     */
    void logout(String token);
}
java 复制代码
// UserServiceImpl.java
package com.example.usercenter.service.impl;

import com.example.usercenter.dto.request.UserLoginRequest;
import com.example.usercenter.dto.request.UserRegisterRequest;
import com.example.usercenter.dto.request.UserUpdateRequest;
import com.example.usercenter.dto.response.LoginVO;
import com.example.usercenter.dto.response.UserVO;
import com.example.usercenter.entity.User;
import com.example.usercenter.exception.BusinessException;
import com.example.usercenter.mapper.UserMapper;
import com.example.usercenter.service.UserService;
import com.example.usercenter.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;
    private final JwtUtil jwtUtil;
    private final StringRedisTemplate redisTemplate;
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    private static final String LOGIN_FAIL_PREFIX = "login:fail:";

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void register(UserRegisterRequest request) {
        log.info("开始用户注册: phone={}", request.getPhone());

        // 1. 检查手机号是否已存在
        User existUser = userMapper.selectByPhone(request.getPhone());
        if (existUser != null) {
            throw new BusinessException("手机号已注册");
        }

        // 2. 创建新用户
        User user = new User();
        user.setPhone(request.getPhone());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setNickname(StringUtils.hasText(request.getNickname()) ? 
                         request.getNickname() : "用户" + request.getPhone().substring(7));
        user.setStatus(1);

        // 3. 保存用户
        userMapper.insert(user);
        log.info("用户注册成功: userId={}", user.getId());
    }

    @Override
    public LoginVO login(UserLoginRequest request) {
        log.info("用户登录: phone={}", request.getPhone());

        // 1. 检查登录失败次数
        String failKey = LOGIN_FAIL_PREFIX + request.getPhone();
        String failCountStr = redisTemplate.opsForValue().get(failKey);
        int failCount = failCountStr == null ? 0 : Integer.parseInt(failCountStr);
        
        if (failCount >= 5) {
            throw new BusinessException("登录失败次数过多,请15分钟后重试");
        }

        // 2. 查询用户
        User user = userMapper.selectByPhone(request.getPhone());
        if (user == null) {
            recordLoginFail(failKey, failCount);
            throw new BusinessException("用户不存在");
        }

        // 3. 校验密码
        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            recordLoginFail(failKey, failCount);
            throw new BusinessException("密码错误");
        }

        // 4. 校验用户状态
        if (user.getStatus() != 1) {
            throw new BusinessException("账号已被禁用");
        }

        // 5. 清除失败记录
        redisTemplate.delete(failKey);

        // 6. 更新最后登录时间
        user.setLastLoginTime(LocalDateTime.now());
        userMapper.updateById(user);

        // 7. 生成 Token
        String token = jwtUtil.generateToken(user.getId());

        log.info("用户登录成功: userId={}", user.getId());
        return LoginVO.builder()
                .userId(user.getId())
                .nickname(user.getNickname())
                .token(token)
                .build();
    }

    @Override
    public UserVO getUserInfo(Long userId) {
        User user = userMapper.selectById(userId);
        if (user == null) {
            throw new BusinessException("用户不存在");
        }

        UserVO vo = new UserVO();
        vo.setId(user.getId());
        vo.setPhone(user.getPhone());
        vo.setNickname(user.getNickname());
        vo.setAvatar(user.getAvatar());
        vo.setStatus(user.getStatus());
        return vo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateUser(Long userId, UserUpdateRequest request) {
        log.info("更新用户信息: userId={}", userId);

        User user = userMapper.selectById(userId);
        if (user == null) {
            throw new BusinessException("用户不存在");
        }

        if (StringUtils.hasText(request.getNickname())) {
            user.setNickname(request.getNickname());
        }
        if (StringUtils.hasText(request.getAvatar())) {
            user.setAvatar(request.getAvatar());
        }

        userMapper.updateById(user);
        log.info("用户信息更新成功: userId={}", userId);
    }

    @Override
    public void logout(String token) {
        log.info("用户登出");
        jwtUtil.addToBlacklist(token);
    }

    private void recordLoginFail(String failKey, int currentFailCount) {
        int newFailCount = currentFailCount + 1;
        if (newFailCount >= 5) {
            redisTemplate.opsForValue().set(failKey, String.valueOf(newFailCount), 15, TimeUnit.MINUTES);
        } else {
            redisTemplate.opsForValue().set(failKey, String.valueOf(newFailCount), 1, TimeUnit.HOURS);
        }
        log.warn("登录失败,失败次数: {}", newFailCount);
    }
}

七、控制器层(Controller)

7.1 生成 Controller

指令

复制代码
生成 UserController,包含以下接口:
1. POST /api/v1/user/register - 用户注册
2. POST /api/v1/user/login - 用户登录
3. GET /api/v1/user/info - 获取当前用户信息
4. PUT /api/v1/user/info - 更新当前用户信息
5. POST /api/v1/user/logout - 用户登出

要求:
- 使用统一返回格式 Result<T>
- 添加参数校验
- 添加接口说明(JavaDoc)
- 需要认证的接口添加拦截

Cursor 输出

java 复制代码
package com.example.usercenter.controller;

import com.example.usercenter.dto.request.UserLoginRequest;
import com.example.usercenter.dto.request.UserRegisterRequest;
import com.example.usercenter.dto.request.UserUpdateRequest;
import com.example.usercenter.dto.response.LoginVO;
import com.example.usercenter.dto.response.UserVO;
import com.example.usercenter.service.UserService;
import com.example.usercenter.common.Result;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * 用户管理 Controller
 */
@Slf4j
@RestController
@RequestMapping("/api/v1/user")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    /**
     * 用户注册
     *
     * @param request 注册请求参数
     * @return 注册结果
     */
    @PostMapping("/register")
    public Result<Void> register(@Valid @RequestBody UserRegisterRequest request) {
        log.info("收到注册请求: phone={}", request.getPhone());
        userService.register(request);
        return Result.success();
    }

    /**
     * 用户登录
     *
     * @param request 登录请求参数
     * @return 登录信息
     */
    @PostMapping("/login")
    public Result<LoginVO> login(@Valid @RequestBody UserLoginRequest request) {
        log.info("收到登录请求: phone={}", request.getPhone());
        LoginVO loginVO = userService.login(request);
        return Result.success(loginVO);
    }

    /**
     * 获取当前用户信息
     *
     * @param userId 当前用户ID(从拦截器获取)
     * @return 用户信息
     */
    @GetMapping("/info")
    public Result<UserVO> getUserInfo(@RequestAttribute("userId") Long userId) {
        UserVO userVO = userService.getUserInfo(userId);
        return Result.success(userVO);
    }

    /**
     * 更新当前用户信息
     *
     * @param userId  当前用户ID(从拦截器获取)
     * @param request 更新请求参数
     * @return 更新结果
     */
    @PutMapping("/info")
    public Result<Void> updateUser(@RequestAttribute("userId") Long userId,
                                   @Valid @RequestBody UserUpdateRequest request) {
        userService.updateUser(userId, request);
        return Result.success();
    }

    /**
     * 用户登出
     *
     * @param token 当前用户的 Token
     * @return 登出结果
     */
    @PostMapping("/logout")
    public Result<Void> logout(@RequestHeader("Authorization") String token) {
        // 去掉 "Bearer " 前缀
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        userService.logout(token);
        return Result.success();
    }
}

八、全局异常处理与统一返回

8.1 统一返回格式

指令

复制代码
生成统一返回类 Result<T>,包含:
- code:响应码
- message:响应消息
- data:响应数据

提供静态方法:
- success()
- success(data)
- error(code, message)

Cursor 输出

java 复制代码
package com.example.usercenter.common;

import lombok.Data;

/**
 * 统一返回结果
 */
@Data
public class Result<T> {

    private Integer code;
    private String message;
    private T data;

    private Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static <T> Result<T> success() {
        return new Result<>(200, "success", null);
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }

    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null);
    }
}

8.2 全局异常处理器

指令

复制代码
生成全局异常处理器 GlobalExceptionHandler,处理:
1. BusinessException(业务异常)
2. MethodArgumentNotValidException(参数校验异常)
3. Exception(未知异常)

返回统一的 Result 格式。

Cursor 输出

java 复制代码
package com.example.usercenter.exception;

import com.example.usercenter.common.Result;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.stream.Collectors;

/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 业务异常处理
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(400, e.getMessage());
    }

    /**
     * 参数校验异常处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.joining("; "));
        log.warn("参数校验失败: {}", message);
        return Result.error(400, message);
    }

    /**
     * 未知异常处理
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error(500, "系统内部错误,请稍后重试");
    }
}

九、认证拦截器

9.1 生成拦截器

指令

复制代码
生成认证拦截器 AuthInterceptor,实现 HandlerInterceptor:
1. 从请求头获取 Authorization
2. 解析 Token 获取用户 ID
3. 将用户 ID 存入 request attribute
4. 排除白名单路径:/api/v1/user/register, /api/v1/user/login

Cursor 输出

java 复制代码
package com.example.usercenter.interceptor;

import com.example.usercenter.exception.BusinessException;
import com.example.usercenter.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {

    private final JwtUtil jwtUtil;

    private static final List<String> WHITE_LIST = List.of(
            "/api/v1/user/register",
            "/api/v1/user/login"
    );

    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        
        String path = request.getRequestURI();
        
        // 白名单放行
        if (WHITE_LIST.contains(path)) {
            return true;
        }

        // 获取 Token
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            throw new BusinessException("未登录或 Token 格式错误");
        }
        token = token.substring(7);

        // 验证 Token
        if (!jwtUtil.validateToken(token)) {
            throw new BusinessException("Token 无效或已过期");
        }

        // 获取用户 ID 并存入 request
        Long userId = jwtUtil.getUserIdFromToken(token);
        request.setAttribute("userId", userId);

        return true;
    }
}

9.2 注册拦截器

指令

复制代码
生成 WebConfig 配置类,注册 AuthInterceptor

Cursor 输出

java 复制代码
package com.example.usercenter.config;

import com.example.usercenter.interceptor.AuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/v1/**");
    }
}

十、单元测试

10.1 生成 Service 测试

指令

复制代码
为 UserServiceImpl 生成单元测试,覆盖:
1. 注册成功
2. 注册时手机号已存在
3. 登录成功
4. 登录时密码错误
5. 登录失败次数限制
使用 Mockito 和 JUnit 5

Cursor 输出

java 复制代码
package com.example.usercenter.service.impl;

import com.example.usercenter.dto.request.UserLoginRequest;
import com.example.usercenter.dto.request.UserRegisterRequest;
import com.example.usercenter.entity.User;
import com.example.usercenter.exception.BusinessException;
import com.example.usercenter.mapper.UserMapper;
import com.example.usercenter.util.JwtUtil;
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.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

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

@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

    @Mock
    private UserMapper userMapper;

    @Mock
    private JwtUtil jwtUtil;

    @Mock
    private StringRedisTemplate redisTemplate;

    @Mock
    private ValueOperations<String, String> valueOperations;

    @InjectMocks
    private UserServiceImpl userService;

    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @BeforeEach
    void setUp() {
        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
    }

    @Test
    void registerSuccess() {
        // given
        UserRegisterRequest request = new UserRegisterRequest();
        request.setPhone("13800138000");
        request.setPassword("123456");
        request.setNickname("测试用户");

        when(userMapper.selectByPhone(anyString())).thenReturn(null);
        when(userMapper.insert(any(User.class))).thenReturn(1);

        // when
        assertDoesNotThrow(() -> userService.register(request));

        // then
        verify(userMapper).insert(any(User.class));
    }

    @Test
    void registerWhenPhoneExists() {
        // given
        UserRegisterRequest request = new UserRegisterRequest();
        request.setPhone("13800138000");
        request.setPassword("123456");

        User existUser = new User();
        existUser.setPhone("13800138000");
        when(userMapper.selectByPhone(anyString())).thenReturn(existUser);

        // when & then
        assertThrows(BusinessException.class, () -> userService.register(request));
        verify(userMapper, never()).insert(any());
    }

    @Test
    void loginSuccess() {
        // given
        UserLoginRequest request = new UserLoginRequest();
        request.setPhone("13800138000");
        request.setPassword("123456");

        User user = new User();
        user.setId(1L);
        user.setPhone("13800138000");
        user.setPassword(passwordEncoder.encode("123456"));
        user.setStatus(1);

        when(valueOperations.get(anyString())).thenReturn(null);
        when(userMapper.selectByPhone(anyString())).thenReturn(user);
        when(jwtUtil.generateToken(anyLong())).thenReturn("test-token");

        // when
        var result = userService.login(request);

        // then
        assertNotNull(result);
        assertEquals("test-token", result.getToken());
        verify(userMapper).updateById(any());
    }

    @Test
    void loginWithWrongPassword() {
        // given
        UserLoginRequest request = new UserLoginRequest();
        request.setPhone("13800138000");
        request.setPassword("wrong-password");

        User user = new User();
        user.setPassword(passwordEncoder.encode("123456"));

        when(valueOperations.get(anyString())).thenReturn(null);
        when(userMapper.selectByPhone(anyString())).thenReturn(user);

        // when & then
        assertThrows(BusinessException.class, () -> userService.login(request));
        verify(valueOperations).set(anyString(), anyString(), anyLong(), any());
    }

    @Test
    void loginExceedsFailLimit() {
        // given
        UserLoginRequest request = new UserLoginRequest();
        request.setPhone("13800138000");
        request.setPassword("123456");

        when(valueOperations.get(anyString())).thenReturn("5");

        // when & then
        assertThrows(BusinessException.class, () -> userService.login(request));
        verify(userMapper, never()).selectByPhone(anyString());
    }
}

十一、运行与测试

11.1 启动应用

bash 复制代码
# 编译打包
mvn clean package

# 启动应用
java -jar target/user-center-1.0.0.jar

# 或者通过 Maven 直接运行
mvn spring-boot:run

11.2 接口测试

注册

bash 复制代码
curl -X POST http://localhost:8080/api/v1/user/register \
  -H "Content-Type: application/json" \
  -d '{"phone":"13800138000","password":"123456","nickname":"测试用户"}'

登录

bash 复制代码
curl -X POST http://localhost:8080/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"phone":"13800138000","password":"123456"}'

获取用户信息

bash 复制代码
curl -X GET http://localhost:8080/api/v1/user/info \
  -H "Authorization: Bearer {token}"

11.3 验证要点

yaml 复制代码
功能验证:
  - [ ] 注册成功,数据库有记录
  - [ ] 密码被 BCrypt 加密
  - [ ] 重复手机号注册被拦截
  - [ ] 登录成功返回 Token
  - [ ] 错误密码登录失败
  - [ ] 连续 5 次失败被锁定
  - [ ] Token 验证生效
  - [ ] 登出后 Token 失效

性能验证:
  - [ ] 注册接口响应时间 < 100ms
  - [ ] 登录接口响应时间 < 100ms
  - [ ] 查询接口响应时间 < 50ms

十二、常见问题与解决方案

12.1 开发中遇到的坑

问题 原因 解决方案
Token 解析失败 secret 长度不足 256 位 使用 32 字节以上的字符串
Redis 连接超时 未启动 Redis 服务 启动 Redis 或禁用 Redis 配置
事务不回滚 异常被 catch 了 确保异常抛出给事务管理器
循环依赖 Service 互相注入 使用 @Lazy 或重构设计

12.2 优化建议

yaml 复制代码
性能优化:
  - 添加 Redis 缓存用户信息
  - 使用 Caffeine 本地二级缓存
  - 批量查询优化

安全加固:
  - 添加接口限流
  - 敏感信息脱敏
  - 增加验证码功能

可观测性:
  - 集成 Actuator
  - 添加链路追踪
  - 配置告警规则

写在最后

从零到一,我们用 Cursor 完成了整个用户中心的开发。回顾整个过程:

yaml 复制代码
时间统计:
  项目初始化: 5 分钟
  实体类 + DTO: 10 分钟
  Mapper + Service: 20 分钟
  Controller + 拦截器: 15 分钟
  异常处理 + 配置: 10 分钟
  单元测试: 15 分钟
  ---
  总计: 约 1.5 小时(纯 AI 生成时间)
  
如果纯手工开发:
  预估时间: 6-8 小时
  效率提升: 4-5 倍

关键心得

  1. 指令越清晰,代码越完美------每次提问前,花 30 秒组织语言
  2. 分步生成比一次生成更可靠------先生成接口定义,再生成实现
  3. 审查比生成更重要------AI 生成的代码需要人工把关
  4. 规范文件是效率倍增器 ------.cursorrules 让 AI 输出更符合项目风格

希望这个实战案例能帮你建立起使用 AI 开发的信心。动手试试吧,你会发现 AI 编程远比你想象的强大。


如需获取更多关于 AI 编程助手实战技巧、Cursor 深度玩法、模型选型策略、提示词工程经验、AI 驱动开发工作流等内容,请持续关注本专栏 《AI Coding 实战之路》 系列文章。

相关推荐
昵称为空C3 小时前
在复杂SpringBoot项目中基于hutool实现临时添加多数据源案例
spring boot·后端
计算机学姐4 小时前
基于SpringBoot的房屋交易系统
java·vue.js·spring boot·后端·spring·intellij-idea·mybatis
java1234_小锋4 小时前
SpringBoot 4 + Spring Security 7 + Vue3 前后端分离项目设计最佳实践
spring boot·后端·spring
我登哥MVP4 小时前
【Spring6笔记】 - 12 - 代理模式
java·spring boot·笔记·spring·代理模式·aop
Flittly5 小时前
【SpringAIAlibaba新手村系列】(17)百炼 RAG 知识库应用
java·人工智能·spring boot·spring·ai
Rick19935 小时前
spring boot和mybatis框架的设计思想和核心逻辑
spring boot·后端·mybatis
隐退山林6 小时前
JavaEE进阶:导读&SpringBoot快速上手
java·spring boot·java-ee
weixin_704266056 小时前
读取Excel 和 显示预约人数
spring boot·mybatis·excel
悟空码字6 小时前
SpringBoot + 微信支付实现“扫码开门,取货自动扣款”售货柜
java·spring boot·后端