当前位置: 首页 > article >正文

用户模块——自定义业务异常

用户信息查询接口

        在开发用户系统时,一个最基本的需求就是获取用户的个人信息,比如昵称、头像、改名卡次数等。本部分将介绍如何实现一个用户信息查询接口,并结合项目中的用户背包表,查询用户是否有可用的改名卡。

1. 用户信息查询逻辑

        在项目中,用户可以通过 API 调用 查询自己的个人信息,返回的数据通常包括用户的基本信息以及一些额外属性(如改名卡次数)。
在代码层面,我们可以直接调用 userService.getUserInfo(uid) 方法获取用户信息,并使用 ApiResult.success() 包装返回值,让前端可以方便地解析结果:

return ApiResult.success(userService.getUserInfo(RequestHolder.get().getUid()));

这里的 RequestHolder.get().getUid() 负责从 ThreadLocal 变量中获取 uid,这个 uid 是通过前端传来的 Token 解析 得到的(稍后详细介绍)。

2. 获取用户的改名卡次数

为什么需要查询背包表?
        改名卡并不是直接存储在 User 表里的,而是放在个人背包表中(例如 user_item_bag),所以我们需要去这个表里查询用户是否拥有可用的改名卡。

查询方式如下:

  1. 通过 uiditemid 查询该用户是否拥有改名卡。

  2. status 字段用于判断改名卡是否可用。

  3. 在项目中,我们有一个 YesOrNo 公共枚举,它定义了 0 代表无效,1 代表有效。

代码示例:

UserItem renameCard = userItemBagMapper.findItemByUidAndItemId(uid, ITEM_ID_RENAME_CARD);
boolean hasRenameCard = renameCard != null && renameCard.getStatus() == YesOrNo.YES;

最终,我们可以将 hasRenameCard 的值加入 UserInfo 返回给前端。

3. Token 解析与用户身份确定

        前端请求接口时,需要在 请求头(header) 中携带 token,这个 token 是用户登录后分配的身份凭证。例如:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

后端会解析这个 token,并从中提取出 uid,然后保存在 ThreadLocal 变量中,确保在整个请求生命周期内都能获取到该用户信息:

public class RequestHolder {
    private static final ThreadLocal<UserToken> threadLocal = new ThreadLocal<>();

    public static void set(UserToken userToken) {
        threadLocal.set(userToken);
    }

    public static UserToken get() {
        return threadLocal.get();
    }

    public static void remove() {
        threadLocal.remove();
    }
}

        这样,业务代码就可以随时通过 RequestHolder.get().getUid() 获取当前用户的 uid,不需要每个方法都手动传递 uid 了。

总结

  • 用户信息接口通过 userService.getUserInfo(uid) 获取用户数据,并返回给前端。

  • 改名卡存储在 用户背包表 中,需要通过 uid + itemid 查询,并用 status 字段判断是否可用。

  • 前端请求时需要携带 token,后端解析 token 并存入 ThreadLocal,确保在整个请求中都能获取到 uid

        这一部分实现了用户基本信息的查询,下一步,我们将开发用户改名接口,让用户可以修改自己的昵称! 🚀

用户改名接口

用户希望修改自己的昵称,但为了防止滥用,我们需要做一些必要的校验,比如:

  • 新名字不能为空,长度不能太长。

  • 用户必须拥有有效的改名卡才能改名。

  • 处理改名前后端的交互方式。

这一部分,我们来实现一个安全、合规的用户改名接口

1. 注册改名接口

UserController 中,我们用 @PutMapping("/name") 注册接口:

@PutMapping("/name")
public ApiResult<?> modifyName(@Valid @RequestBody ModifyNameRequest request) {
    Long uid = RequestHolder.get().getUid(); // 获取当前用户ID
    return userService.modifyName(uid, request.getNewName());
}

这里的关键点:

  • @PutMapping("/name") 代表这是一个 PUT 请求,适用于更新操作。

  • @RequestBody ModifyNameRequest request 用于接收前端提交的 JSON 数据ModifyNameRequest 结构如下:

public class ModifyNameRequest {
    @NotBlank(message = "新名称不能为空")
    @Length(max = 20, message = "新名称不能超过20个字符")
    private String newName;

    // 省略getter/setter
}
  • @Valid 注解启用了参数校验,如果新名字为空或超过 20 个字符,就会抛出异常(下一节介绍如何捕获异常)。

2. 处理改名逻辑

UserService 里实现 modifyName() 方法:

public ApiResult<?> modifyName(Long uid, String newName) {
    // 1. 检查新名称是否和当前名称相同
    User user = userMapper.findById(uid);
    if (user == null) {
        return ApiResult.fail(CommonErrorEnum.USER_NOT_FOUND);
    }
    if (user.getName().equals(newName)) {
        return ApiResult.fail(CommonErrorEnum.NAME_NO_CHANGE);
    }

    // 2. 检查用户是否有可用的改名卡
    UserItem renameCard = userItemBagMapper.findItemByUidAndItemId(uid, ITEM_ID_RENAME_CARD);
    if (renameCard == null || renameCard.getStatus() != YesOrNo.YES) {
        return ApiResult.fail(CommonErrorEnum.NO_RENAME_CARD);
    }

    // 3. 扣除改名卡,并更新用户名
    userItemBagMapper.useItem(renameCard.getId()); // 标记改名卡已使用
    userMapper.updateUserName(uid, newName);
    
    return ApiResult.success("改名成功");
}

这里的关键点:
检查新名称是否合法(不能和当前名称相同)。
查询用户的改名卡,确保用户拥有可用的改名卡。
扣除改名卡,并更新用户昵称

3. 处理异常情况

Spring 提供了 @ExceptionHandler 让我们可以捕获异常,统一返回友好的错误信息。
GlobalExceptionHandler 里,我们添加对参数校验失败的处理:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult<?> handleValidationException(MethodArgumentNotValidException e) {
    String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
    return ApiResult.fail(CommonErrorEnum.INVALID_PARAM.getCode(), message);
}

这样,当用户提交的 newName 为空或超长时,系统会自动返回:

{
    "code": 400,
    "message": "新名称不能为空"
}

此外,我们还可以拦截所有 非预期异常,避免直接暴露给前端:

@ExceptionHandler(Throwable.class)
public ApiResult<?> handleUnknownException(Throwable e) {
    log.error("系统异常", e);
    return ApiResult.fail(CommonErrorEnum.SYSTEM_ERROR);
}

4. 前后端交互示例

前端请求示例:

PUT /api/user/name
Content-Type: application/json
Authorization: Bearer <用户的Token>

{
    "newName": "新的昵称"
}

正常返回:

{
    "code": 200,
    "message": "改名成功"
}

错误示例(改名卡不足):

{
    "code": 403,
    "message": "没有可用的改名卡"
}

总结

  • @PutMapping("/name") 注册了改名接口,接收前端的 JSON 请求。

  • 参数校验:使用 @Valid 确保新名称不能为空且不超长

  • 业务逻辑:检查 改名卡 是否可用,成功后扣除改名卡更新用户名

  • 全局异常处理:拦截校验失败 & 其他异常,避免直接抛出错误给前端。

这样,一个完整的用户改名接口就开发完成了! 🎉
下一步,我们来看看全局异常处理,让错误信息更加清晰统一! 🚀

全局异常处理(Global Exception Handler)

        在开发接口时,异常处理是非常重要的环节。一个好的异常处理机制,能够让我们的系统更加稳定,并且可以给前端返回清晰统一的错误信息。

本节内容,我们来实现一个全局异常处理器,让系统能够优雅地应对各种异常。

1. 为什么需要全局异常处理?

在项目中,异常可能来自多个地方,比如:

  • 用户输入错误(参数为空、格式错误等)。

  • 业务逻辑异常(改名卡不足、用户不存在等)。

  • 系统异常(数据库连接失败、空指针异常等)。

如果每个接口都手动 try-catch,不仅代码冗余,还容易遗漏某些异常。全局异常处理可以拦截所有异常,统一处理后再返回给前端。

2. 如何实现全局异常处理?

        Spring 提供了 @ControllerAdvice@ExceptionHandler 让我们可以全局捕获异常,并返回统一格式的 JSON 响应。

我们创建一个 GlobalExceptionHandler 处理所有的异常情况。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理参数校验失败(@Valid 校验)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult<?> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        return ApiResult.fail(CommonErrorEnum.INVALID_PARAM.getCode(), message);
    }

    /**
     * 处理业务异常(自定义异常)
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResult<?> handleBusinessException(BusinessException e) {
        return ApiResult.fail(e.getErrorCode(), e.getMessage());
    }

    /**
     * 处理所有未知异常(兜底)
     */
    @ExceptionHandler(Throwable.class)
    public ApiResult<?> handleUnknownException(Throwable e) {
        log.error("系统异常", e);
        return ApiResult.fail(CommonErrorEnum.SYSTEM_ERROR);
    }
}

3. 代码解析

1️⃣ 处理参数校验失败
如果用户输入的数据不符合 @Valid 校验(如改名时名字为空),会抛出 MethodArgumentNotValidException,我们在这里提取错误信息并返回给前端:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult<?> handleValidationException(MethodArgumentNotValidException e) {
    String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
    return ApiResult.fail(CommonErrorEnum.INVALID_PARAM.getCode(), message);
}

示例错误返回

{
    "code": 400,
    "message": "新名称不能为空"
}

2️⃣ 处理业务异常
对于我们自己定义的业务异常(如用户没有改名卡),可以创建 BusinessException 并统一处理:

public class BusinessException extends RuntimeException {
    private final int errorCode;

    public BusinessException(CommonErrorEnum errorEnum) {
        super(errorEnum.getMessage());
        this.errorCode = errorEnum.getCode();
    }

    public int getErrorCode() {
        return errorCode;
    }
}

然后,在全局异常处理器中捕获:

@ExceptionHandler(BusinessException.class)
public ApiResult<?> handleBusinessException(BusinessException e) {
    return ApiResult.fail(e.getErrorCode(), e.getMessage());
}

示例错误返回

{
    "code": 403,
    "message": "没有可用的改名卡"
}

3️⃣ 兜底处理所有未知异常
如果某个异常没有被前面捕获,我们用 Throwable.class 兜底处理,同时记录日志,方便排查问题:

@ExceptionHandler(Throwable.class)
public ApiResult<?> handleUnknownException(Throwable e) {
    log.error("系统异常", e);
    return ApiResult.fail(CommonErrorEnum.SYSTEM_ERROR);
}

示例错误返回

{
    "code": 500,
    "message": "系统错误,请稍后再试"
}

4. 错误码枚举(CommonErrorEnum)

为了让错误信息更规范,我们用一个枚举类来管理所有的错误码:

@Getter
public enum CommonErrorEnum {
    INVALID_PARAM(400, "请求参数错误"),
    USER_NOT_FOUND(404, "用户不存在"),
    NAME_NO_CHANGE(400, "新名称不能与当前名称相同"),
    NO_RENAME_CARD(403, "没有可用的改名卡"),
    SYSTEM_ERROR(500, "系统错误,请稍后再试");

    private final int code;
    private final String message;

    CommonErrorEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }
}

这样,所有的错误信息都在这里管理,后续扩展也非常方便! 🎯

5. 前后端交互示例

前端请求

PUT /api/user/name
Content-Type: application/json

{
    "newName": ""
}

返回(参数校验失败)

{
    "code": 400,
    "message": "新名称不能为空"
}

返回(没有改名卡)

{
    "code": 403,
    "message": "没有可用的改名卡"
}

返回(系统异常)

{
    "code": 500,
    "message": "系统错误,请稍后再试"
}

6. 总结

  • 全局异常处理统一管理错误,避免代码中大量 try-catch

  • @ExceptionHandler 用于捕获特定异常,让错误信息友好一致

  • 错误码枚举CommonErrorEnum)可以集中管理错误信息,方便扩展

  • 日志记录能帮助我们快速排查问题,避免未知错误影响用户体验。

至此,我们的全局异常处理机制完成了! 🎉
现在,无论用户输入错误,还是系统出现异常,前端都能收到清晰规范的错误信息! 🚀


http://www.kler.cn/a/613293.html

相关文章:

  • Linux下的socket演示程序3(udp)
  • 新手村:逻辑回归-理解03:逻辑回归中的最大似然函数
  • JavaScript 中Object.assign()和展开运算符在对象合并时的区别,各自的适用场景是什么?
  • 线程同步——读写锁
  • YEUSAI网络广播与舞台音响系统成功应用于武夷山文化馆
  • Windows上使用bash脚本
  • [运维]Linux系统扩容磁盘空间-将未分配的空间进行整合分配
  • 3.26学习总结
  • 5、vim编辑和shell编程【超详细】
  • 书籍学习|基于Java+vue的书籍学习平台(源码+数据库+文档)
  • CXL UIO Direct P2P学习
  • 运维规则之总结(Summary of Operation and Maintenance Rules)
  • Ingredient-oriented Multi-Degradation Learning for Image Restoration论文阅读
  • leetcoed0044. 通配符匹配 hard
  • DQL语句-distinct去重
  • git 问题 master has no tracked branch
  • 第一卷:京口草鞋摊的野望(1-36回)正反人物群像
  • 管家婆财贸ERP BD002.存货销售订单汇总看板
  • 中国劳动统计年鉴pdf+excel(1989-2024年)
  • CSS——变换、过度与动画