weblog/doc/9、用户服务搭建与开发/9.3 用户信息修改接口开发.md
2025-02-17 11:57:55 +08:00

16 KiB
Raw Blame History

title, url, publishedTime
title url publishedTime
用户信息修改接口开发 - 犬小哈专栏 https://www.quanxiaoha.com/column/10311.html null

本小节中,我们正式进入用户信息修改接口的开发工作。

接口定义

接口地址

POST /user/update

注意: 因为此接口涉及到文件上传,所以需要使用表单提交。即 Content-Typeapplication/x-www-form-urlencoded

入参

字段名 含义
avatar 头像
birthday 生日,如 2024-12-12
nickname 昵称
xiaohashuId 小哈书 ID
sex 性别
backgroundImg 背景图
introduction 个人介绍

出参

{
	"success": true,
	"message": null,
	"errorCode": null,
	"data": null
}

创建入参 VO 实体类

本小节中涉及添加的类,大致如上图所示。首先,创建 /model/vo 包,并新建 UpdateUserInfoReqVO 入参实体类,代码如下:

package com.quanxiaoha.xiaohashu.user.biz.model.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;

/**
 * @author: 犬小哈
 * @date: 2024/4/7 15:17
 * @version: v1.0.0
 * @description: 修改用户信息
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UpdateUserInfoReqVO {

    /**
     * 头像
     */
    private MultipartFile avatar;

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

    /**
     * 小哈书 ID
     */
    private String xiaohashuId;

    /**
     * 性别
     */
    private Integer sex;

    /**
     * 生日
     */
    private LocalDate birthday;

    /**
     * 个人介绍
     */
    private String introduction;

    /**
     * 背景图
     */
    private MultipartFile backgroundImg;

}

Tip

: 由于用户修改信息时,可能只修改某一项,所以单独在业务层进行条件判断,而不是每个字段都添加校验注解。

封装参数校验工具类

在小红书中,修改用户信息时,也会有相关校验,如昵称,必须是设置 2-24个字符不能使用@《/等字符 等等,如下图所示:

接下来,我们来封装一个参数校验工具类。编辑 xiaohashu-common 公共模块,在 /utils 包下新建 ParamUtils 工具类:

代码如下:

package com.quanxiaoha.framework.common.util;

import java.util.regex.Pattern;

/**
 * @author: 犬小哈
 * @date: 2024/4/15 16:42
 * @version: v1.0.0
 * @description: 参数条件工具
 **/
public final class ParamUtils {
    private ParamUtils() {
    }

    // ============================== 校验昵称 ==============================
    // 定义昵称长度范围
    private static final int NICK_NAME_MIN_LENGTH = 2;
    private static final int NICK_NAME_MAX_LENGTH = 24;

    // 定义特殊字符的正则表达式
    private static final String NICK_NAME_REGEX = "[!@#$%^&*(),.?\":{}|<>]";

    /**
     * 昵称校验
     * 
     * @param nickname
     * @return
     */
    public static boolean checkNickname(String nickname) {
        // 检查长度
        if (nickname.length() < NICK_NAME_MIN_LENGTH || nickname.length() > NICK_NAME_MAX_LENGTH) {
            return false;
        }

        // 检查是否含有特殊字符
        Pattern pattern = Pattern.compile(NICK_NAME_REGEX);
        return !pattern.matcher(nickname).find();
    }

    // ============================== 校验小哈书号 ==============================
    // 定义 ID 长度范围
    private static final int ID_MIN_LENGTH = 6;
    private static final int ID_MAX_LENGTH = 15;

    // 定义正则表达式
    private static final String ID_REGEX = "^[a-zA-Z0-9_]+$";

    /**
     * 小哈书 ID 校验
     * 
     * @param xiaohashuId
     * @return
     */
    public static boolean checkXiaohashuId(String xiaohashuId) {
        // 检查长度
        if (xiaohashuId.length() < ID_MIN_LENGTH || xiaohashuId.length() > ID_MAX_LENGTH) {
            return false;
        }
        // 检查格式
        Pattern pattern = Pattern.compile(ID_REGEX);
        return pattern.matcher(xiaohashuId).matches();
    }

    /**
     * 字符串长度校验
     * 
     * @param str
     * @param length
     * @return
     */
    public static boolean checkLength(String str, int length) {
        // 检查长度
        if (str.isEmpty() || str.length() > length) {
            return false;
        }
        return true;
    }
}

定义了 3 个后续业务层需要用到的校验方法,主要是使用正则表达式来校验:

  • 昵称,对长度与特殊字符的进行校验;
  • 小哈书 ID仅能使用英文、数字、下划线;
  • 个人介绍,需校验字符串长度,不能多于 100 字;

定义错误枚举

接着,编辑 ResponseCodeEnum , 添加一些等会要用到的错误枚举值,代码如下:

package com.quanxiaoha.xiaohashu.user.biz.enums;

import com.quanxiaoha.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @date: 2023-08-15 10:33
 * @description: 响应异常码
 **/
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {

	// 省略..

    // ----------- 业务异常状态码 -----------
    NICK_NAME_VALID_FAIL("USER-20001", "昵称请设置2-24个字符不能使用@《/等特殊字符"),
    XIAOHASHU_ID_VALID_FAIL("USER-20002", "小哈书号请设置6-15个字符仅可使用英文必须、数字、下划线"),
    SEX_VALID_FAIL("USER-20003", "性别错误"),
    INTRODUCTION_VALID_FAIL("USER-20004", "个人简介请设置1-100个字符"),
    ;

    // 异常码
    private final String errorCode;
    // 错误信息
    private final String errorMessage;

}

另外,在 /eumns 包下,再创建一个性别枚举,并封装一个校验性别的方法,代码如下:

package com.quanxiaoha.xiaohashu.user.biz.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;

/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @date: 2023-08-15 10:33
 * @description: 性别
 **/
@Getter
@AllArgsConstructor
public enum SexEnum {

    WOMAN(0),
    MAN(1);

    private final Integer value;

    public static boolean isValid(Integer value) {
        for (SexEnum loginTypeEnum : SexEnum.values()) {
            if (Objects.equals(value, loginTypeEnum.getValue())) {
                return true;
            }
        }
        return false;
    }

}

添加依赖

编辑 xiaohashu-user-bizpom.xml 文件,添加以下之前封装好的业务组件依赖:

		// 省略...
		
		<!-- 业务接口日志组件 -->
        <dependency>
            <groupId>com.quanxiaoha</groupId>
            <artifactId>xiaoha-spring-boot-starter-biz-operationlog</artifactId>
        </dependency>

        <!-- 上下文组件 -->
        <dependency>
            <groupId>com.quanxiaoha</groupId>
            <artifactId>xiaoha-spring-boot-starter-biz-context</artifactId>
        </dependency>

        <!-- Jackson 组件 -->
        <dependency>
            <groupId>com.quanxiaoha</groupId>
            <artifactId>xiaoha-spring-boot-starter-jackson</artifactId>
        </dependency>
        
        // 省略...

依赖添加完毕后,记得刷新一下 Maven 依赖。

编写 service 业务层

创建 /service 包,并新建 UserService 接口,声明一个更新用户信息方法,代码如下:

package com.quanxiaoha.xiaohashu.user.biz.service;

import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.user.biz.model.vo.UpdateUserInfoReqVO;

/**
 * @author: 犬小哈
 * @date: 2024/4/7 15:41
 * @version: v1.0.0
 * @description: 用户业务
 **/
public interface UserService {

    /**
     * 更新用户信息
     *
     * @param updateUserInfoReqVO
     * @return
     */
    Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO);
}

/service 包下创建 /impl 实现类包,并创建上述接口的实现类,代码如下:

package com.quanxiaoha.xiaohashu.user.biz.service.impl;

import com.google.common.base.Preconditions;
import com.quanxiaoha.framework.biz.context.holder.LoginUserContextHolder;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.framework.common.util.ParamUtils;
import com.quanxiaoha.xiaohashu.user.biz.domain.dataobject.UserDO;
import com.quanxiaoha.xiaohashu.user.biz.domain.mapper.UserDOMapper;
import com.quanxiaoha.xiaohashu.user.biz.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.user.biz.enums.SexEnum;
import com.quanxiaoha.xiaohashu.user.biz.model.vo.UpdateUserInfoReqVO;
import com.quanxiaoha.xiaohashu.user.biz.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;

/**
 * @author: 犬小哈
 * @date: 2024/4/7 15:41
 * @version: v1.0.0
 * @description: 用户业务
 **/
@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Resource
    private UserDOMapper userDOMapper;

    /**
     * 更新用户信息
     *
     * @param updateUserInfoReqVO
     * @return
     */
    @Override
    public Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO) {
        UserDO userDO = new UserDO();
        // 设置当前需要更新的用户 ID
        userDO.setId(LoginUserContextHolder.getUserId());
        // 标识位:是否需要更新
        boolean needUpdate = false;
        
        // 头像
        MultipartFile avatarFile = updateUserInfoReqVO.getAvatar();

        if (Objects.nonNull(avatarFile)) {
            // todo: 调用对象存储服务上传文件
        }

        // 昵称
        String nickname = updateUserInfoReqVO.getNickname();
        if (StringUtils.isNotBlank(nickname)) {
            Preconditions.checkArgument(ParamUtils.checkNickname(nickname), ResponseCodeEnum.NICK_NAME_VALID_FAIL.getErrorMessage());
            userDO.setNickname(nickname);
            needUpdate = true;
        }

        // 小哈书号
        String xiaohashuId = updateUserInfoReqVO.getXiaohashuId();
        if (StringUtils.isNotBlank(xiaohashuId)) {
            Preconditions.checkArgument(ParamUtils.checkXiaohashuId(xiaohashuId), ResponseCodeEnum.XIAOHASHU_ID_VALID_FAIL.getErrorMessage());
            userDO.setXiaohashuId(xiaohashuId);
            needUpdate = true;
        }

        // 性别
        Integer sex = updateUserInfoReqVO.getSex();
        if (Objects.nonNull(sex)) {
            Preconditions.checkArgument(SexEnum.isValid(sex), ResponseCodeEnum.SEX_VALID_FAIL.getErrorMessage());
            userDO.setSex(sex);
            needUpdate = true;
        }

        // 生日
        LocalDate birthday = updateUserInfoReqVO.getBirthday();
        if (Objects.nonNull(birthday)) {
            userDO.setBirthday(birthday);
            needUpdate = true;
        }

        // 个人简介
        String introduction = updateUserInfoReqVO.getIntroduction();
        if (StringUtils.isNotBlank(introduction)) {
            Preconditions.checkArgument(ParamUtils.checkLength(introduction, 100), ResponseCodeEnum.INTRODUCTION_VALID_FAIL.getErrorMessage());
            userDO.setIntroduction(introduction);
            needUpdate = true;
        }

        // 背景图
        MultipartFile backgroundImgFile = updateUserInfoReqVO.getBackgroundImg();
        if (Objects.nonNull(backgroundImgFile)) {
            // todo: 调用对象存储服务上传文件
        }

        if (needUpdate) {
            // 更新用户信息
            userDO.setUpdateTime(LocalDateTime.now());
            userDOMapper.updateByPrimaryKeySelective(userDO);
        }
        return Response.success();
    }
}

解释一下代码逻辑:

  • 初始化一个 UserDO , 用于更新数据库;
  • 通过 LoginUserContextHolder.getUserId() 工具类,从上下文中拿到当前需要更新的用户 ID;
  • 初始化一个 needUpdate 标识位,用于判断最终是否需要更新数据库;
  • 然后,就是各项用户信息的校验与设置了。针对头像、背景图需要调用对象存储服务上传文件,先写个 todo , 后面小节会讲如何通过 Feign 进行服务间调用。
  • 最后,如果 needUpdate 字段为 true , 说明用户更新了某些信息,则最终更新数据库。

新建 controller 层

新建 /controller 包,并新建 UserController 控制器,添加 /user/update 接口,代码如下:

package com.quanxiaoha.xiaohashu.user.biz.controller;

import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.user.biz.model.vo.UpdateUserInfoReqVO;
import com.quanxiaoha.xiaohashu.user.biz.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: 犬小哈
 * @date: 2024/4/4 13:22
 * @version: v1.0.0
 * @description: 用户
 **/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Resource
    private UserService userService;

    /**
     * 用户信息修改
     * 
     * @param updateUserInfoReqVO
     * @return
     */
    @PostMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Response<?> updateUserInfo(@Validated UpdateUserInfoReqVO updateUserInfoReqVO) {
        return userService.updateUserInfo(updateUserInfoReqVO);
    }

}

注意:请勿添加切面日志注解 @ApiOperationLog此接口包含文件流上传Jackson 序列化会有问题!!!

调整服务 Url

因为我们又单独拆分了一个用户服务,用户服务的接口以 /user/** 为前缀,是很合适的。但是之前认证服务中,也有用到 /user/** 为前缀,就显得不太规范了。这里重构一下,编辑 xiaohashu-auth 认证服务中的 UserController , 将 /user 前缀删除掉,区分开来,如下图所示:

同时,网关服务中的相关接口校验也需要适配一下, 将 /user 删除:

配置网关路由转发

编辑 xiaohashu-gateway 网关中的 application.yml 文件,将用户服务的路由转发也添加上,等会直接通过网关转发来测试接口:

配置项如下:

spring:
  cloud:
    gateway:
      routes:
        // 省略...
        - id: user
          uri: lb://xiaohashu-user
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1

自测一波

最后,我们来测试一下。将认证服务、网关服务、用户服务同时跑起来,如下所示:

测试一波 /user/update 接口,因为是走网关转发,所以实际的地址为 localhost:8000/user/user/update , 如下:

注意:以表单的方式请求,同时携带上 Token 令牌。

勾选 form-data 选项,并填写接口所需入参:

点击发送按钮可以看到服务端响应成功。打开数据库确认一下该用户信息是否更新成功了如下OK, 接口调试通过~

本小节源码下载

https://t.zsxq.com/oev24