14 KiB
本小节开始,正式进入到小哈书的用户注册/登录功能开发工作。
流程分析
小伙伴们打开小红书 APP,观察一下就会发现,小红书其实是没有用户注册页的。即使是新用户,也可以直接通过手机号验证码登录成功,非常方便,新用户登录后,系统会自动根据你的手机号,帮你把账号注册好。那么,我们来理一下登录接口的实现逻辑,大致如下图所示:
接口定义
接口的业务逻辑分析完毕后,接下来,定义一下此接口的请求地址、入参,以及出参。
接口地址
POST /user/login
入参
{
"phone": "18011119108", // 手机号
"code": "218603", // 登录验证码,验证码登录时,需要填写
"password": "xx", // 密码登录时,需要填写
"type": 1 // 登录类型,1表示手机号验证码登录;2表示账号密码登录
}
出参
{
"success": true,
"message": null,
"errorCode": null,
"data": "xxxxx" // 登录成功后,返回 Token 令牌
}
权限数据准备
由于现在还没有管理后台,我们先把一些的基础权限数据,初始化到数据库中,如普通用户的角色-权限数据。
权限数据
INSERT INTO `xiaohashu`.`t_permission` (`id`, `parent_id`, `name`, `type`, `menu_url`, `menu_icon`, `sort`, `permission_key`, `status`, `create_time`, `update_time`, `is_deleted`) VALUES (1, 0, '发布笔记', 3, '', '', 1, 'app:note:publish', 0, now(), now(), b'0');
INSERT INTO `xiaohashu`.`t_permission` (`id`, `parent_id`, `name`, `type`, `menu_url`, `menu_icon`, `sort`, `permission_key`, `status`, `create_time`, `update_time`, `is_deleted`) VALUES (2, 0, '发布评论', 3, '', '', 2, 'app:comment:publish', 0, now(), now(), b'0');
先新增两条权限:
- 发布笔记;
- 发布评论
后续如果还有别的权限需要控制,到时候咱们再添加。
角色数据
INSERT INTO `xiaohashu`.`t_role` (`id`, `role_name`, `role_key`, `status`, `sort`, `remark`, `create_time`, `update_time`, `is_deleted`) VALUES (1, '普通用户', 'common_user', 0, 1, '', now(), now(), b'0');
新增一个普通用户角色。
关联数据
然后是该角色与权限的关联关系:
INSERT INTO `xiaohashu`.`t_role_permission_rel` (`id`, `role_id`, `permission_id`, `create_time`, `update_time`, `is_deleted`) VALUES (1, 1, 1, now(), now(), b'0');
INSERT INTO `xiaohashu`.`t_role_permission_rel` (`id`, `role_id`, `permission_id`, `create_time`, `update_time`, `is_deleted`) VALUES (2, 1, 2, now(), now(), b'0');
删除无用的测试类
开始进入编码工作。先将之前测试用的类全部删除掉,如下图标注的这些:
重新生成 DO 、Mapper 接口、XML 文件
重新通过 MyBatis 代码生成器,生成 t_user 表的 DO 实体类、Mapper 接口,以及 XML 文件。生成完毕后,编辑 UserDO 实体类, 为其添加上 Lombok 注解,以及相关字段类型修正,代码如下;
package com.quanxiaoha.xiaohashu.auth.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDO {
private Long id;
private String xiaohashuId;
private String password;
private String nickname;
private String avatar;
private LocalDateTime birthday;
private String backgroundImg;
private String phone;
private Integer sex;
private Integer status;
private String introduction;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Boolean isDeleted;
}
入参 VO
接着,创建登录接口的入参 VO 类,在 /vo 包下,新增 /user 包,并创建 UserLoginReqVO 入参实体类:
代码如下:
package com.quanxiaoha.xiaohashu.auth.model.vo.user;
import com.quanxiaoha.framework.common.validator.PhoneNumber;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
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 UserLoginReqVO {
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@PhoneNumber
private String phone;
/**
* 验证码
*/
private String code;
/**
* 密码
*/
private String password;
/**
* 登录类型:手机号验证码,或者是账号密码
*/
@NotNull(message = "登录类型不能为空")
private Integer type;
}
登录类型枚举类
在接口的入参类中,定义了一个 type 字段,用于表示登录类型。这里我们创建一个枚举类,方便后续获取 type 值,代码如下:
package com.quanxiaoha.xiaohashu.auth.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 LoginTypeEnum {
// 验证码
VERIFICATION_CODE(1),
// 密码
PASSWORD(2);
private final Integer value;
public static LoginTypeEnum valueOf(Integer code) {
for (LoginTypeEnum loginTypeEnum : LoginTypeEnum.values()) {
if (Objects.equals(code, loginTypeEnum.getValue())) {
return loginTypeEnum;
}
}
return null;
}
}
验证码错误状态码
接着,在 ResponseCodeEnum 全局业务状态码枚举类中,新增一个验证码错误的枚举值,代码如下,等会在业务层中,如果用户提交的验证码与保存在 Redis 中的验证码不一致,则抛出该错误提示信息:
package com.quanxiaoha.xiaohashu.auth.enums;
import com.quanxiaoha.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// 省略...
// ----------- 业务异常状态码 -----------
// 省略...
VERIFICATION_CODE_ERROR("AUTH-20001", "验证码错误"),
;
// 省略...
}
mapper 查询方法
另外,编辑 UserDOMapper 接口,定义一个根据手机号查询记录的方法,业务层等会判断用户是否已经注册,需要用到此方法,代码如下、:
package com.quanxiaoha.xiaohashu.auth.domain.mapper;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserDO;
public interface UserDOMapper {
// 省略...
/**
* 根据手机号查询记录
* @param phone
* @return
*/
UserDO selectByPhone(String phone);
// 省略...
}
方法声明完毕后,编辑 UserDOMapper.xml 文件,编写对应的 sql , 代码如下:
// 省略...
<select id="selectByPhone" resultMap="BaseResultMap">
select id, password from t_user where phone = #{phone}
</select>
// 省略...
编写 service 业务层
前置工作都完成后,在 /service 包下,创建 UserService 业务接口,并声明一个登录与注册的方法:
package com.quanxiaoha.xiaohashu.auth.service;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.auth.model.vo.user.UserLoginReqVO;
/**
* @author: 犬小哈
* @date: 2024/4/7 15:41
* @version: v1.0.0
* @description: TODO
**/
public interface UserService {
/**
* 登录与注册
* @param userLoginReqVO
* @return
*/
Response<String> loginAndRegister(UserLoginReqVO userLoginReqVO);
}
然后,在 /impl 包下,创建其实现类,代码如下:
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.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.domain.dataobject.UserDO;
import com.quanxiaoha.xiaohashu.auth.domain.mapper.UserDOMapper;
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.util.Objects;
/**
* @author: 犬小哈
* @date: 2024/4/7 15:41
* @version: v1.0.0
* @description: TODO
**/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Resource
private UserDOMapper userDOMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 登录与注册
*
* @param userLoginReqVO
* @return
*/
@Override
public Response<String> loginAndRegister(UserLoginReqVO userLoginReqVO) {
String phone = userLoginReqVO.getPhone();
Integer type = userLoginReqVO.getType();
LoginTypeEnum loginTypeEnum = LoginTypeEnum.valueOf(type);
Long userId = null;
// 判断登录类型
switch (loginTypeEnum) {
case VERIFICATION_CODE: // 验证码登录
String verificationCode = userLoginReqVO.getCode();
// 校验入参验证码是否为空
if (StringUtils.isBlank(verificationCode)) {
return Response.fail(ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode(), "验证码不能为空");
}
// 构建验证码 Redis Key
String key = RedisKeyConstants.buildVerificationCodeKey(phone);
// 查询存储在 Redis 中该用户的登录验证码
String sentCode = (String) redisTemplate.opsForValue().get(key);
// 判断用户提交的验证码,与 Redis 中的验证码是否一致
if (!StringUtils.equals(verificationCode, sentCode)) {
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_ERROR);
}
// 通过手机号查询记录
UserDO userDO = userDOMapper.selectByPhone(phone);
log.info("==> 用户是否注册, phone: {}, userDO: {}", phone, JsonUtils.toJsonString(userDO));
// 判断是否注册
if (Objects.isNull(userDO)) {
// 若此用户还没有注册,系统自动注册该用户
// todo
} else {
// 已注册,则获取其用户 ID
userId = userDO.getId();
}
break;
case PASSWORD: // 密码登录
// todo
break;
default:
break;
}
// SaToken 登录用户,并返回 token 令牌
// todo
return Response.success("");
}
}
解释一波业务逻辑:
- 拿到入参实体类中的
type字段,通过LoginTypeEnum.valueOf()方法,获取具体的类型枚举值;- 对枚举进行
switch判断,若是手机号验证码登录;
- 获取提交上来的验证码,并与存储在 Redis 中的验证码进行比对;
- 若不一致,返回验证码错误提示信息;
- 否则,通过手机号查询数据库;
- 若
userDO为空,说明是新用户,系统需要自动为该用户注册用户信息。这里代码块中,先写个todo, 后面小节中,再写具体的逻辑;- 若
userDO不为空,则说明是老用户,获取其用户 ID;- 若是账号密码登录,校验密码是否正确;
- 代码块中,先写个
todo, 后面小节中,再写具体的逻辑;- SaToken 登录用户,并返回 token 令牌,暂时先写个
todo;
至此,登录接口的业务大体逻辑骨架就完成了。
controller
最后,在 /controller 包下,创建 UserController 控制器,新增 /user/login 接口,代码如下:
package com.quanxiaoha.xiaohashu.auth.controller;
import com.quanxiaoha.framework.biz.operationlog.aspect.ApiOperationLog;
import com.quanxiaoha.framework.common.response.Response;
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.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/5/29 15:32
* @version: v1.0.0
* @description: TODO
**/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Resource
private UserService userService;
@PostMapping("/login")
@ApiOperationLog(description = "用户登录/注册")
public Response<String> loginAndRegister(@Validated @RequestBody UserLoginReqVO userLoginReqVO) {
return userService.loginAndRegister(userLoginReqVO);
}
}
本小节中,登录接口的业务逻辑还有缺失,就先不测试了,等下面小节中,完全开发完毕后,再来自测一波,看看接口功能是否正常。