16 KiB
title, url, publishedTime
| title | url | publishedTime |
|---|---|---|
| 用户信息修改接口开发 - 犬小哈专栏 | https://www.quanxiaoha.com/column/10311.html | null |
本小节中,我们正式进入用户信息修改接口的开发工作。
接口定义
接口地址
POST /user/update
注意: 因为此接口涉及到文件上传,所以需要使用表单提交。即
Content-Type为application/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-biz 的 pom.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, 接口调试通过~