!548 Redis 缓存替代本地缓存,降低学习成本

Merge pull request !548 from 芋道源码/master-redis
This commit is contained in:
芋道源码 2023-07-29 01:34:24 +00:00 committed by Gitee
commit 7a49e45b4b
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
121 changed files with 2012 additions and 3770 deletions

View File

@ -1,47 +0,0 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import java.time.Duration;
/**
* 多租户拓展的 RedisKeyDefine 实现类
*
* 由于 Redis 不同于 MySQL column 字段无法通过类似 WHERE tenant_id = ? 的方式过滤
* 所以需要通过在 Redis Key 上增加后缀的方式进行租户之间的隔离具体的步骤是
* 1. 假设 Redis Key user:%d示例是 user:1对应到多租户的 Redis Key user:%d:%d
* 2. Redis DAO 需要使用 {@link #formatKey(Object...)} 方法进行 Redis Key 的格式化
*
* 注意大多数情况下并不用使用 TenantRedisKeyDefine 实现主要的使用场景还是 Redis Key 可能存在冲突的情况
* 例如说租户 1 2 都有一个手机号作为 Key则他们会存在冲突的问题
*
* @author 芋道源码
*/
public class TenantRedisKeyDefine extends RedisKeyDefine {
/**
* 多租户的 KEY 模板
*/
private static final String KEY_TEMPLATE_SUFFIX = ":%d";
public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout);
}
public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType);
}
private static String buildKeyTemplate(String keyTemplate) {
return keyTemplate + KEY_TEMPLATE_SUFFIX;
}
@Override
public String formatKey(Object... args) {
args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId());
return super.formatKey(args);
}
}

View File

@ -1,27 +0,0 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TenantRedisKeyDefineTest {
@Test
public void testFormatKey() {
Long tenantId = 30L;
TenantContextHolder.setTenantId(tenantId);
// 准备参数
TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH,
Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED);
Long userId = 10L;
Integer userType = 1;
// 调用
String key = define.formatKey(userId, userType);
// 断言
assertEquals("user:10:1:30", key);
}
}

View File

@ -1,7 +1,5 @@
package cn.iocoder.yudao.framework.captcha.config;
import cn.hutool.core.util.ClassUtil;
import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants;
import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl;
import com.xingyuv.captcha.properties.AjCaptchaProperties;
import com.xingyuv.captcha.service.CaptchaCacheService;
@ -15,12 +13,6 @@ import javax.annotation.Resource;
@AutoConfiguration
public class YudaoCaptchaConfiguration {
static {
// 手动加载 Lock4jRedisKeyConstants 因为它不会被使用到
// 如果不加载会导致 Redis 监控看到它的 Redis Key 枚举
ClassUtil.loadClass(CaptchaRedisKeyConstants.class.getName());
}
@Resource
private StringRedisTemplate stringRedisTemplate;

View File

@ -1,12 +1,5 @@
package cn.iocoder.yudao.framework.captcha.core.enums;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import com.xingyuv.captcha.model.vo.PointVO;
import java.time.Duration;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
/**
* 验证码 Redis Key 枚举类
*
@ -14,12 +7,22 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
*/
public interface CaptchaRedisKeyConstants {
RedisKeyDefine AJ_CAPTCHA_REQ_LIMIT = new RedisKeyDefine("验证码的请求限流",
"AJ.CAPTCHA.REQ.LIMIT-%s-%s",
STRING, Integer.class, Duration.ofSeconds(60)); // 例如说验证失败 5 get 接口锁定
/**
* 验证码的请求限流
*
* KEY 格式AJ.CAPTCHA.REQ.LIMIT-%s-%s
* VALUE 数据类型String // 例如说验证失败 5 get 接口锁定
* 过期时间60
*/
String AJ_CAPTCHA_REQ_LIMIT = "AJ.CAPTCHA.REQ.LIMIT-%s-%s";
RedisKeyDefine AJ_CAPTCHA_RUNNING = new RedisKeyDefine("验证码的坐标",
"RUNNING:CAPTCHA:%s", // AbstractCaptchaService.REDIS_CAPTCHA_KEY
STRING, PointVO.class, Duration.ofSeconds(120)); // {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5}
/**
* 验证码的坐标
*
* KEY 格式RUNNING:CAPTCHA:%s // AbstractCaptchaService.REDIS_CAPTCHA_KEY
* VALUE 数据类型String // PointVO.class {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5}
* 过期时间120
*/
String AJ_CAPTCHA_RUNNING = "RUNNING:CAPTCHA:%s";
}

View File

@ -106,6 +106,7 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
default void updateBatch(Collection<T> entities) {
Db.updateBatchById(entities);
}
default void updateBatch(Collection<T> entities, int size) {
Db.updateBatchById(entities, size);
}

View File

@ -1,13 +1,10 @@
package cn.iocoder.yudao.framework.idempotent.core.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
/**
* 幂等 Redis DAO
*
@ -16,9 +13,14 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
@AllArgsConstructor
public class IdempotentRedisDAO {
private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作",
"idempotent:%s", // 参数为 uuid
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
/**
* 幂等操作
*
* KEY 格式idempotent:%s // 参数为 uuid
* VALUE 格式String
* 过期时间不固定
*/
private static final String IDEMPOTENT = "idempotent:%s";
private final StringRedisTemplate redisTemplate;
@ -28,7 +30,7 @@ public class IdempotentRedisDAO {
}
private static String formatKey(String key) {
return String.format(IDEMPOTENT.getKeyTemplate(), key);
return String.format(IDEMPOTENT, key);
}
}

View File

@ -1,21 +1,13 @@
package cn.iocoder.yudao.framework.lock4j.config;
import cn.hutool.core.util.ClassUtil;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import cn.iocoder.yudao.framework.lock4j.core.DefaultLockFailureStrategy;
import cn.iocoder.yudao.framework.lock4j.core.Lock4jRedisKeyConstants;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration(before = LockAutoConfiguration.class)
public class YudaoLock4jConfiguration {
static {
// 手动加载 Lock4jRedisKeyConstants 因为它不会被使用到
// 如果不加载会导致 Redis 监控看到它的 Redis Key 枚举
ClassUtil.loadClass(Lock4jRedisKeyConstants.class.getName());
}
@Bean
public DefaultLockFailureStrategy lockFailureStrategy() {
return new DefaultLockFailureStrategy();

View File

@ -1,10 +1,5 @@
package cn.iocoder.yudao.framework.lock4j.core;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import org.redisson.api.RLock;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
/**
* Lock4j Redis Key 枚举类
*
@ -12,8 +7,13 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.H
*/
public interface Lock4jRedisKeyConstants {
RedisKeyDefine LOCK4J = new RedisKeyDefine("分布式锁",
"lock4j:%s", // 参数来自 DefaultLockKeyBuilder
HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson Lock 使用 Hash 数据结构
/**
* 分布式锁
*
* KEY 格式lock4j:%s // 参数来自 DefaultLockKeyBuilder
* VALUE 数据格式HASH // RLock.classRedisson Lock 使用 Hash 数据结构
* 过期时间不固定
*/
String LOCK4J = "lock4j:%s";
}

View File

@ -37,6 +37,11 @@
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.framework.redis.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -7,8 +9,15 @@ import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.util.Objects;
import static cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration.buildRedisSerializer;
/**
* Cache 配置类基于 Redis 实现
@ -20,15 +29,19 @@ public class YudaoCacheAutoConfiguration {
/**
* RedisCacheConfiguration Bean
*
* <p>
* 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration createConfiguration 方法
*/
@Bean
@Primary
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
// 设置使用 JSON 序列化方式
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
// 设置使用 : 单冒号而不是双 :: 冒号避免 Redis Desktop Manager 多余空格
// 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客
config = config.computePrefixWith(cacheName -> cacheName + StrUtil.COLON);
// 设置使用 JSON 序列化方式
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer()));
// 设置 CacheProperties.Redis 的属性
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
@ -47,4 +60,14 @@ public class YudaoCacheAutoConfiguration {
return config;
}
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration) {
// 创建 RedisCacheWriter 对象
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
// 创建 TenantRedisCacheManager 对象
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
}
}

View File

@ -1,5 +1,8 @@
package cn.iocoder.yudao.framework.redis.config;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@ -25,9 +28,17 @@ public class YudaoRedisAutoConfiguration {
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式库是 Jackson 序列化 VALUE
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.setValueSerializer(buildRedisSerializer());
template.setHashValueSerializer(buildRedisSerializer());
return template;
}
public static RedisSerializer<?> buildRedisSerializer() {
RedisSerializer<Object> json = RedisSerializer.json();
// 解决 LocalDateTime 的序列化
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
objectMapper.registerModules(new JavaTimeModule());
return json;
}
}

View File

@ -1,113 +0,0 @@
package cn.iocoder.yudao.framework.redis.core;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.time.Duration;
/**
* Redis Key 定义类
*
* @author 芋道源码
*/
@Data
public class RedisKeyDefine {
@Getter
@AllArgsConstructor
public enum KeyTypeEnum {
STRING("String"),
LIST("List"),
HASH("Hash"),
SET("Set"),
ZSET("Sorted Set"),
STREAM("Stream"),
PUBSUB("Pub/Sub");
/**
* 类型
*/
@JsonValue
private final String type;
}
@Getter
@AllArgsConstructor
public enum TimeoutTypeEnum {
FOREVER(1), // 永不超时
DYNAMIC(2), // 动态超时
FIXED(3); // 固定超时
/**
* 类型
*/
@JsonValue
private final Integer type;
}
/**
* Key 模板
*/
private final String keyTemplate;
/**
* Key 类型的枚举
*/
private final KeyTypeEnum keyType;
/**
* Value 类型
*
* 如果是使用分布式锁设置为 {@link java.util.concurrent.locks.Lock} 类型
*/
private final Class<?> valueType;
/**
* 超时类型
*/
private final TimeoutTypeEnum timeoutType;
/**
* 过期时间
*/
private final Duration timeout;
/**
* 备注
*/
private final String memo;
private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType,
TimeoutTypeEnum timeoutType, Duration timeout) {
this.memo = memo;
this.keyTemplate = keyTemplate;
this.keyType = keyType;
this.valueType = valueType;
this.timeout = timeout;
this.timeoutType = timeoutType;
// 添加注册表
RedisKeyRegistry.add(this);
}
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout);
}
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
}
/**
* 格式化 Key
*
* 注意内部采用 {@link String#format(String, Object...)} 实现
*
* @param args 格式化的参数
* @return Key
*/
public String formatKey(Object... args) {
return String.format(keyTemplate, args);
}
}

View File

@ -1,28 +0,0 @@
package cn.iocoder.yudao.framework.redis.core;
import java.util.ArrayList;
import java.util.List;
/**
* {@link RedisKeyDefine} 注册表
*/
public class RedisKeyRegistry {
/**
* Redis RedisKeyDefine 数组
*/
private static final List<RedisKeyDefine> DEFINES = new ArrayList<>();
public static void add(RedisKeyDefine define) {
DEFINES.add(define);
}
public static List<RedisKeyDefine> list() {
return DEFINES;
}
public static int size() {
return DEFINES.size();
}
}

View File

@ -0,0 +1,51 @@
package cn.iocoder.yudao.framework.redis.core;
import cn.hutool.core.util.StrUtil;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
/**
* 支持自定义过期时间的 {@link RedisCacheManager} 实现类
*
* {@link Cacheable#cacheNames()} 格式为 "key#ttl" # 后面的 ttl 为过期时间单位为秒
*
* @author 芋道源码
*/
public class TimeoutRedisCacheManager extends RedisCacheManager {
private static final String SPLIT = "#";
public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
if (StrUtil.isEmpty(name)) {
return super.createRedisCache(name, cacheConfig);
}
// 如果使用 # 分隔大小不为 2则说明不使用自定义过期时间
String[] names = StrUtil.splitToArray(name, SPLIT);
if (names.length != 2) {
return super.createRedisCache(name, cacheConfig);
}
// 核心通过修改 cacheConfig 的过期时间实现自定义过期时间
if (cacheConfig != null) {
// 移除 # 后面的 : 以及后面的内容避免影响解析
names[1] = StrUtil.subBefore(names[1], StrUtil.COLON, false);
// 解析时间
Duration duration = DurationStyle.detectAndParse(names[1], ChronoUnit.SECONDS);
cacheConfig = cacheConfig.entryTtl(duration);
}
return super.createRedisCache(names[0], cacheConfig);
}
}

View File

@ -2,8 +2,3 @@
GET {{baseUrl}}/infra/redis/get-monitor-info
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
### 请求 /infra/redis/get-key-list 接口 => 成功
GET {{baseUrl}}/infra/redis/get-key-list
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}

View File

@ -1,27 +1,20 @@
package cn.iocoder.yudao.module.infra.controller.admin.redis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.framework.redis.core.RedisKeyRegistry;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyDefineRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyValueRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO;
import cn.iocoder.yudao.module.infra.convert.redis.RedisConvert;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.*;
import java.util.Properties;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -47,66 +40,4 @@ public class RedisController {
return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats));
}
@GetMapping("/get-key-define-list")
@Operation(summary = "获得 Redis Key 模板列表")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<List<RedisKeyDefineRespVO>> getKeyDefineList() {
List<RedisKeyDefine> keyDefines = RedisKeyRegistry.list();
return success(RedisConvert.INSTANCE.convertList(keyDefines));
}
@GetMapping("/get-key-list")
@Operation(summary = "获得 Redis keys 键名列表")
@Parameter(name = "keyTemplate", description = "Redis Key 定义", example = "true")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<Set<String>> getKeyDefineList(@RequestParam("keyTemplate") String keyTemplate) {
return success(getKeyDefineList0(keyTemplate));
}
private Set<String> getKeyDefineList0(String keyTemplate) {
// key 格式化
String key = StrUtil.replace(keyTemplate, "%[s|c|b|d|x|o|f|a|e|g]", parameter -> "*");
// scan 扫描 key
Set<String> keys = new LinkedHashSet<>();
stringRedisTemplate.execute((RedisCallback<Set<String>>) connection -> {
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(key).count(100).build())) {
cursor.forEachRemaining(value -> keys.add(StrUtil.utf8Str(value)));
} catch (Exception e) {
throw new RuntimeException(e);
}
return keys;
});
return keys;
}
@GetMapping("/get-key-value")
@Operation(summary = "获得 Redis key 内容")
@Parameter(name = "key", description = "Redis Key", example = "oauth2_access_token:233")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<RedisKeyValueRespVO> getKeyValue(@RequestParam("key") String key) {
String value = stringRedisTemplate.opsForValue().get(key);
return success(new RedisKeyValueRespVO(key, value));
}
@DeleteMapping("/delete-key")
@Operation(summary = "删除 Redis Key")
@Parameter(name = "key", description = "Redis Key", example = "oauth2_access_token:233")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<Boolean> deleteKey(@RequestParam("key") String key) {
stringRedisTemplate.delete(key);
return success(Boolean.TRUE);
}
@DeleteMapping("/delete-keys")
@Operation(summary = "删除 Redis Key 根据模板")
@Parameter(name = "keyTemplate", description = "Redis Key 定义", example = "true")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<Boolean> deleteKeys(@RequestParam("keyTemplate") String keyTemplate) {
Set<String> keys = getKeyDefineList0(keyTemplate);
if (CollUtil.isNotEmpty(keys)) {
stringRedisTemplate.delete(keys);
}
return success(Boolean.TRUE);
}
}

View File

@ -1,35 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.redis.vo;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.time.Duration;
@Schema(description = "管理后台 - Redis Key 信息 Response VO")
@Data
@Builder
@AllArgsConstructor
public class RedisKeyDefineRespVO {
@Schema(description = "Key 模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "login_user:%s")
private String keyTemplate;
@Schema(description = "Key 类型的枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "String")
private RedisKeyDefine.KeyTypeEnum keyType;
@Schema(description = "Value 类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "java.lang.String")
private Class<?> valueType;
@Schema(description = "超时类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private RedisKeyDefine.TimeoutTypeEnum timeoutType;
@Schema(description = "过期时间,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Duration timeout;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "啦啦啦啦~")
private String memo;
}

View File

@ -1,18 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.redis.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Schema(description = "管理后台 - 单个 Redis Key Value Response VO")
@Data
@AllArgsConstructor
public class RedisKeyValueRespVO {
@Schema(description = "c5f6990767804a928f4bb96ca249febf", requiredMode = Schema.RequiredMode.REQUIRED, example = "String")
private String key;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "String")
private String value;
}

View File

@ -1,14 +1,11 @@
package cn.iocoder.yudao.module.infra.convert.redis;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyDefineRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
@Mapper
@ -29,6 +26,4 @@ public interface RedisConvert {
return respVO;
}
List<RedisKeyDefineRespVO> convertList(List<RedisKeyDefine> list);
}

View File

@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
@Mapper
public interface FileConfigMapper extends BaseMapperX<FileConfigDO> {
@ -18,4 +21,7 @@ public interface FileConfigMapper extends BaseMapperX<FileConfigDO> {
.orderByDesc(FileConfigDO::getId));
}
@Select("SELECT COUNT(*) FROM infra_file_config WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime);
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.infra.mq.consumer.file;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage;
import cn.iocoder.yudao.module.infra.service.file.FileConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link FileConfigRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class FileConfigRefreshConsumer extends AbstractChannelMessageListener<FileConfigRefreshMessage> {
@Resource
private FileConfigService fileConfigService;
@Override
public void onMessage(FileConfigRefreshMessage message) {
log.info("[onMessage][收到 FileConfig 刷新消息]");
fileConfigService.initLocalCache();
}
}

View File

@ -1,4 +1,4 @@
/**
* 占位符避免缩进
* 消息队列的消费者
*/
package cn.iocoder.yudao.module.infra.mq.consumer;

View File

@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.infra.mq.message.file;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
/**
* 文件配置数据刷新 Message
*/
@Data
public class FileConfigRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "infra.file-config.refresh";
}
}

View File

@ -1,4 +1,4 @@
/**
* 占位符避免缩进
* 消息队列的消息
*/
package cn.iocoder.yudao.module.infra.mq.message;

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.infra.mq.producer.file;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 文件配置相关消息的 Producer
*/
@Component
public class FileConfigProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link FileConfigRefreshMessage} 消息
*/
public void sendFileConfigRefreshMessage() {
FileConfigRefreshMessage message = new FileConfigRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,4 +1,4 @@
/**
* 占位符避免缩进
* 消息队列的生产者
*/
package cn.iocoder.yudao.module.infra.mq.producer;

View File

@ -16,11 +16,6 @@ import javax.validation.Valid;
*/
public interface FileConfigService {
/**
* 初始化文件客户端
*/
void initLocalCache();
/**
* 创建文件配置
*

View File

@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.file.core.client.FileClient;
@ -15,20 +17,20 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigU
import cn.iocoder.yudao.module.infra.convert.file.FileConfigConvert;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper;
import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER;
@ -46,6 +48,12 @@ public class FileConfigServiceImpl implements FileConfigService {
@Resource
private FileClientFactory fileClientFactory;
/**
* 文件配置的缓存
*/
@Getter
private List<FileConfigDO> fileConfigCache;
/**
* Master FileClient 对象有且仅有一个 {@link FileConfigDO#getMaster()} 对应的
*/
@ -55,13 +63,9 @@ public class FileConfigServiceImpl implements FileConfigService {
@Resource
private FileConfigMapper fileConfigMapper;
@Resource
private FileConfigProducer fileConfigProducer;
@Resource
private Validator validator;
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
@ -76,6 +80,27 @@ public class FileConfigServiceImpl implements FileConfigService {
masterFileClient = fileClientFactory.getFileClient(config.getId());
}
});
this.fileConfigCache = configs;
}
/**
* 通过定时任务轮询刷新缓存
*
* 目的多节点部署时通过轮询通知所有节点进行刷新
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
// 情况一如果缓存里没有数据则直接刷新缓存
if (CollUtil.isEmpty(fileConfigCache)) {
initLocalCache();
return;
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = CollectionUtils.getMaxValue(fileConfigCache, FileConfigDO::getUpdateTime);
if (fileConfigMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
}
@Override
@ -85,9 +110,9 @@ public class FileConfigServiceImpl implements FileConfigService {
.setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig()))
.setMaster(false); // 默认非 master
fileConfigMapper.insert(fileConfig);
// 发送刷新配置的消息
fileConfigProducer.sendFileConfigRefreshMessage();
// 返回
// 刷新缓存
initLocalCache();
return fileConfig.getId();
}
@ -99,8 +124,9 @@ public class FileConfigServiceImpl implements FileConfigService {
FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO)
.setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig()));
fileConfigMapper.updateById(updateObj);
// 发送刷新配置的消息
fileConfigProducer.sendFileConfigRefreshMessage();
// 刷新缓存
initLocalCache();
}
@Override
@ -112,15 +138,9 @@ public class FileConfigServiceImpl implements FileConfigService {
fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false));
// 更新
fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true));
// 发送刷新配置的消息
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
fileConfigProducer.sendFileConfigRefreshMessage();
}
});
// 刷新缓存
initLocalCache();
}
private FileClientConfig parseClientConfig(Integer storage, Map<String, Object> config) {
@ -143,8 +163,9 @@ public class FileConfigServiceImpl implements FileConfigService {
}
// 删除
fileConfigMapper.deleteById(id);
// 发送刷新配置的消息
fileConfigProducer.sendFileConfigRefreshMessage();
// 刷新缓存
initLocalCache();
}
private FileConfigDO validateFileConfigExists(Long id) {

View File

@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigP
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper;
import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
@ -55,8 +54,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
@Resource
private FileConfigMapper fileConfigMapper;
@MockBean
private FileConfigProducer fileConfigProducer;
@MockBean
private Validator validator;
@MockBean
@ -81,6 +78,10 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
verify(fileClientFactory).createOrUpdateFileClient(eq(2L),
eq(configDO2.getStorage()), eq(configDO2.getConfig()));
assertSame(masterFileClient, fileConfigService.getMasterFileClient());
// 断言 fileConfigCache 缓存
assertEquals(2, fileConfigService.getFileConfigCache().size());
assertEquals(configDO1, fileConfigService.getFileConfigCache().get(0));
assertEquals(configDO2, fileConfigService.getFileConfigCache().get(1));
}
@Test
@ -101,8 +102,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
assertFalse(fileConfig.getMaster());
assertEquals("/yunai", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
assertEquals("https://www.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test
@ -126,8 +125,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
assertPojoEquals(reqVO, fileConfig, "config");
assertEquals("/yunai2", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
assertEquals("https://doc.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test
@ -152,8 +149,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
// 断言数据
assertTrue(fileConfigMapper.selectById(dbFileConfig.getId()).getMaster());
assertFalse(fileConfigMapper.selectById(masterFileConfig.getId()).getMaster());
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test
@ -174,8 +169,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
fileConfigService.deleteFileConfig(id);
// 校验数据不存在了
assertNull(fileConfigMapper.selectById(id));
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test

View File

@ -1,8 +1,5 @@
package cn.iocoder.yudao.module.pay.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import org.redisson.api.RLock;
/**
* 支付 Redis Key 枚举类
*
@ -10,9 +7,14 @@ import org.redisson.api.RLock;
*/
public interface RedisKeyConstants {
RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
"pay_notify:lock:%d", // 参数来自 DefaultLockKeyBuilder
RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson Lock 使用 Hash 数据结构
/**
* 通知任务的分布式锁
*
* KEY 格式pay_notify:lock:%d // 参数来自 DefaultLockKeyBuilder
* VALUE 数据格式HASH // RLock.classRedisson Lock 使用 Hash 数据结构
* 过期时间不固定
*/
String PAY_NOTIFY_LOCK = "pay_notify:lock:%d";
/**
* 支付序号的缓存

View File

@ -33,7 +33,7 @@ public class PayNotifyLockRedisDAO {
}
private static String formatKey(Long id) {
return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id);
return String.format(PAY_NOTIFY_LOCK, id);
}
}

View File

@ -21,7 +21,7 @@ public class PermissionApiImpl implements PermissionApi {
@Override
public Set<Long> getUserRoleIdListByRoleIds(Collection<Long> roleIds) {
return permissionService.getUserRoleIdListByRoleIds(roleIds);
return permissionService.getUserRoleIdListByRoleId(roleIds);
}
@Override

View File

@ -2,6 +2,7 @@
POST {{baseUrl}}/system/auth/login
Content-Type: application/json
tenant-id: {{adminTenentId}}
tag: Yunai.local
{
"username": "admin",

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.controller.admin.auth;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
@ -12,8 +11,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
import cn.iocoder.yudao.module.system.service.auth.AdminAuthService;
import cn.iocoder.yudao.module.system.service.permission.MenuService;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
@ -34,9 +33,9 @@ import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization;
import static java.util.Collections.singleton;
@Tag(name = "管理后台 - 认证")
@RestController
@ -52,6 +51,8 @@ public class AuthController {
@Resource
private RoleService roleService;
@Resource
private MenuService menuService;
@Resource
private PermissionService permissionService;
@Resource
private SocialUserService socialUserService;
@ -91,33 +92,24 @@ public class AuthController {
@GetMapping("/get-permission-info")
@Operation(summary = "获取登录用户的权限信息")
public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() {
// 获得用户信息
// 1.1 获得用户信息
AdminUserDO user = userService.getUser(getLoginUserId());
if (user == null) {
return null;
}
// 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus()));
List<RoleDO> roleList = roleService.getRoleListFromCache(roleIds);
// 获得菜单列表
List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(roleIds,
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()),
singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的
// 拼接结果返回
return success(AuthConvert.INSTANCE.convert(user, roleList, menuList));
}
@GetMapping("/list-menus")
@Operation(summary = "获得登录用户的菜单列表")
public CommonResult<List<AuthMenuRespVO>> getMenuList() {
// 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus()));
// 获得用户拥有的菜单列表
List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(roleIds,
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型
singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的
// 转换成 Tree 结构返回
return success(AuthConvert.INSTANCE.buildMenuTree(menuList));
// 1.2 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
List<RoleDO> roles = roleService.getRoleList(roleIds);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
// 1.3 获得菜单列表
Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
List<MenuDO> menuList = menuService.getMenuList(menuIds);
menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); // 移除禁用的菜单
// 2. 拼接结果返回
return success(AuthConvert.INSTANCE.convert(user, roles, menuList));
}
// ========== 短信登录相关 ==========

View File

@ -6,6 +6,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Set;
@Schema(description = "管理后台 - 登录用户的权限信息 Response VO额外包括用户信息和角色列表")
@ -24,6 +25,9 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "操作权限数组", requiredMode = Schema.RequiredMode.REQUIRED)
private Set<String> permissions;
@Schema(description = "菜单树", required = true)
private List<MenuVO> menus;
@Schema(description = "用户信息 VO")
@Data
@NoArgsConstructor
@ -42,4 +46,48 @@ public class AuthPermissionInfoRespVO {
}
@Schema(description = "管理后台 - 登录用户的菜单信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class MenuVO {
@Schema(description = "菜单名称", required = true, example = "芋道")
private Long id;
@Schema(description = "父菜单 ID", required = true, example = "1024")
private Long parentId;
@Schema(description = "菜单名称", required = true, example = "芋道")
private String name;
@Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post")
private String path;
@Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index")
private String component;
@Schema(description = "组件名", example = "SystemUser")
private String componentName;
@Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list")
private String icon;
@Schema(description = "是否可见", required = true, example = "false")
private Boolean visible;
@Schema(description = "是否缓存", required = true, example = "false")
private Boolean keepAlive;
@Schema(description = "是否总是显示", example = "false")
private Boolean alwaysShow;
/**
* 子路由
*/
private List<MenuVO> children;
}
}

View File

@ -37,10 +37,10 @@ public class PermissionController {
@Operation(summary = "获得角色拥有的菜单编号")
@Parameter(name = "roleId", description = "角色编号", required = true)
@GetMapping("/list-role-resources")
@GetMapping("/list-role-menus")
@PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')")
public CommonResult<Set<Long>> listRoleMenus(Long roleId) {
return success(permissionService.getRoleMenuIds(roleId));
public CommonResult<Set<Long>> getRoleMenuList(Long roleId) {
return success(permissionService.getRoleMenuListByRoleId(roleId));
}
@PostMapping("/assign-role-menu")

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import org.slf4j.LoggerFactory;
@ -27,14 +28,16 @@ public interface AuthConvert {
default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
return AuthPermissionInfoRespVO.builder()
.user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname())
.avatar(user.getAvatar()).build())
.user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).build())
.roles(convertSet(roleList, RoleDO::getCode))
// 权限标识信息
.permissions(convertSet(menuList, MenuDO::getPermission))
// 菜单树
.menus(buildMenuTree(menuList))
.build();
}
AuthMenuRespVO convertTreeNode(MenuDO menu);
AuthPermissionInfoRespVO.MenuVO convertTreeNode(MenuDO menu);
/**
* 将菜单列表构建成菜单树
@ -42,20 +45,23 @@ public interface AuthConvert {
* @param menuList 菜单列表
* @return 菜单树
*/
default List<AuthMenuRespVO> buildMenuTree(List<MenuDO> menuList) {
default List<AuthPermissionInfoRespVO.MenuVO> buildMenuTree(List<MenuDO> menuList) {
// 移除按钮
menuList.removeIf(menu -> menu.getType().equals(MenuTypeEnum.BUTTON.getType()));
// 排序保证菜单的有序性
menuList.sort(Comparator.comparing(MenuDO::getSort));
// 构建菜单树
// 使用 LinkedHashMap 的原因是为了排序 实际也可以用 Stream API 就是太丑了
Map<Long, AuthMenuRespVO> treeNodeMap = new LinkedHashMap<>();
Map<Long, AuthPermissionInfoRespVO.MenuVO> treeNodeMap = new LinkedHashMap<>();
menuList.forEach(menu -> treeNodeMap.put(menu.getId(), AuthConvert.INSTANCE.convertTreeNode(menu)));
// 处理父子关系
treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(ID_ROOT)).forEach(childNode -> {
// 获得父节点
AuthMenuRespVO parentNode = treeNodeMap.get(childNode.getParentId());
AuthPermissionInfoRespVO.MenuVO parentNode = treeNodeMap.get(childNode.getParentId());
if (parentNode == null) {
LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]",
childNode.getId(), childNode.getParentId());
childNode.getId(), childNode.getParentId());
return;
}
// 将自己添加到父节点中

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqV
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
@ -25,4 +26,8 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
return selectCount(DeptDO::getParentId, parentId);
}
default List<DeptDO> selectListByParentId(Collection<Long> parentIds) {
return selectList(DeptDO::getParentId, parentIds);
}
}

View File

@ -25,4 +25,7 @@ public interface MenuMapper extends BaseMapperX<MenuDO> {
.eqIfPresent(MenuDO::getStatus, reqVO.getStatus()));
}
default List<MenuDO> selectListByPermission(String permission) {
return selectList(MenuDO::getPermission, permission);
}
}

View File

@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.system.dal.mysql.permission;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List;
@ -13,14 +11,18 @@ import java.util.List;
@Mapper
public interface RoleMenuMapper extends BaseMapperX<RoleMenuDO> {
@Repository
class BatchInsertMapper extends ServiceImpl<RoleMenuMapper, RoleMenuDO> {
}
default List<RoleMenuDO> selectListByRoleId(Long roleId) {
return selectList(RoleMenuDO::getRoleId, roleId);
}
default List<RoleMenuDO> selectListByRoleId(Collection<Long> roleIds) {
return selectList(RoleMenuDO::getRoleId, roleIds);
}
default List<RoleMenuDO> selectListByMenuId(Long menuId) {
return selectList(RoleMenuDO::getMenuId, menuId);
}
default void deleteListByRoleIdAndMenuIds(Long roleId, Collection<Long> menuIds) {
delete(new LambdaQueryWrapper<RoleMenuDO>()
.eq(RoleMenuDO::getRoleId, roleId)

View File

@ -7,7 +7,9 @@ import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.Sensitiv
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
/**
@ -40,4 +42,7 @@ public interface SensitiveWordMapper extends BaseMapperX<SensitiveWordDO> {
return selectOne(SensitiveWordDO::getName, name);
}
@Select("SELECT COUNT(*) FROM system_sensitive_word WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxTime);
}

View File

@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
@Mapper
public interface SmsChannelMapper extends BaseMapperX<SmsChannelDO> {
@ -18,4 +21,7 @@ public interface SmsChannelMapper extends BaseMapperX<SmsChannelDO> {
.orderByDesc(SmsChannelDO::getId));
}
@Select("SELECT COUNT(*) FROM system_sms_channel WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxTime);
}

View File

@ -1,12 +1,7 @@
package cn.iocoder.yudao.module.system.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import java.time.Duration;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
/**
* System Redis Key 枚举类
*
@ -14,16 +9,93 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
*/
public interface RedisKeyConstants {
RedisKeyDefine CAPTCHA_CODE = new RedisKeyDefine("验证码的缓存",
"captcha_code:%s", // 参数为 uuid
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
/**
* 指定部门的所有子部门编号数组的缓存
* <p>
* KEY 格式dept_children_ids:{id}
* VALUE 数据类型String 子部门编号集合
*/
String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
RedisKeyDefine OAUTH2_ACCESS_TOKEN = new RedisKeyDefine("访问令牌的缓存",
"oauth2_access_token:%s", // 参数为访问令牌 token
STRING, OAuth2AccessTokenDO.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
/**
* 角色的缓存
* <p>
* KEY 格式role:{id}
* VALUE 数据类型String 角色信息
*/
String ROLE = "role";
RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意它是被 JustAuth justauth.type.prefix 使用到
"social_auth_state:%s", // 参数为 state
STRING, String.class, Duration.ofHours(24)); // 值为 state
/**
* 用户拥有的角色编号的缓存
* <p>
* KEY 格式user_role_ids:{userId}
* VALUE 数据类型String 角色编号集合
*/
String USER_ROLE_ID_LIST = "user_role_ids";
/**
* 拥有指定菜单的角色编号的缓存
* <p>
* KEY 格式user_role_ids:{menuId}
* VALUE 数据类型String 角色编号集合
*/
String MENU_ROLE_ID_LIST = "menu_role_ids";
/**
* 拥有权限对应的菜单编号数组的缓存
* <p>
* KEY 格式permission_menu_ids:{permission}
* VALUE 数据类型String 菜单编号数组
*/
String PERMISSION_MENU_ID_LIST = "permission_menu_ids";
/**
* OAuth2 客户端的缓存
* <p>
* KEY 格式user:{id}
* VALUE 数据类型String 客户端信息
*/
String OAUTH_CLIENT = "oauth_client";
/**
* 访问令牌的缓存
* <p>
* KEY 格式oauth2_access_token:{token}
* VALUE 数据类型String 访问令牌信息 {@link OAuth2AccessTokenDO}
* <p>
* 由于动态过期时间使用 RedisTemplate 操作
*/
String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s";
/**
* 站内信模版的缓存
* <p>
* KEY 格式notify_template:{code}
* VALUE 数据格式String 模版信息
*/
String NOTIFY_TEMPLATE = "notify_template";
/**
* 邮件账号的缓存
* <p>
* KEY 格式sms_template:{id}
* VALUE 数据格式String 账号信息
*/
String MAIL_ACCOUNT = "mail_account";
/**
* 邮件模版的缓存
* <p>
* KEY 格式mail_template:{code}
* VALUE 数据格式String 模版信息
*/
String MAIL_TEMPLATE = "mail_template";
/**
* 短信模版的缓存
* <p>
* KEY 格式sms_template:{id}
* VALUE 数据格式String 模版信息
*/
String SMS_TEMPLATE = "sms_template";
}

View File

@ -1,41 +0,0 @@
package cn.iocoder.yudao.module.system.dal.redis.common;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.Duration;
import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.CAPTCHA_CODE;
/**
* 验证码的 Redis DAO
*
* @author 芋道源码
*/
@Repository
public class CaptchaRedisDAO {
@Resource
private StringRedisTemplate stringRedisTemplate;
public String get(String uuid) {
String redisKey = formatKey(uuid);
return stringRedisTemplate.opsForValue().get(redisKey);
}
public void set(String uuid, String code, Duration timeout) {
String redisKey = formatKey(uuid);
stringRedisTemplate.opsForValue().set(redisKey, code, timeout);
}
public void delete(String uuid) {
String redisKey = formatKey(uuid);
stringRedisTemplate.delete(redisKey);
}
private static String formatKey(String uuid) {
return String.format(CAPTCHA_CODE.getKeyTemplate(), uuid);
}
}

View File

@ -53,7 +53,7 @@ public class OAuth2AccessTokenRedisDAO {
}
private static String formatKey(String accessToken) {
return String.format(OAUTH2_ACCESS_TOKEN.getKeyTemplate(), accessToken);
return String.format(OAUTH2_ACCESS_TOKEN, accessToken);
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.auth;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link OAuth2ClientRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class OAuth2ClientRefreshConsumer extends AbstractChannelMessageListener<OAuth2ClientRefreshMessage> {
@Resource
private OAuth2ClientService oauth2ClientService;
@Override
public void onMessage(OAuth2ClientRefreshMessage message) {
log.info("[onMessage][收到 OAuth2Client 刷新消息]");
oauth2ClientService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.dept;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link DeptRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class DeptRefreshConsumer extends AbstractChannelMessageListener<DeptRefreshMessage> {
@Resource
private DeptService deptService;
@Override
public void onMessage(DeptRefreshMessage message) {
log.info("[onMessage][收到 Dept 刷新消息]");
deptService.initLocalCache();
}
}

View File

@ -1,31 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.mail;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.mail.MailAccountRefreshMessage;
import cn.iocoder.yudao.module.system.mq.message.mail.MailTemplateRefreshMessage;
import cn.iocoder.yudao.module.system.service.mail.MailAccountService;
import cn.iocoder.yudao.module.system.service.mail.MailTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link MailAccountRefreshMessage} 的消费者
*
* @author wangjingyi
*/
@Component
@Slf4j
public class MailAccountRefreshConsumer extends AbstractChannelMessageListener<MailAccountRefreshMessage> {
@Resource
private MailAccountService mailAccountService;
@Override
public void onMessage(MailAccountRefreshMessage message) {
log.info("[onMessage][收到 Mail Account 刷新信息]");
mailAccountService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.mail;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.mail.MailTemplateRefreshMessage;
import cn.iocoder.yudao.module.system.service.mail.MailTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link MailTemplateRefreshMessage} 的消费者
*
* @author wangjingyi
*/
@Component
@Slf4j
public class MailTemplateRefreshConsumer extends AbstractChannelMessageListener<MailTemplateRefreshMessage> {
@Resource
private MailTemplateService mailTemplateService;
@Override
public void onMessage(MailTemplateRefreshMessage message) {
log.info("[onMessage][收到 Mail Template 刷新信息]");
mailTemplateService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.notify;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.notify.NotifyTemplateRefreshMessage;
import cn.iocoder.yudao.module.system.service.notify.NotifyTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link NotifyTemplateRefreshMessage} 的消费者
*
* @author xrcoder
*/
@Component
@Slf4j
public class NotifyTemplateRefreshConsumer extends AbstractChannelMessageListener<NotifyTemplateRefreshMessage> {
@Resource
private NotifyTemplateService notifyTemplateService;
@Override
public void onMessage(NotifyTemplateRefreshMessage message) {
log.info("[onMessage][收到 NotifyTemplate 刷新消息]");
notifyTemplateService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.MenuRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.MenuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link MenuRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class MenuRefreshConsumer extends AbstractChannelMessageListener<MenuRefreshMessage> {
@Resource
private MenuService menuService;
@Override
public void onMessage(MenuRefreshMessage message) {
log.info("[onMessage][收到 Menu 刷新消息]");
menuService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.RoleMenuRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link RoleMenuRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class RoleMenuRefreshConsumer extends AbstractChannelMessageListener<RoleMenuRefreshMessage> {
@Resource
private PermissionService permissionService;
@Override
public void onMessage(RoleMenuRefreshMessage message) {
log.info("[onMessage][收到 Role 与 Menu 的关联刷新消息]");
permissionService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.RoleRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link RoleRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class RoleRefreshConsumer extends AbstractChannelMessageListener<RoleRefreshMessage> {
@Resource
private RoleService roleService;
@Override
public void onMessage(RoleRefreshMessage message) {
log.info("[onMessage][收到 Role 刷新消息]");
roleService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.UserRoleRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link UserRoleRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class UserRoleRefreshConsumer extends AbstractChannelMessageListener<UserRoleRefreshMessage> {
@Resource
private PermissionService permissionService;
@Override
public void onMessage(UserRoleRefreshMessage message) {
log.info("[onMessage][收到 User 与 Role 的关联刷新消息]");
permissionService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.sensitiveword;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.sensitiveword.SensitiveWordRefreshMessage;
import cn.iocoder.yudao.module.system.service.sensitiveword.SensitiveWordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SensitiveWordRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SensitiveWordRefreshConsumer extends AbstractChannelMessageListener<SensitiveWordRefreshMessage> {
@Resource
private SensitiveWordService sensitiveWordService;
@Override
public void onMessage(SensitiveWordRefreshMessage message) {
log.info("[onMessage][收到 SensitiveWord 刷新消息]");
sensitiveWordService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.sms;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.sms.SmsChannelRefreshMessage;
import cn.iocoder.yudao.module.system.service.sms.SmsChannelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SmsChannelRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SmsChannelRefreshConsumer extends AbstractChannelMessageListener<SmsChannelRefreshMessage> {
@Resource
private SmsChannelService smsChannelService;
@Override
public void onMessage(SmsChannelRefreshMessage message) {
log.info("[onMessage][收到 SmsChannel 刷新消息]");
smsChannelService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.sms;
import cn.iocoder.yudao.module.system.mq.message.sms.SmsTemplateRefreshMessage;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.service.sms.SmsTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SmsTemplateRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SmsTemplateRefreshConsumer extends AbstractChannelMessageListener<SmsTemplateRefreshMessage> {
@Resource
private SmsTemplateService smsTemplateService;
@Override
public void onMessage(SmsTemplateRefreshMessage message) {
log.info("[onMessage][收到 SmsTemplate 刷新消息]");
smsTemplateService.initLocalCache();
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.auth;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* OAuth 2.0 客户端的数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OAuth2ClientRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.oauth2-client.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.dept;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 部门数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DeptRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.dept.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.mail;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 邮箱账号的数据刷新 Message
*
* @author wangjingyi
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MailAccountRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.mail-account.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.mail;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 邮箱模板的数据刷新 Message
*
* @author wangjingyi
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MailTemplateRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.mail-template.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.notify;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 站内信模板的数据刷新 Message
*
* @author xrcoder
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NotifyTemplateRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.notify-template.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 菜单数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MenuRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.menu.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 角色与菜单数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RoleMenuRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.role-menu.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 角色数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RoleRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.role.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户与角色的数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserRoleRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.user-role.refresh";
}
}

View File

@ -1,19 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.sensitiveword;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 敏感词的刷新 Message
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SensitiveWordRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.sensitive-word.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.sms;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 短信渠道的数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SmsChannelRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.sms-channel.refresh";
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.sms;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 短信模板的数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SmsTemplateRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.sms-template.refresh";
}
}

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.auth;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* OAuth 2.0 客户端相关消息的 Producer
*/
@Component
public class OAuth2ClientProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link OAuth2ClientRefreshMessage} 消息
*/
public void sendOAuth2ClientRefreshMessage() {
OAuth2ClientRefreshMessage message = new OAuth2ClientRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.dept;
import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Dept 部门相关消息的 Producer
*/
@Component
public class DeptProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link DeptRefreshMessage} 消息
*/
public void sendDeptRefreshMessage() {
DeptRefreshMessage message = new DeptRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.system.mq.producer.mail;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.mail.MailAccountRefreshMessage;
import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage;
import cn.iocoder.yudao.module.system.mq.message.mail.MailTemplateRefreshMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -22,22 +20,6 @@ public class MailProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link MailTemplateRefreshMessage} 消息
*/
public void sendMailTemplateRefreshMessage() {
MailTemplateRefreshMessage message = new MailTemplateRefreshMessage();
redisMQTemplate.send(message);
}
/**
* 发送 {@link MailAccountRefreshMessage} 消息
*/
public void sendMailAccountRefreshMessage() {
MailAccountRefreshMessage message = new MailAccountRefreshMessage();
redisMQTemplate.send(message);
}
/**
* 发送 {@link MailSendMessage} 消息
*

View File

@ -1,33 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.notify;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.notify.NotifyTemplateRefreshMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Notify 站内信相关消息的 Producer
*
* @author xrcoder
* @since 2022-08-06
*/
@Slf4j
@Component
public class NotifyProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link NotifyTemplateRefreshMessage} 消息
*/
public void sendNotifyTemplateRefreshMessage() {
NotifyTemplateRefreshMessage message = new NotifyTemplateRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,4 +0,0 @@
/**
* 占位
*/
package cn.iocoder.yudao.module.system.mq.producer;

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.permission;
import cn.iocoder.yudao.module.system.mq.message.permission.MenuRefreshMessage;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Menu 菜单相关消息的 Producer
*/
@Component
public class MenuProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link MenuRefreshMessage} 消息
*/
public void sendMenuRefreshMessage() {
MenuRefreshMessage message = new MenuRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,35 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.permission;
import cn.iocoder.yudao.module.system.mq.message.permission.RoleMenuRefreshMessage;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.permission.UserRoleRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Permission 权限相关消息的 Producer
*/
@Component
public class PermissionProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link RoleMenuRefreshMessage} 消息
*/
public void sendRoleMenuRefreshMessage() {
RoleMenuRefreshMessage message = new RoleMenuRefreshMessage();
redisMQTemplate.send(message);
}
/**
* 发送 {@link UserRoleRefreshMessage} 消息
*/
public void sendUserRoleRefreshMessage() {
UserRoleRefreshMessage message = new UserRoleRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,28 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.permission;
import cn.iocoder.yudao.module.system.mq.message.permission.RoleRefreshMessage;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Role 角色相关消息的 Producer
*
* @author 芋道源码
*/
@Component
public class RoleProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link RoleRefreshMessage} 消息
*/
public void sendRoleRefreshMessage() {
RoleRefreshMessage message = new RoleRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.sensitiveword;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.sensitiveword.SensitiveWordRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 敏感词相关的 Producer
*/
@Component
public class SensitiveWordProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link SensitiveWordRefreshMessage} 消息
*/
public void sendSensitiveWordRefreshMessage() {
SensitiveWordRefreshMessage message = new SensitiveWordRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,10 +1,8 @@
package cn.iocoder.yudao.module.system.mq.producer.sms;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.module.system.mq.message.sms.SmsChannelRefreshMessage;
import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage;
import cn.iocoder.yudao.module.system.mq.message.sms.SmsTemplateRefreshMessage;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -15,7 +13,7 @@ import java.util.List;
* Sms 短信相关消息的 Producer
*
* @author zzf
* @date 2021/3/9 16:35
* @since 2021/3/9 16:35
*/
@Slf4j
@Component
@ -24,22 +22,6 @@ public class SmsProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link SmsChannelRefreshMessage} 消息
*/
public void sendSmsChannelRefreshMessage() {
SmsChannelRefreshMessage message = new SmsChannelRefreshMessage();
redisMQTemplate.send(message);
}
/**
* 发送 {@link SmsTemplateRefreshMessage} 消息
*/
public void sendSmsTemplateRefreshMessage() {
SmsTemplateRefreshMessage message = new SmsTemplateRefreshMessage();
redisMQTemplate.send(message);
}
/**
* 发送 {@link SmsSendMessage} 消息
*

View File

@ -7,10 +7,7 @@ import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqV
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
/**
* 部门 Service 接口
@ -19,11 +16,6 @@ import java.util.Map;
*/
public interface DeptService {
/**
* 初始化部门的本地缓存
*/
void initLocalCache();
/**
* 创建部门
*
@ -47,21 +39,12 @@ public interface DeptService {
void deleteDept(Long id);
/**
* 筛选部门列表
* 获得部门信息
*
* @param reqVO 筛选条件请求 VO
* @return 部门列表
* @param id 部门编号
* @return 部门信息
*/
List<DeptDO> getDeptList(DeptListReqVO reqVO);
/**
* 获得所有子部门从缓存中
*
* @param parentId 部门编号
* @param recursive 是否递归获取所有
* @return 子部门列表
*/
List<DeptDO> getDeptListByParentIdFromCache(Long parentId, boolean recursive);
DeptDO getDept(Long id);
/**
* 获得部门信息数组
@ -71,6 +54,14 @@ public interface DeptService {
*/
List<DeptDO> getDeptList(Collection<Long> ids);
/**
* 筛选部门列表
*
* @param reqVO 筛选条件请求 VO
* @return 部门列表
*/
List<DeptDO> getDeptList(DeptListReqVO reqVO);
/**
* 获得指定编号的部门 Map
*
@ -86,12 +77,20 @@ public interface DeptService {
}
/**
* 获得部门信息
* 获得指定部门的所有子部门
*
* @param id 部门编号
* @return 部门信息
* @return 子部门列表
*/
DeptDO getDept(Long id);
List<DeptDO> getChildDeptList(Long id);
/**
* 获得所有子部门从缓存中
*
* @param id 父部门编号
* @return 子部门列表
*/
Set<Long> getChildDeptIdListFromCache(Long id);
/**
* 校验部门们是否有效如下情况视为无效

View File

@ -1,30 +1,29 @@
package cn.iocoder.yudao.module.system.service.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.dept.DeptConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.system.enums.dept.DeptIdEnum;
import cn.iocoder.yudao.module.system.mq.producer.dept.DeptProducer;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import lombok.Getter;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
/**
@ -37,55 +36,12 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@Slf4j
public class DeptServiceImpl implements DeptService {
/**
* 部门缓存
* key部门编号 {@link DeptDO#getId()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Map<Long, DeptDO> deptCache;
/**
* 父部门缓存
* key部门编号 {@link DeptDO#getParentId()}
* value: 直接子部门列表
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Multimap<Long, DeptDO> parentDeptCache;
@Resource
private DeptMapper deptMapper;
@Resource
private DeptProducer deptProducer;
/**
* 初始化 {@link #parentDeptCache} {@link #deptCache} 缓存
*/
@Override
@PostConstruct
public synchronized void initLocalCache() {
// 注意忽略自动多租户因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 第一步查询数据
List<DeptDO> depts = deptMapper.selectList();
log.info("[initLocalCache][缓存部门,数量为:{}]", depts.size());
// 第二步构建缓存
ImmutableMap.Builder<Long, DeptDO> builder = ImmutableMap.builder();
ImmutableMultimap.Builder<Long, DeptDO> parentBuilder = ImmutableMultimap.builder();
depts.forEach(deptDO -> {
builder.put(deptDO.getId(), deptDO);
parentBuilder.put(deptDO.getParentId(), deptDO);
});
deptCache = builder.build();
parentDeptCache = parentBuilder.build();
});
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存因为操作一个部门涉及到多个缓存
public Long createDept(DeptCreateReqVO reqVO) {
// 校验正确性
if (reqVO.getParentId() == null) {
@ -95,12 +51,12 @@ public class DeptServiceImpl implements DeptService {
// 插入部门
DeptDO dept = DeptConvert.INSTANCE.convert(reqVO);
deptMapper.insert(dept);
// 发送刷新消息
deptProducer.sendDeptRefreshMessage();
return dept.getId();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存因为操作一个部门涉及到多个缓存
public void updateDept(DeptUpdateReqVO reqVO) {
// 校验正确性
if (reqVO.getParentId() == null) {
@ -110,11 +66,11 @@ public class DeptServiceImpl implements DeptService {
// 更新部门
DeptDO updateObj = DeptConvert.INSTANCE.convert(reqVO);
deptMapper.updateById(updateObj);
// 发送刷新消息
deptProducer.sendDeptRefreshMessage();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存因为操作一个部门涉及到多个缓存
public void deleteDept(Long id) {
// 校验是否存在
validateDeptExists(id);
@ -124,70 +80,30 @@ public class DeptServiceImpl implements DeptService {
}
// 删除部门
deptMapper.deleteById(id);
// 发送刷新消息
deptProducer.sendDeptRefreshMessage();
}
@Override
public List<DeptDO> getDeptList(DeptListReqVO reqVO) {
return deptMapper.selectList(reqVO);
}
@Override
public List<DeptDO> getDeptListByParentIdFromCache(Long parentId, boolean recursive) {
if (parentId == null) {
return Collections.emptyList();
}
List<DeptDO> result = new ArrayList<>();
// 递归简单粗暴
getDeptsByParentIdFromCache(result, parentId,
recursive ? Integer.MAX_VALUE : 1, // 如果递归获取则无限否则只递归 1
parentDeptCache);
return result;
}
/**
* 递归获取所有的子部门添加到 result 结果
*
* @param result 结果
* @param parentId 父编号
* @param recursiveCount 递归次数
* @param parentDeptMap 父部门 Map使用缓存避免变化
*/
private void getDeptsByParentIdFromCache(List<DeptDO> result, Long parentId, int recursiveCount,
Multimap<Long, DeptDO> parentDeptMap) {
// 递归次数为 0结束
if (recursiveCount == 0) {
return;
}
// 获得子部门
Collection<DeptDO> depts = parentDeptMap.get(parentId);
if (CollUtil.isEmpty(depts)) {
return;
}
// 针对多租户过滤掉非当前租户的部门
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId != null) {
depts = CollUtil.filterNew(depts, dept -> tenantId.equals(dept.getTenantId()));
}
result.addAll(depts);
// 继续递归
depts.forEach(dept -> getDeptsByParentIdFromCache(result, dept.getId(),
recursiveCount - 1, parentDeptMap));
}
private void validateForCreateOrUpdate(Long id, Long parentId, String name) {
// 校验自己存在
validateDeptExists(id);
// 校验父部门的有效性
validateParentDeptEnable(id, parentId);
validateParentDept(id, parentId);
// 校验部门名的唯一性
validateDeptNameUnique(id, parentId, name);
}
private void validateParentDeptEnable(Long id, Long parentId) {
@VisibleForTesting
void validateDeptExists(Long id) {
if (id == null) {
return;
}
DeptDO dept = deptMapper.selectById(id);
if (dept == null) {
throw exception(DEPT_NOT_FOUND);
}
}
@VisibleForTesting
void validateParentDept(Long id, Long parentId) {
if (parentId == null || DeptIdEnum.ROOT.getId().equals(parentId)) {
return;
}
@ -200,49 +116,71 @@ public class DeptServiceImpl implements DeptService {
if (dept == null) {
throw exception(DEPT_PARENT_NOT_EXITS);
}
// 父部门被禁用
if (!CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) {
throw exception(DEPT_NOT_ENABLE);
}
// 父部门不能是原来的子部门
List<DeptDO> children = getDeptListByParentIdFromCache(id, true);
List<DeptDO> children = getChildDeptList(id);
if (children.stream().anyMatch(dept1 -> dept1.getId().equals(parentId))) {
throw exception(DEPT_PARENT_IS_CHILD);
}
}
private void validateDeptExists(Long id) {
if (id == null) {
return;
}
DeptDO dept = deptMapper.selectById(id);
@VisibleForTesting
void validateDeptNameUnique(Long id, Long parentId, String name) {
DeptDO dept = deptMapper.selectByParentIdAndName(parentId, name);
if (dept == null) {
throw exception(DEPT_NOT_FOUND);
}
}
private void validateDeptNameUnique(Long id, Long parentId, String name) {
DeptDO menu = deptMapper.selectByParentIdAndName(parentId, name);
if (menu == null) {
return;
}
// 如果 id 为空说明不用比较是否为相同 id 的岗位
if (id == null) {
throw exception(DEPT_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) {
if (ObjectUtil.notEqual(dept.getId(), id)) {
throw exception(DEPT_NAME_DUPLICATE);
}
}
@Override
public DeptDO getDept(Long id) {
return deptMapper.selectById(id);
}
@Override
public List<DeptDO> getDeptList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return deptMapper.selectBatchIds(ids);
}
@Override
public DeptDO getDept(Long id) {
return deptMapper.selectById(id);
public List<DeptDO> getDeptList(DeptListReqVO reqVO) {
return deptMapper.selectList(reqVO);
}
@Override
public List<DeptDO> getChildDeptList(Long id) {
List<DeptDO> children = new LinkedList<>();
// 遍历每一层
Collection<Long> parentIds = Collections.singleton(id);
for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下存在死循环
// 查询当前层所有的子部门
List<DeptDO> depts = deptMapper.selectListByParentId(parentIds);
// 1. 如果没有子部门则结束遍历
if (CollUtil.isEmpty(depts)) {
break;
}
// 2. 如果有子部门继续遍历
children.addAll(depts);
parentIds = convertSet(depts, DeptDO::getId);
}
return children;
}
@Override
@DataPermission(enable = false) // 禁用数据权限避免简历不正确的缓存
@Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, key = "#id")
public Set<Long> getChildDeptIdListFromCache(Long id) {
List<DeptDO> children = getChildDeptList(id);
return convertSet(children, DeptDO::getId);
}
@Override

View File

@ -17,19 +17,6 @@ import java.util.List;
*/
public interface MailAccountService {
/**
* 初始化邮箱账号的本地缓存
*/
void initLocalCache();
/**
* 从缓存中获取邮箱账号
*
* @param id 编号
* @return 邮箱账号
*/
MailAccountDO getMailAccountFromCache(Long id);
/**
* 创建邮箱账号
*
@ -60,6 +47,14 @@ public interface MailAccountService {
*/
MailAccountDO getMailAccount(Long id);
/**
* 从缓存中获取邮箱账号
*
* @param id 编号
* @return 邮箱账号
*/
MailAccountDO getMailAccountFromCache(Long id);
/**
* 获取邮箱账号分页信息
*

View File

@ -7,20 +7,18 @@ import cn.iocoder.yudao.module.system.controller.admin.mail.vo.account.MailAccou
import cn.iocoder.yudao.module.system.convert.mail.MailAccountConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO;
import cn.iocoder.yudao.module.system.dal.mysql.mail.MailAccountMapper;
import cn.iocoder.yudao.module.system.mq.producer.mail.MailProducer;
import lombok.Getter;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_NOT_EXISTS;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS;
/**
* 邮箱账号 Service 实现类
@ -39,46 +37,16 @@ public class MailAccountServiceImpl implements MailAccountService {
@Resource
private MailTemplateService mailTemplateService;
@Resource
private MailProducer mailProducer;
/**
* 邮箱账号缓存
* key邮箱账号编码 {@link MailAccountDO#getId()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Map<Long, MailAccountDO> mailAccountCache;
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
List<MailAccountDO> accounts = mailAccountMapper.selectList();
log.info("[initLocalCache][缓存邮箱账号,数量:{}]", accounts.size());
// 第二步构建缓存
mailAccountCache = convertMap(accounts, MailAccountDO::getId);
}
@Override
public MailAccountDO getMailAccountFromCache(Long id) {
return mailAccountCache.get(id);
}
@Override
public Long createMailAccount(MailAccountCreateReqVO createReqVO) {
// 插入
MailAccountDO account = MailAccountConvert.INSTANCE.convert(createReqVO);
mailAccountMapper.insert(account);
// 发送刷新消息
mailProducer.sendMailAccountRefreshMessage();
return account.getId();
}
@Override
@Cacheable(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#updateReqVO.id")
public void updateMailAccount(MailAccountUpdateReqVO updateReqVO) {
// 校验是否存在
validateMailAccountExists(updateReqVO.getId());
@ -86,11 +54,10 @@ public class MailAccountServiceImpl implements MailAccountService {
// 更新
MailAccountDO updateObj = MailAccountConvert.INSTANCE.convert(updateReqVO);
mailAccountMapper.updateById(updateObj);
// 发送刷新消息
mailProducer.sendMailAccountRefreshMessage();
}
@Override
@Cacheable(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id")
public void deleteMailAccount(Long id) {
// 校验是否存在账号
validateMailAccountExists(id);
@ -101,8 +68,6 @@ public class MailAccountServiceImpl implements MailAccountService {
// 删除
mailAccountMapper.deleteById(id);
// 发送刷新消息
mailProducer.sendMailAccountRefreshMessage();
}
private void validateMailAccountExists(Long id) {
@ -116,6 +81,12 @@ public class MailAccountServiceImpl implements MailAccountService {
return mailAccountMapper.selectById(id);
}
@Override
@Cacheable(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id", unless = "#result == null")
public MailAccountDO getMailAccountFromCache(Long id) {
return getMailAccount(id);
}
@Override
public PageResult<MailAccountDO> getMailAccountPage(MailAccountPageReqVO pageReqVO) {
return mailAccountMapper.selectPage(pageReqVO);

View File

@ -18,11 +18,6 @@ import java.util.Map;
*/
public interface MailTemplateService {
/**
* 初始化邮件模版的本地缓存
*/
void initLocalCache();
/**
* 邮件模版创建
*

View File

@ -10,14 +10,14 @@ import cn.iocoder.yudao.module.system.controller.admin.mail.vo.template.MailTemp
import cn.iocoder.yudao.module.system.convert.mail.MailTemplateConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO;
import cn.iocoder.yudao.module.system.dal.mysql.mail.MailTemplateMapper;
import cn.iocoder.yudao.module.system.mq.producer.mail.MailProducer;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
@ -25,8 +25,8 @@ import java.util.Map;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_CODE_EXISTS;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_NOT_EXISTS;
/**
* 邮箱模版 Service 实现类
@ -47,29 +47,6 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Resource
private MailTemplateMapper mailTemplateMapper;
@Resource
private MailProducer mailProducer;
/**
* 邮件模板缓存
* key邮件模版标识 {@link MailTemplateDO#getCode()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Map<String, MailTemplateDO> mailTemplateCache;
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
List<MailTemplateDO> templates = mailTemplateMapper.selectList();
log.info("[initLocalCache][缓存邮件模版,数量:{}]", templates.size());
// 第二步构建缓存
mailTemplateCache = convertMap(templates, MailTemplateDO::getCode);
}
@Override
public Long createMailTemplate(MailTemplateCreateReqVO createReqVO) {
// 校验 code 是否唯一
@ -79,12 +56,12 @@ public class MailTemplateServiceImpl implements MailTemplateService {
MailTemplateDO template = MailTemplateConvert.INSTANCE.convert(createReqVO)
.setParams(parseTemplateContentParams(createReqVO.getContent()));
mailTemplateMapper.insert(template);
// 发送刷新消息
mailProducer.sendMailTemplateRefreshMessage();
return template.getId();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE,
allEntries = true) // allEntries 清空所有缓存因为可能修改到 code 字段不好清理
public void updateMailTemplate(@Valid MailTemplateUpdateReqVO updateReqVO) {
// 校验是否存在
validateMailTemplateExists(updateReqVO.getId());
@ -95,12 +72,10 @@ public class MailTemplateServiceImpl implements MailTemplateService {
MailTemplateDO updateObj = MailTemplateConvert.INSTANCE.convert(updateReqVO)
.setParams(parseTemplateContentParams(updateReqVO.getContent()));
mailTemplateMapper.updateById(updateObj);
// 发送刷新消息
mailProducer.sendMailTemplateRefreshMessage();
}
@VisibleForTesting
public void validateCodeUnique(Long id, String code) {
void validateCodeUnique(Long id, String code) {
MailTemplateDO template = mailTemplateMapper.selectByCode(code);
if (template == null) {
return;
@ -113,14 +88,14 @@ public class MailTemplateServiceImpl implements MailTemplateService {
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE,
allEntries = true) // allEntries 清空所有缓存因为 id 不是直接的缓存 code不好清理
public void deleteMailTemplate(Long id) {
// 校验是否存在
validateMailTemplateExists(id);
// 删除
mailTemplateMapper.deleteById(id);
// 发送刷新消息
mailProducer.sendMailTemplateRefreshMessage();
}
private void validateMailTemplateExists(Long id) {
@ -132,6 +107,12 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Override
public MailTemplateDO getMailTemplate(Long id) {return mailTemplateMapper.selectById(id);}
@Override
@Cacheable(value = RedisKeyConstants.MAIL_TEMPLATE, key = "#code", unless = "#result == null")
public MailTemplateDO getMailTemplateByCodeFromCache(String code) {
return mailTemplateMapper.selectByCode(code);
}
@Override
public PageResult<MailTemplateDO> getMailTemplatePage(MailTemplatePageReqVO pageReqVO) {
return mailTemplateMapper.selectPage(pageReqVO);
@ -140,11 +121,6 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Override
public List<MailTemplateDO> getMailTemplateList() {return mailTemplateMapper.selectList();}
@Override
public MailTemplateDO getMailTemplateByCodeFromCache(String code) {
return mailTemplateCache.get(code);
}
@Override
public String formatMailTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params);

View File

@ -16,19 +16,6 @@ import java.util.Map;
*/
public interface NotifyTemplateService {
/**
* 初始化站内信模板的本地缓存
*/
void initLocalCache();
/**
* 获得站内信模板从缓存中
*
* @param code 模板编码
* @return 站内信模板
*/
NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code);
/**
* 创建站内信模版
*
@ -59,6 +46,14 @@ public interface NotifyTemplateService {
*/
NotifyTemplateDO getNotifyTemplate(Long id);
/**
* 获得站内信模板从缓存中
*
* @param code 模板编码
* @return 站内信模板
*/
NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code);
/**
* 获得站内信模版分页
*

View File

@ -3,27 +3,28 @@ package cn.iocoder.yudao.module.system.service.notify;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.notify.NotifyTemplateConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO;
import cn.iocoder.yudao.module.system.dal.mysql.notify.NotifyTemplateMapper;
import cn.iocoder.yudao.module.system.mq.producer.notify.NotifyProducer;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_CODE_DUPLICATE;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS;
/**
* 站内信模版 Service 实现类
@ -43,36 +44,6 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService {
@Resource
private NotifyTemplateMapper notifyTemplateMapper;
@Resource
private NotifyProducer notifyProducer;
/**
* 站内信模板缓存
* key站内信模板编码 {@link NotifyTemplateDO#getCode()}
* <p>
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
private volatile Map<String, NotifyTemplateDO> notifyTemplateCache;
/**
* 初始化站内信模板的本地缓存
*/
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
List<NotifyTemplateDO> templates = notifyTemplateMapper.selectList();
log.info("[initLocalCache][缓存站内信模版,数量为:{}]", templates.size());
// 第二步构建缓存
notifyTemplateCache = CollectionUtils.convertMap(templates, NotifyTemplateDO::getCode);
}
@Override
public NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code) {
return notifyTemplateCache.get(code);
}
@Override
public Long createNotifyTemplate(NotifyTemplateCreateReqVO createReqVO) {
// 校验站内信编码是否重复
@ -82,13 +53,12 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService {
NotifyTemplateDO notifyTemplate = NotifyTemplateConvert.INSTANCE.convert(createReqVO);
notifyTemplate.setParams(parseTemplateContentParams(notifyTemplate.getContent()));
notifyTemplateMapper.insert(notifyTemplate);
// 发送刷新消息
notifyProducer.sendNotifyTemplateRefreshMessage();
return notifyTemplate.getId();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE,
allEntries = true) // allEntries 清空所有缓存因为可能修改到 code 字段不好清理
public void updateNotifyTemplate(NotifyTemplateUpdateReqVO updateReqVO) {
// 校验存在
validateNotifyTemplateExists(updateReqVO.getId());
@ -99,9 +69,6 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService {
NotifyTemplateDO updateObj = NotifyTemplateConvert.INSTANCE.convert(updateReqVO);
updateObj.setParams(parseTemplateContentParams(updateObj.getContent()));
notifyTemplateMapper.updateById(updateObj);
// 发送刷新消息
notifyProducer.sendNotifyTemplateRefreshMessage();
}
@VisibleForTesting
@ -110,13 +77,13 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService {
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE,
allEntries = true) // allEntries 清空所有缓存因为 id 不是直接的缓存 code不好清理
public void deleteNotifyTemplate(Long id) {
// 校验存在
validateNotifyTemplateExists(id);
// 删除
notifyTemplateMapper.deleteById(id);
// 发送刷新消息
notifyProducer.sendNotifyTemplateRefreshMessage();
}
private void validateNotifyTemplateExists(Long id) {
@ -130,13 +97,20 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService {
return notifyTemplateMapper.selectById(id);
}
@Override
@Cacheable(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, key = "#code",
unless = "#result == null")
public NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code) {
return notifyTemplateMapper.selectByCode(code);
}
@Override
public PageResult<NotifyTemplateDO> getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO) {
return notifyTemplateMapper.selectPage(pageReqVO);
}
@VisibleForTesting
public void validateNotifyTemplateCodeDuplicate(Long id, String code) {
void validateNotifyTemplateCodeDuplicate(Long id, String code) {
NotifyTemplateDO template = notifyTemplateMapper.selectByCode(code);
if (template == null) {
return;

View File

@ -18,11 +18,6 @@ import java.util.Collection;
*/
public interface OAuth2ClientService {
/**
* 初始化 OAuth2Client 的本地缓存
*/
void initLocalCache();
/**
* 创建 OAuth2 客户端
*
@ -53,6 +48,14 @@ public interface OAuth2ClientService {
*/
OAuth2ClientDO getOAuth2Client(Long id);
/**
* 获得 OAuth2 客户端从缓存中
*
* @param clientId 客户端编号
* @return OAuth2 客户端
*/
OAuth2ClientDO getOAuth2ClientFromCache(String clientId);
/**
* 获得 OAuth2 客户端分页
*
@ -82,7 +85,7 @@ public interface OAuth2ClientService {
* @param redirectUri 重定向地址
* @return 客户端
*/
OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
String authorizedGrantType, Collection<String> scopes, String redirectUri);
OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType,
Collection<String> scopes, String redirectUri);
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.service.oauth2;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
@ -12,22 +13,18 @@ import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2Cl
import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ClientMapper;
import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
/**
@ -40,48 +37,21 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@Slf4j
public class OAuth2ClientServiceImpl implements OAuth2ClientService {
/**
* 客户端缓存
* key客户端编号 {@link OAuth2ClientDO#getClientId()} ()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter // 解决单测
@Setter // 解决单测
private volatile Map<String, OAuth2ClientDO> clientCache;
@Resource
private OAuth2ClientMapper oauth2ClientMapper;
@Resource
private OAuth2ClientProducer oauth2ClientProducer;
/**
* 初始化 {@link #clientCache} 缓存
*/
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
List<OAuth2ClientDO> clients = oauth2ClientMapper.selectList();
log.info("[initLocalCache][缓存 OAuth2 客户端,数量为:{}]", clients.size());
// 第二步构建缓存
clientCache = convertMap(clients, OAuth2ClientDO::getClientId);
}
@Override
public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) {
validateClientIdExists(null, createReqVO.getClientId());
// 插入
OAuth2ClientDO oauth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO);
oauth2ClientMapper.insert(oauth2Client);
// 发送刷新消息
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
return oauth2Client.getId();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT,
allEntries = true) // allEntries 清空所有缓存因为可能修改到 clientId 字段不好清理
public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) {
// 校验存在
validateOAuth2ClientExists(updateReqVO.getId());
@ -91,18 +61,16 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
// 更新
OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO);
oauth2ClientMapper.updateById(updateObj);
// 发送刷新消息
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT,
allEntries = true) // allEntries 清空所有缓存因为 id 不是直接的缓存 key不好清理
public void deleteOAuth2Client(Long id) {
// 校验存在
validateOAuth2ClientExists(id);
// 删除
oauth2ClientMapper.deleteById(id);
// 发送刷新消息
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
}
private void validateOAuth2ClientExists(Long id) {
@ -131,16 +99,23 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
return oauth2ClientMapper.selectById(id);
}
@Override
@Cacheable(cacheNames = RedisKeyConstants.OAUTH_CLIENT, key = "#clientId",
unless = "#result == null")
public OAuth2ClientDO getOAuth2ClientFromCache(String clientId) {
return oauth2ClientMapper.selectByClientId(clientId);
}
@Override
public PageResult<OAuth2ClientDO> getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO) {
return oauth2ClientMapper.selectPage(pageReqVO);
}
@Override
public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
String authorizedGrantType, Collection<String> scopes, String redirectUri) {
public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType,
Collection<String> scopes, String redirectUri) {
// 校验客户端存在且开启
OAuth2ClientDO client = clientCache.get(clientId);
OAuth2ClientDO client = getSelf().getOAuth2ClientFromCache(clientId);
if (client == null) {
throw exception(OAUTH2_CLIENT_NOT_EXISTS);
}
@ -167,4 +142,13 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
return client;
}
/**
* 获得自身的代理对象解决 AOP 生效问题
*
* @return 自己
*/
private OAuth2ClientServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -15,11 +15,6 @@ import java.util.List;
*/
public interface MenuService {
/**
* 初始化菜单的本地缓存
*/
void initLocalCache();
/**
* 创建菜单
*
@ -67,36 +62,12 @@ public interface MenuService {
List<MenuDO> getMenuList(MenuListReqVO reqVO);
/**
* 获得所有菜单从缓存中
*
* 任一参数为空时则返回为空
*
* @param menuTypes 菜单类型数组
* @param menusStatuses 菜单状态数组
* @return 菜单列表
*/
List<MenuDO> getMenuListFromCache(Collection<Integer> menuTypes, Collection<Integer> menusStatuses);
/**
* 获得指定编号的菜单数组从缓存中
*
* 任一参数为空时则返回为空
*
* @param menuIds 菜单编号数组
* @param menuTypes 菜单类型数组
* @param menusStatuses 菜单状态数组
* @return 菜单数组
*/
List<MenuDO> getMenuListFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses);
/**
* 获得权限对应的菜单数组
* 获得权限对应的菜单编号数组
*
* @param permission 权限标识
* @return 数组
*/
List<MenuDO> getMenuListByPermissionFromCache(String permission);
List<Long> getMenuIdListByPermissionFromCache(String permission);
/**
* 获得菜单
@ -106,4 +77,12 @@ public interface MenuService {
*/
MenuDO getMenu(Long id);
/**
* 获得菜单数组
*
* @param ids 菜单编号数组
* @return 菜单数组
*/
List<MenuDO> getMenuList(Collection<Long> ids);
}

View File

@ -1,36 +1,29 @@
package cn.iocoder.yudao.module.system.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.permission.MenuConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
import cn.iocoder.yudao.module.system.mq.producer.permission.MenuProducer;
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO.ID_ROOT;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@ -43,26 +36,6 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@Slf4j
public class MenuServiceImpl implements MenuService {
/**
* 菜单缓存
* key菜单编号
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
@Setter
private volatile Map<Long, MenuDO> menuCache;
/**
* 权限与菜单缓存
* key权限 {@link MenuDO#getPermission()}
* valueMenuDO 数组因为一个权限可能对应多个 MenuDO 对象
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
@Setter
private volatile Multimap<String, MenuDO> permissionMenuCache;
@Resource
private MenuMapper menuMapper;
@Resource
@ -71,33 +44,8 @@ public class MenuServiceImpl implements MenuService {
@Lazy // 延迟避免循环依赖报错
private TenantService tenantService;
@Resource
private MenuProducer menuProducer;
/**
* 初始化 {@link #menuCache} {@link #permissionMenuCache} 缓存
*/
@Override
@PostConstruct
public synchronized void initLocalCache() {
// 第一步查询数据
List<MenuDO> menuList = menuMapper.selectList();
log.info("[initLocalCache][缓存菜单,数量为:{}]", menuList.size());
// 第二步构建缓存
ImmutableMap.Builder<Long, MenuDO> menuCacheBuilder = ImmutableMap.builder();
ImmutableMultimap.Builder<String, MenuDO> permMenuCacheBuilder = ImmutableMultimap.builder();
menuList.forEach(menuDO -> {
menuCacheBuilder.put(menuDO.getId(), menuDO);
if (StrUtil.isNotEmpty(menuDO.getPermission())) { // 会存在 permission null 的情况导致 put NPE 异常
permMenuCacheBuilder.put(menuDO.getPermission(), menuDO);
}
});
menuCache = menuCacheBuilder.build();
permissionMenuCache = permMenuCacheBuilder.build();
}
@Override
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#reqVO.permission")
public Long createMenu(MenuCreateReqVO reqVO) {
// 校验父菜单存在
validateParentMenu(reqVO.getParentId(), null);
@ -108,13 +56,13 @@ public class MenuServiceImpl implements MenuService {
MenuDO menu = MenuConvert.INSTANCE.convert(reqVO);
initMenuProperty(menu);
menuMapper.insert(menu);
// 发送刷新消息
menuProducer.sendMenuRefreshMessage();
// 返回
return menu.getId();
}
@Override
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST,
allEntries = true) // allEntries 清空所有缓存因为 permission 如果变更涉及到新老两个 permission直接清理简单有效
public void updateMenu(MenuUpdateReqVO reqVO) {
// 校验更新的菜单是否存在
if (menuMapper.selectById(reqVO.getId()) == null) {
@ -129,34 +77,25 @@ public class MenuServiceImpl implements MenuService {
MenuDO updateObject = MenuConvert.INSTANCE.convert(reqVO);
initMenuProperty(updateObject);
menuMapper.updateById(updateObject);
// 发送刷新消息
menuProducer.sendMenuRefreshMessage();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteMenu(Long menuId) {
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST,
allEntries = true) // allEntries 清空所有缓存因为此时不知道 id 对应的 permission 是多少直接清理简单有效
public void deleteMenu(Long id) {
// 校验是否还有子菜单
if (menuMapper.selectCountByParentId(menuId) > 0) {
if (menuMapper.selectCountByParentId(id) > 0) {
throw exception(MENU_EXISTS_CHILDREN);
}
// 校验删除的菜单是否存在
if (menuMapper.selectById(menuId) == null) {
if (menuMapper.selectById(id) == null) {
throw exception(MENU_NOT_EXISTS);
}
// 标记删除
menuMapper.deleteById(menuId);
menuMapper.deleteById(id);
// 删除授予给角色的权限
permissionService.processMenuDeleted(menuId);
// 发送刷新消息. 注意需要事务提交后在进行发送刷新消息不然 db 还未提交结果缓存先刷新了
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
menuProducer.sendMenuRefreshMessage();
}
});
permissionService.processMenuDeleted(id);
}
@Override
@ -178,33 +117,10 @@ public class MenuServiceImpl implements MenuService {
}
@Override
public List<MenuDO> getMenuListFromCache(Collection<Integer> menuTypes, Collection<Integer> menusStatuses) {
// 任一一个参数为空则返回空
if (CollectionUtils.isAnyEmpty(menuTypes, menusStatuses)) {
return Collections.emptyList();
}
// 创建新数组避免缓存被修改
return menuCache.values().stream().filter(menu -> menuTypes.contains(menu.getType())
&& menusStatuses.contains(menu.getStatus()))
.collect(Collectors.toList());
}
@Override
public List<MenuDO> getMenuListFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses) {
// 任一一个参数为空则返回空
if (CollectionUtils.isAnyEmpty(menuIds, menuTypes, menusStatuses)) {
return Collections.emptyList();
}
return menuCache.values().stream().filter(menu -> menuIds.contains(menu.getId())
&& menuTypes.contains(menu.getType())
&& menusStatuses.contains(menu.getStatus()))
.collect(Collectors.toList());
}
@Override
public List<MenuDO> getMenuListByPermissionFromCache(String permission) {
return new ArrayList<>(permissionMenuCache.get(permission));
@Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission")
public List<Long> getMenuIdListByPermissionFromCache(String permission) {
List<MenuDO> menus = menuMapper.selectListByPermission(permission);
return convertList(menus, MenuDO::getId);
}
@Override
@ -212,15 +128,20 @@ public class MenuServiceImpl implements MenuService {
return menuMapper.selectById(id);
}
@Override
public List<MenuDO> getMenuList(Collection<Long> ids) {
return menuMapper.selectBatchIds(ids);
}
/**
* 校验父菜单是否合法
*
* <p>
* 1. 不能设置自己为父菜单
* 2. 父菜单不存在
* 3. 父菜单必须是 {@link MenuTypeEnum#MENU} 菜单类型
*
* @param parentId 父菜单编号
* @param childId 当前菜单编号
* @param childId 当前菜单编号
*/
@VisibleForTesting
void validateParentMenu(Long parentId, Long childId) {
@ -238,19 +159,19 @@ public class MenuServiceImpl implements MenuService {
}
// 父菜单必须是目录或者菜单类型
if (!MenuTypeEnum.DIR.getType().equals(menu.getType())
&& !MenuTypeEnum.MENU.getType().equals(menu.getType())) {
&& !MenuTypeEnum.MENU.getType().equals(menu.getType())) {
throw exception(MENU_PARENT_NOT_DIR_OR_MENU);
}
}
/**
* 校验菜单是否合法
*
* <p>
* 1. 校验相同父菜单编号下是否存在相同的菜单名
*
* @param name 菜单名字
* @param name 菜单名字
* @param parentId 父菜单编号
* @param id 菜单编号
* @param id 菜单编号
*/
@VisibleForTesting
void validateMenu(Long parentId, String name, Long id) {
@ -269,7 +190,7 @@ public class MenuServiceImpl implements MenuService {
/**
* 初始化菜单的通用属性
*
* <p>
* 例如说只有目录或者菜单类型的菜单才设置 icon
*
* @param menu 菜单

View File

@ -1,16 +1,15 @@
package cn.iocoder.yudao.module.system.service.permission;
import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static java.util.Collections.singleton;
/**
* 权限 Service 接口
*
* <p>
* 提供用户-角色角色-菜单角色-部门的关联权限处理
*
* @author 芋道源码
@ -18,81 +17,32 @@ import java.util.Set;
public interface PermissionService {
/**
* 初始化权限的本地缓存
* 判断是否有权限任一一个即可
*
* @param userId 用户编号
* @param permissions 权限
* @return 是否
*/
void initLocalCache();
boolean hasAnyPermissions(Long userId, String... permissions);
/**
* 获得角色们拥有的菜单列表从缓存中获取
* 判断是否有角色任一一个即可
*
* 任一参数为空时则返回为空
*
* @param roleIds 角色编号数组
* @param menuTypes 菜单类型数组
* @param menusStatuses 菜单状态数组
* @return 菜单列表
* @param roles 角色数组
* @return 是否
*/
List<MenuDO> getRoleMenuListFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses);
boolean hasAnyRoles(Long userId, String... roles);
/**
* 获得用户拥有的角色编号集合从缓存中获取
*
* @param userId 用户编号
* @param roleStatuses 角色状态集合. 允许为空为空时不过滤
* @return 角色编号集合
*/
Set<Long> getUserRoleIdsFromCache(Long userId, @Nullable Collection<Integer> roleStatuses);
/**
* 获得角色拥有的菜单编号集合
*
* @param roleId 角色编号
* @return 菜单编号集合
*/
Set<Long> getRoleMenuIds(Long roleId);
/**
* 获得拥有多个角色的用户编号集合
*
* @param roleIds 角色编号集合
* @return 用户编号集合
*/
Set<Long> getUserRoleIdListByRoleIds(Collection<Long> roleIds);
// ========== 角色-菜单的相关方法 ==========
/**
* 设置角色菜单
*
* @param roleId 角色编号
* @param roleId 角色编号
* @param menuIds 菜单编号集合
*/
void assignRoleMenu(Long roleId, Set<Long> menuIds);
/**
* 获得用户拥有的角色编号集合
*
* @param userId 用户编号
* @return 角色编号集合
*/
Set<Long> getUserRoleIdListByUserId(Long userId);
/**
* 设置用户角色
*
* @param userId 角色编号
* @param roleIds 角色编号集合
*/
void assignUserRole(Long userId, Set<Long> roleIds);
/**
* 设置角色的数据权限
*
* @param roleId 角色编号
* @param dataScope 数据范围
* @param dataScopeDeptIds 部门编号数组
*/
void assignRoleDataScope(Long roleId, Integer dataScope, Set<Long> dataScopeDeptIds);
/**
* 处理角色删除时删除关联授权数据
*
@ -108,28 +58,82 @@ public interface PermissionService {
void processMenuDeleted(Long menuId);
/**
* 处理用户删除是删除关联授权数据
* 获得角色拥有的菜单编号集合
*
* @param roleId 角色编号
* @return 菜单编号集合
*/
default Set<Long> getRoleMenuListByRoleId(Long roleId) {
return getRoleMenuListByRoleId(singleton(roleId));
}
/**
* 获得角色们拥有的菜单编号集合
*
* @param roleIds 角色编号数组
* @return 菜单编号集合
*/
Set<Long> getRoleMenuListByRoleId(Collection<Long> roleIds);
/**
* 获得拥有指定菜单的角色编号数组从缓存中获取
*
* @param menuId 菜单编号
* @return 角色编号数组
*/
Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId);
// ========== 用户-角色的相关方法 ==========
/**
* 设置用户角色
*
* @param userId 角色编号
* @param roleIds 角色编号集合
*/
void assignUserRole(Long userId, Set<Long> roleIds);
/**
* 处理用户删除时删除关联授权数据
*
* @param userId 用户编号
*/
void processUserDeleted(Long userId);
/**
* 判断是否有权限任一一个即可
* 获得拥有多个角色的用户编号集合
*
* @param userId 用户编号
* @param permissions 权限
* @return 是否
* @param roleIds 角色编号集合
* @return 用户编号集合
*/
boolean hasAnyPermissions(Long userId, String... permissions);
Set<Long> getUserRoleIdListByRoleId(Collection<Long> roleIds);
/**
* 判断是否有角色任一一个即可
* 获得用户拥有的角色编号集合
*
* @param roles 角色数组
* @return 是否
* @param userId 用户编号
* @return 角色编号集合
*/
boolean hasAnyRoles(Long userId, String... roles);
Set<Long> getUserRoleIdListByUserId(Long userId);
/**
* 获得用户拥有的角色编号集合从缓存中获取
*
* @param userId 用户编号
* @return 角色编号集合
*/
Set<Long> getUserRoleIdListByUserIdFromCache(Long userId);
// ========== 用户-部门的相关方法 ==========
/**
* 设置角色的数据权限
*
* @param roleId 角色编号
* @param dataScope 数据范围
* @param dataScopeDeptIds 部门编号数组
*/
void assignRoleDataScope(Long roleId, Integer dataScope, Set<Long> dataScopeDeptIds);
/**
* 获得登陆用户的部门数据权限

View File

@ -3,45 +3,38 @@ package cn.iocoder.yudao.module.system.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO;
import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuMapper;
import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum;
import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.function.Supplier;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static java.util.Collections.singleton;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 权限 Service 实现类
@ -52,38 +45,6 @@ import static java.util.Collections.singleton;
@Slf4j
public class PermissionServiceImpl implements PermissionService {
/**
* 角色编号与菜单编号的缓存映射
* key角色编号
* value菜单编号的数组
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
@Setter // 单元测试需要
private volatile Multimap<Long, Long> roleMenuCache;
/**
* 菜单编号与角色编号的缓存映射
* key菜单编号
* value角色编号的数组
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
@Setter // 单元测试需要
private volatile Multimap<Long, Long> menuRoleCache;
/**
* 用户编号与角色编号的缓存映射
* key用户编号
* value角色编号的数组
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
@Setter // 单元测试需要
private volatile Map<Long, Set<Long>> userRoleCache;
@Resource
private RoleMenuMapper roleMenuMapper;
@Resource
@ -98,115 +59,89 @@ public class PermissionServiceImpl implements PermissionService {
@Resource
private AdminUserService userService;
@Resource
private PermissionProducer permissionProducer;
@Override
@PostConstruct
public void initLocalCache() {
initLocalCacheForRoleMenu();
initLocalCacheForUserRole();
public boolean hasAnyPermissions(Long userId, String... permissions) {
// 如果为空说明已经有权限
if (ArrayUtil.isEmpty(permissions)) {
return true;
}
// 获得当前登录的角色如果为空说明没有权限
List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId);
if (CollUtil.isEmpty(roles)) {
return false;
}
// 情况一遍历判断每个权限如果有一满足说明有权限
for (String permission : permissions) {
if (hasAnyPermission(roles, permission)) {
return true;
}
}
// 情况二如果是超管也说明有权限
return roleService.hasAnySuperAdmin(convertSet(roles, RoleDO::getId));
}
/**
* 刷新 RoleMenu 本地缓存
* 判断指定角色是否拥有该 permission 权限
*
* @param roles 指定角色数组
* @param permission 权限标识
* @return 是否拥有
*/
@VisibleForTesting
void initLocalCacheForRoleMenu() {
// 注意忽略自动多租户因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 第一步查询数据
List<RoleMenuDO> roleMenus = roleMenuMapper.selectList();
log.info("[initLocalCacheForRoleMenu][缓存角色与菜单,数量为:{}]", roleMenus.size());
private boolean hasAnyPermission(List<RoleDO> roles, String permission) {
List<Long> menuIds = menuService.getMenuIdListByPermissionFromCache(permission);
// 采用严格模式如果权限找不到对应的 Menu 的话也认为没有权限
if (CollUtil.isEmpty(menuIds)) {
return false;
}
// 第二步构建缓存
ImmutableMultimap.Builder<Long, Long> roleMenuCacheBuilder = ImmutableMultimap.builder();
ImmutableMultimap.Builder<Long, Long> menuRoleCacheBuilder = ImmutableMultimap.builder();
roleMenus.forEach(roleMenuDO -> {
roleMenuCacheBuilder.put(roleMenuDO.getRoleId(), roleMenuDO.getMenuId());
menuRoleCacheBuilder.put(roleMenuDO.getMenuId(), roleMenuDO.getRoleId());
});
roleMenuCache = roleMenuCacheBuilder.build();
menuRoleCache = menuRoleCacheBuilder.build();
});
}
/**
* 刷新 UserRole 本地缓存
*/
@VisibleForTesting
void initLocalCacheForUserRole() {
// 注意忽略自动多租户因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 第一步加载数据
List<UserRoleDO> userRoles = userRoleMapper.selectList();
log.info("[initLocalCacheForUserRole][缓存用户与角色,数量为:{}]", userRoles.size());
// 第二步构建缓存
ImmutableMultimap.Builder<Long, Long> userRoleCacheBuilder = ImmutableMultimap.builder();
userRoles.forEach(userRoleDO -> userRoleCacheBuilder.put(userRoleDO.getUserId(), userRoleDO.getRoleId()));
userRoleCache = CollectionUtils.convertMultiMap2(userRoles, UserRoleDO::getUserId, UserRoleDO::getRoleId);
});
// 判断是否有权限
Set<Long> roleIds = convertSet(roles, RoleDO::getId);
for (Long menuId : menuIds) {
// 获得拥有该菜单的角色编号集合
Set<Long> menuRoleIds = getSelf().getMenuRoleIdListByMenuIdFromCache(menuId);
// 如果有交集说明有权限
if (CollUtil.containsAny(menuRoleIds, roleIds)) {
return true;
}
}
return false;
}
@Override
public List<MenuDO> getRoleMenuListFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses) {
// 任一一个参数为空时不返回任何菜单
if (CollectionUtils.isAnyEmpty(roleIds, menuTypes, menusStatuses)) {
return Collections.emptyList();
public boolean hasAnyRoles(Long userId, String... roles) {
// 如果为空说明已经有权限
if (ArrayUtil.isEmpty(roles)) {
return true;
}
// 判断角色是否包含超级管理员如果是超级管理员获取到全部
List<RoleDO> roleList = roleService.getRoleListFromCache(roleIds);
if (roleService.hasAnySuperAdmin(roleList)) {
return menuService.getMenuListFromCache(menuTypes, menusStatuses);
// 获得当前登录的角色如果为空说明没有权限
List<RoleDO> roleList = getEnableUserRoleListByUserIdFromCache(userId);
if (CollUtil.isEmpty(roleList)) {
return false;
}
// 获得角色拥有的菜单关联
List<Long> menuIds = MapUtils.getList(roleMenuCache, roleIds);
return menuService.getMenuListFromCache(menuIds, menuTypes, menusStatuses);
// 判断是否有角色
Set<String> userRoles = convertSet(roleList, RoleDO::getCode);
return CollUtil.containsAny(userRoles, Sets.newHashSet(roles));
}
@Override
public Set<Long> getUserRoleIdsFromCache(Long userId, Collection<Integer> roleStatuses) {
Set<Long> cacheRoleIds = userRoleCache.get(userId);
// 创建用户的时候没有分配角色会存在空指针异常
if (CollUtil.isEmpty(cacheRoleIds)) {
return Collections.emptySet();
}
Set<Long> roleIds = new HashSet<>(cacheRoleIds);
// 过滤角色状态
if (CollectionUtil.isNotEmpty(roleStatuses)) {
roleIds.removeIf(roleId -> {
RoleDO role = roleService.getRoleFromCache(roleId);
return role == null || !roleStatuses.contains(role.getStatus());
});
}
return roleIds;
}
// ========== 角色-菜单的相关方法 ==========
@Override
public Set<Long> getRoleMenuIds(Long roleId) {
// 如果是管理员的情况下获取全部菜单编号
if (roleService.hasAnySuperAdmin(Collections.singleton(roleId))) {
return convertSet(menuService.getMenuList(), MenuDO::getId);
}
// 如果是非管理员的情况下获得拥有的菜单编号
return convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
}
@Override
@Transactional(rollbackFor = Exception.class)
@DSTransactional // 多数据源使用 @DSTransactional 保证本地事务以及数据源的切换
@CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST,
allEntries = true) // allEntries 清空所有缓存主要一次更新涉及到的 menuIds 较多反倒批量会更快
public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
// 获得角色拥有菜单编号
Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId),
RoleMenuDO::getMenuId);
Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
// 计算新增和删除的菜单编号
Collection<Long> createMenuIds = CollUtil.subtract(menuIds, dbMenuIds);
Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIds);
// 执行新增和删除对于已经授权的菜单不用做任何处理
if (!CollectionUtil.isEmpty(createMenuIds)) {
if (CollUtil.isNotEmpty(createMenuIds)) {
roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> {
RoleMenuDO entity = new RoleMenuDO();
entity.setRoleId(roleId);
@ -214,34 +149,57 @@ public class PermissionServiceImpl implements PermissionService {
return entity;
}));
}
if (!CollectionUtil.isEmpty(deleteMenuIds)) {
if (CollUtil.isNotEmpty(deleteMenuIds)) {
roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds);
}
// 发送刷新消息. 注意需要事务提交后在进行发送刷新消息不然 db 还未提交结果缓存先刷新了
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
permissionProducer.sendRoleMenuRefreshMessage();
}
});
}
@Override
public Set<Long> getUserRoleIdListByUserId(Long userId) {
return convertSet(userRoleMapper.selectListByUserId(userId),
UserRoleDO::getRoleId);
}
@Override
public Set<Long> getUserRoleIdListByRoleIds(Collection<Long> roleIds) {
return convertSet(userRoleMapper.selectListByRoleIds(roleIds),
UserRoleDO::getUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
@Caching(evict = {
@CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST,
allEntries = true), // allEntries 清空所有缓存此处无法方便获得 roleId 对应的 menu 缓存们
@CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST,
allEntries = true) // allEntries 清空所有缓存此处无法方便获得 roleId 对应的 user 缓存们
})
public void processRoleDeleted(Long roleId) {
// 标记删除 UserRole
userRoleMapper.deleteListByRoleId(roleId);
// 标记删除 RoleMenu
roleMenuMapper.deleteListByRoleId(roleId);
}
@Override
@CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId")
public void processMenuDeleted(Long menuId) {
roleMenuMapper.deleteListByMenuId(menuId);
}
@Override
public Set<Long> getRoleMenuListByRoleId(Collection<Long> roleIds) {
if (CollUtil.isEmpty(roleIds)) {
return Collections.emptySet();
}
// 如果是管理员的情况下获取全部菜单编号
if (roleService.hasAnySuperAdmin(roleIds)) {
return convertSet(menuService.getMenuList(), MenuDO::getId);
}
// 如果是非管理员的情况下获得拥有的菜单编号
return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId);
}
@Override
@Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId")
public Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId) {
return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId);
}
// ========== 用户-角色的相关方法 ==========
@Override
@DSTransactional // 多数据源使用 @DSTransactional 保证本地事务以及数据源的切换
@CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public void assignUserRole(Long userId, Set<Long> roleIds) {
// 获得角色拥有角色编号
Set<Long> dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId),
@ -261,137 +219,68 @@ public class PermissionServiceImpl implements PermissionService {
if (!CollectionUtil.isEmpty(deleteMenuIds)) {
userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds);
}
// 发送刷新消息. 注意需要事务提交后在进行发送刷新消息不然 db 还未提交结果缓存先刷新了
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
permissionProducer.sendUserRoleRefreshMessage();
}
});
}
@Override
@CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public void processUserDeleted(Long userId) {
userRoleMapper.deleteListByUserId(userId);
}
@Override
public Set<Long> getUserRoleIdListByUserId(Long userId) {
return convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId);
}
@Override
@Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public Set<Long> getUserRoleIdListByUserIdFromCache(Long userId) {
return getUserRoleIdListByUserId(userId);
}
@Override
public Set<Long> getUserRoleIdListByRoleId(Collection<Long> roleIds) {
return convertSet(userRoleMapper.selectListByRoleIds(roleIds), UserRoleDO::getUserId);
}
/**
* 获得用户拥有的角色并且这些角色是开启状态的
*
* @param userId 用户编号
* @return 用户拥有的角色
*/
@VisibleForTesting
List<RoleDO> getEnableUserRoleListByUserIdFromCache(Long userId) {
// 获得用户拥有的角色编号
Set<Long> roleIds = getSelf().getUserRoleIdListByUserIdFromCache(userId);
// 获得角色数组并移除被禁用的
List<RoleDO> roles = roleService.getRoleListFromCache(roleIds);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus()));
return roles;
}
// ========== 用户-部门的相关方法 ==========
@Override
public void assignRoleDataScope(Long roleId, Integer dataScope, Set<Long> dataScopeDeptIds) {
roleService.updateRoleDataScope(roleId, dataScope, dataScopeDeptIds);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void processRoleDeleted(Long roleId) {
// 标记删除 UserRole
userRoleMapper.deleteListByRoleId(roleId);
// 标记删除 RoleMenu
roleMenuMapper.deleteListByRoleId(roleId);
// 发送刷新消息. 注意需要事务提交后在进行发送刷新消息不然 db 还未提交结果缓存先刷新了
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
permissionProducer.sendRoleMenuRefreshMessage();
permissionProducer.sendUserRoleRefreshMessage();
}
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void processMenuDeleted(Long menuId) {
roleMenuMapper.deleteListByMenuId(menuId);
// 发送刷新消息. 注意需要事务提交后在进行发送刷新消息不然 db 还未提交结果缓存先刷新了
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
permissionProducer.sendRoleMenuRefreshMessage();
}
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void processUserDeleted(Long userId) {
userRoleMapper.deleteListByUserId(userId);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
permissionProducer.sendUserRoleRefreshMessage();
}
});
}
@Override
public boolean hasAnyPermissions(Long userId, String... permissions) {
// 如果为空说明已经有权限
if (ArrayUtil.isEmpty(permissions)) {
return true;
}
// 获得当前登录的角色如果为空说明没有权限
Set<Long> roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus()));
if (CollUtil.isEmpty(roleIds)) {
return false;
}
// 判断是否是超管如果是当然符合条件
if (roleService.hasAnySuperAdmin(roleIds)) {
return true;
}
// 遍历权限判断是否有一个满足
return Arrays.stream(permissions).anyMatch(permission -> {
List<MenuDO> menuList = menuService.getMenuListByPermissionFromCache(permission);
// 采用严格模式如果权限找不到对应的 Menu 的话认为
if (CollUtil.isEmpty(menuList)) {
return false;
}
// 获得是否拥有该权限任一一个
return menuList.stream().anyMatch(menu -> CollUtil.containsAny(roleIds,
menuRoleCache.get(menu.getId())));
});
}
@Override
public boolean hasAnyRoles(Long userId, String... roles) {
// 如果为空说明已经有权限
if (ArrayUtil.isEmpty(roles)) {
return true;
}
// 获得当前登录的角色如果为空说明没有权限
Set<Long> roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus()));
if (CollUtil.isEmpty(roleIds)) {
return false;
}
// 判断是否是超管如果是当然符合条件
if (roleService.hasAnySuperAdmin(roleIds)) {
return true;
}
Set<String> userRoles = convertSet(roleService.getRoleListFromCache(roleIds),
RoleDO::getCode);
return CollUtil.containsAny(userRoles, Sets.newHashSet(roles));
}
@Override
@DataPermission(enable = false) // 关闭数据权限不然就会出现递归获取数据权限的问题
@TenantIgnore // 忽略多租户的自动过滤如果不忽略会导致添加租户时因为切换租户导致获取不到 User即使忽略本身该方法不存在跨租户的操作不会存在问题
public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) {
// 获得用户的角色
Set<Long> roleIds = getUserRoleIdsFromCache(userId, singleton(CommonStatusEnum.ENABLE.getStatus()));
List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId);
// 如果角色为空则只能查看自己
DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO();
if (CollUtil.isEmpty(roleIds)) {
if (CollUtil.isEmpty(roles)) {
result.setSelf(true);
return result;
}
List<RoleDO> roles = roleService.getRoleListFromCache(roleIds);
// 获得用户的部门编号的缓存通过 Guava Suppliers 惰性求值即有且仅有第一次发起 DB 的查询
Supplier<Long> userDeptIdCache = Suppliers.memoize(() -> userService.getUser(userId).getDeptId());
Supplier<Long> userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId());
// 遍历每个角色计算
for (RoleDO role : roles) {
// 为空时跳过
@ -408,20 +297,19 @@ public class PermissionServiceImpl implements PermissionService {
CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds());
// 自定义可见部门时保证可以看到自己所在的部门否则一些场景下可能会有问题
// 例如说登录时基于 t_user username 查询会可能被 dept_id 过滤掉
CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get());
CollUtil.addAll(result.getDeptIds(), userDeptId.get());
continue;
}
// 情况三DEPT_ONLY
if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) {
CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptIdCache.get());
CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get());
continue;
}
// 情况四DEPT_DEPT_AND_CHILD
if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) {
List<DeptDO> depts = deptService.getDeptListByParentIdFromCache(userDeptIdCache.get(), true);
CollUtil.addAll(result.getDeptIds(), CollectionUtils.convertList(depts, DeptDO::getId));
CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get()));
// 添加本身部门编号
CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get());
CollUtil.addAll(result.getDeptIds(), userDeptId.get());
continue;
}
// 情况五SELF
@ -430,9 +318,18 @@ public class PermissionServiceImpl implements PermissionService {
continue;
}
// 未知情况error log 即可
log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, JsonUtils.toJsonString(result));
log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, toJsonString(result));
}
return result;
}
/**
* 获得自身的代理对象解决 AOP 生效问题
*
* @return 自己
*/
private PermissionServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleEx
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import org.springframework.lang.Nullable;
import javax.validation.Valid;
import java.util.Collection;
@ -20,11 +19,6 @@ import java.util.Set;
*/
public interface RoleService {
/**
* 初始化角色的本地缓存
*/
void initLocalCache();
/**
* 创建角色
*
@ -65,6 +59,14 @@ public interface RoleService {
*/
void updateRoleDataScope(Long id, Integer dataScope, Set<Long> dataScopeDeptIds);
/**
* 获得角色
*
* @param id 角色编号
* @return 角色
*/
RoleDO getRole(Long id);
/**
* 获得角色从缓存中
*
@ -76,10 +78,10 @@ public interface RoleService {
/**
* 获得角色列表
*
* @param statuses 筛选的状态允许空空时不筛选
* @param ids 角色编号数组
* @return 角色列表
*/
List<RoleDO> getRoleListByStatus(@Nullable Collection<Integer> statuses);
List<RoleDO> getRoleList(Collection<Long> ids);
/**
* 获得角色数组从缓存中
@ -90,30 +92,19 @@ public interface RoleService {
List<RoleDO> getRoleListFromCache(Collection<Long> ids);
/**
* 判断角色数组中是否有超级管理员
* 获得角色列表
*
* @param roleList 角色数组
* @return 是否有管理员
* @param statuses 筛选的状态
* @return 角色列表
*/
boolean hasAnySuperAdmin(Collection<RoleDO> roleList);
List<RoleDO> getRoleListByStatus(Collection<Integer> statuses);
/**
* 判断角色编号数组中是否有管理员
* 获得所有角色列表
*
* @param ids 角色编号数组
* @return 是否有管理员
* @return 角色列表
*/
default boolean hasAnySuperAdmin(Set<Long> ids) {
return hasAnySuperAdmin(getRoleListFromCache(ids));
}
/**
* 获得角色
*
* @param id 角色编号
* @return 角色
*/
RoleDO getRole(Long id);
List<RoleDO> getRoleList();
/**
* 获得角色分页
@ -131,6 +122,14 @@ public interface RoleService {
*/
List<RoleDO> getRoleList(RoleExportReqVO reqVO);
/**
* 判断角色编号数组中是否有管理员
*
* @param ids 角色编号数组
* @return 是否有管理员
*/
boolean hasAnySuperAdmin(Collection<Long> ids);
/**
* 校验角色们是否有效如下情况视为无效
* 1. 角色编号不存在

View File

@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.system.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO;
@ -13,26 +13,23 @@ import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleUp
import cn.iocoder.yudao.module.system.convert.permission.RoleConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMapper;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum;
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum;
import cn.iocoder.yudao.module.system.mq.producer.permission.RoleProducer;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@ -45,43 +42,14 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@Slf4j
public class RoleServiceImpl implements RoleService {
/**
* 角色缓存
* key角色编号 {@link RoleDO#getId()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Map<Long, RoleDO> roleCache;
@Resource
private PermissionService permissionService;
@Resource
private RoleMapper roleMapper;
@Resource
private RoleProducer roleProducer;
/**
* 初始化 {@link #roleCache} 缓存
*/
@Override
@PostConstruct
public void initLocalCache() {
// 注意忽略自动多租户因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 第一步查询数据
List<RoleDO> roleList = roleMapper.selectList();
log.info("[initLocalCache][缓存角色,数量为:{}]", roleList.size());
// 第二步构建缓存
roleCache = convertMap(roleList, RoleDO::getId);
});
}
@Override
@Transactional
@Transactional(rollbackFor = Exception.class)
public Long createRole(RoleCreateReqVO reqVO, Integer type) {
// 校验角色
validateRoleDuplicate(reqVO.getName(), reqVO.getCode(), null);
@ -91,18 +59,12 @@ public class RoleServiceImpl implements RoleService {
role.setStatus(CommonStatusEnum.ENABLE.getStatus());
role.setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据原因是可能一些项目不需要项目权限
roleMapper.insert(role);
// 发送刷新消息
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
roleProducer.sendRoleRefreshMessage();
}
});
// 返回
return role.getId();
}
@Override
@CacheEvict(value = RedisKeyConstants.ROLE, key = "#reqVO.id")
public void updateRole(RoleUpdateReqVO reqVO) {
// 校验是否可以更新
validateRoleForUpdate(reqVO.getId());
@ -112,11 +74,10 @@ public class RoleServiceImpl implements RoleService {
// 更新到数据库
RoleDO updateObj = RoleConvert.INSTANCE.convert(reqVO);
roleMapper.updateById(updateObj);
// 发送刷新消息
roleProducer.sendRoleRefreshMessage();
}
@Override
@CacheEvict(value = RedisKeyConstants.ROLE, key = "#id")
public void updateRoleStatus(Long id, Integer status) {
// 校验是否可以更新
validateRoleForUpdate(id);
@ -124,11 +85,10 @@ public class RoleServiceImpl implements RoleService {
// 更新状态
RoleDO updateObj = new RoleDO().setId(id).setStatus(status);
roleMapper.updateById(updateObj);
// 发送刷新消息
roleProducer.sendRoleRefreshMessage();
}
@Override
@CacheEvict(value = RedisKeyConstants.ROLE, key = "#id")
public void updateRoleDataScope(Long id, Integer dataScope, Set<Long> dataScopeDeptIds) {
// 校验是否可以更新
validateRoleForUpdate(id);
@ -139,12 +99,11 @@ public class RoleServiceImpl implements RoleService {
updateObject.setDataScope(dataScope);
updateObject.setDataScopeDeptIds(dataScopeDeptIds);
roleMapper.updateById(updateObject);
// 发送刷新消息
roleProducer.sendRoleRefreshMessage();
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = RedisKeyConstants.ROLE, key = "#id")
public void deleteRole(Long id) {
// 校验是否可以更新
validateRoleForUpdate(id);
@ -152,60 +111,6 @@ public class RoleServiceImpl implements RoleService {
roleMapper.deleteById(id);
// 删除相关数据
permissionService.processRoleDeleted(id);
// 发送刷新消息. 注意需要事务提交后在进行发送刷新消息不然 db 还未提交结果缓存先刷新了
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
roleProducer.sendRoleRefreshMessage();
}
});
}
@Override
public RoleDO getRoleFromCache(Long id) {
return roleCache.get(id);
}
@Override
public List<RoleDO> getRoleListByStatus(@Nullable Collection<Integer> statuses) {
if (CollUtil.isEmpty(statuses)) {
return roleMapper.selectList();
}
return roleMapper.selectListByStatus(statuses);
}
@Override
public List<RoleDO> getRoleListFromCache(Collection<Long> ids) {
if (CollectionUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return roleCache.values().stream().filter(roleDO -> ids.contains(roleDO.getId()))
.collect(Collectors.toList());
}
@Override
public boolean hasAnySuperAdmin(Collection<RoleDO> roleList) {
if (CollectionUtil.isEmpty(roleList)) {
return false;
}
return roleList.stream().anyMatch(role -> RoleCodeEnum.isSuperAdmin(role.getCode()));
}
@Override
public RoleDO getRole(Long id) {
return roleMapper.selectById(id);
}
@Override
public PageResult<RoleDO> getRolePage(RolePageReqVO reqVO) {
return roleMapper.selectPage(reqVO);
}
@Override
public List<RoleDO> getRoleList(RoleExportReqVO reqVO) {
return roleMapper.selectList(reqVO);
}
/**
@ -257,6 +162,69 @@ public class RoleServiceImpl implements RoleService {
}
}
@Override
public RoleDO getRole(Long id) {
return roleMapper.selectById(id);
}
@Override
@Cacheable(value = RedisKeyConstants.ROLE, key = "#id",
unless = "#result == null")
public RoleDO getRoleFromCache(Long id) {
return roleMapper.selectById(id);
}
@Override
public List<RoleDO> getRoleListByStatus(Collection<Integer> statuses) {
return roleMapper.selectListByStatus(statuses);
}
@Override
public List<RoleDO> getRoleList() {
return roleMapper.selectList();
}
@Override
public List<RoleDO> getRoleList(Collection<Long> ids) {
if (CollectionUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return roleMapper.selectBatchIds(ids);
}
@Override
public List<RoleDO> getRoleListFromCache(Collection<Long> ids) {
if (CollectionUtil.isEmpty(ids)) {
return Collections.emptyList();
}
// 这里采用 for 循环从缓存中获取主要考虑 Spring CacheManager 无法批量操作的问题
RoleServiceImpl self = getSelf();
return convertList(ids, self::getRoleFromCache);
}
@Override
public PageResult<RoleDO> getRolePage(RolePageReqVO reqVO) {
return roleMapper.selectPage(reqVO);
}
@Override
public List<RoleDO> getRoleList(RoleExportReqVO reqVO) {
return roleMapper.selectList(reqVO);
}
@Override
public boolean hasAnySuperAdmin(Collection<Long> ids) {
if (CollectionUtil.isEmpty(ids)) {
return false;
}
RoleServiceImpl self = getSelf();
return ids.stream().anyMatch(id -> {
RoleDO role = self.getRoleFromCache(id);
return role != null && RoleCodeEnum.isSuperAdmin(role.getCode());
});
}
@Override
public void validateRoleList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
@ -276,4 +244,14 @@ public class RoleServiceImpl implements RoleService {
}
});
}
/**
* 获得自身的代理对象解决 AOP 生效问题
*
* @return 自己
*/
private RoleServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -18,11 +18,6 @@ import java.util.Set;
*/
public interface SensitiveWordService {
/**
* 初始化本地缓存
*/
void initLocalCache();
/**
* 创建敏感词
*

View File

@ -11,21 +11,24 @@ import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.Sensitiv
import cn.iocoder.yudao.module.system.convert.sensitiveword.SensitiveWordConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import cn.iocoder.yudao.module.system.dal.mysql.sensitiveword.SensitiveWordMapper;
import cn.iocoder.yudao.module.system.mq.producer.sensitiveword.SensitiveWordProducer;
import cn.iocoder.yudao.module.system.util.collection.SimpleTrie;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_EXISTS;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
@ -39,6 +42,11 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SENSITIVE_
@Validated
public class SensitiveWordServiceImpl implements SensitiveWordService {
/**
* 敏感词列表缓存
*/
@Getter
private volatile List<SensitiveWordDO> sensitiveWordCache = Collections.emptyList();
/**
* 敏感词标签缓存
* key敏感词编号 {@link SensitiveWordDO#getId()}
@ -51,9 +59,6 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
@Resource
private SensitiveWordMapper sensitiveWordMapper;
@Resource
private SensitiveWordProducer sensitiveWordProducer;
/**
* 默认的敏感词的字典树包含所有敏感词
*/
@ -68,7 +73,6 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
/**
* 初始化缓存
*/
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
@ -80,6 +84,7 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
Set<String> tags = new HashSet<>();
sensitiveWords.forEach(word -> tags.addAll(word.getTags()));
sensitiveWordTagsCache = tags;
sensitiveWordCache = sensitiveWords;
// 写入 defaultSensitiveWordTrietagSensitiveWordTries 缓存
initSensitiveWordTrie(sensitiveWords);
}
@ -105,6 +110,26 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
this.tagSensitiveWordTries = tagSensitiveWordTries;
}
/**
* 通过定时任务轮询刷新缓存
*
* 目的多节点部署时通过轮询通知所有节点进行刷新
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
// 情况一如果缓存里没有数据则直接刷新缓存
if (CollUtil.isEmpty(sensitiveWordCache)) {
initLocalCache();
return;
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = getMaxValue(sensitiveWordCache, SensitiveWordDO::getUpdateTime);
if (sensitiveWordMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
}
@Override
public Long createSensitiveWord(SensitiveWordCreateReqVO createReqVO) {
// 校验唯一性
@ -113,8 +138,9 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
// 插入
SensitiveWordDO sensitiveWord = SensitiveWordConvert.INSTANCE.convert(createReqVO);
sensitiveWordMapper.insert(sensitiveWord);
// 发送消息刷新缓存
sensitiveWordProducer.sendSensitiveWordRefreshMessage();
// 刷新缓存
initLocalCache();
return sensitiveWord.getId();
}
@ -127,8 +153,9 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
// 更新
SensitiveWordDO updateObj = SensitiveWordConvert.INSTANCE.convert(updateReqVO);
sensitiveWordMapper.updateById(updateObj);
// 发送消息刷新缓存
sensitiveWordProducer.sendSensitiveWordRefreshMessage();
// 刷新缓存
initLocalCache();
}
@Override
@ -137,8 +164,9 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
validateSensitiveWordExists(id);
// 删除
sensitiveWordMapper.deleteById(id);
// 发送消息刷新缓存
sensitiveWordProducer.sendSensitiveWordRefreshMessage();
// 刷新缓存
initLocalCache();
}
private void validateSensitiveWordNameUnique(Long id, String name) {

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.system.service.sms;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
@ -9,15 +10,20 @@ import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannel
import cn.iocoder.yudao.module.system.convert.sms.SmsChannelConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper;
import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
@ -30,6 +36,12 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNE
@Slf4j
public class SmsChannelServiceImpl implements SmsChannelService {
/**
* 短信渠道列表的缓存
*/
@Getter
private volatile List<SmsChannelDO> channelCache = Collections.emptyList();
@Resource
private SmsClientFactory smsClientFactory;
@ -39,9 +51,6 @@ public class SmsChannelServiceImpl implements SmsChannelService {
@Resource
private SmsTemplateService smsTemplateService;
@Resource
private SmsProducer smsProducer;
@Override
@PostConstruct
public void initLocalCache() {
@ -52,6 +61,27 @@ public class SmsChannelServiceImpl implements SmsChannelService {
// 第二步构建缓存创建或更新短信 Client
List<SmsChannelProperties> propertiesList = SmsChannelConvert.INSTANCE.convertList02(channels);
propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties));
this.channelCache = channels;
}
/**
* 通过定时任务轮询刷新缓存
*
* 目的多节点部署时通过轮询通知所有节点进行刷新
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
// 情况一如果缓存里没有数据则直接刷新缓存
if (CollUtil.isEmpty(channelCache)) {
initLocalCache();
return;
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = getMaxValue(channelCache, SmsChannelDO::getUpdateTime);
if (smsChannelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
}
@Override
@ -59,9 +89,9 @@ public class SmsChannelServiceImpl implements SmsChannelService {
// 插入
SmsChannelDO smsChannel = SmsChannelConvert.INSTANCE.convert(createReqVO);
smsChannelMapper.insert(smsChannel);
// 发送刷新消息
smsProducer.sendSmsChannelRefreshMessage();
// 返回
// 刷新缓存
initLocalCache();
return smsChannel.getId();
}
@ -72,8 +102,9 @@ public class SmsChannelServiceImpl implements SmsChannelService {
// 更新
SmsChannelDO updateObj = SmsChannelConvert.INSTANCE.convert(updateReqVO);
smsChannelMapper.updateById(updateObj);
// 发送刷新消息
smsProducer.sendSmsChannelRefreshMessage();
// 刷新缓存
initLocalCache();
}
@Override
@ -86,8 +117,9 @@ public class SmsChannelServiceImpl implements SmsChannelService {
}
// 删除
smsChannelMapper.deleteById(id);
// 发送刷新消息
smsProducer.sendSmsChannelRefreshMessage();
// 刷新缓存
initLocalCache();
}
private void validateSmsChannelExists(Long id) {

View File

@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@ -16,40 +15,10 @@ import java.util.Map;
* 短信模板 Service 接口
*
* @author zzf
* @date 2021/1/25 9:24
* @since 2021/1/25 9:24
*/
public interface SmsTemplateService {
/**
* 初始化短信模板的本地缓存
*/
void initLocalCache();
/**
* 获得短信模板从缓存中
*
* @param code 模板编码
* @return 短信模板
*/
SmsTemplateDO getSmsTemplateByCodeFromCache(String code);
/**
* 格式化短信内容
*
* @param content 短信模板的内容
* @param params 内容的参数
* @return 格式化后的内容
*/
String formatSmsTemplateContent(String content, Map<String, Object> params);
/**
* 获得短信模板
*
* @param code 模板编码
* @return 短信模板
*/
SmsTemplateDO getSmsTemplateByCode(String code);
/**
* 创建短信模板
*
@ -81,12 +50,12 @@ public interface SmsTemplateService {
SmsTemplateDO getSmsTemplate(Long id);
/**
* 获得短信模板列表
* 获得短信模板从缓存中
*
* @param ids 编号
* @return 短信模板列表
* @param code 模板编码
* @return 短信模板
*/
List<SmsTemplateDO> getSmsTemplateList(Collection<Long> ids);
SmsTemplateDO getSmsTemplateByCodeFromCache(String code);
/**
* 获得短信模板分页
@ -112,4 +81,14 @@ public interface SmsTemplateService {
*/
Long countByChannelId(Long channelId);
/**
* 格式化短信内容
*
* @param content 短信模板的内容
* @param params 内容的参数
* @return 格式化后的内容
*/
String formatSmsTemplateContent(String content, Map<String, Object> params);
}

View File

@ -4,7 +4,6 @@ import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
@ -17,16 +16,15 @@ import cn.iocoder.yudao.module.system.convert.sms.SmsTemplateConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO;
import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsTemplateMapper;
import cn.iocoder.yudao.module.system.mq.producer.sms.SmsProducer;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -59,49 +57,6 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
@Resource
private SmsClientFactory smsClientFactory;
@Resource
private SmsProducer smsProducer;
/**
* 短信模板缓存
* key短信模板编码 {@link SmsTemplateDO#getCode()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter // 为了方便测试这里提供 getter 方法
private volatile Map<String, SmsTemplateDO> smsTemplateCache;
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
List<SmsTemplateDO> smsTemplateList = smsTemplateMapper.selectList();
log.info("[initLocalCache][缓存短信模版,数量为:{}]", smsTemplateList.size());
// 第二步构建缓存
smsTemplateCache = CollectionUtils.convertMap(smsTemplateList, SmsTemplateDO::getCode);
}
@Override
public SmsTemplateDO getSmsTemplateByCodeFromCache(String code) {
return smsTemplateCache.get(code);
}
@Override
public String formatSmsTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params);
}
@Override
public SmsTemplateDO getSmsTemplateByCode(String code) {
return smsTemplateMapper.selectByCode(code);
}
@VisibleForTesting
public List<String> parseTemplateContentParams(String content) {
return ReUtil.findAllGroup1(PATTERN_PARAMS, content);
}
@Override
public Long createSmsTemplate(SmsTemplateCreateReqVO createReqVO) {
// 校验短信渠道
@ -116,13 +71,13 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
template.setParams(parseTemplateContentParams(template.getContent()));
template.setChannelCode(channelDO.getCode());
smsTemplateMapper.insert(template);
// 发送刷新消息
smsProducer.sendSmsTemplateRefreshMessage();
// 返回
return template.getId();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE,
allEntries = true) // allEntries 清空所有缓存因为可能修改到 code 字段不好清理
public void updateSmsTemplate(SmsTemplateUpdateReqVO updateReqVO) {
// 校验存在
validateSmsTemplateExists(updateReqVO.getId());
@ -138,18 +93,16 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
updateObj.setParams(parseTemplateContentParams(updateObj.getContent()));
updateObj.setChannelCode(channelDO.getCode());
smsTemplateMapper.updateById(updateObj);
// 发送刷新消息
smsProducer.sendSmsTemplateRefreshMessage();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE,
allEntries = true) // allEntries 清空所有缓存因为 id 不是直接的缓存 code不好清理
public void deleteSmsTemplate(Long id) {
// 校验存在
validateSmsTemplateExists(id);
// 更新
smsTemplateMapper.deleteById(id);
// 发送刷新消息
smsProducer.sendSmsTemplateRefreshMessage();
}
private void validateSmsTemplateExists(Long id) {
@ -164,8 +117,10 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
}
@Override
public List<SmsTemplateDO> getSmsTemplateList(Collection<Long> ids) {
return smsTemplateMapper.selectBatchIds(ids);
@Cacheable(cacheNames = RedisKeyConstants.SMS_TEMPLATE, key = "#code",
unless = "#result == null")
public SmsTemplateDO getSmsTemplateByCodeFromCache(String code) {
return smsTemplateMapper.selectByCode(code);
}
@Override
@ -217,7 +172,7 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
* @param apiTemplateId API 模板编号
*/
@VisibleForTesting
public void validateApiTemplate(Long channelId, String apiTemplateId) {
void validateApiTemplate(Long channelId, String apiTemplateId) {
// 获得短信模板
SmsClient smsClient = smsClientFactory.getSmsClient(channelId);
Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId));
@ -226,4 +181,14 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
templateResult.checkError();
}
@Override
public String formatSmsTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params);
}
@VisibleForTesting
List<String> parseTemplateContentParams(String content) {
return ReUtil.findAllGroup1(PATTERN_PARAMS, content);
}
}

Some files were not shown because too many files have changed in this diff Show More