Compare commits

...

3 Commits

Author SHA1 Message Date
bunny ec1603885f feat: 优化Minio注释和函数名称 2025-05-01 14:32:26 +08:00
bunny 0e84f0934e feat: 优化多语言、角色、权限更新添加基本注释
更新多语言注释信息;更新角色和权限改用事件驱动和并行流加异步
2025-05-01 14:01:13 +08:00
bunny 0570ddd249 feat: 优化登录功能;
删除不用的函数和冗余代码,将用户业务功能放在service下;Minio更名为MinioService将业务类和服务类拆分
2025-05-01 11:57:31 +08:00
58 changed files with 1635 additions and 1176 deletions

View File

@ -197,7 +197,7 @@ AntPath详情https://juejin.cn/spost/7498247273660743732
### 开发环境
根据不docker 启动方式不一样
根据不docker 启动方式不一样
```bash
# 一键启动依赖服务
@ -244,10 +244,10 @@ docker compose up -d
- [ ] 权限树型结构动态添加、更新、删除
- [ ] 用户设置持久化存储到数据库
- [ ] 权限弹窗页面优化
- [ ] 后端文档注释完善
- [x] 后端文档注释完善
- [x] 系统监控后端返回403停止请求
- [ ] 优化用户配置权限逻辑,配置后热更新逻辑等
- [ ] 完善后端注释有需要添加ReadMe文档
- [x] 优化用户配置权限逻辑,配置后热更新逻辑等
- [x] 完善后端注释有需要添加ReadMe文档
- [ ] Redis中获取活跃用户
## 前后端接口规范

View File

@ -2,8 +2,8 @@ package cn.bunny.services.aop;
import cn.bunny.services.domain.common.constant.LocalDateTimeConstant;
import cn.bunny.services.domain.common.enums.JobEnums;
import cn.bunny.services.domain.system.log.entity.ScheduleExecuteLog;
import cn.bunny.services.domain.common.model.dto.quartz.ScheduleExecuteLogJson;
import cn.bunny.services.domain.system.log.entity.ScheduleExecuteLog;
import cn.bunny.services.mapper.log.ScheduleExecuteLogMapper;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.Resource;

View File

@ -1,10 +1,10 @@
package cn.bunny.services.controller.system;
import cn.bunny.services.aop.scanner.ControllerApiPermissionScanner;
import cn.bunny.services.domain.common.model.dto.scanner.ScannerControllerInfoVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.Result;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.common.scanner.ScannerControllerInfoVo;
import cn.bunny.services.domain.system.system.dto.power.PermissionAddDto;
import cn.bunny.services.domain.system.system.dto.power.PermissionDto;
import cn.bunny.services.domain.system.system.dto.power.PermissionUpdateBatchByParentIdDto;

View File

@ -1,16 +1,15 @@
package cn.bunny.services.controller.system;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.Result;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.user.*;
import cn.bunny.services.domain.system.system.dto.user.AdminUserAddDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.vo.user.AdminUserVo;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.domain.system.system.vo.user.UserVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.service.system.UserService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
@ -18,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -33,49 +31,6 @@ public class UserController {
@Resource
private UserService userService;
// -----------------------------------------
// 用户登录和退出
// -----------------------------------------
@Operation(summary = "用户登录", description = "前端用户登录")
@PostMapping("login")
public Result<LoginVo> login(@Valid @RequestBody LoginDto loginDto) {
LoginVo loginVo = userService.login(loginDto);
return Result.success(loginVo);
}
@Operation(summary = "登录发送邮件验证码", description = "登录发送邮件验证码")
@PostMapping("public/sendLoginEmail")
public Result<String> sendLoginEmail(String email) {
if (!StringUtils.hasText(email)) throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);
userService.sendLoginEmail(email);
return Result.success(ResultCodeEnum.EMAIL_CODE_SEND_SUCCESS);
}
@Operation(summary = "刷新token", description = "刷新用户token")
@PostMapping("private/refreshToken")
public Result<RefreshTokenVo> refreshToken(@Valid @RequestBody RefreshTokenDto dto) {
RefreshTokenVo vo = userService.refreshToken(dto);
return Result.success(vo);
}
@Operation(summary = "获取本地登录用户信息", description = "获取用户信息从Redis中获取")
@GetMapping("private/userinfo")
public Result<LoginVo> userinfo() {
LoginVo vo = BaseContext.getLoginVo();
return Result.success(vo);
}
@Operation(summary = "退出登录", description = "退出登录")
@PostMapping("private/logout")
public Result<String> logout() {
userService.logout();
return Result.success(ResultCodeEnum.LOGOUT_SUCCESS);
}
// -----------------------------------------
// 管理用户CURD
// -----------------------------------------
@Operation(summary = "分页查询", description = "分页查询用户信息", tags = "user::query")
@GetMapping("{page}/{limit}")
public Result<PageResult<AdminUserVo>> getUserPageByAdmin(
@ -89,21 +44,19 @@ public class UserController {
return Result.success(pageResult);
}
@Operation(summary = "添加", description = "添加用户信息", tags = "user::add")
@Operation(summary = "添加用户", description = "添加用户信息", tags = "user::add")
@PostMapping()
public Result<Object> addUserByAdmin(@Valid @RequestBody AdminUserAddDto dto) {
userService.addUserByAdmin(dto);
return Result.success(ResultCodeEnum.ADD_SUCCESS);
}
@Operation(summary = "更新", description = "更新用户信息需要更新Redis中的内容", tags = "user::update")
@Operation(summary = "更新用户", description = "更新用户信息需要更新Redis中的内容", tags = "user::update")
@PutMapping()
public Result<String> updateUserByAdmin(
@Valid AdminUserUpdateDto dto,
@RequestPart(value = "avatar", required = false) MultipartFile avatar) {
if (avatar != null) {
dto.setAvatar(avatar);
}
public Result<String> updateUserByAdmin(@Valid AdminUserUpdateDto dto,
@RequestPart(value = "avatar", required = false) MultipartFile avatar) {
if (avatar != null) dto.setAvatar(avatar);
userService.updateUserByAdmin(dto);
return Result.success(ResultCodeEnum.UPDATE_SUCCESS);
}
@ -129,16 +82,13 @@ public class UserController {
return Result.success(voList);
}
@Operation(summary = "强制退出", description = "强制退出", tags = "user::update")
@Operation(summary = "强制退出用户", description = "强制退出", tags = "user::update")
@PutMapping("forcedOffline")
public Result<String> forcedOfflineByAdmin(@RequestBody Long id) {
userService.forcedOfflineByAdmin(id);
return Result.success();
}
// -----------------------------------------
// 普通用户
// -----------------------------------------
@Operation(summary = "更新本地用户信息", description = "更新本地用户信息需要更新Redis中的内容")
@PutMapping("private/update/userinfo")
public Result<String> updateAdminUserByLocalUser(@Valid @RequestBody AdminUserUpdateByLocalUserDto dto) {

View File

@ -0,0 +1,63 @@
package cn.bunny.services.controller.system;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.Result;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.dto.user.RefreshTokenDto;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.service.system.UserLoginService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@Tag(name = "用户登录", description = "用户登录相关接口")
@RestController
@RequestMapping("/api/user")
public class UserLoginController {
@Resource
private UserLoginService userLoginService;
@Operation(summary = "用户登录", description = "前端用户登录")
@PostMapping("login")
public Result<LoginVo> login(@Valid @RequestBody LoginDto loginDto) {
LoginVo loginVo = userLoginService.login(loginDto);
return Result.success(loginVo);
}
@Operation(summary = "登录发送邮件验证码", description = "登录发送邮件验证码")
@PostMapping("public/sendLoginEmail")
public Result<String> sendLoginEmail(String email) {
if (!StringUtils.hasText(email)) throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);
userLoginService.sendLoginEmail(email);
return Result.success(ResultCodeEnum.EMAIL_CODE_SEND_SUCCESS);
}
@Operation(summary = "刷新token", description = "刷新用户token")
@PostMapping("private/refreshToken")
public Result<RefreshTokenVo> refreshToken(@Valid @RequestBody RefreshTokenDto dto) {
RefreshTokenVo vo = userLoginService.refreshToken(dto);
return Result.success(vo);
}
@Operation(summary = "获取本地登录用户信息", description = "获取用户信息从Redis中获取")
@GetMapping("private/userinfo")
public Result<LoginVo> userinfo() {
LoginVo vo = BaseContext.getLoginVo();
return Result.success(vo);
}
@Operation(summary = "退出登录", description = "退出登录")
@PostMapping("private/logout")
public Result<String> logout() {
userLoginService.logout();
return Result.success(ResultCodeEnum.LOGOUT_SUCCESS);
}
}

View File

@ -0,0 +1,10 @@
public abstract class AbstractPermissionCheckHandler {
private AbstractPermissionCheckHandler abstractPermissionCheckHandler;
public AbstractPermissionCheckHandler(AbstractPermissionCheckHandler abstractPermissionCheckHandler) {
this.abstractPermissionCheckHandler = abstractPermissionCheckHandler;
}
abstract protected void checkPermission(String requestUrl);
}

View File

@ -8,20 +8,20 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@TestConfiguration
public class WebConfig {
@Value("${server.port}")
private String port;
@Autowired
private TokenUtilsTest tokenUtils;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
String token = tokenUtils.getToken();
return builder.rootUri("http://localhost:" + port)
.defaultHeader("token", token)
.defaultHeader("Content-Type", "application/json")
.build();
}
}
// @TestConfiguration
// public class WebConfig {
// @Value("${server.port}")
// private String port;
//
// @Autowired
// private TokenUtilsTest tokenUtils;
//
// @Bean
// public RestTemplate restTemplate(RestTemplateBuilder builder) {
// String token = tokenUtils.getToken();
// return builder.rootUri("http://localhost:" + port)
// .defaultHeader("token", token)
// .defaultHeader("Content-Type", "application/json")
// .build();
// }
// }

View File

@ -1,7 +1,7 @@
package cn.bunny.services.controller;
import cn.bunny.services.aop.scanner.ControllerApiPermissionScanner;
import cn.bunny.services.domain.common.scanner.ScannerControllerInfoVo;
import cn.bunny.services.domain.common.model.dto.scanner.ScannerControllerInfoVo;
import cn.bunny.services.domain.system.system.entity.Permission;
import cn.bunny.services.service.system.PermissionService;
import org.junit.jupiter.api.Test;

View File

@ -0,0 +1,58 @@
package cn.bunny.services.controller.system;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import com.alibaba.fastjson2.JSONObject;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserControllerTest {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Test
void test() {
// Set<String> keys = redisTemplate.keys("admin::login_info::*");
// for (String key : keys) {
// System.out.println(key);
// }
Map<String, Object> adminLoginInfoWithScan = getAdminLoginInfoWithScan();
JSONObject adminLoginInfo = new JSONObject(adminLoginInfoWithScan);
System.out.println(adminLoginInfo);
}
public Map<String, Object> getAdminLoginInfoWithScan() {
String pattern = "admin::login_info::*";
Map<String, Object> result = new HashMap<>();
// 使用scan命令迭代查找
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build();
Cursor<String> cursor = redisTemplate.scan(options);
while (cursor.hasNext()) {
String key = cursor.next();
Object value = redisTemplate.opsForValue().get(key);
result.put(key, value);
}
try {
cursor.close();
} catch (Exception e) {
// 处理异常
}
return result;
}
}

View File

@ -1,9 +1,9 @@
package cn.bunny.services.utils;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.utils.system.UserUtil;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -11,7 +11,7 @@ import org.springframework.stereotype.Component;
@Component
public class TokenUtilsTest {
@Autowired
private UserUtil userUtil;
private UserLoginHelper userUtil;
@Autowired
private UserMapper userMapper;

View File

@ -3,16 +3,12 @@ package impl;
import cn.bunny.services.domain.common.model.dto.excel.I18nExcel;
import cn.bunny.services.domain.system.i18n.entity.I18n;
import cn.bunny.services.mapper.configuration.I18nMapper;
import cn.bunny.services.utils.i8n.I18nUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -21,34 +17,6 @@ import java.util.zip.ZipOutputStream;
public class I18nServiceImplTest extends ServiceImpl<I18nMapper, I18n> {
@Test
void downloadI18n() {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
// 查找默认语言内容
List<I18n> i18nList = list();
HashMap<String, Object> hashMap = I18nUtil.getMap(i18nList);
hashMap.forEach((k, v) -> {
try {
ZipEntry zipEntry = new ZipEntry(k + ".json");
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(JSON.toJSONString(v).getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
try {
zipOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
void downloadI18nByExcel() {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

View File

@ -0,0 +1,33 @@
package system;
import cn.bunny.services.controller.system.UserController;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.service.system.impl.UserServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(classes = UserServiceImpl.class)
class UserServiceTest {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Test
void test() {
String prefix = RedisUserConstant.getAdminUserEmailCodePrefix("");
Set<String> keys = redisTemplate.keys(prefix);
for (String key : keys) {
System.out.println(key);
}
}
}

View File

@ -17,6 +17,25 @@
</properties>
<dependencies>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.30.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@ -1,214 +0,0 @@
package cn.bunny.services.config.minio;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.common.constant.MinioConstant;
import cn.bunny.services.domain.common.model.dto.file.MinioFilePath;
import cn.bunny.services.exception.AuthCustomerException;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
/**
* Minio操作工具类 简化操作步骤
* 自定义封装
*/
@Component
@Slf4j
public class MinioUtil {
private final MinioProperties properties;
private final MinioClient minioClient;
public MinioUtil(MinioProperties properties, MinioClient minioClient) {
this.properties = properties;
this.minioClient = minioClient;
}
/**
* 获取Minio文件路径
*/
public static MinioFilePath initUploadFileReturnMinioFilePath(String buckName, String minioPreType, MultipartFile file) {
String uuid = UUID.randomUUID().toString();
// 定义日期时间格式
LocalDateTime currentDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM-dd");
String extension = "";
// 原始文件名
String filename = file.getOriginalFilename();
if (StringUtils.hasText(filename) && filename.contains(".")) {
extension = "." + filename.substring(filename.lastIndexOf(".") + 1);
}
// UUID防止重名
String uuidFilename = uuid + extension;
// 拼接时间+UUID文件名
String timeUuidFilename = currentDateTime.format(formatter) + "/" + uuidFilename;// 加上时间路径
// 上传根文件夹+拼接时间+UUID文件名
String filepath = MinioConstant.getType(minioPreType) + timeUuidFilename;
// 桶名称+上传根文件夹+拼接时间+UUID文件名
String buckNameFilepath = "/" + buckName + MinioConstant.getType(minioPreType) + timeUuidFilename;
// 设置及Minio基础信息
MinioFilePath minioFIlePath = new MinioFilePath();
minioFIlePath.setFilename(filename);
minioFIlePath.setUuidFilename(uuidFilename);
minioFIlePath.setTimeUuidFilename(timeUuidFilename);
minioFIlePath.setFilepath(filepath);
minioFIlePath.setBucketNameFilepath(buckNameFilepath);
return minioFIlePath;
}
/**
* * 上传文件并返回处理信息
*/
public MinioFilePath uploadObjectReturnFilePath(MultipartFile file, String minioPreType) throws IOException {
if (file == null) return null;
String bucketName = properties.getBucketName();
// 上传对象
try {
MinioFilePath minioFile = initUploadFileReturnMinioFilePath(bucketName, minioPreType, file);
minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(minioFile.getFilepath()).stream(file.getInputStream(), file.getSize(), -1).build());
return minioFile;
} catch (Exception exception) {
throw new AuthCustomerException(ResultCodeEnum.UPLOAD_ERROR);
}
}
/**
* 获取默认bucket文件并返回字节数组
*
* @param objectName 对象名称
* @return 文件流对象
*/
public byte[] getBucketObjectByte(String objectName) {
String bucketName = properties.getBucketName();
try {
GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
return getObjectResponse.readAllBytes();
} catch (Exception exception) {
exception.printStackTrace();
}
throw new AuthCustomerException(ResultCodeEnum.GET_BUCKET_EXCEPTION);
}
/**
* 获取Minio全路径名Object带有桶名称
*
* @param objectName 对象名称
* @return 全路径
*/
public String getObjectNameFullPath(String objectName) {
String url = properties.getEndpointUrl();
return url + objectName;
}
/**
* 上传文件
*
* @param bucketName 桶名称
* @param filename 文件名
* @param inputStream 输入流
* @param size 大小
*/
public void putObject(String bucketName, String filename, InputStream inputStream, Long size) {
try {
minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(filename).stream(inputStream, size, -1).build());
} catch (Exception exception) {
log.error("上传文件失败:{}", (Object) exception.getStackTrace());
throw new AuthCustomerException(ResultCodeEnum.UPLOAD_ERROR);
}
}
/**
* * 删除目标文件
*
* @param list 对象文件列表
*/
public void removeObjects(List<String> list) {
try {
String bucketName = properties.getBucketName();
List<DeleteObject> objectList = list.stream().map(DeleteObject::new).toList();
Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objectList).build());
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
throw new AuthCustomerException("Error in deleting object " + error.objectName() + "; " + error.message());
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* * 更新文件
*
* @param bucketName 桶名称
* @param filepath 文件路径
* @param file 文件
*/
public void updateFile(String bucketName, String filepath, MultipartFile file) {
try {
putObject(bucketName, filepath, file.getInputStream(), file.getSize());
} catch (IOException e) {
throw new AuthCustomerException(e.getMessage());
}
}
/**
* 判断桶是否存在
*
* @param bucketName 桶名称
* @return 布尔值是否存在
*/
public boolean createBucketIfNotExists(String bucketName) {
boolean found = false;
try {
found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
// 如果 bucket 不存在就创建
if (!found) makeBucket(bucketName);
return found;
} catch (Exception exception) {
log.error("判断桶是否存在 ------ 失败消息:{}", exception.getLocalizedMessage());
exception.getStackTrace();
}
throw new AuthCustomerException("未初始化 bucket");
}
/**
* 创建桶
*
* @param bucketName 桶的名称
*/
public void makeBucket(String bucketName) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
} catch (Exception exception) {
exception.getStackTrace();
throw new AuthCustomerException("创建失败");
}
}
}

View File

@ -4,14 +4,9 @@ import java.util.ArrayList;
import java.util.List;
public class SecurityConfigConstant {
public static String[] REQUEST_MATCHERS_PERMIT_ALL = {
"/", "/**.html", "/error", "/media.ico", "/favicon.ico",
"/webjars/**", "/v3/api-docs/**", "/swagger-ui/**",
"/*/*/noAuth/**", "/*/noAuth/**", "/noAuth/**",
"/*/i18n/getI18n"
};
public static List<String> PERMIT_ALL_LIST = new ArrayList<>() {{
/* 可以放行的权限 */
public static List<String> PERMIT_ACCESS_LIST = new ArrayList<>() {{
add("admin");
}};
}

View File

@ -6,4 +6,7 @@ import lombok.Data;
public class UserConstant {
public static final String USER_AVATAR = "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132";
public static final String PERSON_DESCRIPTION = "这个人很懒没有介绍...";
public static final String LOGIN = "login";
public static final String LOGOUT = "logout";
public static final String FORCE_LOGOUT = "force_logout";
}

View File

@ -1,18 +0,0 @@
package cn.bunny.services.domain.common.model.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MinioFilePath {
private String filename;
private String uuidFilename;
private String timeUuidFilename;
private String filepath;
private String bucketNameFilepath;
}

View File

@ -1,7 +1,6 @@
package cn.bunny.services.domain.common.model.dto.ip;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -11,13 +10,13 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel(value = "IpEntity对象", description = "用户IP相关信息")
@Schema(name = "IpEntity对象", title = "用户IP相关信息")
public class IpEntity {
@ApiModelProperty("原始地址")
@Schema(name = "ipAddr", title = "原始地址")
private String ipAddr;
@ApiModelProperty("IP归属地")
@Schema(name = "ipRegion", title = "IP归属地")
private String ipRegion;
}

View File

@ -0,0 +1,31 @@
package cn.bunny.services.domain.common.model.dto.minio;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Schema(name = "MinioFilePath", title = "Minion上传返回西悉尼")
public class MinioUploadFileInfo {
@Schema(name = "filename", title = "文件名")
private String filename;
@Schema(name = "uuidFilename", title = "uuid文件名防止重复")
private String uuidFilename;
@Schema(name = "timeUuidFilename", title = "时间+uuid文件名")
private String timeUuidFilename;
@Schema(name = "filepath", title = "文件路径")
private String filepath;
@Schema(name = "bucketNameFilepath", title = "上传桶名称文件路径")
private String bucketNameFilepath;
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.domain.common.scanner;
package cn.bunny.services.domain.common.model.dto.scanner;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;

View File

@ -1,4 +1,4 @@
package cn.bunny.services.domain.common.scanner;
package cn.bunny.services.domain.common.model.dto.scanner;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;

View File

@ -1,4 +1,4 @@
package cn.bunny.services.domain.common.scanner;
package cn.bunny.services.domain.common.model.dto.scanner;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;

View File

@ -0,0 +1,111 @@
package cn.bunny.services.minio;
import cn.bunny.services.domain.common.constant.UserConstant;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@Component
public class MinioHelper {
@Resource
private MinioProperties properties;
/**
* 格式化用户头像URL
*
* <p>实现头像URL的统一存储和访问格式转换</p>
*
* <ol>
* <li><b>存储处理</b>将带HTTP前缀的头像URL转换为数据库存储格式
* <ul>
* <li>匹配正则表达式^https?://.*?/(.*)</li>
* <li>提取路径部分matcher.group(1)</li>
* <li>转换为存储格式"/" + 提取的路径</li>
* </ul>
* </li>
* <li><b>访问处理</b>返回带HTTP前缀的完整访问URL</li>
* </ol>
*
* <p>典型用例</p>
* <pre>
* 输入"http|s://example.com/images/avatar.jpg"
* 存储"images/avatar.jpg"
* 访问"http|s://xxx/images/avatar.jpg"
* </pre>
*
* @param avatar 头像URL可能包含HTTP前缀或数据库存储格式
* @return 格式化后的头像URL确保包含HTTP前缀
* @throws PatternSyntaxException 当正则表达式匹配失败时抛出
* @throws IllegalArgumentException 当头像参数为空时抛出
*/
public String formatUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (!matcher.matches()) return avatar;
// 匹配后返回内容
return "/" + matcher.group(1);
}
/**
* 检查并格式化用户头像URL
*
* <p>处理逻辑</p>
* <ol>
* <li>当头像为空或与默认头像相同时返回系统默认头像</li>
* <li>尝试移除头像URL中的HTTP协议和域名部分如果存在</li>
* <li>最终返回MinIO存储中的完整对象访问路径</li>
* </ol>
*
* @param avatar 用户头像URL可能为以下格式
* - /null使用默认头像
* - 完整HTTP URL"http|s://example.com/images/1.jpg"
* - MinIO对象路径"images/1.jpg"
* @return 格式化后的头像URL保证是可访问的完整路径
* - 默认头像当输入无效时
* - MinIO完整访问路径处理成功时
* @see UserConstant#USER_AVATAR 默认头像常量
* @see #getObjectNameFullPath MinIO路径处理方法
*/
public String getUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (matcher.matches()) return avatar;
// 匹配后返回内容
return getObjectNameFullPath(avatar);
}
/**
* 获取Minio全路径名Object带有桶名称
*
* @param objectName 对象名称
* @return 全路径
*/
public String getObjectNameFullPath(String objectName) {
String url = properties.getEndpointUrl();
return url + objectName;
}
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.config.minio;
package cn.bunny.services.minio;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
@ -12,7 +12,8 @@ import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "bunny.minio")
@ConditionalOnProperty(name = "bunny.minio.bucket-name")// 当属性有值时这个配置才生效
// 当属性有值时这个配置才生效
@ConditionalOnProperty(name = "bunny.minio.bucket-name")
@Data
public class MinioProperties {
@ -30,6 +31,7 @@ public class MinioProperties {
MinioClient minioClient = MinioClient.builder().endpoint(endpointUrl).credentials(accessKey, secretKey).build();
try {
// 判断桶是否存在不存在则创建并且可以有公开访问权限
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());

View File

@ -0,0 +1,227 @@
package cn.bunny.services.minio;
import cn.bunny.services.domain.common.constant.MinioConstant;
import cn.bunny.services.domain.common.model.dto.minio.MinioUploadFileInfo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.exception.AuthCustomerException;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* Minio操作工具类 简化操作步骤
* 自定义封装
*/
@Slf4j
@Service
public class MinioService {
@Resource
private MinioProperties properties;
@Resource
private MinioClient minioClient;
/**
* 上传文件到MinIO并返回文件处理信息
*
* <p><b>文件存储规则</b></p>
* <ul>
* <li>按日期分目录存储格式yyyy/MM-dd</li>
* <li>使用UUID重命名文件防止冲突</li>
* <li>保留原始文件名信息</li>
* <li>支持按类型分类存储通过minioPreType参数</li>
* </ul>
*
* <p><b>返回信息包含</b></p>
* <ol>
* <li>原始文件名</li>
* <li>UUID重命名后的文件名</li>
* <li>带日期路径的文件名</li>
* <li>完整存储路径不含桶名</li>
* <li>完整访问路径含桶名</li>
* </ol>
*
* @param file 上传的文件对象不可为空
* @param minioPreType 文件类型前缀用于分类存储参考MinioConstant定义
* @return 文件处理信息对象包含各种路径信息当file为null时返回null
* @throws IOException 当文件流读取异常时抛出
* @throws AuthCustomerException 当上传失败时抛出错误码UPLOAD_ERROR
* @see MinioConstant 文件类型前缀常量
* @see MinioUploadFileInfo 返回信息数据结构
*/
public MinioUploadFileInfo uploadWithFileInfo(MultipartFile file, String minioPreType) throws IOException {
if (file == null) return null;
// 上传文件时的桶名称
String bucketName = properties.getBucketName();
// 上传对象
try {
String uuid = UUID.randomUUID().toString();
// 定义日期时间格式
String dateFormatter = new SimpleDateFormat("yyyy/MM-dd").format(new Date());
// 文件后缀
String extension = "";
// 原始文件名
String filename = file.getOriginalFilename();
if (StringUtils.hasText(filename) && filename.contains(".")) {
extension = "." + filename.substring(filename.lastIndexOf(".") + 1);
}
// UUID防止重名
String uuidFilename = uuid + extension;
// 拼接时间+UUID文件名 加上时间路径
String timeUuidFilename = dateFormatter + "/" + uuidFilename;
// 上传根文件夹+拼接时间+UUID文件名
String filepath = MinioConstant.getType(minioPreType) + timeUuidFilename;
// 桶名称+上传根文件夹+拼接时间+UUID文件名
String buckNameFilepath = "/" + bucketName + MinioConstant.getType(minioPreType) + timeUuidFilename;
// 设置及Minio基础信息
MinioUploadFileInfo minioUploadFIleInfo = new MinioUploadFileInfo();
minioUploadFIleInfo.setFilename(filename);
minioUploadFIleInfo.setUuidFilename(uuidFilename);
minioUploadFIleInfo.setTimeUuidFilename(timeUuidFilename);
minioUploadFIleInfo.setFilepath(filepath);
minioUploadFIleInfo.setBucketNameFilepath(buckNameFilepath);
minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(minioUploadFIleInfo.getFilepath()).stream(file.getInputStream(), file.getSize(), -1).build());
return minioUploadFIleInfo;
} catch (Exception exception) {
throw new AuthCustomerException(ResultCodeEnum.UPLOAD_ERROR);
}
}
/**
* 获取默认存储桶中的文件字节数组
*
* <p><b>注意</b>会一次性读取全部文件内容到内存大文件慎用</p>
*
* @param objectName 对象全路径名称包含目录路径
* @return 文件内容的字节数组
* @throws AuthCustomerException 当获取失败时抛出错误码{@link ResultCodeEnum#GET_BUCKET_EXCEPTION}
*/
public byte[] getBucketObjectByte(String objectName) {
String bucketName = properties.getBucketName();
try {
GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
return getObjectResponse.readAllBytes();
} catch (Exception exception) {
exception.printStackTrace();
}
throw new AuthCustomerException(ResultCodeEnum.GET_BUCKET_EXCEPTION);
}
/**
* 上传文件到指定存储桶
*
* @param bucketName 存储桶名称
* @param filename 对象存储路径包含文件名
* @param inputStream 文件输入流调用方负责关闭
* @param size 文件大小字节
* @throws AuthCustomerException 当上传失败时抛出错误码{@link ResultCodeEnum#UPLOAD_ERROR}
*/
public void putObject(String bucketName, String filename, InputStream inputStream, Long size) {
try {
minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(filename).stream(inputStream, size, -1).build());
} catch (Exception exception) {
log.error("上传文件失败:{}", (Object) exception.getStackTrace());
throw new AuthCustomerException(ResultCodeEnum.UPLOAD_ERROR);
}
}
/**
* 批量删除存储桶中的对象文件
*
* @param list 要删除的对象路径列表
* @throws AuthCustomerException 当删除失败时抛出包含错误对象信息
*/
public void removeObjects(List<String> list) {
try {
String bucketName = properties.getBucketName();
List<DeleteObject> objectList = list.stream().map(DeleteObject::new).toList();
Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objectList).build());
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
throw new AuthCustomerException("Error in deleting object " + error.objectName() + "; " + error.message());
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* 更新存储桶中的文件覆盖写入
*
* @param bucketName 存储桶名称
* @param filepath 对象存储路径
* @param file 新的文件对象
* @throws AuthCustomerException 当更新失败时抛出
*/
public void updateFile(String bucketName, String filepath, MultipartFile file) {
try {
putObject(bucketName, filepath, file.getInputStream(), file.getSize());
} catch (IOException e) {
throw new AuthCustomerException(e.getMessage());
}
}
/**
* 检查并创建存储桶如果不存在
*
* @param bucketName 存储桶名称
* @return true-已存在false-新创建
* @throws AuthCustomerException 当操作失败时抛出
*/
public boolean createBucketIfNotExists(String bucketName) {
boolean found = false;
try {
found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
// 如果 bucket 不存在就创建
if (!found) makeBucket(bucketName);
return found;
} catch (Exception exception) {
log.error("判断桶是否存在 ------ 失败消息:{}", exception.getLocalizedMessage());
exception.getStackTrace();
}
throw new AuthCustomerException("未初始化 bucket");
}
/**
* 创建新的存储桶
*
* @param bucketName 存储桶名称需符合命名规范
* @throws AuthCustomerException 当创建失败时抛出
*/
public void makeBucket(String bucketName) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
} catch (Exception exception) {
exception.getStackTrace();
throw new AuthCustomerException("创建失败");
}
}
}

View File

@ -0,0 +1,14 @@
import org.junit.jupiter.api.Test;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MinioTest {
@Test
void test() {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM-dd");
String date = formatter.format(new Date());
System.out.println(date);
}
}

View File

@ -29,24 +29,6 @@
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.30.0</version>
</dependency>
<!-- asp 切面 -->
<dependency>
@ -57,12 +39,12 @@
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.springframework.boot</groupId> -->
<!-- <artifactId>spring-boot-devtools</artifactId> -->
<!-- <scope>runtime</scope> -->
<!-- <optional>true</optional> -->
<!-- </dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- quartz -->
<dependency>

View File

@ -1,8 +1,8 @@
package cn.bunny.services.aop.scanner;
import cn.bunny.services.domain.common.scanner.ControllerInfo;
import cn.bunny.services.domain.common.scanner.MethodInfo;
import cn.bunny.services.domain.common.scanner.ScannerControllerInfoVo;
import cn.bunny.services.domain.common.model.dto.scanner.ControllerInfo;
import cn.bunny.services.domain.common.model.dto.scanner.MethodInfo;
import cn.bunny.services.domain.common.model.dto.scanner.ScannerControllerInfoVo;
import cn.bunny.services.security.config.WebSecurityConfig;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

View File

@ -6,7 +6,7 @@ import cn.bunny.services.domain.system.system.entity.Role;
import cn.bunny.services.mapper.system.PermissionMapper;
import cn.bunny.services.mapper.system.RoleMapper;
import cn.bunny.services.security.config.WebSecurityConfig;
import cn.bunny.services.utils.system.RoleUtil;
import cn.bunny.services.service.system.helper.role.RoleHelper;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@ -44,7 +44,7 @@ public class PermissionCheckService {
List<String> roleCodeList = roleList.stream().map(Role::getRoleCode).toList();
// 判断是否是管理员用户
boolean checkedAdmin = RoleUtil.checkAdmin(roleCodeList);
boolean checkedAdmin = RoleHelper.checkAdmin(roleCodeList);
if (checkedAdmin) return true;
// 判断请求地址是否是登录之后才需要访问的已经登录了不需要验证的

View File

@ -1,9 +1,9 @@
package cn.bunny.services.utils.email;
package cn.bunny.services.service.configuration.helper.email;
import cn.bunny.services.config.mail.MailSenderConfiguration;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.common.model.dto.email.EmailSend;
import cn.bunny.services.domain.common.model.dto.email.EmailSendInit;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.email.entity.EmailTemplate;
import cn.bunny.services.domain.system.email.entity.EmailUsers;
import cn.bunny.services.exception.AuthCustomerException;

View File

@ -1,4 +1,4 @@
package cn.bunny.services.utils.email;
package cn.bunny.services.service.configuration.helper.email;
import cn.bunny.services.domain.system.email.entity.EmailTemplate;
import cn.bunny.services.mapper.configuration.EmailTemplateMapper;

View File

@ -1,7 +1,8 @@
package cn.bunny.services.utils.i8n;
package cn.bunny.services.service.configuration.helper.i18n;
import cn.bunny.services.domain.common.model.dto.excel.I18nExcel;
import cn.bunny.services.domain.system.i18n.entity.I18n;
import cn.bunny.services.service.configuration.impl.I18nServiceImpl;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import org.jetbrains.annotations.NotNull;
@ -16,46 +17,20 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class I18nUtil {
@NotNull
public static HashMap<String, Object> getMap(@NotNull List<I18n> i18nList) {
// 整理集合
Map<String, Map<String, String>> map = i18nList.stream()
.collect(Collectors.groupingBy(
I18n::getTypeName,
Collectors.toMap(I18n::getKeyName, I18n::getTranslation)));
// 返回集合
return new HashMap<>(map);
}
public class I18nHelper {
/**
* 使用zip写入json
* 将国际化资源列表写入Excel并打包到ZIP输出流
*
* @param i18nList i18nList
* @param zipOutputStream zipOutputStream
*/
public static void writeJson(List<I18n> i18nList, ZipOutputStream zipOutputStream) {
HashMap<String, Object> hashMap = getMap(i18nList);
hashMap.forEach((k, v) -> {
try {
ZipEntry zipEntry = new ZipEntry(k + ".json");
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(JSON.toJSONString(v).getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
/**
* 使用zip写入excel
* <p>处理流程</p>
* <ol>
* <li>按资源类型(typeName)分组 --- 多语言的类型英文?中文?</li>
* <li>每组数据生成单独多语言 key的Excel工作表</li>
* <li>将Excel文件写入ZIP输出流</li>
* </ol>
*
* @param i18nList i18nList
* @param zipOutputStream zipOutputStream
* @param i18nList 国际化资源列表包含key-value对和类型信息
* @param zipOutputStream ZIP输出流用于写入打包后的Excel文件
* @throws RuntimeException 当IO操作失败时抛出
*/
public static void writeExcel(List<I18n> i18nList, ZipOutputStream zipOutputStream) {
Map<String, List<I18nExcel>> hashMap = i18nList.stream()
@ -86,4 +61,62 @@ public class I18nUtil {
}
});
}
/**
* 将国际化资源列表写入JSON并打包到ZIP输出流
*
* <p>处理流程</p>
* <ol>
* <li>按资源类型(typeName)分组 --- 多语言的类型英文?中文?</li>
* <li>每组数据生成单独多语言 key的Excel工作表</li>
* <li>将JSON文件写入ZIP输出流</li>
* </ol>
*
* @param i18nList 国际化资源列表
* @param zipOutputStream ZIP输出流
* @throws RuntimeException 当IO操作失败时抛出
*/
public static void writeJson(List<I18n> i18nList, ZipOutputStream zipOutputStream) {
HashMap<String, Object> hashMap = getMapByI18nList(i18nList);
hashMap.forEach((k, v) -> {
try {
ZipEntry zipEntry = new ZipEntry(k + ".json");
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(JSON.toJSONString(v).getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
/**
* 将国际化资源列表转换为结构化Map
*
* <p>转换规则</p>
* <ul>
* <li>外层Key: 资源类型(typeName)</li>
* <li>内层Key: 资源键名(keyName)</li>
* <li>: 翻译文本(translation)</li>
* </ul>
* <p>详细结构和结果示例看前端传递的 {@link I18nServiceImpl#getI18nMap} 控制器</p>
* <p>/api/i18n/public</p>
*
* @param i18nList 国际化资源列表
* @return 结构化Map {typeName: {keyName: translation}}
* @throws IllegalArgumentException 当参数为null时抛出
*/
@NotNull
public static HashMap<String, Object> getMapByI18nList(@NotNull List<I18n> i18nList) {
// 整理集合
Map<String, Map<String, String>> map = i18nList.stream()
.collect(Collectors.groupingBy(
I18n::getTypeName,
Collectors.toMap(I18n::getKeyName, I18n::getTranslation)));
// 返回集合
return new HashMap<>(map);
}
}

View File

@ -1,8 +1,8 @@
package cn.bunny.services.service.configuration.impl;
import cn.bunny.services.domain.common.constant.FileType;
import cn.bunny.services.domain.common.model.entity.BaseEntity;
import cn.bunny.services.domain.common.model.dto.excel.I18nExcel;
import cn.bunny.services.domain.common.model.entity.BaseEntity;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.i18n.dto.I18nAddDto;
@ -17,8 +17,8 @@ import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.configuration.I18nMapper;
import cn.bunny.services.mapper.configuration.I18nTypeMapper;
import cn.bunny.services.service.configuration.I18nService;
import cn.bunny.services.service.configuration.helper.i18n.I18nHelper;
import cn.bunny.services.utils.FileUtil;
import cn.bunny.services.utils.i8n.I18nUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
@ -76,7 +76,7 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
I18nType i18nType = i18nTypeMapper.selectOne(Wrappers.<I18nType>lambdaQuery().eq(I18nType::getIsDefault, true));
List<I18n> i18nList = list();
HashMap<String, Object> hashMap = I18nUtil.getMap(i18nList);
HashMap<String, Object> hashMap = I18nHelper.getMapByI18nList(i18nList);
hashMap.put("local", Objects.requireNonNull(i18nType.getTypeName(), "zh"));
return hashMap;
@ -170,22 +170,21 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
// 类型是Excel写入Excel
if (type.equals(FileType.EXCEL)) {
I18nUtil.writeExcel(i18nList, zipOutputStream);
I18nHelper.writeExcel(i18nList, zipOutputStream);
}
// 其他格式写入JSON
else {
I18nUtil.writeJson(i18nList, zipOutputStream);
I18nHelper.writeJson(i18nList, zipOutputStream);
}
// 设置响应头
HttpHeaders headers = FileUtil.buildHttpHeadersByBinary("i18n-configuration.zip");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return new ResponseEntity<>(byteArrayInputStream.readAllBytes(), headers, HttpStatus.OK);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 设置响应头
HttpHeaders headers = FileUtil.buildHttpHeadersByBinary("i18n-configuration.zip");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return new ResponseEntity<>(byteArrayInputStream.readAllBytes(), headers, HttpStatus.OK);
}
/**

View File

@ -1,5 +1,8 @@
package cn.bunny.services.service.message.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.message.dto.MessageReceivedDto;
import cn.bunny.services.domain.system.message.dto.MessageReceivedUpdateDto;
import cn.bunny.services.domain.system.message.dto.MessageUserDto;
@ -7,13 +10,10 @@ import cn.bunny.services.domain.system.message.entity.Message;
import cn.bunny.services.domain.system.message.entity.MessageReceived;
import cn.bunny.services.domain.system.message.vo.MessageReceivedWithMessageVo;
import cn.bunny.services.domain.system.message.vo.MessageUserVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.message.MessageReceivedMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.service.message.MessageReceivedService;
import cn.bunny.services.utils.system.UserUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -39,7 +39,7 @@ import java.util.List;
public class MessageReceivedServiceImpl extends ServiceImpl<MessageReceivedMapper, MessageReceived> implements MessageReceivedService {
@Resource
private UserUtil userUtil;
private MinioHelper minioHelper;
/**
* 管理员管理用户消息接收分页查询
@ -58,7 +58,7 @@ public class MessageReceivedServiceImpl extends ServiceImpl<MessageReceivedMappe
// 设置封面返回内容
String cover = vo.getCover();
cover = userUtil.checkGetUserAvatar(cover);
cover = minioHelper.getUserAvatar(cover);
vo.setCover(cover);
return vo;
}).toList();
@ -113,7 +113,7 @@ public class MessageReceivedServiceImpl extends ServiceImpl<MessageReceivedMappe
// 设置封面返回内容
String cover = vo.getCover();
cover = userUtil.checkGetUserAvatar(cover);
cover = minioHelper.getUserAvatar(cover);
vo.setCover(cover);
return vo;
}).toList();

View File

@ -1,6 +1,9 @@
package cn.bunny.services.service.message.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.entity.BaseEntity;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.message.dto.MessageAddDto;
import cn.bunny.services.domain.system.message.dto.MessageDto;
import cn.bunny.services.domain.system.message.dto.MessageUpdateDto;
@ -10,16 +13,13 @@ import cn.bunny.services.domain.system.message.vo.MessageDetailVo;
import cn.bunny.services.domain.system.message.vo.MessageReceivedWithMessageVo;
import cn.bunny.services.domain.system.message.vo.MessageReceivedWithUserVo;
import cn.bunny.services.domain.system.message.vo.MessageVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.message.MessageMapper;
import cn.bunny.services.mapper.message.MessageReceivedMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.service.message.MessageReceivedService;
import cn.bunny.services.service.message.MessageService;
import cn.bunny.services.utils.system.UserUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@ -47,17 +47,14 @@ import java.util.stream.Collectors;
@Transactional
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements MessageService {
@Resource
private UserUtil userUtil;
@Resource
private MessageReceivedMapper messageReceivedMapper;
@Resource
private UserMapper userMapper;
@Resource
private MessageReceivedService messageReceivedService;
@Resource
private MinioHelper minioHelper;
/**
* 分页查询发送消息
@ -152,7 +149,7 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
// 设置封面返回内容
String cover = dto.getCover();
dto.setCover(userUtil.checkGetUserAvatar(cover));
dto.setCover(minioHelper.getUserAvatar(cover));
// 先保存消息数据之后拿到保存消息的id
Message message = new Message();
@ -198,7 +195,7 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
// 设置封面返回内容
String cover = dto.getCover();
dto.setCover(userUtil.checkGetUserAvatar(cover));
dto.setCover(minioHelper.getUserAvatar(cover));
// 更新内容
Message message = new Message();

View File

@ -0,0 +1,42 @@
package cn.bunny.services.service.system;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.dto.user.RefreshTokenDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jetbrains.annotations.NotNull;
public interface UserLoginService extends IService<AdminUser> {
/**
* 前台用户登录接口
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
LoginVo login(LoginDto loginDto);
/**
* 登录发送邮件验证码
*
* @param email 邮箱
*/
void sendLoginEmail(@NotNull String email);
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto);
/**
* * 退出登录
*/
void logout();
}

View File

@ -1,16 +1,16 @@
package cn.bunny.services.service.system;
import cn.bunny.services.domain.system.system.dto.user.*;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.system.system.dto.user.AdminUserAddDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.vo.user.AdminUserVo;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.domain.system.system.vo.user.UserVo;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@ -24,14 +24,6 @@ import java.util.List;
*/
public interface UserService extends IService<AdminUser> {
/**
* 前台用户登录接口
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
LoginVo login(LoginDto loginDto);
/**
* * 获取用户信息列表
*
@ -60,27 +52,6 @@ public interface UserService extends IService<AdminUser> {
*/
void deleteUserByAdmin(List<Long> ids);
/**
* 登录发送邮件验证码
*
* @param email 邮箱
*/
void sendLoginEmail(@NotNull String email);
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto);
/**
* * 退出登录
*/
void logout();
/**
* * 获取用户信息
*

View File

@ -1,4 +1,4 @@
package cn.bunny.services.utils.system;
package cn.bunny.services.service.system.helper;
import cn.bunny.services.domain.common.model.dto.excel.PermissionExcel;
import com.alibaba.excel.EasyExcel;
@ -11,12 +11,20 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class PermissionUtil {
/**
* 权限数据处理工具类
*
* <p>提供权限数据的树形结构处理扁平化处理以及导出功能</p>
*/
public class PermissionHelper {
/**
* 将属性结构扁平化
* 树形结构权限数据扁平化为列表
*
* @param list 属性结构
* @return 扁平化数组
* <p>使用递归处理树形结构</p>
*
* @param list 树形结构的权限列表每个节点可能包含children子节点
* @return 扁平化后的权限列表
*/
public static List<PermissionExcel> flattenTree(List<PermissionExcel> list) {
List<PermissionExcel> result = new ArrayList<>();
@ -32,10 +40,12 @@ public class PermissionUtil {
}
/**
* 设置子集
* 递归设置子节点内部方法
*
* @param parent 父级节点
* @param list 要构建的列表
* <p>为父节点查找并设置所有子节点递归处理子节点的子节点</p>
*
* @param parent 当前父节点
* @param list 完整的权限数据列表
*/
private static void setChildren(PermissionExcel parent, List<PermissionExcel> list) {
List<PermissionExcel> children = list.stream()
@ -51,12 +61,13 @@ public class PermissionUtil {
}
}
/**
* 构建属性结构
* 构建权限树形结构
*
* @param list 要构建的列表
* @return 构建完成的列表
* <p>从扁平列表中构建树形结构根节点的判断条件为parentId为null或0</p>
*
* @param list 扁平化的权限数据列表
* @return 构建完成的树形结构列表只包含根节点
*/
public static List<PermissionExcel> buildTree(List<PermissionExcel> list) {
List<PermissionExcel> permissionExcels = list.stream()
@ -70,11 +81,11 @@ public class PermissionUtil {
}
/**
* 写入JSON
* 将权限数据写入JSON格式到ZIP压缩包
*
* @param list 写入的列表
* @param zipOutputStream zip输出流
* @param zipName zip文件名
* @param list 要导出的权限数据列表
* @param zipOutputStream ZIP输出流
* @param zipName 在ZIP包中的文件名
*/
public static void writeJson(List<PermissionExcel> list, ZipOutputStream zipOutputStream, String zipName) {
try {
@ -88,11 +99,11 @@ public class PermissionUtil {
}
/**
* 写入JSON
* 将权限数据写入Excel格式到ZIP压缩包
*
* @param list 写入的列表
* @param zipOutputStream zip输出流
* @param zipName zip文件名
* @param list 要导出的权限数据列表
* @param zipOutputStream ZIP输出流
* @param zipName 在ZIP包中的文件名需包含.xlsx后缀
*/
public static void writExcel(List<PermissionExcel> list, ZipOutputStream zipOutputStream, String zipName) {
try {

View File

@ -1,13 +1,14 @@
package cn.bunny.services.utils.system;
package cn.bunny.services.service.system.helper;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.system.system.entity.RouterRole;
import cn.bunny.services.domain.system.system.entity.router.Router;
import cn.bunny.services.domain.system.system.entity.router.RouterMeta;
import cn.bunny.services.domain.system.system.views.ViewRolePermission;
import cn.bunny.services.domain.system.system.views.ViewRouterRole;
import cn.bunny.services.domain.system.system.vo.router.WebUserRouterVo;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.service.system.RouterRoleService;
import cn.bunny.services.service.system.helper.role.RoleHelper;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@ -20,7 +21,7 @@ import java.util.*;
@Slf4j
@Component
public class RouterUtil {
public class RouterHelper {
@Resource
private RouterRoleService routerRoleService;
@ -44,10 +45,12 @@ public class RouterUtil {
}
/**
* 查询新的路由权限和角色
* 保存路由角色关联关系
*
* @param meta RouterMeta
* @param id 路由id
* <p>将路由的权限角色配置保存到数据库</p>
*
* @param meta 路由元数据包含角色配置信息
* @param id 路由ID
*/
public void insertRouterRoleAndPermission(RouterMeta meta, Long id) {
List<String> roles = meta.getRoles();
@ -63,18 +66,31 @@ public class RouterUtil {
}
/**
* 整理web用户所能看到的路由列表
* 构建前端可访问的路由列表
*
* @param routerList 所有的路由列表
* @param routerRoleList 路由和角色列表
* @param rolePermissionList 角色和权限列表
* @return web用户所能看到的路由列表
* <p>处理流程</p>
* <ol>
* <li>检查当前用户是否为管理员拥有全部权限</li>
* <li>转换路由基础信息</li>
* <li>处理路由元数据meta</li>
* <li>设置路由关联的角色信息</li>
* <li>设置路由关联的权限信息</li>
* <li>按rank排序返回</li>
* </ol>
* <a href="https://pure-admin.cn/pages/routerMenu/#%E8%B7%AF%E7%94%B1%E5%92%8C%E8%8F%9C%E5%8D%95%E9%85%8D%E7%BD%AE">
* 路由结构参考这个文档
* </a>
*
* @param routerList 所有路由列表
* @param routerRoleList 路由-角色关联Mapkey: 路由ID, value: 角色列表
* @param rolePermissionList 角色-权限关联Mapkey: 角色ID, value: 权限列表
* @return 前端可访问的路由列表包含完整的权限信息
*/
@NotNull
public List<WebUserRouterVo> getWebUserRouterVos(List<Router> routerList, Map<Long, List<ViewRouterRole>> routerRoleList, Map<Long, List<ViewRolePermission>> rolePermissionList) {
// 检查当前是否是 admin 用户
List<String> roles = BaseContext.getLoginVo().getRoles();
List<String> allAuths = !RoleUtil.checkAdmin(roles) ? new ArrayList<>() : List.of("*:*:*", "*:*", "*", "admin");
List<String> allAuths = !RoleHelper.checkAdmin(roles) ? new ArrayList<>() : List.of("*:*:*", "*:*", "*", "admin");
// 查询路由所有数据整理前端需要的路和角色权限
return routerList.stream().map(view -> {

View File

@ -0,0 +1,189 @@
package cn.bunny.services.service.system.helper;
import cn.bunny.services.domain.common.constant.LocalDateTimeConstant;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.UserConstant;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.system.log.entity.UserLoginLog;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.Permission;
import cn.bunny.services.domain.system.system.entity.Role;
import cn.bunny.services.mapper.log.UserLoginLogMapper;
import cn.bunny.services.mapper.system.PermissionMapper;
import cn.bunny.services.mapper.system.RoleMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.service.system.helper.role.RoleHelper;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@Transactional
public class UserLoginHelper {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private UserMapper userMapper;
@Resource
private UserLoginLogMapper userLoginLogMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private PermissionMapper permissionMapper;
@Resource
private MinioHelper minioHelper;
/**
* 构建用户登录返回对象(LoginVo)
*
* <p>主要处理流程</p>
* <ol>
* <li><b>参数校验</b>检查用户对象是否为空</li>
* <li><b>权限处理</b>
* <ul>
* <li>查询用户角色和权限数据</li>
* <li>非管理员用户从数据库加载权限信息</li>
* <li>管理员用户通过RoleUtil.checkAdmin()自动设置管理员权限</li>
* <li>可选对角色和权限列表去重</li>
* </ul>
* </li>
* <li><b>信息装配</b>
* <ul>
* <li>记录用户IP等访问信息</li>
* <li>使用BeanUtils.copyProperties()复制用户基础属性</li>
* <li>设置记住我功能及token过期时间</li>
* </ul>
* </li>
* <li><b>缓存处理</b>将完整用户信息存入Redis</li>
* </ol>
*
* <p>注意事项</p>
* <ul>
* <li>属性复制操作放在流程最后确保所有字段正确同步</li>
* <li>IP信息需要同时更新到用户实体和返回对象</li>
* </ul>
*
* @param user 用户实体对象不可为空
* @param readMeDay 记住我时长单位
* @return 完整的登录响应对象
* @throws IllegalArgumentException 当用户对象为空时抛出
*/
public LoginVo buildLoginUserVo(AdminUser user, long readMeDay) {
String username = user.getUsername();
Long userId = user.getId();
// 用户角色
List<String> roles = new ArrayList<>(roleMapper.selectListByUserId(userId).stream().map(Role::getRoleCode).toList());
// 判断是否是 admin 如果是admin 赋予所有权限
List<String> permissions = new ArrayList<>();
boolean isAdmin = RoleHelper.checkAdmin(roles, permissions, user);
if (!isAdmin) {
permissions = permissionMapper.selectListByUserId(userId).stream()
.map(Permission::getPowerCode)
.toList();
}
// 为这两个去重
permissions = permissions.stream().distinct().toList();
roles = roles.stream().distinct().toList();
// 获取IP地址并更新用户登录信息
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 设置用户IP地址并更新用户信息
user.setIpAddress(ipAddr);
user.setIpRegion(ipRegion);
userMapper.updateById(user);
LoginVo loginVo = new LoginVo();
BeanUtils.copyProperties(user, loginVo);
loginVo.setPersonDescription(user.getSummary());
loginVo.setRoles(roles);
loginVo.setPermissions(permissions);
// 使用用户名创建token
String token = JwtTokenUtil.createToken(userId, username, (int) readMeDay);
loginVo.setToken(token);
loginVo.setRefreshToken(token);
loginVo.setReadMeDay(readMeDay);
// 计算过期时间并格式化返回
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime plusDay = localDateTime.plusDays(readMeDay);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD_HH_MM_SS_SLASH);
String expires = plusDay.format(dateTimeFormatter);
loginVo.setExpires(expires);
// 设置用户头像
String userAvatar = minioHelper.getUserAvatar(user.getAvatar());
loginVo.setAvatar(userAvatar);
// 将用户登录保存在用户登录日志表中
setUserLoginLog(user, token, UserConstant.LOGIN);
// 将信息保存在Redis中一定要确保用户名是唯一的
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(username);
redisTemplate.opsForValue().set(loginInfoPrefix, loginVo, readMeDay, TimeUnit.DAYS);
return loginVo;
}
/**
* 设置用户登录日志内容
* <p>
* 该方法用于将管理员用户信息复制到用户登录日志对象中同时处理特殊字段映射关系
* <p>
* 实现说明
* 1. 使用BeanUtils.copyProperties()复制属性时会自动将AdminUser.id复制到UserLoginLog.id
* 2. 由于UserLoginLog实际需要的是userId字段而非id字段需要特殊处理
* - 先进行属性复制
* - 然后将UserLoginLog.userId设置为AdminUser.id
* - 最后将UserLoginLog.id显式设为null避免自动生成的id被覆盖
*
* @param user 管理员用户实体对象包含用户基本信息
* @param token 本次登录/退出的认证令牌
* @param type 操作类型LOGIN-登录/LOGOUT-退出
*/
public void setUserLoginLog(AdminUser user, String token, String type) {
UserLoginLog userLoginLog = new UserLoginLog();
BeanUtils.copyProperties(user, userLoginLog);
userLoginLog.setUserId(user.getId());
userLoginLog.setId(null);
userLoginLog.setToken(token);
userLoginLog.setType(type);
// 当前请求request
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
// 获取User-Agent
String userAgent = request.getHeader("User-Agent");
userLoginLog.setUserAgent(userAgent);
// 获取X-Requested-With
String xRequestedWith = request.getHeader("X-Requested-With");
userLoginLog.setXRequestedWith(xRequestedWith);
}
userLoginLogMapper.insert(userLoginLog);
}
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
@ -31,4 +31,16 @@ public class DefaultLoginStrategy implements LoginStrategy {
queryWrapper.eq(AdminUser::getUsername, username);
return userMapper.selectOne(queryWrapper);
}
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
* @param adminUser 用户
*/
@Override
public void authenticateAfter(LoginDto loginDto, AdminUser adminUser) {
}
}

View File

@ -1,9 +1,9 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.mapper.system.UserMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.data.redis.core.RedisTemplate;
@ -53,4 +53,18 @@ public class EmailLoginStrategy implements LoginStrategy {
queryWrapper.eq(AdminUser::getEmail, username);
return userMapper.selectOne(queryWrapper);
}
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
* @param adminUser 用户
*/
@Override
public void authenticateAfter(LoginDto loginDto, AdminUser adminUser) {
// 将Redis中验证码删除
String emailCodePrefix = RedisUserConstant.getAdminUserEmailCodePrefix(loginDto.getUsername());
redisTemplate.delete(emailCodePrefix);
}
}

View File

@ -1,9 +1,8 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Map;
@ -13,11 +12,9 @@ import java.util.Map;
public class LoginContext {
private final Map<String, LoginStrategy> strategies;
private final PasswordEncoder passwordEncoder;
public LoginContext(Map<String, LoginStrategy> strategies, PasswordEncoder passwordEncoder) {
public LoginContext(Map<String, LoginStrategy> strategies) {
this.strategies = strategies;
this.passwordEncoder = passwordEncoder;
}
/**
@ -37,4 +34,16 @@ public class LoginContext {
return strategy.authenticate(loginDto);
}
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
*/
public void loginAfter(LoginDto loginDto, AdminUser adminUser) {
String type = loginDto.getType();
LoginStrategy strategy = strategies.get(type);
strategy.authenticateAfter(loginDto, adminUser);
}
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
@ -16,4 +16,12 @@ public interface LoginStrategy {
* @return 鉴定身份验证
*/
AdminUser authenticate(LoginDto loginDto);
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
* @param adminUser
*/
void authenticateAfter(LoginDto loginDto, AdminUser adminUser);
}

View File

@ -0,0 +1,124 @@
package cn.bunny.services.service.system.helper.role;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.SecurityConfigConstant;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RoleHelper {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private UserMapper userMapper;
@Resource
private UserLoginHelper userloginHelper;
/**
* 判断用户是否具有管理员权限
* <p>
* 管理员判定规则
* 1. 系统默认管理员用户ID为1的预设管理员账号用户名为Administrator
* 2. 角色授权管理员用户角色列表包含"admin"角色的账号
* <p>
* 权限控制说明
* - 管理员权限用于前端按钮级权限控制支持以下通配符格式
* - "*::*::*"全部模块的全部操作权限
* - "*::*" 指定模块的全部操作权限
* - "*" 基础通配权限
* - 若无细粒度按钮控制需求可不设置具体权限
* 详细查看
* <a href="https://pure-admin.cn/pages/RBAC/#%E7%AC%AC%E4%BA%8C%E7%A7%8D%E6%A8%A1%E5%BC%8F-%E7%99%BB%E5%BD%95%E6%8E%A5%E5%8F%A3%E8%BF%94%E5%9B%9E%E6%8C%89%E9%92%AE%E7%BA%A7%E5%88%AB%E6%9D%83%E9%99%90">
* 查看前端权限设置
* </a>
*
* @param roleList 用户角色编码列表可能包含"admin"角色
* @param permissions 用户权限列表用于前端按钮控制
* @param adminUser 用户实体对象需包含userId字段
* @return true-是管理员false-非管理员
*/
public static boolean checkAdmin(List<String> roleList, List<String> permissions, AdminUser adminUser) {
// 判断是否是超级管理员
boolean isIdAdmin = adminUser.getId().equals(1L);
boolean isAdmin = roleList.stream().anyMatch(role -> role.equals("admin"));
// 判断是否是 admin
if (isIdAdmin || isAdmin) {
roleList.add("admin");
permissions.add("*");
permissions.add("*::*");
permissions.add("*::*::*");
return true;
}
return false;
}
/**
* 判断是否是管理员
*
* @param roleList 角色代码列表
* @return 是否是管理员
*/
public static boolean checkAdmin(List<String> roleList) {
// 可以放行的权限
List<String> permissionList = SecurityConfigConstant.PERMIT_ACCESS_LIST;
// 判断是否是超级管理员
if (BaseContext.getUserId().equals(1L)) return true;
// 判断是否是 admin
return roleList.stream().anyMatch(permissionList::contains);
}
/**
* 批量更新Redis中用户权限信息
*
* <p><b>使用场景</b>当用户角色或权限变更时同步更新Redis中的用户权限数据</p>
*
* <p><b>实现策略</b></p>
* <ol>
* <li><b>主动更新当前实现</b>重新构建用户权限信息并更新Redis缓存</li>
* <li><b>强制下线</b>删除用户登录态强制重新认证获取最新权限</li>
* </ol>
*
* <p><b>技术实现</b></p>
* <ul>
* <li>采用Spring事件驱动机制触发更新</li>
* <li>使用并行流(parallelStream)提高批量处理效率</li>
* <li>仅更新Redis中存在登录态的用户</li>
* </ul>
*
* @param userIds 需要更新的用户ID集合
* 仅处理集合中存在的有效用户
* @see RedisUserConstant Redis键前缀常量
* @see UserLoginHelper#buildLoginUserVo 用户登录信息构建方法
*/
public void updateUserRedisInfo(List<Long> userIds) {
if (userIds.isEmpty()) return;
// 批量查询用户
List<AdminUser> adminUsers = userMapper.selectBatchIds(userIds);
// 并行处理用户更新
adminUsers.parallelStream()
.filter(user -> redisTemplate.hasKey(RedisUserConstant.getAdminLoginInfoPrefix(user.getUsername())))
.forEach(user -> {
// 策略1: 更新用户权限信息
userloginHelper.buildLoginUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
// 或者策略2: 强制用户下线
// redisTemplate.delete(RedisUserConstant.getAdminLoginInfoPrefix(user.getUsername()));
});
}
}

View File

@ -0,0 +1,59 @@
package cn.bunny.services.service.system.helper.role;
import cn.bunny.services.domain.system.system.entity.UserRole;
import cn.bunny.services.mapper.system.UserRoleMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 角色更新事件处理器
*
* <p><b>职责说明</b>监听并处理角色变更事件同步更新关联用户的权限信息</p>
*
* <p><b>处理流程</b></p>
* <ol>
* <li>监听{@code RoleUpdatedEvent}事件</li>
* <li>查询关联该角色的所有用户ID</li>
* <li>通过{@code RoleHelper}批量更新用户Redis缓存</li>
* </ol>
*
* <p><b>注意事项</b></p>
* <ul>
* <li>使用异步处理@{@link Async}避免阻塞主线程</li>
* <li>仅处理直接关联的用户不包含通过用户组等间接关联的情况</li>
* <li>更新操作采用批量处理提高效率</li>
* </ul>
*/
@Component
public class RoleUpdateHandler {
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private RoleHelper roleHelper;
/**
* 处理角色更新事件
*
* @param event 角色更新事件包含变更的角色ID
* @see RoleUpdatedEvent 角色更新事件定义
* @see RoleHelper#updateUserRedisInfo 用户缓存更新方法
*/
@Async
@EventListener
public void handleRoleUpdatedEvent(RoleUpdatedEvent event) {
// 查询当前用户角色中角色id
LambdaQueryWrapper<UserRole> lambdaQueryWrapper = Wrappers.<UserRole>lambdaQuery().eq(UserRole::getRoleId, event.getRoleId());
// 批量查询关联用户ID
List<UserRole> userRoles = userRoleMapper.selectList(lambdaQueryWrapper);
List<Long> userIds = userRoles.stream().map(UserRole::getUserId).toList();
roleHelper.updateUserRedisInfo(userIds);
}
}

View File

@ -0,0 +1,17 @@
package cn.bunny.services.service.system.helper.role;
import lombok.Getter;
import lombok.Setter;
import org.springframework.context.ApplicationEvent;
// 角色更新事件
@Getter
@Setter
public class RoleUpdatedEvent extends ApplicationEvent {
private final Long roleId;
public RoleUpdatedEvent(Object source, Long roleId) {
super(source);
this.roleId = roleId;
}
}

View File

@ -1,11 +1,9 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.config.minio.MinioProperties;
import cn.bunny.services.config.minio.MinioUtil;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.dto.minio.MinioUploadFileInfo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.common.model.dto.file.MinioFilePath;
import cn.bunny.services.domain.system.files.dto.FileUploadDto;
import cn.bunny.services.domain.system.files.dto.FilesAddDto;
import cn.bunny.services.domain.system.files.dto.FilesDto;
@ -15,6 +13,9 @@ import cn.bunny.services.domain.system.files.vo.FileInfoVo;
import cn.bunny.services.domain.system.files.vo.FilesVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.system.FilesMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.minio.MinioProperties;
import cn.bunny.services.minio.MinioService;
import cn.bunny.services.service.system.FilesService;
import cn.bunny.services.utils.FileUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -59,11 +60,15 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
private MinioProperties properties;
@Resource
private MinioUtil minioUtil;
private MinioService minioService;
@Resource
private MinioHelper minioHelper;
@Resource
private FilesMapper filesMapper;
/**
* * 系统文件表 服务实现类
*
@ -76,10 +81,8 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
IPage<FilesVo> page = baseMapper.selectListByPage(pageParams, dto);
return PageResult.<FilesVo>builder()
.list(page.getRecords())
.pageNo(page.getCurrent())
.pageSize(page.getSize())
.total(page.getTotal())
.list(page.getRecords()).pageNo(page.getCurrent())
.pageSize(page.getSize()).total(page.getTotal())
.build();
}
@ -92,13 +95,13 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
public void addFiles(FilesAddDto dto) {
List<Files> list = dto.getFiles().stream().map(file -> {
try {
MinioFilePath minioFilePath = minioUtil.uploadObjectReturnFilePath(file, dto.getFilepath());
MinioUploadFileInfo minioUploadFileInfo = minioService.uploadWithFileInfo(file, dto.getFilepath());
Files files = new Files();
files.setFileType(file.getContentType());
files.setFileSize(file.getSize());
files.setFilepath("/" + properties.getBucketName() + minioFilePath.getFilepath());
files.setFilename(minioFilePath.getFilename());
files.setFilepath("/" + properties.getBucketName() + minioUploadFileInfo.getFilepath());
files.setFilename(minioUploadFileInfo.getFilename());
files.setDownloadCount(dto.getDownloadCount());
return files;
} catch (IOException e) {
@ -106,7 +109,6 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
}
}).toList();
// 保存数据
saveBatch(list);
}
@ -124,7 +126,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
if (file != null) {
// 文件路径
String filePath = files.getFilepath().replace("/" + properties.getBucketName() + "/", "");
minioUtil.updateFile(properties.getBucketName(), filePath, file);
minioService.updateFile(properties.getBucketName(), filePath, file);
// 设置文件信息
files.setFileSize(file.getSize());
@ -160,8 +162,8 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
String filename = file.getOriginalFilename();
// 上传文件
MinioFilePath minioFIlePath = minioUtil.uploadObjectReturnFilePath(file, type);
String bucketNameFilepath = minioFIlePath.getBucketNameFilepath();
MinioUploadFileInfo minioUploadFIleInfo = minioService.uploadWithFileInfo(file, type);
String bucketNameFilepath = minioUploadFIleInfo.getBucketNameFilepath();
// 盘读研数据是否过大
String mb = maxFileSize.replace("MB", "");
@ -178,12 +180,9 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
// 返回信息内容化
return FileInfoVo.builder()
.size(FileUtil.getSize(fileSize))
.filepath(bucketNameFilepath)
.fileSize(fileSize)
.fileType(contentType)
.filename(filename)
.url(minioUtil.getObjectNameFullPath(bucketNameFilepath))
.size(FileUtil.getSize(fileSize)).filepath(bucketNameFilepath)
.fileSize(fileSize).fileType(contentType)
.filename(filename).url(minioHelper.getObjectNameFullPath(bucketNameFilepath))
.build();
}
@ -205,7 +204,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
}).toList();
// 删除目标文件
minioUtil.removeObjects(list);
minioService.removeObjects(list);
// 删除数据库内容
removeByIds(ids);
@ -229,7 +228,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
String filepath = files.getFilepath();
int end = filepath.indexOf("/", 1);
filepath = filepath.substring(end + 1);
byte[] bytes = minioUtil.getBucketObjectByte(filepath);
byte[] bytes = minioService.getBucketObjectByte(filepath);
// 设置响应头
HttpHeaders headers = new HttpHeaders();

View File

@ -15,8 +15,8 @@ import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.system.PermissionMapper;
import cn.bunny.services.mapper.system.RolePermissionMapper;
import cn.bunny.services.service.system.PermissionService;
import cn.bunny.services.service.system.helper.PermissionHelper;
import cn.bunny.services.utils.FileUtil;
import cn.bunny.services.utils.system.PermissionUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
@ -187,7 +187,7 @@ public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permiss
}).toList();
// 构建树型结构
List<PermissionExcel> buildTree = PermissionUtil.buildTree(permissionExcelList);
List<PermissionExcel> buildTree = PermissionHelper.buildTree(permissionExcelList);
// 创建btye输出流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@ -197,9 +197,9 @@ public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permiss
// 判断导出类型是什么
if (type.equals(FileType.EXCEL)) {
PermissionUtil.writExcel(permissionExcelList, zipOutputStream, filename + ".xlsx");
PermissionHelper.writExcel(permissionExcelList, zipOutputStream, filename + ".xlsx");
} else {
PermissionUtil.writeJson(buildTree, zipOutputStream, filename + ".json");
PermissionHelper.writeJson(buildTree, zipOutputStream, filename + ".json");
}
} catch (Exception e) {
throw new RuntimeException(e);
@ -235,7 +235,7 @@ public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permiss
List<PermissionExcel> list = JSON.parseObject(json, new TypeReference<>() {
});
// 格式化数据保存到数据库
List<PermissionExcel> flattenedTree = PermissionUtil.flattenTree(list);
List<PermissionExcel> flattenedTree = PermissionHelper.flattenTree(list);
List<Permission> permissionList = flattenedTree.stream().map(permissionExcel -> {
Permission permission = new Permission();
BeanUtils.copyProperties(permissionExcel, permission);

View File

@ -8,7 +8,7 @@ import cn.bunny.services.mapper.system.RolePermissionMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.mapper.system.UserRoleMapper;
import cn.bunny.services.service.system.RolePermissionService;
import cn.bunny.services.utils.system.RoleUtil;
import cn.bunny.services.service.system.helper.role.RoleHelper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
@ -33,7 +33,7 @@ public class RolePermissionServiceImpl extends ServiceImpl<RolePermissionMapper,
private UserMapper userMapper;
@Resource
private RoleUtil roleUtil;
private RoleHelper roleHelper;
@Resource
private UserRoleMapper userRoleMapper;
@ -86,6 +86,6 @@ public class RolePermissionServiceImpl extends ServiceImpl<RolePermissionMapper,
// 更新Redis中用户信息
List<Long> userIds = adminUsers.stream().map(AdminUser::getId).toList();
roleUtil.updateUserRedisInfo(userIds);
roleHelper.updateUserRedisInfo(userIds);
}
}

View File

@ -16,8 +16,8 @@ import cn.bunny.services.mapper.system.RolePermissionMapper;
import cn.bunny.services.mapper.system.RouterRoleMapper;
import cn.bunny.services.mapper.system.UserRoleMapper;
import cn.bunny.services.service.system.RoleService;
import cn.bunny.services.service.system.helper.role.RoleUpdatedEvent;
import cn.bunny.services.utils.FileUtil;
import cn.bunny.services.utils.system.RoleUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@ -28,6 +28,7 @@ import jakarta.validation.Valid;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -67,7 +68,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
private RouterRoleMapper routerRoleMapper;
@Resource
private RoleUtil roleUtil;
private ApplicationEventPublisher eventPublisher;
/**
* * 角色 服务实现类
@ -94,7 +95,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @return 所有角色列表
*/
@Override
@Cacheable(cacheNames = "role", key = "'allRole'", cacheManager = "cacheManagerWithMouth")
@Cacheable(cacheNames = "role", key = "'roleList'", cacheManager = "cacheManagerWithMouth")
public List<RoleVo> roleList() {
return list().stream().map(role -> {
RoleVo roleVo = new RoleVo();
@ -155,7 +156,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param file Excel文件
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void updateRoleByFile(MultipartFile file) {
if (file == null) {
throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);
@ -176,7 +177,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param dto 角色添加
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void addRole(@Valid RoleAddDto dto) {
Role role = new Role();
BeanUtils.copyProperties(dto, role);
@ -189,7 +190,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param dto 角色更新
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void updateRole(@Valid RoleUpdateDto dto) {
// 查询更新的角色是否存在
List<Role> roleList = list(Wrappers.<Role>lambdaQuery().eq(Role::getId, dto.getId()));
@ -203,7 +204,10 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
// 找到所有和当前更新角色相同的用户并更新Redis中用户信息
List<Long> userIds = userRoleMapper.selectList(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getRoleId, dto.getId()))
.stream().map(UserRole::getUserId).toList();
roleUtil.updateUserRedisInfo(userIds);
// TODO 1
// roleUtil.updateUserRedisInfo(userIds);
// 发布角色更新事件
eventPublisher.publishEvent(new RoleUpdatedEvent(this, dto.getId()));
}
@ -213,7 +217,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param ids 删除id列表
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void deleteRole(List<Long> ids) {
// 判断数据请求是否为空
if (ids.isEmpty()) throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);

View File

@ -1,5 +1,6 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.router.RouterAddDto;
import cn.bunny.services.domain.system.system.dto.router.RouterUpdateDto;
import cn.bunny.services.domain.system.system.entity.router.Router;
@ -10,13 +11,12 @@ import cn.bunny.services.domain.system.system.views.ViewRouterRole;
import cn.bunny.services.domain.system.system.vo.router.RouterManageVo;
import cn.bunny.services.domain.system.system.vo.router.RouterVo;
import cn.bunny.services.domain.system.system.vo.router.WebUserRouterVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.system.RolePermissionMapper;
import cn.bunny.services.mapper.system.RouterMapper;
import cn.bunny.services.mapper.system.RouterRoleMapper;
import cn.bunny.services.service.system.RouterService;
import cn.bunny.services.utils.system.RouterUtil;
import cn.bunny.services.service.system.helper.RouterHelper;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -48,7 +48,7 @@ public class RouterServiceImpl extends ServiceImpl<RouterMapper, Router> impleme
private RouterRoleMapper routerRoleMapper;
@Resource
private RouterUtil routerUtil;
private RouterHelper routerHelper;
@Resource
private RolePermissionMapper rolePermissionMapper;
@ -75,13 +75,13 @@ public class RouterServiceImpl extends ServiceImpl<RouterMapper, Router> impleme
.collect(Collectors.groupingBy(ViewRolePermission::getRoleId, Collectors.toList()));
// 整理web用户所能看到的路由列表并检查当前用户是否是admin
List<WebUserRouterVo> webUserRouterVoList = routerUtil.getWebUserRouterVos(routerList, routerRoleList, rolePermissionList);
List<WebUserRouterVo> webUserRouterVoList = routerHelper.getWebUserRouterVos(routerList, routerRoleList, rolePermissionList);
// 添加 admin 管理路由权限
webUserRouterVoList.forEach(routerVo -> {
// 递归添加路由节点
if (routerVo.getParentId() == 0) {
routerVo.setChildren(routerUtil.buildTreeSetChildren(routerVo.getId(), webUserRouterVoList));
routerVo.setChildren(routerHelper.buildTreeSetChildren(routerVo.getId(), webUserRouterVoList));
voList.add(routerVo);
}
});
@ -141,7 +141,7 @@ public class RouterServiceImpl extends ServiceImpl<RouterMapper, Router> impleme
// 将数据提出role power 存储到数据库
Long id = router.getId();
routerUtil.insertRouterRoleAndPermission(meta, id);
routerHelper.insertRouterRoleAndPermission(meta, id);
// 添加路由
save(router);
@ -169,7 +169,7 @@ public class RouterServiceImpl extends ServiceImpl<RouterMapper, Router> impleme
routerRoleMapper.deleteBatchIdsByRouterIds(List.of(id));
// 将数据提出role power 存储到数据库
routerUtil.insertRouterRoleAndPermission(meta, id);
routerHelper.insertRouterRoleAndPermission(meta, id);
// 更新路由信息
updateById(router);

View File

@ -0,0 +1,201 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.UserConstant;
import cn.bunny.services.domain.common.enums.EmailTemplateEnums;
import cn.bunny.services.domain.common.enums.LoginEnums;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.email.entity.EmailTemplate;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.dto.user.RefreshTokenDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.configuration.EmailTemplateMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.service.configuration.helper.email.ConcreteSenderEmailTemplate;
import cn.bunny.services.service.system.UserLoginService;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import cn.bunny.services.service.system.helper.login.DefaultLoginStrategy;
import cn.bunny.services.service.system.helper.login.EmailLoginStrategy;
import cn.bunny.services.service.system.helper.login.LoginContext;
import cn.bunny.services.service.system.helper.login.LoginStrategy;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@Service
public class UserLoginServiceImpl extends ServiceImpl<UserMapper, AdminUser> implements UserLoginService {
@Resource
private UserLoginHelper userloginHelper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private UserMapper userMapper;
@Resource
private EmailTemplateMapper emailTemplateMapper;
@Resource
private ConcreteSenderEmailTemplate concreteSenderEmailTemplate;
/**
* 前台用户登录接口
* 这里不用判断用户是否为空因为在登录时已经校验过了
* <p>
* 抛出异常使用自带的 UsernameNotFoundException 或者自己封装<br/>
* 但是这两个效果传入参数都是一样的所以全部使用 UsernameNotFoundException
* </p>
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
@Override
public LoginVo login(LoginDto loginDto) {
// 初始化所有策略可扩展
HashMap<String, LoginStrategy> loginStrategyHashMap = new HashMap<>();
// 默认的登录方式
loginStrategyHashMap.put(LoginEnums.default_STRATEGY.getValue(), new DefaultLoginStrategy(userMapper));
// 注册邮箱
loginStrategyHashMap.put(LoginEnums.EMAIL_STRATEGY.getValue(), new EmailLoginStrategy(redisTemplate, userMapper));
// 使用登录上下文调用登录策略
LoginContext loginContext = new LoginContext(loginStrategyHashMap);
AdminUser user = loginContext.executeStrategy(loginDto);
// 验证登录逻辑
if (user == null) throw new UsernameNotFoundException(ResultCodeEnum.USER_IS_EMPTY.getMessage());
// 数据库密码
String dbPassword = user.getPassword();
String password = loginDto.getPassword();
if (!passwordEncoder.matches(password, dbPassword)) {
throw new UsernameNotFoundException(ResultCodeEnum.LOGIN_ERROR.getMessage());
}
// 判断用户是否禁用
if (user.getStatus()) {
throw new UsernameNotFoundException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED.getMessage());
}
// 登录结束后的操作
loginContext.loginAfter(loginDto, user);
user.setUpdateUser(user.getId());
user.setCreateUser(user.getId());
return userloginHelper.buildLoginUserVo(user, loginDto.getReadMeDay());
}
/**
* 发送登录邮件验证码
*
* <p>完整处理流程</p>
* <ol>
* <li><b>查询模板</b>从数据库获取默认的验证码邮件模板</li>
* <li><b>生成验证码</b>创建4位数字验证码</li>
* <li><b>模板处理</b>替换模板中的动态变量系统名称验证码等</li>
* <li><b>发送邮件</b>通过邮件服务发送处理后的模板</li>
* <li><b>缓存验证码</b>将验证码存入Redis 有效期 xxx</li>
* </ol>
*
* @param email 接收邮箱地址不可为空
* <ul>
* <li>未找到默认邮件模板</li>
* <li>邮件发送失败</li>
* <li>Redis操作异常</li>
* </ul>
* @see EmailTemplateEnums#VERIFICATION_CODE 验证码模板类型枚举
* @see RedisUserConstant Redis键和过期时间常量
*/
@Override
public void sendLoginEmail(@NotNull String email) {
// 查询验证码邮件模板
LambdaQueryWrapper<EmailTemplate> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(EmailTemplate::getIsDefault, true);
lambdaQueryWrapper.eq(EmailTemplate::getType, EmailTemplateEnums.VERIFICATION_CODE.getType());
EmailTemplate emailTemplate = emailTemplateMapper.selectOne(lambdaQueryWrapper);
// 生成验证码
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2);
String emailCode = captcha.getCode();
// 需要替换模板内容
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("#title#", "BunnyAdmin");
hashMap.put("#verifyCode#", emailCode);
hashMap.put("#expires#", 15);
hashMap.put("#sendEmailUser#", emailTemplate.getEmailUser());
hashMap.put("#companyName#", "BunnyAdmin");
// 发送邮件
concreteSenderEmailTemplate.sendEmailTemplate(email, emailTemplate, hashMap);
// 在Redis中存储验证码
String emailCodePrefix = RedisUserConstant.getAdminUserEmailCodePrefix(email);
redisTemplate.opsForValue().set(emailCodePrefix, emailCode, RedisUserConstant.REDIS_EXPIRATION_TIME, TimeUnit.MINUTES);
}
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
@Override
public RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto) {
Long userId = JwtTokenUtil.getUserId(dto.getRefreshToken());
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
// 用户存在且没有禁用
if (adminUser == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
if (adminUser.getStatus()) throw new AuthCustomerException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED);
LoginVo buildUserVo = userloginHelper.buildLoginUserVo(adminUser, dto.getReadMeDay());
RefreshTokenVo refreshTokenVo = new RefreshTokenVo();
BeanUtils.copyProperties(buildUserVo, refreshTokenVo);
return refreshTokenVo;
}
/**
* 退出登录
*/
@Override
public void logout() {
// 获取上下文对象中的用户ID和用户token
LoginVo loginVo = BaseContext.getLoginVo();
String token = loginVo.getToken();
Long userId = BaseContext.getUserId();
// 获取IP地址
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 查询用户信息
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
adminUser.setIpAddress(ipAddr);
adminUser.setIpRegion(ipRegion);
userloginHelper.setUserLoginLog(adminUser, token, UserConstant.LOGOUT);
// 删除Redis中用户信息
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername());
redisTemplate.delete(loginInfoPrefix);
}
}

View File

@ -1,17 +1,17 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.user.AssignRolesToUsersDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.UserRole;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.mapper.system.UserRoleMapper;
import cn.bunny.services.service.system.UserRoleService;
import cn.bunny.services.utils.system.UserUtil;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
@ -35,14 +35,11 @@ import java.util.concurrent.TimeUnit;
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> implements UserRoleService {
@Resource
private UserRoleMapper userRoleMapper;
private UserLoginHelper userloginHelper;
@Resource
private UserUtil userUtil;
private UserRoleMapper userRoleMapper;
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@ -94,7 +91,7 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i
// 重新设置Redis中的用户存储信息vo对象
String username = adminUser.getUsername();
loginVo = userUtil.buildLoginUserVo(adminUser, readMeDay);
loginVo = userloginHelper.buildLoginUserVo(adminUser, readMeDay);
redisTemplate.opsForValue().set(RedisUserConstant.getAdminLoginInfoPrefix(username), loginVo, readMeDay, TimeUnit.DAYS);
}
}

View File

@ -1,40 +1,34 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.MinioConstant;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.enums.EmailTemplateEnums;
import cn.bunny.services.domain.common.enums.LoginEnums;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.constant.UserConstant;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.email.entity.EmailTemplate;
import cn.bunny.services.domain.system.files.dto.FileUploadDto;
import cn.bunny.services.domain.system.files.vo.FileInfoVo;
import cn.bunny.services.domain.system.log.entity.UserLoginLog;
import cn.bunny.services.domain.system.system.dto.user.*;
import cn.bunny.services.domain.system.system.dto.user.AdminUserAddDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.Role;
import cn.bunny.services.domain.system.system.entity.UserDept;
import cn.bunny.services.domain.system.system.views.ViewUserDept;
import cn.bunny.services.domain.system.system.vo.user.AdminUserVo;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.domain.system.system.vo.user.UserVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.configuration.EmailTemplateMapper;
import cn.bunny.services.mapper.log.UserLoginLogMapper;
import cn.bunny.services.mapper.system.RoleMapper;
import cn.bunny.services.mapper.system.UserDeptMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.mapper.system.UserRoleMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.service.system.FilesService;
import cn.bunny.services.service.system.UserService;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import cn.bunny.services.utils.email.ConcreteSenderEmailTemplate;
import cn.bunny.services.utils.login.DefaultLoginStrategy;
import cn.bunny.services.utils.login.EmailLoginStrategy;
import cn.bunny.services.utils.login.LoginContext;
import cn.bunny.services.utils.login.LoginStrategy;
import cn.bunny.services.utils.system.UserUtil;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@ -42,18 +36,15 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@ -67,150 +58,23 @@ import java.util.concurrent.TimeUnit;
public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implements UserService {
@Resource
private UserUtil userUtil;
private UserLoginHelper userloginHelper;
@Resource
private ConcreteSenderEmailTemplate concreteSenderEmailTemplate;
private PasswordEncoder passwordEncoder;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private FilesService filesService;
@Resource
private UserDeptMapper userDeptMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private UserLoginLogMapper userLoginLogMapper;
@Resource
private EmailTemplateMapper emailTemplateMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private UserMapper userMapper;
@Resource
private PasswordEncoder passwordEncoder;
/**
* 前台用户登录接口
* 这里不用判断用户是否为空因为在登录时已经校验过了
* <p>
* 抛出异常使用自带的 UsernameNotFoundException 或者自己封装<br/>
* 但是这两个效果传入参数都是一样的所以全部使用 UsernameNotFoundException
* </p>
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
@Override
public LoginVo login(LoginDto loginDto) {
Long readMeDay = loginDto.getReadMeDay();
// 初始化所有策略可扩展
HashMap<String, LoginStrategy> loginStrategyHashMap = new HashMap<>();
// 默认的登录方式
loginStrategyHashMap.put(LoginEnums.default_STRATEGY.getValue(), new DefaultLoginStrategy(userMapper));
// 注册邮箱
loginStrategyHashMap.put(LoginEnums.EMAIL_STRATEGY.getValue(), new EmailLoginStrategy(redisTemplate, userMapper));
// 使用登录上下文调用登录策略
LoginContext loginContext = new LoginContext(loginStrategyHashMap, passwordEncoder);
AdminUser user = loginContext.executeStrategy(loginDto);
// 验证登录逻辑
if (user == null) throw new UsernameNotFoundException(ResultCodeEnum.USER_IS_EMPTY.getMessage());
// 数据库密码
String dbPassword = user.getPassword();
// 用户登录密码
String password = loginDto.getPassword();
if (!passwordEncoder.matches(password, dbPassword)) {
throw new UsernameNotFoundException(ResultCodeEnum.LOGIN_ERROR.getMessage());
}
// 判断用户是否禁用
if (user.getStatus()) {
throw new UsernameNotFoundException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED.getMessage());
}
return userUtil.buildLoginUserVo(user, readMeDay);
}
/**
* 登录发送邮件验证码
*
* @param email 邮箱
*/
@Override
public void sendLoginEmail(@NotNull String email) {
// 查询验证码邮件模板
LambdaQueryWrapper<EmailTemplate> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(EmailTemplate::getIsDefault, true);
lambdaQueryWrapper.eq(EmailTemplate::getType, EmailTemplateEnums.VERIFICATION_CODE.getType());
EmailTemplate emailTemplate = emailTemplateMapper.selectOne(lambdaQueryWrapper);
// 生成验证码
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2);
String emailCode = captcha.getCode();
// 需要替换模板内容
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("#title#", "BunnyAdmin");
hashMap.put("#verifyCode#", emailCode);
hashMap.put("#expires#", 15);
hashMap.put("#sendEmailUser#", emailTemplate.getEmailUser());
hashMap.put("#companyName#", "BunnyAdmin");
// 发送邮件
concreteSenderEmailTemplate.sendEmailTemplate(email, emailTemplate, hashMap);
// 在Redis中存储验证码
redisTemplate.opsForValue().set(RedisUserConstant.getAdminUserEmailCodePrefix(email), emailCode, RedisUserConstant.REDIS_EXPIRATION_TIME, TimeUnit.MINUTES);
}
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
@Override
public RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto) {
Long userId = JwtTokenUtil.getUserId(dto.getRefreshToken());
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
// 用户存在且没有禁用
if (adminUser == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
if (adminUser.getStatus()) throw new AuthCustomerException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED);
LoginVo buildUserVo = userUtil.buildLoginUserVo(adminUser, dto.getReadMeDay());
RefreshTokenVo refreshTokenVo = new RefreshTokenVo();
BeanUtils.copyProperties(buildUserVo, refreshTokenVo);
return refreshTokenVo;
}
/**
* 退出登录
*/
@Override
public void logout() {
// 获取上下文对象中的用户ID和用户token
LoginVo loginVo = BaseContext.getLoginVo();
String token = loginVo.getToken();
Long userId = BaseContext.getUserId();
// 获取IP地址
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 查询用户信息
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
UserLoginLog userLoginLog = userUtil.setUserLoginLog(adminUser, token, ipAddr, ipRegion, "logout");
userLoginLogMapper.insert(userLoginLog);
// 删除Redis中用户信息
redisTemplate.delete(RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername()));
}
private MinioHelper minioHelper;
/**
* * 获取用户信息
@ -226,20 +90,40 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 用户是否存在
if (user == null) throw new AuthCustomerException(ResultCodeEnum.DATA_NOT_EXIST);
// 用户头像
String avatar = user.getAvatar();
UserVo userVo = new UserVo();
BeanUtils.copyProperties(user, userVo);
userVo.setAvatar(userUtil.checkGetUserAvatar(avatar));
String userAvatar = minioHelper.getUserAvatar(avatar);
userVo.setAvatar(userAvatar);
return userVo;
}
/**
* * 强制退出
* 管理员强制用户下线
*
* @param id 用户id
* <p><b>功能说明</b>管理员强制指定用户退出登录状态并记录操作日志</p>
*
* <p><b>处理流程</b></p>
* <ol>
* <li>参数校验检查用户ID是否为空</li>
* <li>查询用户信息根据ID获取用户实体</li>
* <li>记录操作日志保存强制下线记录到用户登录日志表</li>
* <li>清除登录状态删除Redis中的用户登录信息</li>
* </ol>
*
* <p><b>注意事项</b></p>
* <ul>
* <li>会中断用户当前会话无需用户确认</li>
* <li>操作会记录到用户登录日志用于审计追踪</li>
* <li>Redis键使用用户名作为唯一标识</li>
* </ul>
*
* @param id 用户ID不可为空
* @see RedisUserConstant#getAdminLoginInfoPrefix Redis键生成规则
* @see UserConstant#FORCE_LOGOUT 强制下线类型常量
*/
@Override
public void forcedOfflineByAdmin(Long id) {
@ -251,11 +135,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 将用户登录保存在用户登录日志表中
UserLoginLog userLoginLog = new UserLoginLog();
BeanUtils.copyProperties(adminUser, userLoginLog);
userLoginLog.setId(null);
userLoginLog.setUserId(adminUser.getId());
userLoginLog.setIpAddress(adminUser.getIpAddress());
userLoginLog.setIpRegion(adminUser.getIpRegion());
userLoginLog.setToken(null);
userLoginLog.setType("forcedOffline");
userLoginLog.setType(UserConstant.FORCE_LOGOUT);
userLoginLogMapper.insert(userLoginLog);
// 删除Redis中用户信息
@ -304,8 +187,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
AdminUser user = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
if (user == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
// 检查用户头像
dto.setAvatar(userUtil.checkPostUserAvatar(dto.getAvatar()));
// 检查用户头像因为更新用户信息会带着用户之前的信息如果没有更新头像前端显示的http:xxx
String userAvatar = minioHelper.formatUserAvatar(dto.getAvatar());
dto.setAvatar(userAvatar);
// 更新用户
AdminUser adminUser = new AdminUser();
@ -315,7 +199,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 重新生成用户信息到Redis中
BeanUtils.copyProperties(dto, user);
userUtil.buildUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
userloginHelper.buildLoginUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
}
/**
@ -363,7 +247,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
List<AdminUserVo> voList = page.getRecords().stream()
.map(adminUser -> {
// 如果存在用户头像则设置用户头像
String avatar = userUtil.checkGetUserAvatar(adminUser.getAvatar());
String avatar = minioHelper.getUserAvatar(adminUser.getAvatar());
AdminUserVo adminUserVo = new AdminUserVo();
BeanUtils.copyProperties(adminUser, adminUserVo);
@ -442,15 +326,18 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
}
// 更新头像
userUtil.uploadAvatarByAdmin(dto, adminUser);
uploadAvatarByAdmin(dto, adminUser);
// 构建用户返回信息同步到redis
userUtil.buildUserVo(adminUser, RedisUserConstant.REDIS_EXPIRATION_TIME);
userloginHelper.buildLoginUserVo(adminUser, RedisUserConstant.REDIS_EXPIRATION_TIME);
// 更新密码放在最后如果更新密码就将密码删除
userUtil.updateUserPasswordByAdmin(adminUser);
updateUserPasswordByAdmin(adminUser);
updateById(adminUser);
// 删除Redis中用户信息
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername());
redisTemplate.delete(loginInfoPrefix);
}
/**
@ -471,11 +358,56 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 逻辑删除
removeByIds(ids);
// 删除用 也要删除对应的 角色和部门但是如果做的时物理删除就不需要因为数据库中设置了外键检查如果删除用户相关表也会删除
// 删除部门相关
userDeptMapper.deleteBatchIdsByUserIds(ids);
// 删除用户角色相关
userRoleMapper.deleteBatchIdsByUserIds(ids);
}
/**
* * 管理员修改管理员用户密码
*
* @param adminUser 管理员用户修改密码
*/
private void updateUserPasswordByAdmin(AdminUser adminUser) {
Long userId = adminUser.getId();
String password = adminUser.getPassword();
// 密码更新是否存在
if (!StringUtils.hasText(password)) return;
// 对密码加密
String encode = passwordEncoder.encode(password);
// 判断新密码是否与旧密码相同
if (adminUser.getPassword().equals(encode)) {
throw new AuthCustomerException(ResultCodeEnum.UPDATE_NEW_PASSWORD_SAME_AS_OLD_PASSWORD);
}
// 更新用户密码
adminUser.setPassword(encode);
adminUser.setId(userId);
}
/**
* 管理员上传用户头像
*
* @param dto 管理员用户修改头像
*/
private void uploadAvatarByAdmin(AdminUserUpdateDto dto, AdminUser adminUser) {
MultipartFile avatar = dto.getAvatar();
Long userId = dto.getId();
// 上传头像是否存在
if (avatar == null) return;
// 上传头像
FileUploadDto uploadDto = FileUploadDto.builder().file(avatar).type(MinioConstant.avatar).build();
FileInfoVo fileInfoVo = filesService.upload(uploadDto);
// 更新用户
adminUser.setId(userId);
adminUser.setAvatar(fileInfoVo.getFilepath());
}
}

View File

@ -1,92 +0,0 @@
package cn.bunny.services.utils.system;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.SecurityConfigConstant;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.mapper.system.UserMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RoleUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private UserMapper userMapper;
@Resource
private UserUtil userUtil;
/**
* 判断是否是管理员
*
* @param roleList 角色代码列表
* @param permissions 权限列表
* @param adminUser 用户信息
* @return 是否是管理员
*/
public static boolean checkAdmin(List<String> roleList, List<String> permissions, AdminUser adminUser) {
// 判断是否是超级管理员
boolean isIdAdmin = adminUser.getId().equals(1L);
boolean isAdmin = roleList.stream().anyMatch(role -> role.equals("admin"));
// 判断是否是 admin
if (isIdAdmin || isAdmin) {
roleList.add("admin");
permissions.add("*");
permissions.add("*::*");
permissions.add("*::*::*");
return true;
}
return false;
}
/**
* 判断是否是管理员
*
* @param roleList 角色代码列表
* @return 是否是管理员
*/
public static boolean checkAdmin(List<String> roleList) {
// 可以放行的权限
List<String> permitAllList = SecurityConfigConstant.PERMIT_ALL_LIST;
// 判断是否是超级管理员
if (BaseContext.getUserId().equals(1L)) return true;
// 判断是否是 admin
return roleList.stream().anyMatch(permitAllList::contains);
}
/**
* 批量更新Redis中用户信息
*
* @param userIds 用户Id列表
*/
public void updateUserRedisInfo(List<Long> userIds) {
if (userIds.isEmpty()) return;
// 根据Id查找所有用户
List<AdminUser> adminUsers = userMapper.selectList(Wrappers.<AdminUser>lambdaQuery().in(AdminUser::getId, userIds));
// 用户为空时不更新Redis的key
if (adminUsers.isEmpty()) return;
// 更新Redis中用户信息
adminUsers.stream()
.filter(user -> {
String adminLoginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(user.getUsername());
Object object = redisTemplate.opsForValue().get(adminLoginInfoPrefix);
return object != null;
})
.forEach(user -> userUtil.buildUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME));
}
}

View File

@ -1,316 +0,0 @@
package cn.bunny.services.utils.system;
import cn.bunny.services.config.minio.MinioUtil;
import cn.bunny.services.domain.common.constant.LocalDateTimeConstant;
import cn.bunny.services.domain.common.constant.MinioConstant;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.UserConstant;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.files.dto.FileUploadDto;
import cn.bunny.services.domain.system.files.vo.FileInfoVo;
import cn.bunny.services.domain.system.log.entity.UserLoginLog;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.Permission;
import cn.bunny.services.domain.system.system.entity.Role;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.log.UserLoginLogMapper;
import cn.bunny.services.mapper.system.PermissionMapper;
import cn.bunny.services.mapper.system.RoleMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.service.system.FilesService;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class UserUtil {
@Resource
private MinioUtil minioUtil;
@Resource
private PermissionMapper permissionMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private UserMapper userMapper;
@Resource
private UserLoginLogMapper userLoginLogMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private FilesService filesService;
/**
* 构建登录用户返回对象
*
* @param user 用户
* @param readMeDay 保存登录信息时间
* @return 登录信息
*/
@Transactional(rollbackFor = AuthCustomerException.class)
public LoginVo buildLoginUserVo(AdminUser user, long readMeDay) {
Long userId = user.getId();
String email = user.getEmail();
String username = user.getUsername();
// 使用用户名创建token
String token = JwtTokenUtil.createToken(userId, username, (int) readMeDay);
// 获取IP地址并更新用户登录信息
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 设置用户IP地址并更新用户信息
AdminUser updateUser = new AdminUser();
updateUser.setId(userId);
updateUser.setIpAddress(ipAddr);
updateUser.setIpRegion(ipRegion);
userMapper.updateById(updateUser);
// 将用户登录保存在用户登录日志表中
userLoginLogMapper.insert(setUserLoginLog(user, token, ipAddr, ipRegion, "login"));
// 设置用户返回信息
LoginVo loginVo = setLoginVo(user, token, readMeDay, ipAddr, ipRegion);
// 将Redis中验证码删除
redisTemplate.delete(RedisUserConstant.getAdminUserEmailCodePrefix(email));
// 将信息保存在Redis中一定要确保用户名是唯一的
redisTemplate.opsForValue().set(RedisUserConstant.getAdminLoginInfoPrefix(username), loginVo, readMeDay, TimeUnit.DAYS);
return loginVo;
}
/**
* * 构建用户返回对象LoginVo
*
* @param user 用户对象
* @param readMeDay 记住我时间
*/
public void buildUserVo(AdminUser user, long readMeDay) {
Long userId = user.getId();
String username = user.getUsername();
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(username);
// 使用用户名创建token
String token = JwtTokenUtil.createToken(userId, username, (int) readMeDay);
// 设置用户返回信息
LoginVo loginVo = setLoginVo(user, token, readMeDay, user.getIpAddress(), user.getIpRegion());
// 将信息保存在Redis中一定要确保用户名是唯一的
redisTemplate.opsForValue().set(loginInfoPrefix, loginVo, readMeDay, TimeUnit.DAYS);
}
/**
* 设置更新用户设置内容
*
* @param user AdminUser
* @param token token
* @param readMeDay 记住我天数
* @param ipAddr IP地址
* @param ipRegion IP属地
* @return LoginVo
*/
public LoginVo setLoginVo(AdminUser user, String token, long readMeDay, String ipAddr, String ipRegion) {
Long userId = user.getId();
// 判断用户是否有头像如果没有头像设置默认头像并且用户头像不能和默认头像相同
String avatar = checkGetUserAvatar(user.getAvatar());
// 查找用户橘色
List<String> roles = new ArrayList<>(roleMapper.selectListByUserId(userId).stream().map(Role::getRoleCode).toList());
List<String> permissions = new ArrayList<>();
// 判断是否是 admin 如果是admin 赋予所有权限
boolean isAdmin = RoleUtil.checkAdmin(roles, permissions, user);
if (!isAdmin) {
permissions = permissionMapper.selectListByUserId(userId).stream().map(Permission::getPowerCode).toList();
}
// 计算过期时间并格式化返回
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime plusDay = localDateTime.plusDays(readMeDay);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD_HH_MM_SS_SLASH);
String expires = plusDay.format(dateTimeFormatter);
// 构建返回对象设置用户需要内容
LoginVo loginVo = new LoginVo();
BeanUtils.copyProperties(user, loginVo);
loginVo.setNickname(user.getNickname());
loginVo.setAvatar(avatar);
loginVo.setToken(token);
loginVo.setRefreshToken(token);
loginVo.setIpAddress(ipAddr);
loginVo.setIpRegion(ipRegion);
loginVo.setRoles(roles);
loginVo.setPermissions(permissions);
loginVo.setUpdateUser(userId);
loginVo.setPersonDescription(user.getSummary());
loginVo.setExpires(expires);
loginVo.setReadMeDay(readMeDay);
return loginVo;
}
/**
* 检查用户头像是否合规
*
* @param avatar 头像字符串
* @return 整理好的头像内容
*/
public String checkPostUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (!matcher.matches()) return avatar;
// 匹配后返回内容
return "/" + matcher.group(1);
}
/**
* 检查用户头像是否合规
*
* @param avatar 头像字符串
* @return 整理好的头像内容
*/
public String checkGetUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (matcher.matches()) return avatar;
// 匹配后返回内容
return minioUtil.getObjectNameFullPath(avatar);
}
/**
* 设置用户登录日志内容
*
* @param user AdminUser
* @param token token
* @param ipAddr IP地址
* @param ipRegion IP属地
* @param type 类型登录/退出
* @return UserLoginLog
*/
public UserLoginLog setUserLoginLog(AdminUser user, String token, String ipAddr, String ipRegion, String type) {
Long userId = user.getId();
UserLoginLog userLoginLog = new UserLoginLog();
userLoginLog.setUsername(user.getUsername());
userLoginLog.setUserId(userId);
userLoginLog.setIpAddress(ipAddr);
userLoginLog.setIpRegion(ipRegion);
userLoginLog.setToken(token);
userLoginLog.setType(type);
userLoginLog.setCreateUser(userId);
userLoginLog.setUpdateUser(userId);
userLoginLog.setCreateTime(LocalDateTime.now());
userLoginLog.setUpdateTime(LocalDateTime.now());
// 当前请求request
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) return userLoginLog;
HttpServletRequest request = requestAttributes.getRequest();
// 获取User-Agent
String userAgent = request.getHeader("User-Agent");
userLoginLog.setUserAgent(userAgent);
// 获取X-Requested-With
String xRequestedWith = request.getHeader("X-Requested-With");
userLoginLog.setXRequestedWith(xRequestedWith);
return userLoginLog;
}
/**
* * 管理员修改管理员用户密码
*
* @param adminUser 管理员用户修改密码
*/
public void updateUserPasswordByAdmin(AdminUser adminUser) {
Long userId = adminUser.getId();
String password = adminUser.getPassword();
// 密码更新是否存在
if (!StringUtils.hasText(password)) return;
// 对密码加密
String encode = passwordEncoder.encode(password);
// 判断新密码是否与旧密码相同
if (adminUser.getPassword().equals(encode))
throw new AuthCustomerException(ResultCodeEnum.UPDATE_NEW_PASSWORD_SAME_AS_OLD_PASSWORD);
// 更新用户密码
adminUser.setPassword(encode);
adminUser.setId(userId);
// 删除Redis中用户信息
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername());
redisTemplate.delete(loginInfoPrefix);
}
/**
* 管理员上传用户头像
*
* @param dto 管理员用户修改头像
*/
public void uploadAvatarByAdmin(AdminUserUpdateDto dto, AdminUser adminUser) {
MultipartFile avatar = dto.getAvatar();
Long userId = dto.getId();
// 上传头像是否存在
if (avatar == null) return;
// 上传头像
FileUploadDto uploadDto = FileUploadDto.builder().file(avatar).type(MinioConstant.avatar).build();
FileInfoVo fileInfoVo = filesService.upload(uploadDto);
// 更新用户
adminUser.setId(userId);
adminUser.setAvatar(fileInfoVo.getFilepath());
}
}