云图库平台(三)——后端用户模块开发

需求分析:对于用户模块而言,通常要实现下列功能:

  • 用户注册:用户输入账号、密码、确认密码进行注册账号
  • 用户登录:用户通过输入账号、密码登录注册账号
  • 获取当前登录用户信息:即得到当前已登录用户的信息
  • 用户注销/退出:用户可以退出登录
  • 用户权限控制:一般分为管理员和普通用户
  • 用户管理:这里是针对管理员而言,可以对用户进行管理,比如删除、搜索用户等

目录

一、方案设计

数据库表设计

数据库名:cloud-picture

表名:user

user表的核心是用户登录凭证(账号密码)和个人信息,SQL建表语句如下:

sql 复制代码
-- 用户表
create table if not exists user
(
    id           bigint auto_increment comment 'id' primary key,
    userAccount  varchar(256)                           not null comment '账号',
    userPassword varchar(512)                           not null comment '密码',
    userName     varchar(256)                           null comment '用户昵称',
    userAvatar   varchar(1024)                          null comment '用户头像',
    userProfile  varchar(512)                           null comment '用户简介',
    userRole     varchar(256) default 'user'            not null comment '用户角色:user/admin',
    editTime     datetime     default CURRENT_TIMESTAMP not null comment '编辑时间',
    createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete     tinyint      default 0                 not null comment '是否删除',
    UNIQUE KEY uk_userAccount (userAccount),
    INDEX idx_userName (userName)
) comment '用户' collate = utf8mb4_unicode_ci;

注意事项如下:

  • updateTimeeditTime区别:updateTime表示这条用户记录任何字段发生的更新时间(由数据库自动更新);而eixtTime则表示用户编辑个人信息的时间(需要我们编写业务代码来进行实现);两者相比updateTime会更容易改变,比如我们直接从数据库中对id值进行改变,此时updateTime发生了改变,而editTime没有发生改变。初始的默认时间都是创建的时间(即default CURRENT_TIMESTAMP)。
  • 给唯一值添加唯一键(唯一索引):比如userAccount,这样可以利用数据库防重复,可以大大提高查询效率。
  • 给经常用户查询、区分度比较的的字段添加索引:比如说可以给userName用户名称添加索引。

用户登录流程

对用户进行权限控制

用户权限一般分为如下4中:

  • 未登录但也可以使用(最为宽松的一种方式)
  • 只有登录用户才可以使用
  • 未登录也可以使用,但是登录用户可以进行更多的操作
  • 管理员可以进行更多的操作

传统的用户权限校验的方式是:在每个接口单独编写逻辑,先获取到当前登录用户的信息,然后判断用户的权限是否符合要求。这种传统的方式灵活但是会写很多的代码,而且开发者无法一眼得知接口需要哪些权限。

权限校验是一个比较通用的业务逻辑,一般会通过Spring AOP切面自定义权限校验注解,如果有特殊的逻辑的话再单独往接口中添加相应的逻辑代码即可。

二、后端开发

建议每次进行接口开发时,都遵循下面的流程:

数据访问层代码生成

首先在idea中连接数据库,然后执行SQL脚本创建数据库表。数据访问层的代码一般包括实体类Mybatis中的Mapper类和XML等。这里使用MyBatisX代码生成插件来快速得到这些文件。

使用步骤如下:

第一步,选中数据库中的表,右键选择MybatisX中的生成器:

配置如下,然后点击next:

生成成功:

可以看到生成的代码中包括实体类MapperService

现在需要将上述生成的代码移到项目中对应的位置,比如将Mapper移动到mapper包、User移动到model.entity包、Service移动到service包

数据模型开发

实体类

使用MybatisX插件生成的代码不能完全满足我们的要求,比如数据库实体类User.java,还需要我们手动更改其字段配置,指定主键策略和逻辑删除。

  • id默认是连续生成的,容易被爬虫抓取,所以更换策略为ASSIGN_ID雪花算法生成。
  • 数据删除时认为彻底删除记录,为了防止出现误删,所以采用逻辑删除------即通过修改isDelete字段为1表示已失效的数据。

修改User.java中的部分内容,如下:

修改的代码如下所示:

java 复制代码
@TableName(value ="user")
@Data
public class User implements Serializable {
    /**
     * id(要指定主键策略)
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    // ...
    
    /**
     * 是否删除(逻辑删除)
     */
    @TableLogic
    private Integer isDelete;
}

可以参照MyBatis Plus的主键生成注解官方文档进行学习。

枚举类

对于用户角色这样值的数量有限的、可枚举的字段,最好新建一个枚举类,在model.enums包下新建UserRoleEnum.java文件,该文件代码如下:

java 复制代码
@Getter
public enum UserRoleEnum {

    USER("用户", "user"),
    ADMIN("管理员", "admin");

    private final String text;

    private final String value;

    UserRoleEnum(String text, String value) {
        this.text = text;
        this.value = value;
    }

    /**
     * 根据 value 获取枚举
     *
     * @param value 枚举值的value
     * @return 枚举值
     */
    public static UserRoleEnum getEnumByValue(String value) {
        if (ObjUtil.isEmpty(value)) {
            return null;
        }
        for (UserRoleEnum anEnum : UserRoleEnum.values()) {
            if (anEnum.value.equals(value)) {
                return anEnum;
            }
        }
        return null;
    }
}

上述代码中,getEnumByValue是通过value来找到具体的枚举对象。当然如果枚举的值特别多的话,比如说有成千上万条值的话,可以使用Map缓存所有的值来加速查找。

下面我们正式进入各个接口的开发。

三、用户模块接口开发

用户注册

既然我们要开发接口的话,每个接口要接收前端传来的值,所以每个接口一般要定义一个请求接受类(或者说请求封装类),这样便于对参数进行统一校验和扩展。在model包下创建dtodto表示封装对象,一般用于接收前端传来的值或者是Service之间传递的值,比如说一个服务和另外一个服务的参数需要的是一样的,此时我们就可以定义同一个对象来进行接收。

数据模型

model.dto.user下创建UserRegisterRequest.java(用户注册请求封装类),用于接收请求参数的类:

java 复制代码
@Data
public class UserRegisterRequest implements Serializable {

    private static final long serialVersionUID = 3191241716373120793L;

    /**
     * 账号
     */
    private String userAccount;

    /**
     * 密码
     */
    private String userPassword;

    /**
     * 确认密码
     */
    private String checkPassword;
}

接下来开发用户注册服务。

服务开发

下面实现用户注册服务开发:

在Service包下的UserService中添加方法声明:

java 复制代码
/**
 * 用户注册
 *
 * @param userAccount   用户账户
 * @param userPassword  用户密码
 * @param checkPassword 校验密码
 * @return 新用户 id
 */
long userRegister(String userAccount, String userPassword, String checkPassword);

UserService中添加l具体的实现代码,同时要多添加一些校验条件:

java 复制代码
@Override
public long userRegister(String userAccount, String userPassword, String checkPassword) {
    // 1. 校验
    if (StrUtil.hasBlank(userAccount, userPassword, checkPassword)) {
        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 (!userPassword.equals(checkPassword)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
    }
    // 2. 检查是否重复
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("userAccount", userAccount);
    long count = this.baseMapper.selectCount(queryWrapper);
    if (count > 0) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
    }
    // 3. 加密
    String encryptPassword = getEncryptPassword(userPassword);
    // 4. 插入数据
    User user = new User();
    user.setUserAccount(userAccount);
    user.setUserPassword(encryptPassword);
    user.setUserName("无名");
    user.setUserRole(UserRoleEnum.USER.getValue());
    boolean saveResult = this.save(user);
    if (!saveResult) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
    }
    return user.getId();
}

在上述代码中,我们需要将用户密码加密后进行存储,所以可以封装一个方法,方便后续复用。如下:

java 复制代码
@Override
public String getEncryptPassword(String userPassword) {
    // 加盐,混淆密码
    final String SALT = "yf";
    return DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
}

接口开发

在controller包中新建UserController,新增用户注册接口:

typescript 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    /**
     * 用户注册
     */
    @PostMapping("/register")
    public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
        ThrowUtils.throwIf(userRegisterRequest == null, ErrorCode.PARAMS_ERROR);
        String userAccount = userRegisterRequest.getUserAccount();
        String userPassword = userRegisterRequest.getUserPassword();
        String checkPassword = userRegisterRequest.getCheckPassword();
        long result = userService.userRegister(userAccount, userPassword, checkPassword);
        return ResultUtils.success(result);
    }
}

接口测试

现在测试注册用户接口,使用Swagger接口文档来进行测试:

用户登录

数据模型

model.dto.user下创建UserLoginRequest.java(用户登录请求封装类):

java 复制代码
@Data
public class UserLoginRequest implements Serializable {

    private static final long serialVersionUID = 3191241716373120793L;

    /**
     * 账号
     */
    private String userAccount;

    /**
     * 密码
     */
    private String userPassword;
}

接着进行服务开发

服务开发

在Service包下的UserService中增加方法声明:

java 复制代码
**/**
 * 用户登录
 *
 * @param userAccount  用户账户
 * @param userPassword 用户密码
 * @param request
 * @return 脱敏后的用户信息
 */
LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request);
**

在UserServiceImpl中增加实现代码,用户登录成功后,将用户信息存储在当前的Session中:

java 复制代码
@Override
public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {
    // 1. 校验
    if (StrUtil.hasBlank(userAccount, userPassword)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
    }
    if (userAccount.length() < 4) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");
    }
    if (userPassword.length() < 8) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
    }
    // 2. 加密
    String encryptPassword = getEncryptPassword(userPassword);
    // 查询用户是否存在
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("userAccount", userAccount);
    queryWrapper.eq("userPassword", encryptPassword);
    User user = this.baseMapper.selectOne(queryWrapper);
    // 用户不存在
    if (user == null) {
        log.info("user login failed, userAccount cannot match userPassword");
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
    }
    // 3. 记录用户的登录态
    request.getSession().setAttribute(USER_LOGIN_STATE, user);
    return this.getLoginUserVO(user);
}

注意注册用户时存入数据库的密码是加密后的,查询用户信息时,也要对用户输入的密码进行同样算法的加密 ,才能跟数据库的信息对应上。

可以把上述的Session 理解为一个Map,可以给Map设置key和value,每个不同的SessionID对应的Session存储都是不同的,不用担心会污染。所以上述代码中,给Session设置了固定的 key (USER LOGIN STATE),可以将这个key值提取为常量,便于后续获取。

在constant包下新建UserConstant类

java 复制代码
/**
 * 用户常量
 */
public interface UserConstant {

    /**
     * 用户登录态键
     */
    String USER_LOGIN_STATE = "user_login";

    //  region 权限

    /**
     * 默认角色
     */
    String DEFAULT_ROLE = "user";

    /**
     * 管理员角色
     */
    String ADMIN_ROLE = "admin";

    // endregion
}

接口开发

在UserController中新增用户登录接口:

java 复制代码
@PostMapping("/login")
public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
    ThrowUtils.throwIf(userLoginRequest == null, ErrorCode.PARAMS_ERROR);
    String userAccount = userLoginRequest.getUserAccount();
    String userPassword = userLoginRequest.getUserPassword();
    LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request);
    return ResultUtils.success(loginUserVO);
}

测试

获取当前登录用户

这里可以直接从request请求对象对应的Session中直接获取到之前保存的登录用户信息,不需要在请求其它参数。

服务开发

在Service包下的UserService中增加方法声明:

java 复制代码
User getLoginUser(HttpServletRequest request);

在UserServiceImpl中实现代码,如果不追求性能的话,最好从数据库中再查一遍,为了保证获取到的数据保持最新。所以先从Session中获取登录用户的id,然后再从数据库中查询到最新的结果。另外,这个方法前端是调用不到的,此方法是给Service之间用的。

java 复制代码
@Override
public User getLoginUser(HttpServletRequest request) {
    // 先判断是否已登录
    Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
    User currentUser = (User) userObj;
    if (currentUser == null || currentUser.getId() == null) {
        throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
    }
    // 从数据库查询(追求性能的话可以注释,直接返回上述结果)
    long userId = currentUser.getId();
    currentUser = this.getById(userId);
    if (currentUser == null) {
        throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
    }
    return currentUser;
}

接口开发

在UserController中新增获取当前登录用户接口:

java 复制代码
@GetMapping("/get/login")
public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {
    User loginUser = userService.getLoginUser(request);
    return ResultUtils.success(userService.getLoginUserVO(loginUser));
}

数据脱敏

model.vo包下新增LoginUserVO类,表示脱敏后的登录用户信息:

java 复制代码
@Data
public class LoginUserVO implements Serializable {

    /**
     * 用户 id
     */
    private Long id;

    /**
     * 账号
     */
    private String userAccount;

    /**
     * 用户昵称
     */
    private String userName;

    /**
     * 用户头像
     */
    private String userAvatar;

    /**
     * 用户简介
     */
    private String userProfile;

    /**
     * 用户角色:user/admin
     */
    private String userRole;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 更新时间
     */
    private Date updateTime;

    private static final long serialVersionUID = 1L;
}

编写方法对应的实现类,意思就是将User类的属性复制到LoginUserVo中,不存在的字段就被过滤掉了:

java 复制代码
@Override
public LoginUserVO getLoginUserVO(User user) {
    if (user == null) {
        return null;
    }
    LoginUserVO loginUserVO = new LoginUserVO();
    BeanUtils.copyProperties(user, loginUserVO);
    return loginUserVO;
}

修改Controller的getLoginUser接口,改为返回脱敏后的用户信息:

java 复制代码
@GetMapping("/get/login")
public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {
    User user = userService.getLoginUser(request);
    return ResultUtils.success(userService.getLoginUserVO(user));
}

用户注销

用户注销和获取当前登录用户一样,都是从request请求对象对应的Session中直接获取到之前保存的登录用户信息,无需其它请求参数。

服务开发

在Service包下的UserService中增加方法声明:

java 复制代码
/**
 * 用户注销
 *
 * @param request
 * @return
 */
boolean userLogout(HttpServletRequest request);

在UserServiceImpl中增加实现代码,从Session中移除掉当前用户的登录态即可:

java 复制代码
@Override
public boolean userLogout(HttpServletRequest request) {
    // 先判断是否已登录
    Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
    if (userObj == null) {
        throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");
    }
    // 移除登录态
    request.getSession().removeAttribute(USER_LOGIN_STATE);
    return true;
}

接口开发

在UserController中新增用户注销接口:

java 复制代码
@PostMapping("/logout")
public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
    ThrowUtils.throwIf(request == null, ErrorCode.PARAMS_ERROR);
    boolean result = userService.userLogout(request);
    return ResultUtils.success(result);
}

四、用户权限控制

之前提到过,权限控制是一个比较通用的业务,一般通过Spring AOP切面自定义权限校验注解实现统一的接口拦截和权限校验;如果有特殊的权限校验逻辑的话再单独在接口中编码即可。

权限校验注解

在annotation包中编写权限校验注解:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {

    /**
     * 必须有某个角色
     */
    String mustRole() default "";
}

权限校验切面

AOP包下编写权限校验AOP,如下:

java 复制代码
@Aspect
@Component
public class AuthInterceptor {

    @Resource
    private UserService userService;

    /**
     * 执行拦截
     *
     * @param joinPoint 切入点
     * @param authCheck 权限校验注解
     */
    @Around("@annotation(authCheck)")
    public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
        String mustRole = authCheck.mustRole();
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        // 当前登录用户
        User loginUser = userService.getLoginUser(request);
        UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
        // 不需要权限,放行
        if (mustRoleEnum == null) {
            return joinPoint.proceed();
        }
        // 以下为:必须有该权限才通过
        // 获取当前用户具有的权限
        UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());
        // 没有权限,拒绝
        if (userRoleEnum == null) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 要求必须有管理员权限,但用户没有管理员权限,拒绝
        if (UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 通过权限校验,放行
        return joinPoint.proceed();
    }
}

注解测试

现在使用该注解(即权限切面检验注解),只要给方法添加了AuthCheck注解,就必须先登录,否则就会抛出异常。使用场景:比如可以给mustRole设置为管理员,这样就仅仅只有管理员才能使用该接口:

java 复制代码
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)

五、用户管理

用户管理功能具体可分为以下几点:

  • 创建用户(管理员)
  • 根据id删除用户(管理员)
  • 更新用户(管理员)
  • 分页获取用户列表(需要脱敏)
  • 根据id获取用户(未脱敏)
  • 根据id获取用户(脱敏)

数据模型

每个操作都需要提供请求类,放到dto.user包下:

用户创建请求如下:

java 复制代码
@Data
public class UserAddRequest implements Serializable {

    /**
     * 用户昵称
     */
    private String userName;

    /**
     * 账号
     */
    private String userAccount;

    /**
     * 用户头像
     */
    private String userAvatar;

    /**
     * 用户简介
     */
    private String userProfile;

    /**
     * 用户角色: user, admin
     */
    private String userRole;

    private static final long serialVersionUID = 1L;
}

用户更新请求如下:

java 复制代码
@Data
public class UserUpdateRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 用户昵称
     */
    private String userName;

    /**
     * 用户头像
     */
    private String userAvatar;

    /**
     * 简介
     */
    private String userProfile;

    /**
     * 用户角色:user/admin
     */
    private String userRole;

    private static final long serialVersionUID = 1L;
}

用户查询请求如下(该请求需要继承common中的PageRequest来支持分页查询):

java 复制代码
@EqualsAndHashCode(callSuper = true)
@Data
public class UserQueryRequest extends PageRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 用户昵称
     */
    private String userName;

    /**
     * 账号
     */
    private String userAccount;

    /**
     * 简介
     */
    private String userProfile;

    /**
     * 用户角色:user/admin/ban
     */
    private String userRole;

    private static final long serialVersionUID = 1L;
}

还有一个需求就是给普通用户获取到脱敏后的用户信息,和LoginUserVO类似,在model.vo包下新建UserVO类来表示脱敏后的用户信息,如下:

java 复制代码
@Data
public class UserVO implements Serializable {

    /**
     * id
     */
    private Long id;
    
    /**
     * 账号
     */
    private String userAccount;

    /**
     * 用户昵称
     */
    private String userName;

    /**
     * 用户头像
     */
    private String userAvatar;

    /**
     * 用户简介
     */
    private String userProfile;

    /**
     * 用户角色:user/admin
     */
    private String userRole;

    /**
     * 创建时间
     */
    private Date createTime;

    private static final long serialVersionUID = 1L;
}

服务开发

  • 在UserService中编写获取脱敏后的单个用户信息,获取脱敏后的用户列表方法:
java 复制代码
@Override
public UserVO getUserVO(User user) {
    if (user == null) {
        return null;
    }
    UserVO userVO = new UserVO();
    BeanUtils.copyProperties(user, userVO);
    return userVO;
}

@Override
public List<UserVO> getUserVOList(List<User> userList) {
    if (CollUtil.isEmpty(userList)) {
        return new ArrayList<>();
    }
    return userList.stream().map(this::getUserVO).collect(Collectors.toList());
}
  • 除上述方法外,对于分页查询接口,需要根据用户传入的参数来构造SQL查询。由于使用Mybatis Plus框架,不要要自己拼接SQL了,而是通过构造QueryWrapper对象来生成SQL查询。所以可以在UserService中编写一个方法,专门用于将查询请求转为QueryWrapper对象,如下:

在UserService中新增方法声明:

java 复制代码
QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest);

在UserServiceImpl中编写方法实现:

java 复制代码
@Override
public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {
    if (userQueryRequest == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
    }
    Long id = userQueryRequest.getId();
    String userAccount = userQueryRequest.getUserAccount();
    String userName = userQueryRequest.getUserName();
    String userProfile = userQueryRequest.getUserProfile();
    String userRole = userQueryRequest.getUserRole();
    String sortField = userQueryRequest.getSortField();
    String sortOrder = userQueryRequest.getSortOrder();
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq(ObjUtil.isNotNull(id), "id", id);
    queryWrapper.eq(StrUtil.isNotBlank(userRole), "userRole", userRole);
    queryWrapper.like(StrUtil.isNotBlank(userAccount), "userAccount", userAccount);
    queryWrapper.like(StrUtil.isNotBlank(userName), "userName", userName);
    queryWrapper.like(StrUtil.isNotBlank(userProfile), "userProfile", userProfile);
    queryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField);
    return queryWrapper;
}

接口开发

有了上面的基础的代码后,我们就可以开发增删改查的代码了。开发过程中注意添加对应的权限注解,做好参数校验即可,如下:

java 复制代码
/**
 * 创建用户
 */
@PostMapping("/add")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest) {
    ThrowUtils.throwIf(userAddRequest == null, ErrorCode.PARAMS_ERROR);
    User user = new User();
    BeanUtils.copyProperties(userAddRequest, user);
    // 默认密码 12345678
    final String DEFAULT_PASSWORD = "12345678";
    String encryptPassword = userService.getEncryptPassword(DEFAULT_PASSWORD);
    user.setUserPassword(encryptPassword);
    boolean result = userService.save(user);
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
    return ResultUtils.success(user.getId());
}

/**
 * 根据 id 获取用户(仅管理员)
 */
@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<User> getUserById(long id) {
    ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
    User user = userService.getById(id);
    ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);
    return ResultUtils.success(user);
}

/**
 * 根据 id 获取包装类
 */
@GetMapping("/get/vo")
public BaseResponse<UserVO> getUserVOById(long id) {
    BaseResponse<User> response = getUserById(id);
    User user = response.getData();
    return ResultUtils.success(userService.getUserVO(user));
}

/**
 * 删除用户
 */
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest) {
    if (deleteRequest == null || deleteRequest.getId() <= 0) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    boolean b = userService.removeById(deleteRequest.getId());
    return ResultUtils.success(b);
}

/**
 * 更新用户
 */
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest) {
    if (userUpdateRequest == null || userUpdateRequest.getId() == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    User user = new User();
    BeanUtils.copyProperties(userUpdateRequest, user);
    boolean result = userService.updateById(user);
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
    return ResultUtils.success(true);
}

/**
 * 分页获取用户封装列表(仅管理员)
 *
 * @param userQueryRequest 查询请求参数
 */
@PostMapping("/list/page/vo")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest) {
    ThrowUtils.throwIf(userQueryRequest == null, ErrorCode.PARAMS_ERROR);
    long current = userQueryRequest.getCurrent();
    long pageSize = userQueryRequest.getPageSize();
    Page<User> userPage = userService.page(new Page<>(current, pageSize),
            userService.getQueryWrapper(userQueryRequest));
    Page<UserVO> userVOPage = new Page<>(current, pageSize, userPage.getTotal());
    List<UserVO> userVOList = userService.getUserVOList(userPage.getRecords());
    userVOPage.setRecords(userVOList);
    return ResultUtils.success(userVOPage);
}

分页功能修复

现在,经过Swagger接口文档对上面接口进行测试后发现,listUserVOByPage接口中分页功能并没有生效,并且查出了全部的数据。

我们使用的Mybatis Plus来操作数据库,可参照官方文档解决。查阅官方文档后发现,需要配置一个分页插件,本项目使用的v3.5.9版本引入分页插件的方式和之前不同,v3.5.9版本需要独立安装分页插件依赖

pom.xml中引入分页插件的依赖:

xml 复制代码
<!-- MyBatis Plus 分页插件 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>

仅仅引入上面这一条大概率是不行的,还需要再pom.xml的依赖管理中补充mybatis-plus-bom,如下:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-bom</artifactId>
            <version>3.5.9</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

依赖下载成功后,在config包下新建Mybatis Plus拦截器配置,添加分页插件,如下:

java 复制代码
@Configuration
@MapperScan("com.yf.cloudpicturebackend.mapper")
public class MyBatisPlusConfig {

    /**
     * 拦截器配置
     *
     * @return {@link MybatisPlusInterceptor}
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

现在重启项目就能正常完成分页功能了。

数据精度修复

现在,再次进行测试,启动项目后F12打开控制台,通过预览查看响应数据,发现了存在精度问题。原因就是前端js的精度范围有限,而后端返回的id范围过大,导致前端精度丢失,会影响前端页面获取到的数据结果。我们可以在后端config包下新建一个全局JSON配置,将整个后端Spring MVC接口返回值的长类型数字转换为字符串进行返回。如下:

java 复制代码
/**
 * Spring MVC Json 配置
 */
@JsonComponent
public class JsonConfig {

    /**
     * 添加 Long 转 json 精度丢失的配置
     */
    @Bean
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule module = new SimpleModule();
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(module);
        return objectMapper;
    }
}

配置完成重启项目后,此时就没有精度问题了:

好了,该项目的后端用户模块到这里就结束了。

相关推荐
core51221 分钟前
flink sink doris
大数据·mysql·flink·doris·存储·sink·过程正常
爱码少年1 小时前
springboot中责任链模式之简单应用
spring boot·责任链模式
苹果酱05672 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
Minxinbb2 小时前
MySQL中Performance Schema库的详解(上)
数据库·mysql·dba
滚雪球~2 小时前
2002 - Can‘t connect to server on ‘192.168.1.XX‘ (36)
mysql·navicat
武昌库里写JAVA2 小时前
【MySQL】7.0 入门学习(七)——MySQL基本指令:帮助、清除输入、查询等
spring boot·spring·毕业设计·layui·课程设计
mmsx3 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
zpjing~.~4 小时前
Mongo 分页判断是否有下一页
数据库
2401_857600954 小时前
技术与教育的融合:构建现代成绩管理系统
数据库·oracle
秋恬意4 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis