weblog/doc/9、用户服务搭建与开发/9.7 代码重构:用户注册功能.md
2025-02-17 11:57:55 +08:00

21 KiB
Raw Blame History

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;
                
                // 省略...

至此,用户注册功能的重构工作就搞定了,别忘了,自己再测试一波用户注册接口,保证重构完成后,功能也是正常的。

本小节源码下载

https://t.zsxq.com/JwvyZ