从零学习一个基于Springboot的权限管理系统(三)用户管理

697 阅读6分钟

用户相关接口实现

一、用户分页数据

1.配置mybatis分页插件

利用mybatis本身的拦截器机制对beforeQuery方法进行了实现,根据指定的方言类型拼接上分页sql语句和对应的参数映射

@Configuration
@EnableTransactionManagement//激活spring事务管理器
public class MybatisConfig {

    /**
     * 分页插件和数据权限插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //数据权限拦截器
        interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
        //分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        return interceptor;
    }

    /**
     * 自动填充数据库创建人、创建时间、更新人、更新时间
     */
    @Bean
    public GlobalConfig globalConfig() {
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
        return globalConfig;
    }

}

数据权限以及自动填充数据库某些公有字段后续再进行解释

2.定义分页响应结构体PageResult以及视图对象UserPageVO

  • 因为定义的是分页的响应结构,所以还需要在PageResult中添加一个构造。接收IPage<T>类型的参数。并将前端所需要的total以及list封装给PageResult对象
@Data
public class PageResult<T> implements Serializable {

    private String code;

    private Data<T> data;

    private String msg;

    public static <T> PageResult<T> success(IPage<T> page) {
        PageResult<T> result = new PageResult<>();
        result.setCode(ResultCode.SUCCESS.getCode());
        Data data = new Data<T>();
        data.setList(page.getRecords());
        data.setTotal(page.getTotal())
        result.setData(data);
        result.setMsg(ResultCode.SUCCESS.getMsg());
        return result;
    }

    @Data
    public static class Data<T> {
        private List<T> list;
        private long total;
    }

}
  • 定义返回给前端的VO对象 (代码省略)需要注意的是可以添加@Schema注解,为接口文档添加描述。
  • 定义接收前端传入的query参数对象 UserPageQuery 注意添加@DateTimeFormat(pattern = "yyyy-MM-dd")注解以及接口文档的解释注解 (如果传入的是Json 还需要注意使用@JsonFormat注解)
  • SysUserController--->SysUserService--->SysUserServiceImpl
    • 根据前端传入的query参数封装成mybatis-plus的Page对象
    • 格式化为数据库的日期格式,避免日期比较格式化函数导致索引失效
    • 传入Page对象以及query参数调用mapper层进行查询
    • 查询出来的对象一般封装为BO
    • 利用MapStruct实现对象之间赋值(过于复杂的 直接set就好 推荐博客文章 blog.csdn.net/yangshangwe…)
    • 返回page给前端
    controller部分代码
@Operation(summary = "用户分页列表")
@GetMapping("/page")
//返回一个分页查询对象 泛型为UserPageVO
public PageResult<UserPageVO> listPagedUsers(
       UserPageQuery queryParams
) {
  //返回mybatis-plus分页插件类型的接口
   IPage<UserPageVO> result = userService.listPagedUsers(queryParams);
   return PageResult.success(result);
}

service部分代码

@Override
public IPage<UserPageVO> listPagedUsers(UserPageQuery queryParams) {

   // 参数构建
   int pageNum = queryParams.getPageNum();
   int pageSize = queryParams.getPageSize();
   Page<UserBO> page = new Page<>(pageNum, pageSize);

   // 格式化为数据库日期格式,避免日期比较使用格式化函数导致索引失效
   DateUtils.toDatabaseFormat(queryParams, "startTime", "endTime");

   // 查询数据
   Page<UserBO> userPage = this.baseMapper.listPagedUsers(page, queryParams);

   // 实体转换
   return userConverter.toPageVo(userPage);
}

二、用户信息的增删改操作

新增与修改

mybatis-plus提供了类似于saveOrUpdate这种方法 所以一般service层写到一起进行判断

新增与修改返回值都是boolean,表单提交上来的参数也都要进行校验@Valid(@NotNull @NotBlank等)。

1.逻辑实现

  • 根据前端传入的user_id判断是新增还是修改
  • 数据库查询用户名是否唯一
  • 新增时添加密码编码器对明文密码进行加密
  • 使用mapstruct将表单对象转换为entity
  • 调用saveOrUpdate方法 插入到sys_user表
  • 保存用户与角色相关的信息 插入到sys_user_role
  • 如果是修改 还需要删除已经不生效的关联信息
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveUserRoles(Long userId, List<Long> roleIds) {

    if (userId == null || CollectionUtil.isEmpty(roleIds)) {
        return false;
    }

    // 用户原角色ID集合
    List<Long> userRoleIds = this.list(new LambdaQueryWrapper<SysUserRole>()
                    .eq(SysUserRole::getUserId, userId))
            .stream()
            .map(SysUserRole::getRoleId)
            .collect(Collectors.toList());

    // 新增用户角色
    List<Long> saveRoleIds;
    if (CollectionUtil.isEmpty(userRoleIds)) {
        saveRoleIds = roleIds;
    } else {
        saveRoleIds = roleIds.stream()
                .filter(roleId -> !userRoleIds.contains(roleId))
                .collect(Collectors.toList());
    }

    List<SysUserRole> saveUserRoles = saveRoleIds
            .stream()
            .map(roleId -> new SysUserRole(userId, roleId))
            .collect(Collectors.toList());
    this.saveBatch(saveUserRoles);

    // 删除用户角色
    if (CollectionUtil.isNotEmpty(userRoleIds)) {
        List<Long> removeRoleIds = userRoleIds.stream()
                .filter(roleId -> !roleIds.contains(roleId))
                .collect(Collectors.toList());

        if (CollectionUtil.isNotEmpty(removeRoleIds)) {
            this.remove(new LambdaQueryWrapper<SysUserRole>()
                    .eq(SysUserRole::getUserId, userId)
                    .in(SysUserRole::getRoleId, removeRoleIds)
            );
        }
    }
    return true;

}

2.防止重复提交

利用redisson的分布式锁以及aop实现的防止重复提交

DuplicateSubmitAspect切面通过在被PreventRepeatSubmit注解标记的方法执行前尝试获取一个基于请求信息和JWT Token的分布式锁,有效地防止了重复提交。如果锁获取失败,表明有其他实例正在处理相同请求,从而阻止了重复执行,确保了操作的原子性和一致性

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PreventRepeatSubmit {

    /**
     * 锁过期时间(秒)
     * <p>
     * 默认5秒内不允许重复提交
     */
    int expire() default 5;

}
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class DuplicateSubmitAspect {

    private final RedissonClient redissonClient;
    private static final String RESUBMIT_LOCK_PREFIX = "LOCK:RESUBMIT:";

    /**
     * 防重复提交切点
     */
    @Pointcut("@annotation(preventRepeatSubmit)")
    public void preventDuplicateSubmitPointCut(PreventRepeatSubmit preventRepeatSubmit) {
        log.info("定义防重复提交切点");
    }

    @Around("preventDuplicateSubmitPointCut(preventRepeatSubmit)")
    public Object doAround(ProceedingJoinPoint pjp, PreventRepeatSubmit preventRepeatSubmit) throws Throwable {

        String resubmitLockKey = generateResubmitLockKey();
        if (resubmitLockKey != null) {
            int expire = preventRepeatSubmit.expire(); // 防重提交锁过期时间
            RLock lock = redissonClient.getLock(resubmitLockKey);
            boolean lockResult = lock.tryLock(0, expire, TimeUnit.SECONDS); // 立刻尝试获取锁;失败,直接返回 false
            if (!lockResult) {
                throw new BusinessException(ResultCode.REPEAT_SUBMIT_ERROR); // 抛出重复提交提示信息
            }
        }
        return pjp.proceed();
    }


    /**
     * 获取重复提交锁的 key
     */
    private String generateResubmitLockKey() {
        String resubmitLockKey = null;
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
            token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
            // 从 JWT Token 中获取 jti
            String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID);
            resubmitLockKey = RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI();
        }
        return resubmitLockKey;
    }

}

根据id删除用户

前端传过来的是字符串,id之间由逗号进行分割;逻辑删除记着在mybatis-plus配置中添加逻辑删除字段和默认值 或者在实体类添加@TableLogic注解

controller代码

@Operation(summary = "删除用户")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:user:delete')")
public Result deleteUsers(
        @Parameter(description = "用户ID,多个以英文逗号(,)分割") @PathVariable String ids
) {
    boolean result = userService.deleteUsers(ids);
    return Result.judge(result);
}

service代码

@Transactional
@Override
public boolean deleteUsers(String idsStr) {
    Assert.isTrue(StrUtil.isNotBlank(idsStr), "删除的用户数据为空");
    // 逻辑删除
    List<Long> ids = Arrays.stream(idsStr.split(","))
            .map(Long::parseLong)
            .collect(Collectors.toList());
    return this.removeByIds(ids);

}

三、用户的导入导出(easyexcel)

推荐阅读官方文档 easyexcel.opensource.alibaba.com/docs/curren…

DEMO代码地址:github.com/alibaba/eas…

   /**
     * 文件下载(失败了会返回一个有部分数据的Excel)
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DownloadData}
     * <p>
     * 2. 设置返回的 参数
     * <p>
     * 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
     */
    @GetMapping("download")
    public void download(HttpServletResponse response) throws IOException {
        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
    }

    /**
     * 文件上传
     * <p>1. 创建excel对应的实体对象 参照{@link UploadData}
     * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}
     * <p>3. 直接读即可
     */
    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();
        return "success";
    }

1.用户信息导出

controller代码实现 查询数据库逻辑在此省略。封装UserExportDTO时要注意使用注解 @ExcelProperty指定列名 @DateTimeFormat指定时间格式化

@Operation(summary = "导出用户")
@GetMapping("/export")
public void exportUsers(UserPageQuery queryParams, HttpServletResponse response) throws IOException {
    String fileName = "用户列表.xlsx";
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
    List<UserExportDTO> exportUserList = userService.listExportUsers(queryParams);
    EasyExcel.write(response.getOutputStream(), UserExportDTO.class).sheet("用户列表")
            .doWrite(exportUserList);
}

2.用户导入模板下载

在项目下新建文件 resources/templates/excel/用户导入模板.xlsx

@Operation(summary = "用户导入模板下载")
@GetMapping("/template")
public void downloadTemplate(HttpServletResponse response) throws IOException {
    String fileName = "用户导入模板.xlsx";
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));

    String fileClassPath = "templates" + File.separator + "excel" + File.separator + fileName;
    InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileClassPath);

    ServletOutputStream outputStream = response.getOutputStream();
    ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(inputStream).build();

    excelWriter.finish();
}

3.导入用户

读取excel的核心方法就是

EasyExcel.read(is, clazz, listener).sheet().doRead();
创建自定义监听器对象继承AnalysisEventListener

判断数据格式是否正确 正确之后进行数据保存