21 KiB
title, url, publishedTime
| title | url | publishedTime |
|---|---|---|
| 代码重构:用户注册功能 - 犬小哈专栏 | https://www.quanxiaoha.com/column/10315.html | null |
本小节中,我们来重构一下登录接口中的自动注册用户部分代码,将这部分功能放置到用户服务中,以接口的形式提供出来,由认证服务通过 Feign 去调用,而不是自己去操作 t_user 用户表。
认证服务与用户服务的职责划分
为什么要拆分到用户服务中呢? 先来明确划分一下,两个服务各自应该负责的部分:
- 认证服务:负责处理所有与用户身份验证相关的操作。它主要确保用户身份的真实性和合法性,管理用户登录、登出以及令牌(token)的生成与密码加密等。
- 用户服务:负责用户的管理操作,包括用户的注册、个人信息更新与查询、用户角色和权限等等。它主要处理与用户数据相关的业务逻辑。
既然如此,那么之前登录接口中,操作用户表的相关操作,都是不规范的,需要单独剥离到用户服务中,如下图所示:
从项目结构角度看,下面标注的所有代码,都需要移动到用户服务中,而认证服务中的,在抽离完毕后,需要最终删除掉:
明确目标后,这就来逐步重构一下项目。
复制 DO 类、Mapper 接口、XML 映射文件
首先,将认证服务中的相关 DO 数据库实体类、Mapper 接口、XML 映射文件,复制一份到用户服务中,如下图标注所示:
复制到 xiaohashu-user-biz 模块中的 /domain/dataobject 和 /domain/mapper 包下后,文件会有爆红情况,将包路径改正确即可解决。
复制过去的 XML 映射文件,同样有爆红情况,也是包路径的问题,将路径中的 auth 修改为 user.biz 即可解决,注意,每个文件都需要改一下,改完后,重新刷新一下 Maven 依赖。
重构启动任务
基础工作完成后,首先将认证服务中 /runner 包下的 PushRolePermissions2RedisRunner 推送角色权限数据到 Redis 启动任务,转移到用户服务中。
整合 Redis
该功能需要操作 Redis, 还需要为用户服务整合一波 Redis, 编辑 xiaohashu-user-biz 模块的 pom.xml 文件,添加相关依赖:
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
添加配置
编辑 xiaohashu-user-biz 模块中的 application.yml 文件,添加 Redis 相关配置项:
spring:
// 省略...
data:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: 127.0.0.1 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: qwe123!@# # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
connect-timeout: 5s # 链接超时时间
lettuce:
pool:
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
添加相关类
将认证服务中的 Redis 配置类、常量类,以及 Runner 启动任务类,如上图标注所示,复制到用户服务中。复制过去后,无需改动代码,仅需将 RedisKeyConstants 修改一下,只保留用户服务需要用到的常量定义,验证码归认证服务管,这部分代码都删除掉,代码如下:
package com.quanxiaoha.xiaohashu.user.biz.constant;
/**
* @author: 犬小哈
* @date: 2024/5/21 15:04
* @version: v1.0.0
* @description: TODO
**/
public class RedisKeyConstants {
/**
* 小哈书全局 ID 生成器 KEY
*/
public static final String XIAOHASHU_ID_GENERATOR_KEY = "xiaohashu.id.generator";
/**
* 用户角色数据 KEY 前缀
*/
private static final String USER_ROLES_KEY_PREFIX = "user:roles:";
/**
* 角色对应的权限集合 KEY 前缀
*/
private static final String ROLE_PERMISSIONS_KEY_PREFIX = "role:permissions:";
/**
* 用户对应的角色集合 KEY
* @param userId
* @return
*/
public static String buildUserRoleKey(Long userId) {
return USER_ROLES_KEY_PREFIX + userId;
}
/**
* 构建角色对应的权限集合 KEY
* @param roleKey
* @return
*/
public static String buildRolePermissionsKey(String roleKey) {
return ROLE_PERMISSIONS_KEY_PREFIX + roleKey;
}
}
至此,推送角色权限数据到 Redis 启动任务就重构完毕了。小伙伴记得重启用户服务,自测一下,看看代码运行有没有问题,确保没有问题了,再进行下一步的重构,一步一个脚印。另外,功能测试正常,就可以将已经转移过来的相关代码删除掉了,现在这部分工作,已经交接给用户服务来做了。
重构用户注册
接着,开始重构用户注册部分。
添加 xiaohashu-user-api 依赖
编辑项目最外层的 pom.xml, 添加如下依赖,将 xiaohashu-user-api 模块的依赖统一管理起来:
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaohashu-user-api</artifactId>
<version>${revision}</version>
</dependency>
编辑 xiaohashu-auth 认证服务的 pom.xml, 添加该模块,因为需要调用用户服务:
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaohashu-user-api</artifactId>
</dependency>
记得刷新一波 Maven 依赖。
添加 DTO 实体类
什么是 DTO 实体类?和 VO 实体类有什么区别?
DTO是一个用于数据传输的对象,通常服务之间传递数据时,会定义一个DTO实体类。VO是一个用于表现层的对象,通常用来封装页面数据或前端显示的数据。它用于控制层和视图层之间的数据传递。
编辑 xiaohashu-user-api 模块,创建 /dto/req 包,用于存放请求相关的 DTO 实体类,代码如下:
package com.quanxiaoha.xiaohashu.user.dto.req;
import com.quanxiaoha.framework.common.validator.PhoneNumber;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author: 犬小哈
* @date: 2024/4/7 15:17
* @version: v1.0.0
* @description: 用户注册
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RegisterUserReqDTO {
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@PhoneNumber
private String phone;
}
biz 模块添加 api 模块依赖
为了能够在 xiaohashu-user-biz 模块中使用 RegisterUserReqDTO 实体类,还需要编辑小哈书最外层的 pom.xml 文件,先将 xiaohashu-user-api 模块的版本号管理起来:
<!-- 统一依赖管理 -->
<dependencyManagement>
<dependencies>
// 省略...
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaohashu-user-api</artifactId>
<version>${revision}</version>
</dependency>
// 省略...
</dependencies>
</dependencyManagement>
接着,编辑 xiaohashu-user-biz 模块的 pom.xml, 添加 xiaohashu-user-api 依赖:
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaohashu-user-api</artifactId>
</dependency>
编写 service 业务层
回到 xiaohashu-user-biz 模块中,编辑 UserService , 声明一个用户注册方法:
package com.quanxiaoha.xiaohashu.user.biz.service;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.user.biz.model.vo.UpdateUserInfoReqVO;
import com.quanxiaoha.xiaohashu.user.dto.req.RegisterUserReqDTO;
/**
* @author: 犬小哈
* @date: 2024/4/7 15:41
* @version: v1.0.0
* @description: 用户业务
**/
public interface UserService {
// 省略...
/**
* 用户注册
*
* @param registerUserReqDTO
* @return
*/
Response<Long> register(RegisterUserReqDTO registerUserReqDTO);
}
在其实现类中,实现上述方法,代码如下:
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.enums.DeletedEnum;
import com.quanxiaoha.framework.common.enums.StatusEnum;
import com.quanxiaoha.framework.common.exception.BizException;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.framework.common.util.JsonUtils;
import com.quanxiaoha.framework.common.util.ParamUtils;
import com.quanxiaoha.xiaohashu.oss.api.FileFeignApi;
import com.quanxiaoha.xiaohashu.user.biz.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.user.biz.constant.RoleConstants;
import com.quanxiaoha.xiaohashu.user.biz.domain.dataobject.RoleDO;
import com.quanxiaoha.xiaohashu.user.biz.domain.dataobject.UserDO;
import com.quanxiaoha.xiaohashu.user.biz.domain.dataobject.UserRoleDO;
import com.quanxiaoha.xiaohashu.user.biz.domain.mapper.RoleDOMapper;
import com.quanxiaoha.xiaohashu.user.biz.domain.mapper.UserDOMapper;
import com.quanxiaoha.xiaohashu.user.biz.domain.mapper.UserRoleDOMapper;
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.rpc.OssRpcService;
import com.quanxiaoha.xiaohashu.user.biz.service.UserService;
import com.quanxiaoha.xiaohashu.user.dto.req.RegisterUserReqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
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 UserRoleDOMapper userRoleDOMapper;
@Resource
private RoleDOMapper roleDOMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 省略...
/**
* 用户注册
*
* @param registerUserReqDTO
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Response<Long> register(RegisterUserReqDTO registerUserReqDTO) {
String phone = registerUserReqDTO.getPhone();
// 先判断该手机号是否已被注册
UserDO userDO1 = userDOMapper.selectByPhone(phone);
log.info("==> 用户是否注册, phone: {}, userDO: {}", phone, JsonUtils.toJsonString(userDO1));
// 若已注册,则直接返回用户 ID
if (Objects.nonNull(userDO1)) {
return Response.success(userDO1.getId());
}
// 否则注册新用户
// 获取全局自增的小哈书 ID
Long xiaohashuId = redisTemplate.opsForValue().increment(RedisKeyConstants.XIAOHASHU_ID_GENERATOR_KEY);
UserDO userDO = UserDO.builder()
.phone(phone)
.xiaohashuId(String.valueOf(xiaohashuId)) // 自动生成小红书号 ID
.nickname("小红薯" + xiaohashuId) // 自动生成昵称, 如:小红薯10000
.status(StatusEnum.ENABLE.getValue()) // 状态为启用
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeletedEnum.NO.getValue()) // 逻辑删除
.build();
// 添加入库
userDOMapper.insert(userDO);
// 获取刚刚添加入库的用户 ID
Long userId = userDO.getId();
// 给该用户分配一个默认角色
UserRoleDO userRoleDO = UserRoleDO.builder()
.userId(userId)
.roleId(RoleConstants.COMMON_USER_ROLE_ID)
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeletedEnum.NO.getValue())
.build();
userRoleDOMapper.insert(userRoleDO);
RoleDO roleDO = roleDOMapper.selectByPrimaryKey(RoleConstants.COMMON_USER_ROLE_ID);
// 将该用户的角色 ID 存入 Redis 中
List<String> roles = new ArrayList<>(1);
roles.add(roleDO.getRoleKey());
String userRolesKey = RedisKeyConstants.buildUserRoleKey(userId);
redisTemplate.opsForValue().set(userRolesKey, JsonUtils.toJsonString(roles));
return Response.success(userId);
}
}
直接从认证服务中,将用户注册部分的代码复制过来。复制过来后,需要做如下改动:
selectByPhone爆红问题:这个方法还没定义,同样的,从认证服务中复制过来;另外,为用户
insert()对应的映射文件中的方法,添加useGeneratedKeys="true" keyProperty="id",以获取插入后记录的主键 ID;
编写 controller
编辑 UserController 控制器,添加 /user/register 用户注册接口,代码如下:
package com.quanxiaoha.xiaohashu.user.biz.controller;
import com.quanxiaoha.framework.biz.operationlog.aspect.ApiOperationLog;
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 com.quanxiaoha.xiaohashu.user.dto.req.RegisterUserReqDTO;
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.RequestBody;
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;
// 省略...
// ===================================== 对其他服务提供的接口 =====================================
@PostMapping("/register")
@ApiOperationLog(description = "用户注册")
public Response<Long> register(@Validated @RequestBody RegisterUserReqDTO registerUserReqDTO) {
return userService.register(registerUserReqDTO);
}
}
编写 Feign 客户端接口
接口编写完毕后,就需要在 api 模块中,将 Feign 客户端接口封装好,以便其他服务使用。首先,编辑 xiaohashu-user-api 模块的 pom.xml 文件,添加 OpenFeign 相关依赖,如下:
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
创建 Feign 客户端接口,以及常量类,如下图所示:
代码如下:
package com.quanxiaoha.xiaohashu.user.constant;
/**
* @author: 犬小哈
* @date: 2024/4/13 23:23
* @version: v1.0.0
* @description: TODO
**/
public interface ApiConstants {
/**
* 服务名称
*/
String SERVICE_NAME = "xiaohashu-user";
}
package com.quanxiaoha.xiaohashu.user.api;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.user.constant.ApiConstants;
import com.quanxiaoha.xiaohashu.user.dto.req.RegisterUserReqDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @author: 犬小哈
* @date: 2024/4/13 22:56
* @version: v1.0.0
* @description: TODO
**/
@FeignClient(name = ApiConstants.SERVICE_NAME)
public interface UserFeignApi {
String PREFIX = "/user";
/**
* 用户注册
*
* @param registerUserReqDTO
* @return
*/
@PostMapping(value = PREFIX + "/register")
Response<Long> registerUser(@RequestBody RegisterUserReqDTO registerUserReqDTO);
}
auth 服务引入 api 模块
然后,编辑 xiaohashu-auth 认证服务的 pom.xml, 引入 xiaohashu-user-api 模块:
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaohashu-user-api</artifactId>
</dependency>
并编辑 auth 服务的启动类,添加 @EnableFeignClients 注解:
package com.quanxiaoha.xiaohashu.auth;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
// 省略...
@MapperScan("com.quanxiaoha.xiaohashu.auth.domain.mapper")
@EnableFeignClients(basePackages = "com.quanxiaoha.xiaohashu")
public class XiaohashuAuthApplication {
public static void main(String[] args) {
SpringApplication.run(XiaohashuAuthApplication.class, args);
}
}
封装 rpc 调用层
在认证服务中创建 /rpc 调用层,并创建一个 UserRpcService 类,将调用用户服务的注册接口封装起来,代码如下:
package com.quanxiaoha.xiaohashu.auth.rpc;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.user.api.UserFeignApi;
import com.quanxiaoha.xiaohashu.user.dto.req.RegisterUserReqDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
* @author: 犬小哈
* @date: 2024/4/13 23:29
* @version: v1.0.0
* @description: 用户服务
**/
@Component
public class UserRpcService {
@Resource
private UserFeignApi userFeignApi;
/**
* 用户注册
*
* @param phone
* @return
*/
public Long registerUser(String phone) {
RegisterUserReqDTO registerUserReqDTO = new RegisterUserReqDTO();
registerUserReqDTO.setPhone(phone);
Response<Long> response = userFeignApi.registerUser(registerUserReqDTO);
if (!response.isSuccess()) {
return null;
}
return response.getData();
}
}
添加全局枚举
编辑 ResponseCodeEnum 全局枚举类,添加一个登录失败的异常状态枚举,代码如下:
package com.quanxiaoha.xiaohashu.auth.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 {
// 省略...
LOGIN_FAIL("AUTH-20005", "登录失败"),
;
// 异常码
private final String errorCode;
// 错误信息
private final String errorMessage;
}
调用用户服务
最后,编辑认证服务的 UserServiceImpl 类,将用户注册重构成调用用户服务,代码如下:
@Resource
private UserRpcService userRpcService;
// 省略...
// RPC: 调用用户服务,注册用户
Long userIdTmp = userRpcService.registerUser(phone);
// 若调用用户服务,返回的用户 ID 为空,则提示登录失败
if (Objects.isNull(userIdTmp)) {
throw new BizException(ResponseCodeEnum.LOGIN_FAIL);
}
userId = userIdTmp;
// 省略...
至此,用户注册功能的重构工作就搞定了,别忘了,自己再测试一波用户注册接口,保证重构完成后,功能也是正常的。