用户相关接口实现
一、用户分页数据
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给前端
@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
判断数据格式是否正确 正确之后进行数据保存