12 KiB
本小节中,我们继续开发 —— 注册/登录接口, 将手机号验证码方式登录的剩余逻辑补充完整。
添加公共枚举类
编辑 xiaoha-common 公共模块,添加 /eumns 包,用于放置全局通用的枚举类。并添加以下两个枚举,等会业务层中,自动注册用户需要用到:
- 逻辑删除枚举;
- 开启/禁用状态枚举;
package com.quanxiaoha.framework.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author: 犬小哈
* @url: www.quanxiaoha.com
* @date: 2023-08-15 10:33
* @description: 逻辑删除
**/
@Getter
@AllArgsConstructor
public enum DeletedEnum {
YES(true),
NO(false);
private final Boolean value;
}
package com.quanxiaoha.framework.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author: 犬小哈
* @url: www.quanxiaoha.com
* @date: 2023-08-15 10:33
* @description: 状态
**/
@Getter
@AllArgsConstructor
public enum StatusEnum {
// 启用
ENABLE(0),
// 禁用
DISABLED(1);
private final Integer value;
}
生成 RBAC 模型其他表的 DO、Mapper 接口、XML 文件
上小节 中,已经将 t_user 表对应的 DO 实体类、Mapper 接口、XML 文件已经生成好了,这里顺手将其他几张表,也通过 Mybatis 代码生成器插件生成一下,如下图所示:
Tip
: 这里就不贴代码了,小伙伴们有需要的话,可下载本小节源码来查看。
Redis 全局 ID 自增
当新用户登录时,系统需要为该手机号自动注册一个用户,同时,还需要分配一个小红书 ID, 如小红薯10000、小红薯10001 ,一直自增的方式,并且需要保证全局唯一。要如何实现呢?
这里我们可以借助 Redis 实现,执行如下命令,设置一个 key 为 xiaohashu_id_generator 的生成器,初始值设置为 10000 :
set xiaohashu_id_generator 10000
然后,通过 INCR 命令即可实现每次对其自增 1 , 命令如下:
INCR xiaohashu_id_generator
添加全局 ID 生成器常量 KEY
方案定好后,编辑 RedisKeyConstants 全局常量类,添加小哈书全局 ID 生成器 KEY , 代码如下:
package com.quanxiaoha.xiaohashu.auth.constant;
public class RedisKeyConstants {
// 省略...
/**
* 小哈书全局 ID 生成器 KEY
*/
public static final String XIAOHASHU_ID_GENERATOR_KEY = "xiaohashu_id_generator";
// 省略...
}
添加角色全局常量类
接着,在 /constant 常量包下,再创建一个 RoleConstants 角色全局常量类,用于放置角色相关的全局常量,代码如下:
package com.quanxiaoha.xiaohashu.auth.constant;
/**
* @author: 犬小哈
* @date: 2024/5/21 15:04
* @version: v1.0.0
* @description: 角色全局常量
**/
public class RoleConstants {
/**
* 普通用户的角色 ID
*/
public static final Long COMMON_USER_ROLE_ID = 1L;
}
定义一个普通用户的角色 ID, 该角色数据,已经在上小节中提前准备好了。等会自动注册用户时,需要为用户自动分配上该角色。
用户角色 Redis Key
在系统自动注册用户完成后,还需要将该用户的角色,存储到 Redis 缓存中,方便后续鉴权使用。编辑 RedisKeyConstants 常量类,添加用户角色数据 KEY 前缀, 代码如下:
package com.quanxiaoha.xiaohashu.auth.constant;
public class RedisKeyConstants {
// 省略...
/**
* 用户角色数据 KEY 前缀
*/
private static final String USER_ROLES_KEY_PREFIX = "user:roles:";
/**
* 构建验证码 KEY
* @param phone
* @return
*/
public static String buildUserRoleKey(String phone) {
return USER_ROLES_KEY_PREFIX + phone;
}
}
完善用户注册逻辑
前置工作完成后,准备编辑 UserServiceImpl 业务实现类,补充上自动注册用户的逻辑,代码如下:
package com.quanxiaoha.xiaohashu.auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
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.xiaohashu.auth.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.auth.constant.RoleConstants;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.PermissionDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.RoleDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserRoleDO;
import com.quanxiaoha.xiaohashu.auth.domain.mapper.UserDOMapper;
import com.quanxiaoha.xiaohashu.auth.domain.mapper.UserRoleDOMapper;
import com.quanxiaoha.xiaohashu.auth.enums.LoginTypeEnum;
import com.quanxiaoha.xiaohashu.auth.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.auth.model.vo.user.UserLoginReqVO;
import com.quanxiaoha.xiaohashu.auth.service.UserService;
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 java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
// 省略...
@Resource
private UserRoleDOMapper userRoleDOMapper;
/**
* 登录与注册
*
* @param userLoginReqVO
* @return
*/
@Override
public Response<String> loginAndRegister(UserLoginReqVO userLoginReqVO) {
// 省略...
// 判断登录类型
switch (loginTypeEnum) {
case VERIFICATION_CODE: // 验证码登录
// 省略...
// 判断是否注册
if (Objects.isNull(userDO)) {
// 若此用户还没有注册,系统自动注册该用户
userId = registerUser(phone);
} else {
// 已注册,则获取其用户 ID
userId = userDO.getId();
}
break;
case PASSWORD: // 密码登录
// todo
break;
default:
break;
}
// SaToken 登录用户,并返回 token 令牌
// todo
return Response.success("");
}
/**
* 系统自动注册用户
* @param phone
* @return
*/
@Transactional(rollbackFor = Exception.class)
public Long registerUser(String phone) {
// 获取全局自增的小哈书 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);
// 将该用户的角色 ID 存入 Redis 中
List<Long> roles = Lists.newArrayList();
roles.add(RoleConstants.COMMON_USER_ROLE_ID);
String userRolesKey = RedisKeyConstants.buildUserRoleKey(phone);
redisTemplate.opsForValue().set(userRolesKey, JsonUtils.toJsonString(roles));
return userId;
}
}
解释一下
registerUser用户注册方法的逻辑:
为方法添加
@Transactional(rollbackFor = Exception.class)事务注解,保证方法块内代码的原子性,要么全部成功,要么全部失败;操作 Redis , 获取全局自增的小哈书 ID;
构建
UserDO实体类,包括分配小红书 ID, 昵称等;插入用户数据,并获取其主键 ID ;
给该用户分配一个普通用户角色,并入库;
最后将给用户的角色数据,存储到 Redis 中,供后续鉴权使用;
返回用户 ID ;
Mybatis 获取自增 ID
上面的业务逻辑中,用户数据插入表中后,需要获取到该用户的主键 ID, Mybatis 要如何获取呢? 可编辑 xml 文件中的 insert 方法,添加 useGeneratedKeys="true" keyProperty="id" ,代码如下。当数据新增成功后,Mybatis 会自动将该条记录的主键 ID 设置到入参中,我们直接从入参 UserDO 中,即可获取主键 ID:
<insert id="insert" parameterType="com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserDO" useGeneratedKeys="true" keyProperty="id">
insert into t_user (xiaohashu_id, `password`,
nickname, avatar, birthday,
background_img, phone, sex,
`status`, introduction, create_time,
update_time, is_deleted)
values (#{xiaohashuId,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR},
#{nickname,jdbcType=VARCHAR}, #{avatar,jdbcType=VARCHAR}, #{birthday,jdbcType=DATE},
#{backgroundImg,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR}, #{sex,jdbcType=TINYINT},
#{status,jdbcType=TINYINT}, #{introduction,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP},
#{updateTime,jdbcType=TIMESTAMP}, #{isDeleted,jdbcType=BIT})
</insert>
Tip
: 自动生成的
xml文件中insertSQL 中的id项可以删掉,让其自增即可,无需手动填入。
返回 Token 令牌
注册用户逻辑编写完毕后,再来补充一下返回 Token 令牌部分代码。前面我们已经获取到了登录用户的 ID ,可直接通过 StpUtil.login() 方法完成登录,并从 SaTokenInfo 对象中获取 Token 令牌,代码如下:
// 省略...
// SaToken 登录用户, 入参为用户 ID
StpUtil.login(userId);
// 获取 Token 令牌
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
// 返回 Token 令牌
return Response.success(tokenInfo.tokenValue);
// 省略...
至此,手机号验证码登录注册的整体功能就开发完毕了。
自测一波
重启项目,自测一波登录接口。先调用获取验证码接口,拿到一个新的验证码,然后,将该验证码填入到登录接口的入参中,点击发送,如下图所示:
可以看到,成功返回了一个 Token 令牌,另外,确认一下 t_user 表中,是否有自动注册该手机号的用户信息:
以及 t_user_role 表中,是否有该用户的角色信息:
OK, 一切正常。