feat: 优化多语言、角色、权限更新添加基本注释

更新多语言注释信息;更新角色和权限改用事件驱动和并行流加异步
This commit is contained in:
bunny 2025-05-01 13:44:10 +08:00
parent 0570ddd249
commit 0e84f0934e
19 changed files with 324 additions and 169 deletions

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

@ -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,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,4 +1,4 @@
package cn.bunny.services.utils.email;
package cn.bunny.services.service.configuration.helper.email;
import cn.bunny.services.domain.common.model.dto.email.EmailSend;
import cn.bunny.services.domain.common.model.dto.email.EmailSendInit;

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,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

@ -13,9 +13,9 @@ 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 cn.bunny.services.utils.system.RoleUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@ -94,7 +94,7 @@ public class UserLoginHelper {
// 判断是否是 admin 如果是admin 赋予所有权限
List<String> permissions = new ArrayList<>();
boolean isAdmin = RoleUtil.checkAdmin(roles, permissions, user);
boolean isAdmin = RoleHelper.checkAdmin(roles, permissions, user);
if (!isAdmin) {
permissions = permissionMapper.selectListByUserId(userId).stream()
.map(Permission::getPowerCode)

View File

@ -1,4 +1,4 @@
package cn.bunny.services.utils.system;
package cn.bunny.services.service.system.helper.role;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
@ -6,7 +6,6 @@ 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 com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@ -14,7 +13,7 @@ import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RoleUtil {
public class RoleHelper {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@ -73,36 +72,53 @@ public class RoleUtil {
*/
public static boolean checkAdmin(List<String> roleList) {
// 可以放行的权限
List<String> permitAllList = SecurityConfigConstant.PERMIT_ALL_LIST;
List<String> permissionList = SecurityConfigConstant.PERMIT_ACCESS_LIST;
// 判断是否是超级管理员
if (BaseContext.getUserId().equals(1L)) return true;
// 判断是否是 admin
return roleList.stream().anyMatch(permitAllList::contains);
return roleList.stream().anyMatch(permissionList::contains);
}
/**
* 批量更新Redis中用户信息
* 批量更新Redis中用户权限信息
*
* @param userIds 用户Id列表
* <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;
// 根据Id查找所有用户
List<AdminUser> adminUsers = userMapper.selectList(Wrappers.<AdminUser>lambdaQuery().in(AdminUser::getId, userIds));
// 批量查询用户
List<AdminUser> adminUsers = userMapper.selectBatchIds(userIds);
// 用户为空时不更新Redis的key
if (adminUsers.isEmpty()) return;
// 并行处理用户更新
adminUsers.parallelStream()
.filter(user -> redisTemplate.hasKey(RedisUserConstant.getAdminLoginInfoPrefix(user.getUsername())))
.forEach(user -> {
// 策略1: 更新用户权限信息
userloginHelper.buildLoginUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
// 更新Redis中用户信息
adminUsers.stream()
.filter(user -> {
String adminLoginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(user.getUsername());
Object object = redisTemplate.opsForValue().get(adminLoginInfoPrefix);
return object != null;
})
.forEach(user -> 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

@ -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;
/**
* * 角色 服务实现类
@ -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()));
}

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

@ -15,6 +15,7 @@ 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;
@ -23,7 +24,6 @@ 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.bunny.services.utils.email.ConcreteSenderEmailTemplate;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -103,9 +103,25 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, AdminUser> imp
}
/**
* 登录发送邮件验证码
* 发送登录邮件验证码
*
* @param email 邮箱
* <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) {
@ -131,7 +147,8 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, AdminUser> imp
concreteSenderEmailTemplate.sendEmailTemplate(email, emailTemplate, hashMap);
// 在Redis中存储验证码
redisTemplate.opsForValue().set(RedisUserConstant.getAdminUserEmailCodePrefix(email), emailCode, RedisUserConstant.REDIS_EXPIRATION_TIME, TimeUnit.MINUTES);
String emailCodePrefix = RedisUserConstant.getAdminUserEmailCodePrefix(email);
redisTemplate.opsForValue().set(emailCodePrefix, emailCode, RedisUserConstant.REDIS_EXPIRATION_TIME, TimeUnit.MINUTES);
}
/**

View File

@ -102,9 +102,28 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
}
/**
* 强制退出
* 管理员强制用户下线
*
* @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) {
@ -117,6 +136,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 将用户登录保存在用户登录日志表中
UserLoginLog userLoginLog = new UserLoginLog();
BeanUtils.copyProperties(adminUser, userLoginLog);
userLoginLog.setId(null);
userLoginLog.setUserId(adminUser.getId());
userLoginLog.setType(UserConstant.FORCE_LOGOUT);
userLoginLogMapper.insert(userLoginLog);