feat: 系统服务集成OSS、SMS,新增用户注册和用户个人中心信息等接口

This commit is contained in:
haoxr 2024-02-01 17:38:54 +08:00
parent 7916b82dff
commit 8aa45c1d8a
27 changed files with 596 additions and 273 deletions

View File

@ -18,13 +18,6 @@
<dependencies>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Cloud & Alibaba -->
<dependency>
<groupId>org.springframework.cloud</groupId>
@ -59,6 +52,7 @@
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Sentinel规则持久化至Nacos配置 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
@ -69,12 +63,11 @@
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
@ -104,12 +97,7 @@
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-file</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-resourceserver</artifactId>
<artifactId>common-security</artifactId>
</dependency>
<dependency>
@ -117,6 +105,13 @@
<artifactId>common-apidoc</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,49 @@
package com.youlai.system.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
/**
* 阿里云短信配置
*
* @author haoxr
* @since 2021/10/13
*/
@Configuration
@ConfigurationProperties(prefix = "sms.aliyun")
@Data
public class AliyunSmsProperties {
/**
* 阿里云账户的Access Key ID用于API请求认证
*/
private String accessKeyId;
/**
*阿里云账户的Access Key Secret用于API请求认证
*/
private String accessKeySecret;
/**
* 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com
*/
private String domain;
/**
* 阿里云服务的区域ID如cn-shanghai
*/
private String regionId;
/**
* 短信签名必须是已经在阿里云短信服务中注册并通过审核的
*/
private String signName;
/**
* 模板编码
*/
private Map<String, String> templateCodes;
}

View File

@ -1,8 +1,8 @@
package com.youlai.common.file.controller;
package com.youlai.system.controller;
import com.youlai.common.file.model.FileInfo;
import com.youlai.common.file.service.OssService;
import com.youlai.common.result.Result;
import com.youlai.system.model.vo.FileInfoVO;
import com.youlai.system.service.OssService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -20,10 +20,10 @@ public class FileController {
@PostMapping
@Operation(summary= "文件上传")
public Result<FileInfo> uploadFile(
public Result<FileInfoVO> uploadFile(
@Parameter(name = "表单文件对象") @RequestParam(value = "file") MultipartFile file
) {
FileInfo fileInfo = ossService.uploadFile(file);
FileInfoVO fileInfo = ossService.uploadFile(file);
return Result.success(fileInfo);
}

View File

@ -4,7 +4,6 @@ import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.common.constant.ExcelConstants;
import com.youlai.common.result.PageResult;
import com.youlai.common.result.Result;
import com.youlai.common.web.annotation.PreventDuplicateResubmit;
@ -12,11 +11,9 @@ import com.youlai.system.dto.UserAuthInfo;
import com.youlai.system.listener.excel.UserImportListener;
import com.youlai.system.model.entity.SysUser;
import com.youlai.system.model.form.UserForm;
import com.youlai.system.model.form.UserRegisterForm;
import com.youlai.system.model.query.UserPageQuery;
import com.youlai.system.model.vo.UserExportVO;
import com.youlai.system.model.vo.UserImportVO;
import com.youlai.system.model.vo.UserInfoVO;
import com.youlai.system.model.vo.UserPageVO;
import com.youlai.system.model.vo.*;
import com.youlai.system.service.SysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -124,7 +121,7 @@ public class SysUserController {
return Result.judge(result);
}
@Operation(summary = "根据用户名获取认证信息", hidden = true)
@Operation(summary = "获取用户认证信息", hidden = true)
@GetMapping("/{username}/authInfo")
public Result<UserAuthInfo> getUserAuthInfo(
@Parameter(description = "用户名") @PathVariable String username
@ -140,14 +137,13 @@ public class SysUserController {
return Result.success(userInfoVO);
}
@Operation(summary = "用户注销")
@Operation(summary = "注销登出")
@DeleteMapping("/logout")
public Result logout() {
boolean result = userService.logout();
return Result.judge(result);
}
@Operation(summary = "用户导入模板下载")
@GetMapping("/template")
public void downloadTemplate(HttpServletResponse response) throws IOException {
@ -155,7 +151,7 @@ public class SysUserController {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
String fileClassPath = ExcelConstants.EXCEL_TEMPLATE_DIR + File.separator + fileName;
String fileClassPath = "excel-templates" + File.separator + fileName;
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileClassPath);
ServletOutputStream outputStream = response.getOutputStream();
@ -165,7 +161,7 @@ public class SysUserController {
}
@Operation(summary = "导入用户")
@PostMapping("/_import")
@PostMapping("/import")
public Result importUsers(@Parameter(description = "部门ID") Long deptId, MultipartFile file) throws IOException {
UserImportListener listener = new UserImportListener(deptId);
EasyExcel.read(file.getInputStream(), UserImportVO.class, listener).sheet().doRead();
@ -174,7 +170,7 @@ public class SysUserController {
}
@Operation(summary = "导出用户")
@GetMapping("/_export")
@GetMapping("/export")
public void exportUsers(UserPageQuery queryParams, HttpServletResponse response) throws IOException {
String fileName = "用户列表.xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@ -184,4 +180,39 @@ public class SysUserController {
EasyExcel.write(response.getOutputStream(), UserExportVO.class).sheet("用户列表")
.doWrite(exportUserList);
}
@Operation(summary = "注册用户")
@PostMapping("/register")
public Result registerUser(
@RequestBody @Valid UserRegisterForm userRegisterForm
) {
boolean result = userService.registerUser(userRegisterForm);
return Result.judge(result);
}
@Operation(summary = "发送注册短信验证码")
@PostMapping("/register/sms_code")
public Result sendRegisterSmsCode(
@Parameter(description = "手机号") @RequestParam String mobile
) {
boolean result = userService.sendRegisterSmsCode(mobile);
return Result.judge(result);
}
@Operation(summary = "发送登录短信验证码")
@PostMapping("/login/sms_code")
public Result sendLoginSmsCode(
@Parameter(description = "手机号") @RequestParam String mobile
) {
boolean result = userService.sendLoginSmsCode(mobile);
return Result.judge(result);
}
@Operation(summary = "获取用户个人中心信息")
@GetMapping("/profile")
public Result getUserProfile() {
UserProfileVO userProfile = userService.getUserProfile();
return Result.success(userProfile);
}
}

View File

@ -3,11 +3,13 @@ package com.youlai.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.system.model.bo.UserBO;
import com.youlai.system.model.bo.UserFormBO;
import com.youlai.system.model.bo.UserProfileBO;
import com.youlai.system.model.entity.SysUser;
import com.youlai.system.model.form.UserForm;
import com.youlai.system.model.vo.UserImportVO;
import com.youlai.system.model.vo.UserInfoVO;
import com.youlai.system.model.vo.UserPageVO;
import com.youlai.system.model.vo.UserProfileVO;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@ -43,4 +45,8 @@ public interface UserConverter {
SysUser importVo2Entity(UserImportVO vo);
@Mappings({
@Mapping(target = "genderLabel", expression = "java(com.youlai.common.base.IBaseEnum.getLabelByValue(bo.getGender(), com.youlai.common.enums.GenderEnum.class))")
})
UserProfileVO userProfileBo2Vo(UserProfileBO bo);
}

View File

@ -19,12 +19,4 @@ import java.util.Set;
public interface SysMenuMapper extends BaseMapper<SysMenu> {
List<RouteBO> listRoutes();
/**
* 获取角色权限集合
*
* @param roles
* @return
*/
Set<String> listRolePerms(Set<String> roles);
}

View File

@ -6,6 +6,7 @@ import com.youlai.common.mybatis.annotation.DataPermission;
import com.youlai.system.dto.UserAuthInfo;
import com.youlai.system.model.bo.UserBO;
import com.youlai.system.model.bo.UserFormBO;
import com.youlai.system.model.bo.UserProfileBO;
import com.youlai.system.model.entity.SysUser;
import com.youlai.system.model.query.UserPageQuery;
import com.youlai.system.model.vo.UserExportVO;
@ -25,9 +26,9 @@ public interface SysUserMapper extends BaseMapper<SysUser> {
/**
* 获取用户分页列表
*
* @param page
* @param page 分页参数
* @param queryParams 查询参数
* @return
* @return {@link List<UserBO>}
*/
@DataPermission(deptAlias = "u")
Page<UserBO> getUserPage(Page<UserBO> page, UserPageQuery queryParams);
@ -36,24 +37,32 @@ public interface SysUserMapper extends BaseMapper<SysUser> {
* 获取用户表单详情
*
* @param userId 用户ID
* @return
* @return {@link UserFormBO}
*/
UserFormBO getUserDetail(Long userId);
/**
* 根据用户名获取认证信息
*
* @param username
* @return
* @param username 用户名
* @return {@link UserAuthInfo}
*/
UserAuthInfo getUserAuthInfo(String username);
/**
* 获取导出用户列表
*
* @param queryParams
* @return
* @param queryParams 查询参数
* @return {@link List<UserExportVO>}
*/
@DataPermission(deptAlias = "u")
List<UserExportVO> listExportUsers(UserPageQuery queryParams);
/**
* 获取用户个人中心信息
*
* @param userId 用户ID
* @return {@link UserProfileBO}
*/
UserProfileBO getUserProfile(Long userId);
}

View File

@ -0,0 +1,68 @@
package com.youlai.system.model.bo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
import java.util.Set;
/**
* 用户个人中心对象
*
* @author haoxr
* @since 3.1.0
*/
@Data
public class UserProfileBO {
/**
* 用户ID
*/
private Long id;
/**
* 登录账号
*/
private String username;
/**
* 用户昵称
*/
private String nickname;
/**
* 手机号码
*/
private String mobile;
/**
* 头像地址
*/
private String avatar;
/**
* 用户角色名称集合
*/
private Set<String> roleNames;
/**
* 部门名称
*/
private String deptName;
/**
* 邮箱
*/
private String email;
/**
* 性别
*/
private Integer gender;
/**
* 创建时间
*/
private Date createTime;
}

View File

@ -1,59 +0,0 @@
package com.youlai.system.model.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 用户导入对象
*
* @author haoxr
* @since 2022/4/10
*/
@Data
public class UserImportDTO {
/**
* 部门ID
*/
private Long deptId;
/**
* 角色ID
*/
private String roleIds;
private MultipartFile file;
/**
* 导入的用户列表
*/
private List<UserItem> userList;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class UserItem {
@ExcelProperty(value = "用户名")
private String username;
@ExcelProperty(value = "用户昵称")
private String nickname;
@ExcelProperty(value = "性别")
private String gender;
@ExcelProperty(value = "手机号码")
private String mobile;
@ExcelProperty(value = "邮箱")
private String email;
}
}

View File

@ -52,7 +52,7 @@ public class SysUser extends BaseEntity {
private String mobile;
/**
* 用户状态((1:正常;0:禁用))
* 用户状态(1:正常;0:禁用)
*/
private Integer status;

View File

@ -0,0 +1,37 @@
package com.youlai.system.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.util.List;
/**
* 用户注册表单
*
* @author haoxr
* @since 3.1.0
*/
@Schema(description = "用户注册表单")
@Data
public class UserRegisterForm {
@Schema(description="登录账号")
@NotBlank(message = "登录账号不能为空")
private String username;
@Schema(description="手机号码")
@Pattern(regexp = "^$|^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$", message = "手机号码格式不正确")
private String mobile;
@Schema(description="密码")
@NotBlank(message = "密码不能为空")
private String password;
@Schema(description="验证码")
@NotBlank(message = "验证码不能为空")
private String code;
}

View File

@ -0,0 +1,45 @@
package com.youlai.system.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Set;
/**
* 个人中心用户视图对象
*
* @author haoxr
* @since 2022/1/14
*/
@Schema(description ="个人中心用户视图对象")
@Data
public class UserProfileVO {
@Schema(description="用户ID")
private Long id;
@Schema(description="登录账号")
private String username;
@Schema(description="用户昵称")
private String nickname;
@Schema(description="手机号码")
private String mobile;
@Schema(description="头像地址")
private String avatar;
@Schema(description="用户角色名称集合")
private Set<String> roleNames;
@Schema(description="部门名称")
private String deptName;
@Schema(description="邮箱")
private String email;
@Schema(description="性别")
private String genderLabel;
}

View File

@ -1,6 +1,6 @@
package com.youlai.common.file.service;
package com.youlai.system.service;
import com.youlai.common.file.model.FileInfo;
import com.youlai.system.model.vo.FileInfoVO;
import org.springframework.web.multipart.MultipartFile;
/**
@ -16,7 +16,7 @@ public interface OssService {
* @param file 表单文件对象
* @return 文件信息
*/
FileInfo uploadFile(MultipartFile file);
FileInfoVO uploadFile(MultipartFile file);
/**
* 删除文件

View File

@ -0,0 +1,24 @@
package com.youlai.system.service;
/**
* 短信服务接口层
*
* @author haoxr
* @since 3.0.0
*/
public interface SmsService {
/**
* 发送短信验证码
*
* @param mobile 手机号 13388886666
* @param templateCode 短信模板 SMS_194640010
* @param templateParam 模板参数 "[{"code":"123456"}]"
*
* @return boolean 是否发送成功
*/
boolean sendSms(String mobile, String templateCode, String templateParam);
}

View File

@ -58,14 +58,6 @@ public interface SysMenuService extends IService<SysMenu> {
*/
boolean updateMenuVisible(Long menuId, Integer visible);
/**
* 获取角色权限集合
*
* @param roles
* @return
*/
Set<String> listRolePerms(Set<String> roles);
/**
* 获取菜单表单数据
*

View File

@ -6,14 +6,20 @@ import com.youlai.system.model.entity.SysUserRole;
import java.util.List;
/**
* 用户角色关联业务接口
*
* @author haoxr
* @since 0.0.1
*/
public interface SysUserRoleService extends IService<SysUserRole> {
/**
* 保存用户角色
*
* @param userId
* @param roleIds
* @return
* @param userId 用户ID
* @param roleIds 角色ID集合
* @return boolean 是否保存成功
*/
boolean saveUserRoles(Long userId, List<Long> roleIds);

View File

@ -6,10 +6,12 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.system.dto.UserAuthInfo;
import com.youlai.system.model.entity.SysUser;
import com.youlai.system.model.form.UserForm;
import com.youlai.system.model.form.UserRegisterForm;
import com.youlai.system.model.query.UserPageQuery;
import com.youlai.system.model.vo.UserExportVO;
import com.youlai.system.model.vo.UserInfoVO;
import com.youlai.system.model.vo.UserPageVO;
import com.youlai.system.model.vo.UserProfileVO;
import java.util.List;
@ -24,7 +26,7 @@ public interface SysUserService extends IService<SysUser> {
/**
* 用户分页列表
*
* @return
* @return {@link IPage<UserPageVO>}
*/
IPage<UserPageVO> getUserPage(UserPageQuery queryParams);
@ -32,8 +34,8 @@ public interface SysUserService extends IService<SysUser> {
/**
* 获取用户表单数据
*
* @param userId
* @return
* @param userId 用户ID
* @return {@link UserForm}
*/
UserForm getUserFormData(Long userId);
@ -42,7 +44,7 @@ public interface SysUserService extends IService<SysUser> {
* 新增用户
*
* @param userForm 用户表单对象
* @return
* @return {@link Boolean}
*/
boolean saveUser(UserForm userForm);
@ -51,7 +53,7 @@ public interface SysUserService extends IService<SysUser> {
*
* @param userId 用户ID
* @param userForm 用户表单对象
* @return
* @return {@link Boolean}
*/
boolean updateUser(Long userId, UserForm userForm);
@ -60,7 +62,7 @@ public interface SysUserService extends IService<SysUser> {
* 删除用户
*
* @param idsStr 用户ID多个以英文逗号(,)分割
* @return
* @return {@link Boolean}
*/
boolean deleteUsers(String idsStr);
@ -70,7 +72,7 @@ public interface SysUserService extends IService<SysUser> {
*
* @param userId 用户ID
* @param password 用户密码
* @return
* @return {@link Boolean}
*/
boolean updatePassword(Long userId, String password);
@ -87,8 +89,8 @@ public interface SysUserService extends IService<SysUser> {
/**
* 获取导出用户列表
*
* @param queryParams
* @return
* @param queryParams 查询参数
* @return {@link List<UserExportVO>}
*/
List<UserExportVO> listExportUsers(UserPageQuery queryParams);
@ -96,14 +98,47 @@ public interface SysUserService extends IService<SysUser> {
/**
* 获取登录用户信息
*
* @return
* @return {@link UserInfoVO}
*/
UserInfoVO getCurrentUserInfo();
/**
* 注销登出
*
* @return
* @return {@link Boolean}
*/
boolean logout();
/**
* 注册用户
*
* @param userRegisterForm 用户注册表单对象
* @return {@link Boolean}
*/
boolean registerUser(UserRegisterForm userRegisterForm);
/**
* 发送注册短信验证码
*
* @param mobile 手机号
* @return {@link Boolean} 是否发送成功
*/
boolean sendRegisterSmsCode(String mobile);
/**
* 发送登录短信验证码
*
* @param mobile 手机号
* @return {@link Boolean} 是否发送成功
*/
boolean sendLoginSmsCode(String mobile);
/**
* 获取用户个人中心信息
*
* @return {@link UserProfileVO}
*/
UserProfileVO getUserProfile();
}

View File

@ -256,17 +256,6 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
);
}
/**
* 获取角色权限集合
*
* @param roles 角色编码集合
* @return 权限标识集合
*/
@Override
public Set<String> listRolePerms(Set<String> roles) {
return this.baseMapper.listRolePerms(roles);
}
/**
* 获取菜单表单数据
*

View File

@ -2,7 +2,7 @@ package com.youlai.system.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.constant.RedisConstants;
import com.youlai.system.mapper.SysRoleMenuMapper;
import com.youlai.system.model.bo.RolePermsBO;
import com.youlai.system.model.entity.SysRoleMenu;
@ -39,14 +39,14 @@ public class SysRoleMenuServiceImpl extends ServiceImpl<SysRoleMenuMapper, SysRo
@Override
public void refreshRolePermsCache() {
// 清理权限缓存
redisTemplate.opsForHash().delete(SecurityConstants.ROLE_PERMS_PREFIX, "*");
redisTemplate.opsForHash().delete(RedisConstants.ROLE_PERMS_PREFIX, "*");
List<RolePermsBO> list = this.baseMapper.getRolePermsList(null);
if (CollectionUtil.isNotEmpty(list)) {
list.forEach(item -> {
String roleCode = item.getRoleCode();
Set<String> perms = item.getPerms();
redisTemplate.opsForHash().put(SecurityConstants.ROLE_PERMS_PREFIX, roleCode, perms);
redisTemplate.opsForHash().put(RedisConstants.ROLE_PERMS_PREFIX, roleCode, perms);
});
}
}
@ -57,7 +57,7 @@ public class SysRoleMenuServiceImpl extends ServiceImpl<SysRoleMenuMapper, SysRo
@Override
public void refreshRolePermsCache(String roleCode) {
// 清理权限缓存
redisTemplate.opsForHash().delete(SecurityConstants.ROLE_PERMS_PREFIX, roleCode);
redisTemplate.opsForHash().delete(RedisConstants.ROLE_PERMS_PREFIX, roleCode);
List<RolePermsBO> list = this.baseMapper.getRolePermsList(roleCode);
if (CollectionUtil.isNotEmpty(list)) {
@ -67,7 +67,7 @@ public class SysRoleMenuServiceImpl extends ServiceImpl<SysRoleMenuMapper, SysRo
}
Set<String> perms = rolePerms.getPerms();
redisTemplate.opsForHash().put(SecurityConstants.ROLE_PERMS_PREFIX, roleCode, perms);
redisTemplate.opsForHash().put(RedisConstants.ROLE_PERMS_PREFIX, roleCode, perms);
}
}
@ -77,7 +77,7 @@ public class SysRoleMenuServiceImpl extends ServiceImpl<SysRoleMenuMapper, SysRo
@Override
public void refreshRolePermsCache(String oldRoleCode, String newRoleCode) {
// 清理旧角色权限缓存
redisTemplate.opsForHash().delete(SecurityConstants.ROLE_PERMS_PREFIX, oldRoleCode);
redisTemplate.opsForHash().delete(RedisConstants.ROLE_PERMS_PREFIX, oldRoleCode);
// 添加新角色权限缓存
List<RolePermsBO> list =this.baseMapper.getRolePermsList(newRoleCode);
@ -88,7 +88,7 @@ public class SysRoleMenuServiceImpl extends ServiceImpl<SysRoleMenuMapper, SysRo
}
Set<String> perms = rolePerms.getPerms();
redisTemplate.opsForHash().put(SecurityConstants.ROLE_PERMS_PREFIX, newRoleCode, perms);
redisTemplate.opsForHash().put(RedisConstants.ROLE_PERMS_PREFIX, newRoleCode, perms);
}
}

View File

@ -5,7 +5,6 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.constant.SystemConstants;
@ -14,7 +13,6 @@ import com.youlai.system.converter.RoleConverter;
import com.youlai.system.mapper.SysRoleMapper;
import com.youlai.system.model.entity.SysRole;
import com.youlai.system.model.entity.SysRoleMenu;
import com.youlai.system.model.entity.SysUserRole;
import com.youlai.system.model.form.RoleForm;
import com.youlai.system.model.query.RolePageQuery;
import com.youlai.common.web.model.Option;

View File

@ -2,40 +2,46 @@ package com.youlai.system.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.constant.GlobalConstants;
import com.youlai.common.constant.RedisConstants;
import com.youlai.common.constant.SystemConstants;
import com.youlai.common.security.service.PermissionService;
import com.youlai.common.security.util.SecurityUtils;
import com.youlai.system.config.AliyunSmsProperties;
import com.youlai.system.converter.UserConverter;
import com.youlai.system.dto.UserAuthInfo;
import com.youlai.system.mapper.SysUserMapper;
import com.youlai.system.model.bo.UserBO;
import com.youlai.system.model.bo.UserFormBO;
import com.youlai.system.model.bo.UserProfileBO;
import com.youlai.system.model.entity.SysUser;
import com.youlai.system.model.form.UserForm;
import com.youlai.system.model.form.UserRegisterForm;
import com.youlai.system.model.query.UserPageQuery;
import com.youlai.system.model.vo.UserExportVO;
import com.youlai.system.model.vo.UserInfoVO;
import com.youlai.system.model.vo.UserPageVO;
import com.youlai.system.service.SysMenuService;
import com.youlai.system.model.vo.UserProfileVO;
import com.youlai.system.service.SmsService;
import com.youlai.system.service.SysRoleService;
import com.youlai.system.service.SysUserRoleService;
import com.youlai.system.service.SysUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -47,6 +53,7 @@ import java.util.stream.Collectors;
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
private final PasswordEncoder passwordEncoder;
@ -57,12 +64,14 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
private final UserConverter userConverter;
private final RedisTemplate<String,Object> redisTemplate;
private final SysMenuService menuService;
private final PermissionService permissionService;
private final SmsService smsService;
private final AliyunSmsProperties aliyunSmsProperties;
private final RedisTemplate<String, Object> redisTemplate;
/**
* 获取用户分页列表
*
@ -81,9 +90,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
Page<UserBO> userBoPage = this.baseMapper.getUserPage(page, queryParams);
// 实体转换
Page<UserPageVO> userVoPage = userConverter.bo2Vo(userBoPage);
return userVoPage;
return userConverter.bo2Vo(userBoPage);
}
/**
@ -96,8 +103,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
public UserForm getUserFormData(Long userId) {
UserFormBO userFormBO = this.baseMapper.getUserDetail(userId);
// 实体转换po->form
UserForm userForm = userConverter.bo2Form(userFormBO);
return userForm;
return userConverter.bo2Form(userFormBO);
}
/**
@ -136,7 +142,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
*
* @param userId 用户ID
* @param userForm 用户表单对象
* @return
* @return true|false 是否更新成功
*/
@Override
@Transactional
@ -167,12 +173,10 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
* 删除用户
*
* @param idsStr 用户ID多个以英文逗号(,)分割
* @return 删除结果 true/false
* @return true/false 是否删除成功
*/
@Override
public boolean deleteUsers(String idsStr) {
Assert.isTrue(StrUtil.isNotBlank(idsStr), "删除的用户数据为空");
// 逻辑删除
List<Long> ids = Arrays.stream(idsStr.split(","))
.map(Long::parseLong).
collect(Collectors.toList());
@ -207,10 +211,6 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
if (userAuthInfo != null) {
Set<String> roles = userAuthInfo.getRoles();
if (CollectionUtil.isNotEmpty(roles)) {
// 根据角色编码集合获取权限标识集合
Set<String> perms = menuService.listRolePerms(roles);
userAuthInfo.setPerms(perms);
// 获取最大范围的数据权限(目前设定DataScope越小拥有的数据权限范围越大所以获取得到角色列表中最小的DataScope)
Integer dataScope = roleService.getMaxDataRangeDataScope(roles);
userAuthInfo.setDataScope(dataScope);
@ -228,8 +228,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
*/
@Override
public List<UserExportVO> listExportUsers(UserPageQuery queryParams) {
List<UserExportVO> list = this.baseMapper.listExportUsers(queryParams);
return list;
return this.baseMapper.listExportUsers(queryParams);
}
/**
@ -251,11 +250,11 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
// entity->VO
UserInfoVO userInfoVO = userConverter.entity2UserInfoVo(user);
// 用户角色集合
// 获取用户角色集合
Set<String> roles = SecurityUtils.getRoles();
userInfoVO.setRoles(roles);
// 用户权限集合
// 获取用户权限集合
if (CollectionUtil.isNotEmpty(roles)) {
Set<String> perms = permissionService.getRolePermsFormCache(roles);
userInfoVO.setPerms(perms);
@ -272,19 +271,133 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
@Override
public boolean logout() {
String jti = SecurityUtils.getJti();
Optional<Long> expireTimeOpt = Optional.ofNullable(SecurityUtils.getExp()); // 使用Optional处理可能的null值
Long expireTime = SecurityUtils.getExp(); // token 过期时间戳()
long currentTimeInSeconds = System.currentTimeMillis() / 1000; // 当前时间单位
long currentTime = System.currentTimeMillis() / 1000;// 当前时间单位
if (expireTime != null) {
if (expireTime > currentTime) {
// token未过期添加至缓存作为黑名单限制访问缓存时间为token过期剩余时间
redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (expireTime - currentTime), TimeUnit.SECONDS);
expireTimeOpt.ifPresent(expireTime -> {
if (expireTime > currentTimeInSeconds) {
// token未过期添加至缓存作为黑名单缓存时间为token剩余的有效时间
long remainingTimeInSeconds = expireTime - currentTimeInSeconds;
redisTemplate.opsForValue().set(RedisConstants.TOKEN_BLACKLIST_PREFIX + jti, "", remainingTimeInSeconds, TimeUnit.SECONDS);
}
} else {
});
if (!expireTimeOpt.isPresent()) {
// token 永不过期则永久加入黑名单
redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null);
redisTemplate.opsForValue().set(RedisConstants.TOKEN_BLACKLIST_PREFIX + jti, "");
}
return true;
}
/**
* 注册用户
*
* @param userRegisterForm 用户注册表单对象
* @return true|false 是否注册成功
*/
@Override
public boolean registerUser(UserRegisterForm userRegisterForm) {
String mobile = userRegisterForm.getMobile();
String code = userRegisterForm.getCode();
// 校验验证码
String cacheCode = (String) redisTemplate.opsForValue().get(RedisConstants.REGISTER_SMS_CODE_PREFIX + mobile);
if (!StrUtil.equals(code, cacheCode)) {
log.warn("验证码不匹配或不存在: {}", mobile);
return false; // 验证码不匹配或不存在时返回false
}
// 校验通过删除验证码
redisTemplate.delete(RedisConstants.REGISTER_SMS_CODE_PREFIX + mobile);
// 校验手机号是否已注册
long count = this.count(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getMobile, mobile)
.or()
.eq(SysUser::getUsername, mobile)
);
Assert.isTrue(count == 0, "手机号已注册");
SysUser entity = new SysUser();
entity.setUsername(mobile);
entity.setMobile(mobile);
entity.setStatus(GlobalConstants.STATUS_YES);
// 设置默认加密密码
String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD);
entity.setPassword(defaultEncryptPwd);
// 新增用户并直接返回结果
return this.save(entity);
}
/**
* 发送注册短信验证码
*
* @param mobile 手机号
* @return true|false 是否发送成功
*/
@Override
public boolean sendRegisterSmsCode(String mobile) {
// 获取短信模板代码
String templateCode = aliyunSmsProperties.getTemplateCodes().get("register");
// 生成随机4位数验证码
String code = RandomUtil.randomNumbers(4);
// 短信模板: 您的验证码${code}该验证码5分钟内有效请勿泄漏于他人
// 其中 ${code} 是模板参数使用时需要替换为实际值
String templateParams = JSONUtil.toJsonStr(Collections.singletonMap("code", code));
boolean result = smsService.sendSms(mobile, templateCode, templateParams);
if (result) {
// 将验证码存入redis有效期5分钟
redisTemplate.opsForValue().set(RedisConstants.REGISTER_SMS_CODE_PREFIX + mobile, code, 5, TimeUnit.MINUTES);
// TODO 考虑记录每次发送短信的详情如发送时间手机号和短信内容等以便后续审核或分析短信发送效果
}
return result;
}
/**
* 发送登录短信验证码
*
* @param mobile 手机号
* @return true|false 是否发送成功
*/
@Override
public boolean sendLoginSmsCode(String mobile) {
// 获取短信模板代码
String templateCode = aliyunSmsProperties.getTemplateCodes().get("login");
// 生成随机4位数验证码
String code = RandomUtil.randomNumbers(4);
// 短信模板: 您的验证码${code}该验证码5分钟内有效请勿泄漏于他人
// 其中 ${code} 是模板参数使用时需要替换为实际值
String templateParams = JSONUtil.toJsonStr(Collections.singletonMap("code", code));
boolean result = smsService.sendSms(mobile, templateCode, templateParams);
if (result) {
// 将验证码存入redis有效期5分钟
redisTemplate.opsForValue().set(RedisConstants.REGISTER_SMS_CODE_PREFIX + mobile, code, 5, TimeUnit.MINUTES);
// TODO 考虑记录每次发送短信的详情如发送时间手机号和短信内容等以便后续审核或分析短信发送效果
}
return result;
}
/**
* 获取用户个人中心信息
*
* @return {@link UserProfileVO}
*/
@Override
public UserProfileVO getUserProfile() {
Long userId = SecurityUtils.getUserId();
// 获取用户个人中心信息
UserProfileBO userProfileBO = this.baseMapper.getUserProfile(userId);
return userConverter.userProfileBo2Vo(userProfileBO);
}
}

View File

@ -1,4 +1,4 @@
package com.youlai.common.file.service.impl;
package com.youlai.system.service.impl.oss;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
@ -8,27 +8,27 @@ import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.youlai.common.file.model.FileInfo;
import com.youlai.common.file.service.OssService;
import com.youlai.system.model.vo.FileInfoVO;
import com.youlai.system.service.OssService;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
/**
* Aliyun 对象存储服务类
* 阿里云OSS服务
*
* @author haoxr
* @since 2.3.0
*/
@Component
@Service
@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun")
@ConfigurationProperties(prefix = "oss.aliyun")
@RequiredArgsConstructor
@ -60,7 +60,7 @@ public class AliyunOssService implements OssService {
@Override
@SneakyThrows
public FileInfo uploadFile(MultipartFile file) {
public FileInfoVO uploadFile(MultipartFile file) {
// 生成文件名(日期文件夹)
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
@ -81,7 +81,7 @@ public class AliyunOssService implements OssService {
}
// 获取文件访问路径
String fileUrl = "https://" + bucketName + "." + endpoint + "/" + fileName;
FileInfo fileInfo = new FileInfo();
FileInfoVO fileInfo = new FileInfoVO();
fileInfo.setName(fileName);
fileInfo.setUrl(fileUrl);
return fileInfo;

View File

@ -1,12 +1,12 @@
package com.youlai.common.file.service.impl;
package com.youlai.system.service.impl.oss;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.common.file.model.FileInfo;
import com.youlai.common.file.service.OssService;
import com.youlai.system.model.vo.FileInfoVO;
import com.youlai.system.service.OssService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
@ -17,6 +17,7 @@ import lombok.SneakyThrows;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@ -29,7 +30,7 @@ import java.time.LocalDateTime;
* @author haoxr
* @since 2023/6/2
*/
@Component
@Service
@ConditionalOnProperty(value = "oss.type", havingValue = "minio")
@ConfigurationProperties(prefix = "oss.minio")
@RequiredArgsConstructor
@ -76,7 +77,7 @@ public class MinioOssService implements OssService {
* @return
*/
@Override
public FileInfo uploadFile(MultipartFile file) {
public FileInfoVO uploadFile(MultipartFile file) {
// 生成文件名(日期文件夹)
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
@ -107,7 +108,7 @@ public class MinioOssService implements OssService {
fileUrl = customDomain + '/' + bucketName + "/" + fileName;
}
FileInfo fileInfo = new FileInfo();
FileInfoVO fileInfo = new FileInfoVO();
fileInfo.setName(fileName);
fileInfo.setUrl(fileUrl);
return fileInfo;

View File

@ -1,7 +1,5 @@
package com.youlai.common.sms.service;
package com.youlai.system.service.impl.sms;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
@ -10,39 +8,35 @@ import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.sms.config.AliyunSmsProperties;
import com.youlai.system.config.AliyunSmsProperties;
import com.youlai.system.service.SmsService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 阿里云短信业务类
*
* @author <a href="mailto:xianrui0365@163.com">haoxr</a>
* @since 2021/10/13 23:04
* @author haoxr
* @since 3.1.0
*/
@Service
@RequiredArgsConstructor
public class AliyunSmsService {
public class AliyunSmsService implements SmsService {
private final AliyunSmsProperties aliyunSmsProperties;
private final StringRedisTemplate stringRedisTemplate;
/**
* 发送短信
* 发送短信验证码
*
* @param phoneNumber 手机号
* @return
* @param mobile 手机号 13388886666
* @param templateCode 短信模板 SMS_194640010
* @param templateParam 模板参数 "[{"code":"123456"}]"
*
* @return boolean 是否发送成功
*/
public boolean sendSmsCode(String phoneNumber) {
String code = RandomUtil.randomNumbers(6); // 随机生成6位的验证码
stringRedisTemplate.opsForValue().set(SecurityConstants.SMS_CODE_PREFIX + phoneNumber, code, 600, TimeUnit.SECONDS);
@Override
public boolean sendSms(String mobile,String templateCode,String templateParam) {
DefaultProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(),
aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret());
@ -52,26 +46,22 @@ public class AliyunSmsService {
CommonRequest request = new CommonRequest();
// 指定请求方式
request.setSysMethod(MethodType.POST);
// 短信api的请求地址 固定
// 短信api的请求地址(固定)
request.setSysDomain(aliyunSmsProperties.getDomain());
// 签名算法版 固定
// 签名算法版(固定)
request.setSysVersion("2017-05-25");
// 请求 API 的名称
// 请求 API 的名称(固定)
request.setSysAction("SendSms");
// 指定地域名称
request.putQueryParameter("RegionId", aliyunSmsProperties.getRegionId());
// 要给哪个手机号发送短信 指定手机号
request.putQueryParameter("PhoneNumbers", phoneNumber);
request.putQueryParameter("PhoneNumbers", mobile);
// 您的申请签名
request.putQueryParameter("SignName", aliyunSmsProperties.getSignName());
// 您申请的模板 code
request.putQueryParameter("TemplateCode", aliyunSmsProperties.getTemplateCode());
request.putQueryParameter("TemplateCode", templateCode);
Map<String, Object> params = new HashMap<>();
// 这里的key就是短信模板中的 ${xxxx}
params.put("code", code);
request.putQueryParameter("TemplateParam", JSONUtil.toJsonStr(params));
request.putQueryParameter("TemplateParam", templateParam);
try {
CommonResponse response = client.getCommonResponse(request);
@ -82,7 +72,6 @@ public class AliyunSmsService {
e.printStackTrace();
}
return false;
}

View File

@ -46,27 +46,4 @@
ORDER BY t1.sort asc
</select>
<!-- 获取角色拥有的权限集合 -->
<select id="listRolePerms" resultType="java.lang.String">
SELECT
DISTINCT t1.perm
FROM
sys_menu t1
INNER JOIN sys_role_menu t2
INNER JOIN sys_role t3
WHERE
t1.type = '${@com.youlai.common.enums.MenuTypeEnum@BUTTON.getValue()}'
AND t1.perm IS NOT NULL
<choose>
<when test="roles!=null and roles.size()>0">
AND t3.CODE IN
<foreach collection="roles" item="role" separator="," open="(" close=")">
#{role}
</foreach>
</when>
<otherwise>
AND t1.id = -1
</otherwise>
</choose>
</select>
</mapper>

View File

@ -33,7 +33,7 @@
INNER JOIN sys_role t2 ON t1.role_id = t2.id AND t2.deleted = 0 AND t2.`status` = 1
INNER JOIN sys_menu t3 ON t1.menu_id = t3.id
WHERE
type = '${@com.youlai.system.common.enums.MenuTypeEnum@BUTTON.getValue()}'
type = '${@com.youlai.system.enums.MenuTypeEnum@BUTTON.getValue()}'
<if test="roleCode!=null and roleCode.trim() neq ''">
AND t2.`code` = #{roleCode}
</if>

View File

@ -23,7 +23,7 @@
LEFT JOIN sys_user_role sur ON u.id = sur.user_id
LEFT JOIN sys_role r ON sur.role_id = r.id
<where>
u.deleted = 0 AND u.username != 'root'
u.deleted = 0 AND u.username != ${@com.youlai.common.constant.SystemConstants@ROOT_ROLE_CODE}
<if test='queryParams.keywords!=null and queryParams.keywords.trim() neq ""'>
AND (
u.username LIKE CONCAT('%',#{queryParams.keywords},'%')
@ -51,7 +51,7 @@
<result property="avatar" column="avatar" jdbcType="VARCHAR"/>
<result property="email" column="email" jdbcType="VARCHAR"/>
<result property="status" column="status" jdbcType="BOOLEAN"/>
<result property="deptId" column="dept_id" jdbcType="BIGINT"></result>
<result property="deptId" column="dept_id" jdbcType="BIGINT"/>
<collection
property="roleIds"
column="id"
@ -62,7 +62,8 @@
<!-- 根据用户ID获取用户详情 -->
<select id="getUserDetail" resultMap="UserFormMap">
SELECT id,
SELECT
id,
username,
nickname,
mobile,
@ -71,7 +72,8 @@
email,
STATUS,
dept_id
FROM sys_user
FROM
sys_user
WHERE id = #{userId}
AND deleted = 0
</select>
@ -82,9 +84,9 @@
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="status" column="status" jdbcType="BOOLEAN"/>
<result property="deptId" column="dept_id" jdbcType="BIGINT"></result>
<result property="deptId" column="dept_id" jdbcType="BIGINT"/>
<collection property="roles" ofType="string" javaType="java.util.Set">
<result column="code"></result>
<result column="code"/>
</collection>
</resultMap>
@ -103,7 +105,8 @@
LEFT JOIN sys_user_role t2 ON t2.user_id = t1.id
LEFT JOIN sys_role t3 ON t3.id = t2.role_id
WHERE
t1.username = #{username} AND t1.deleted=0
t1.username = #{username}
AND t1.deleted=0
</select>
<!-- 获取用户导出列表 -->
@ -139,4 +142,27 @@
GROUP BY u.id
</select>
<!-- 获取用户个人中心信息 -->
<select id="getUserProfile" resultType="com.youlai.system.model.bo.UserProfileBO">
SELECT
u.id,
u.username,
u.nickname,
u.mobile,
u.gender,
u.avatar,
u.STATUS,
d.NAME AS dept_name,
GROUP_CONCAT( r.NAME ) AS roleNames,
u.create_time
FROM
sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.id
LEFT JOIN sys_user_role sur ON u.id = sur.user_id
LEFT JOIN sys_role r ON sur.role_id = r.id
WHERE
u.id = #{userId}
AND u.deleted = 0
</select>
</mapper>