一、详细设计——02
后端接口,业务逻辑开发
在上一篇博客中(企业员工信息中心管理系统(前后端从0到1开发—01))。后端部分实现了对后端项目的初始化,并且创建了数据库和对应的表。使用TableGo自动生成基础代码模板,引用了mybatis-plus框架,并进行了单元测试。
接下来则是根据详细设计中准备实现的功能,使用 mybatis-plus 的条件构造器和相关逻辑去实现对应的接口,并且完成前后端联调。
注册功能
思路:根据用户账号,密码,校验密码,用户编号这四个参数校验(非空,用户账号不能小于6位且账号不能包含特殊字符,密码不能小于8位且密码和校验密码要相同),之后进行将这些数据插入到数据库中(密码加密加盐之后再进行存储)完成之后给前端返回一个注册成功之后的用户ID。
controller 层
@PostMapping("/register")
public BaseResponse<Long> userRegister (@NonNull @RequestBody UserRegisterRequest userRegisterRequest){
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
String planetCode = userRegisterRequest.getPlanetCode();
long register = userService.userRegister(userAccount, userPassword, checkPassword, planetCode);
return ResultUtils.success(register);
}
当然这里还用到了一个IDEA插件 Auto Filling Java Call Arguments 还有 上一篇中使用到的GenerateAllSetter 在对象属性较多的时候,可以大幅提高编码的效率。
同时,还使用了一个dto中间对象,对传入参数进行了封装,并且加入了基本的参数校验:
/**
* 用户注册请求体
*/
@Data
public class UserRegisterRequest implements Serializable {
private static final long serialVersionUID = 3191241716373120793L;
/**
* 用户账号
*/
@NotBlank(message = "用户账号不能为空")
@Size(min = 6, message = "用户账号不能少于6位")
@Pattern(regexp = "[^`~!@#$%^&*()+=|{}':;,[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]+",
message = "用户账号不能包含特殊字符")
private String userAccount;
/**
* 用户密码
*/
@NotBlank(message = "用户密码不能为空")
@Size(min = 8, message = "用户密码不能少于8位")
private String userPassword;
/**
* 校验密码
*/
@NotBlank(message = "校验密码不能为空")
private String checkPassword;
/**
* 用户编号
*/
@NotBlank(message = "用户编号不能为空")
private String planetCode;
}
当然,我这里还提前配置了一下,自动导包,代码自动补全SpringBoot快捷键 以及常用包名解释_java规范代码格式快捷键
service 层
虽然在controller 层进行了参数的校验,但是在service 层还是有必要再校验一层。在service 层中主要实现了对密码的加密和数据的插入操作。
需要说明的是,关于参数判空详细的说明可以看Java 判空的常见方法_java判空,这里使用的是 StringUtils.isAnyBlank 。对应的是需要导入 import org.apache.commons.lang3.StringUtils; 而一般的单个参数判空直接导入 import org.springframework.util.StringUtils;
/**
* 给密码加盐值,混淆密码
*/
private static final String SALT = "salt";
/**
* 用户注册
*
* @param userAccount 用户账户
* @param userPassword 用户密码
* @param checkPassword 校验密码
* @param planetCode 星球编号
* @return 新用户 id
*/
@Override
public long userRegister(String userAccount, String userPassword, String checkPassword, String planetCode) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, planetCode)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
}
if (planetCode.length() > 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户编号过长");
}
// 账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return -1;
}
// 密码和校验密码相同
if (!userPassword.equals(checkPassword)) {
return -1;
}
// 账户不能重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
}
// 星球编号不能重复
queryWrapper = new QueryWrapper<>();
queryWrapper.eq("planetCode", planetCode);
count = userMapper.selectCount(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "编号重复");
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 3. 插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
user.setPlanetCode(planetCode);
boolean saveResult = this.save(user);
if (!saveResult) {
return -1;
}
return user.getId();
}
登录功能
思路:目前作为单体项目进行开发,有两种方式,一个是使用 Jwt 的方式进行登录,另外一个是直接进行登录,将请求 request 存放到session 中,具体原理参考从”登录“过程看Jwt和Token。依据实际情况(此项目业务不复杂,登录要求不高),因此选择第二种方式。参数校验方面,直接参考注册逻辑来写就行。需要注意的是,当登录成功后返回给前端数据时,需要给用户数据脱敏(密码等敏感数据,不能返回)
我们使用request,用getsession拿到session,用 setAttribute 往session里设置一些值(比如用户信息) ,可已将 session 中的 attributes 当成是一个map,至于为什么这样说,可以Ctrl+B 点进去看一下源码 。
发现这个类,有一个获取属性的值。
有get属性,那就还有set属性。
点进set属性的实现方法(随便点击一个),
发现这里面有一个put方法,知道了 attributes 是一个 map
controller 层
/**
* 用户登录
*
* @param userLoginRequest
* @param request
* @return
*/
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR);
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR);
}
User user = userService.userLogin(userAccount, userPassword, request);
return ResultUtils.success(user);
}
service 层
/**
* 用户登录
*
* @param userAccount 用户账户
* @param userPassword 用户密码
* @param request
* @return 脱敏后的用户信息
*/
@Override
public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return null;
}
if (userAccount.length() < 4) {
return null;
}
if (userPassword.length() < 8) {
return null;
}
// 账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return null;
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 查询用户是否存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
queryWrapper.eq("userPassword", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
// 用户不存在
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
return null;
}
// 3. 用户脱敏
User safetyUser = getSafetyUser(user);
// 4. 记录用户的登录态
request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
return safetyUser;
}
/**
* 用户脱敏
*
* @param originUser
* @return
*/
@Override
public User getSafetyUser(User originUser) {
if (originUser == null) {
return null;
}
User safetyUser = new User();
safetyUser.setId(originUser.getId());
safetyUser.setUsername(originUser.getUsername());
safetyUser.setUserAccount(originUser.getUserAccount());
safetyUser.setAvatarUrl(originUser.getAvatarUrl());
safetyUser.setGender(originUser.getGender());
safetyUser.setPhone(originUser.getPhone());
safetyUser.setEmail(originUser.getEmail());
safetyUser.setPlanetCode(originUser.getPlanetCode());
safetyUser.setUserRole(originUser.getUserRole());
safetyUser.setUserStatus(originUser.getUserStatus());
safetyUser.setCreateTime(originUser.getCreateTime());
return safetyUser;
}
用户注销功能
思路:注销功能其实很简单(并不是直接把用户的数据给删除了)只是设置了一个用户常量类
/**
* 用户常量
*/
public interface UserConstant {
/**
* 用户登录态键
*/
String USER_LOGIN_STATE = "userLoginState";
/**
* 默认权限
*/
int DEFAULT_ROLE = 0;
/**
* 管理员权限
*/
int ADMIN_ROLE = 1;
}
设置了几个常量,当要注销用户的时候,就把这个状态值给去除掉就行了。
controller 层
/**
* 用户注销
*
* @param request
* @return
*/
@PostMapping("/logout")
public BaseResponse<Integer> userLogout(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
int result = userService.userLogout(request);
return ResultUtils.success(result);
}
service 层
/**
* 用户注销
*
* @param request
*/
@Override
public int userLogout(HttpServletRequest request) {
// 移除登录态
request.getSession().removeAttribute(USER_LOGIN_STATE);
return 1;
}
获取当前用户功能
controller 层
/**
* 获取当前用户
*
* @param request
* @return
*/
@GetMapping("/current")
public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN);
}
long userId = currentUser.getId();
// TODO 校验用户是否合法
User user = userService.getById(userId);
User safetyUser = userService.getSafetyUser(user);
return ResultUtils.success(safetyUser);
}
根据姓名查找用户功能
controller 层
@GetMapping("/search")
public BaseResponse<List<User>> searchUsers(String username, HttpServletRequest request) {
if (!isAdmin(request)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like("username", username);
}
List<User> userList = userService.list(queryWrapper);
List<User> list = userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
return ResultUtils.success(list);
}
根据ID删除用户功能
controller 层
@PostMapping("/delete")
public BaseResponse<Boolean> deleteUser(@RequestBody long id, HttpServletRequest request) {
if (!isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean b = userService.removeById(id);
return ResultUtils.success(b);
}
判断是否为管理员功能
controller 层
/**
* 是否为管理员
*
* @param request
* @return
*/
private boolean isAdmin(HttpServletRequest request) {
// 仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userObj;
return user != null && user.getUserRole() == ADMIN_ROLE;
}
后端接口测试
这里使用postman 或者 apifox 或则 swagger 都行,不过 idea 中自带的api 调试工具也不错。下面对登录接口进行测试演示。
这是接口对应的返回值。
二、需要说明的地方
三层结构,并不是绝对的三层,只是按照实际情况下,对程序进行解耦和简化,但是如果代码逻辑过于简单,几行代码就写完了也不用特意分成三层结构。像使用了mybatis-plus之后,CRUD功能全部集成好了,直接传参就行了,所以说后面几个接口直接在controller层就写完了。