feat: initial iShare project code
This commit is contained in:
56
pigx-common/pigx-common-data/pom.xml
Normal file
56
pigx-common/pigx-common-data/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common</artifactId>
|
||||
<version>5.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>pigx-common-data</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<description>pigx 数据操作相关</description>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<!--工具类核心包-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-core</artifactId>
|
||||
</dependency>
|
||||
<!--mybatis plus-->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-extension</artifactId>
|
||||
</dependency>
|
||||
<!-- 连表查询依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.github.yulichang</groupId>
|
||||
<artifactId>mybatis-plus-join-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<!-- druid 连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!--安全依赖获取上下文信息-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-security</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!--feign client-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>as-upms-api</artifactId>
|
||||
</dependency>
|
||||
<!--缓存依赖-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.cache;
|
||||
|
||||
import org.springframework.dao.PessimisticLockingFailureException;
|
||||
import org.springframework.data.redis.cache.CacheStatistics;
|
||||
import org.springframework.data.redis.cache.CacheStatisticsCollector;
|
||||
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to
|
||||
* Redis in {@literal standalone} and {@literal cluster} environments. Works upon a given
|
||||
* {@link RedisConnectionFactory} to obtain the actual {@link RedisConnection}. <br />
|
||||
* {@link DefaultRedisCacheWriter} can be used in
|
||||
* {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or
|
||||
* {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking}
|
||||
* mode. While {@literal non-locking} aims for maximum performance it may result in
|
||||
* overlapping, non atomic, command execution for operations spanning multiple Redis
|
||||
* interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents
|
||||
* command overlap by setting an explicit lock key and checking against presence of this
|
||||
* key which leads to additional requests and potential command wait times.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
class DefaultRedisCacheWriter implements RedisCacheWriter {
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
|
||||
private final Duration sleepTime;
|
||||
|
||||
/**
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
*/
|
||||
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory) {
|
||||
this(connectionFactory, Duration.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
* @param sleepTime sleep time between lock request attempts. Must not be
|
||||
* {@literal null}. Use {@link Duration#ZERO} to disable locking.
|
||||
*/
|
||||
private DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) {
|
||||
|
||||
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
|
||||
Assert.notNull(sleepTime, "SleepTime must not be null!");
|
||||
|
||||
this.connectionFactory = connectionFactory;
|
||||
this.sleepTime = sleepTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
|
||||
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
Assert.notNull(key, "Key must not be null!");
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
|
||||
execute(name, connection -> {
|
||||
|
||||
if (shouldExpireWithin(ttl)) {
|
||||
connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
|
||||
}
|
||||
else {
|
||||
connection.set(key, value);
|
||||
}
|
||||
|
||||
return "OK";
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] get(String name, byte[] key) {
|
||||
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
Assert.notNull(key, "Key must not be null!");
|
||||
|
||||
return execute(name, connection -> connection.get(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
|
||||
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
Assert.notNull(key, "Key must not be null!");
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
|
||||
return execute(name, connection -> {
|
||||
|
||||
if (isLockingCacheWriter()) {
|
||||
doLock(name, connection);
|
||||
}
|
||||
|
||||
try {
|
||||
if (Boolean.TRUE.equals(connection.setNX(key, value))) {
|
||||
|
||||
if (shouldExpireWithin(ttl)) {
|
||||
connection.pExpire(key, ttl.toMillis());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return connection.get(key);
|
||||
}
|
||||
finally {
|
||||
|
||||
if (isLockingCacheWriter()) {
|
||||
doUnlock(name, connection);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除,原来是删除指定的键,目前修改为既可以删除指定键的数据,也是可以删除某个前缀开始的所有数据
|
||||
* @param name
|
||||
* @param key
|
||||
*/
|
||||
@Override
|
||||
public void remove(String name, byte[] key) {
|
||||
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
Assert.notNull(key, "Key must not be null!");
|
||||
|
||||
execute(name, connection -> {
|
||||
// 获取某个前缀所拥有的所有的键,某个前缀开头,后面肯定是*
|
||||
Set<byte[]> keys = connection.keys(key);
|
||||
int delNum = 0;
|
||||
Assert.notNull(keys, "keys must not be null!");
|
||||
for (byte[] keyByte : keys) {
|
||||
delNum += connection.del(keyByte);
|
||||
}
|
||||
return delNum;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clean(String name, byte[] pattern) {
|
||||
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
Assert.notNull(pattern, "Pattern must not be null!");
|
||||
|
||||
execute(name, connection -> {
|
||||
|
||||
boolean wasLocked = false;
|
||||
|
||||
try {
|
||||
|
||||
if (isLockingCacheWriter()) {
|
||||
doLock(name, connection);
|
||||
wasLocked = true;
|
||||
}
|
||||
|
||||
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
|
||||
.toArray(new byte[0][]);
|
||||
|
||||
if (keys.length > 0) {
|
||||
connection.del(keys);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
|
||||
if (wasLocked && isLockingCacheWriter()) {
|
||||
doUnlock(name, connection);
|
||||
}
|
||||
}
|
||||
|
||||
return "OK";
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearStatistics(String s) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisCacheWriter withStatisticsCollector(CacheStatisticsCollector cacheStatisticsCollector) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly set a write lock on a cache.
|
||||
* @param name the name of the cache to lock.
|
||||
*/
|
||||
void lock(String name) {
|
||||
execute(name, connection -> doLock(name, connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly remove a write lock from a cache.
|
||||
* @param name the name of the cache to unlock.
|
||||
*/
|
||||
void unlock(String name) {
|
||||
executeLockFree(connection -> doUnlock(name, connection));
|
||||
}
|
||||
|
||||
private Boolean doLock(String name, RedisConnection connection) {
|
||||
return connection.setNX(createCacheLockKey(name), new byte[0]);
|
||||
}
|
||||
|
||||
private Long doUnlock(String name, RedisConnection connection) {
|
||||
return connection.del(createCacheLockKey(name));
|
||||
}
|
||||
|
||||
boolean doCheckLock(String name, RedisConnection connection) {
|
||||
return connection.exists(createCacheLockKey(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if {@link RedisCacheWriter} uses locks.
|
||||
*/
|
||||
private boolean isLockingCacheWriter() {
|
||||
return !sleepTime.isZero() && !sleepTime.isNegative();
|
||||
}
|
||||
|
||||
private <T> T execute(String name, Function<RedisConnection, T> callback) {
|
||||
|
||||
try (RedisConnection connection = connectionFactory.getConnection()) {
|
||||
checkAndPotentiallyWaitUntilUnlocked(name, connection);
|
||||
return callback.apply(connection);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeLockFree(Consumer<RedisConnection> callback) {
|
||||
try (RedisConnection connection = connectionFactory.getConnection()) {
|
||||
callback.accept(connection);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
|
||||
|
||||
if (!isLockingCacheWriter()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (doCheckLock(name, connection)) {
|
||||
Thread.sleep(sleepTime.toMillis());
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
|
||||
// Re-interrupt current thread, to allow other participants to react.
|
||||
Thread.currentThread().interrupt();
|
||||
|
||||
throw new PessimisticLockingFailureException(
|
||||
String.format("Interrupted while waiting to unlock cache %s", name), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldExpireWithin(@Nullable Duration ttl) {
|
||||
return ttl != null && !ttl.isZero() && !ttl.isNegative();
|
||||
}
|
||||
|
||||
private static byte[] createCacheLockKey(String name) {
|
||||
return (name + "~lock").getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheStatistics getCacheStatistics(String s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.cache;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.convert.DurationStyle;
|
||||
import org.springframework.cache.Cache;
|
||||
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 org.springframework.lang.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* redis cache 扩展cache name自动化配置
|
||||
*
|
||||
* @author L.cm
|
||||
* @author lengleng
|
||||
* <p>
|
||||
* cachename = xx#ttl
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisAutoCacheManager extends RedisCacheManager {
|
||||
|
||||
private static final String SPLIT_FLAG = "#";
|
||||
|
||||
private static final int CACHE_LENGTH = 2;
|
||||
|
||||
RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
|
||||
Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
|
||||
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
|
||||
if (StrUtil.isBlank(name) || !name.contains(SPLIT_FLAG)) {
|
||||
return super.createRedisCache(name, cacheConfig);
|
||||
}
|
||||
|
||||
String[] cacheArray = name.split(SPLIT_FLAG);
|
||||
if (cacheArray.length < CACHE_LENGTH) {
|
||||
return super.createRedisCache(name, cacheConfig);
|
||||
}
|
||||
|
||||
if (cacheConfig != null) {
|
||||
Duration duration = DurationStyle.detectAndParse(cacheArray[1], ChronoUnit.SECONDS);
|
||||
cacheConfig = cacheConfig.entryTtl(duration);
|
||||
}
|
||||
return super.createRedisCache(cacheArray[0], cacheConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从上下文中获取租户ID,重写@Cacheable value 值
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Cache getCache(String name) {
|
||||
// see https://gitee.wang/pig/pigx/issues/613
|
||||
if (name.startsWith(CacheConstants.GLOBALLY)) {
|
||||
return super.getCache(name);
|
||||
}
|
||||
return super.getCache(TenantContextHolder.getTenantId() + StrUtil.COLON + name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.cache;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 扩展redis-cache支持注解cacheName添加超时时间
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Configuration
|
||||
@AutoConfigureAfter({ RedisAutoConfiguration.class })
|
||||
@ConditionalOnBean({ RedisConnectionFactory.class })
|
||||
@EnableConfigurationProperties(CacheProperties.class)
|
||||
public class RedisCacheAutoConfiguration {
|
||||
|
||||
private final CacheProperties cacheProperties;
|
||||
|
||||
private final CacheManagerCustomizers customizerInvoker;
|
||||
|
||||
@Nullable
|
||||
private final RedisCacheConfiguration redisCacheConfiguration;
|
||||
|
||||
RedisCacheAutoConfiguration(CacheProperties cacheProperties, CacheManagerCustomizers customizerInvoker,
|
||||
ObjectProvider<RedisCacheConfiguration> redisCacheConfiguration) {
|
||||
this.cacheProperties = cacheProperties;
|
||||
this.customizerInvoker = customizerInvoker;
|
||||
this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory, ResourceLoader resourceLoader) {
|
||||
DefaultRedisCacheWriter redisCacheWriter = new DefaultRedisCacheWriter(connectionFactory);
|
||||
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration(resourceLoader.getClassLoader());
|
||||
List<String> cacheNames = this.cacheProperties.getCacheNames();
|
||||
Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
|
||||
if (!cacheNames.isEmpty()) {
|
||||
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
|
||||
cacheNames.forEach(it -> cacheConfigMap.put(it, cacheConfiguration));
|
||||
initialCaches.putAll(cacheConfigMap);
|
||||
}
|
||||
RedisAutoCacheManager cacheManager = new RedisAutoCacheManager(redisCacheWriter, cacheConfiguration,
|
||||
initialCaches, true);
|
||||
cacheManager.setTransactionAware(false);
|
||||
return this.customizerInvoker.customize(cacheManager);
|
||||
}
|
||||
|
||||
private RedisCacheConfiguration determineConfiguration(ClassLoader classLoader) {
|
||||
if (this.redisCacheConfiguration != null) {
|
||||
return this.redisCacheConfiguration;
|
||||
}
|
||||
else {
|
||||
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
|
||||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
|
||||
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
|
||||
if (redisProperties.getTimeToLive() != null) {
|
||||
config = config.entryTtl(redisProperties.getTimeToLive());
|
||||
}
|
||||
|
||||
if (redisProperties.getKeyPrefix() != null) {
|
||||
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
|
||||
}
|
||||
|
||||
if (!redisProperties.isCacheNullValues()) {
|
||||
config = config.disableCachingNullValues();
|
||||
}
|
||||
|
||||
if (!redisProperties.isUseKeyPrefix()) {
|
||||
config = config.disableKeyPrefix();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.cache;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CacheManagerCustomizers配置
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnMissingBean(CacheManagerCustomizers.class)
|
||||
public class RedisCacheManagerConfiguration {
|
||||
|
||||
@Bean
|
||||
public CacheManagerCustomizers cacheManagerCustomizers(
|
||||
ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
|
||||
return new CacheManagerCustomizers(customizers.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.pig4cloud.pigx.common.data.cache;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/2/4
|
||||
*
|
||||
* redis message 信道相关配置
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class RedisMessageConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(redisConnectionFactory);
|
||||
return container;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.cache;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* RedisTemplate 配置
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
@AutoConfigureBefore(name = { "org.redisson.spring.starter.RedissonAutoConfiguration",
|
||||
"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration" })
|
||||
public class RedisTemplateConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
|
||||
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/8/30 数据权限查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DataScope extends HashMap {
|
||||
|
||||
/**
|
||||
* 限制范围的字段名称
|
||||
*/
|
||||
private String scopeDeptName = "dept_id";
|
||||
|
||||
/**
|
||||
* 本人权限范围字段
|
||||
*/
|
||||
private String scopeUserName = "create_by";
|
||||
|
||||
/**
|
||||
* 具体的数据范围
|
||||
*/
|
||||
private List<Long> deptList = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 具体查询的用户数据权限范围
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 是否只查询本部门
|
||||
*/
|
||||
private Boolean isOnly = false;
|
||||
|
||||
/**
|
||||
* 函数名称,默认 SELECT * ;
|
||||
*
|
||||
* <ul>
|
||||
* <li>COUNT(1)</li>
|
||||
* </ul>
|
||||
*/
|
||||
private DataScopeFuncEnum func = DataScopeFuncEnum.ALL;
|
||||
|
||||
/**
|
||||
* of 获取实例
|
||||
*/
|
||||
public static DataScope of() {
|
||||
return new DataScope();
|
||||
}
|
||||
|
||||
public DataScope deptIds(List<Long> deptIds) {
|
||||
this.deptList = deptIds;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DataScope only(boolean isOnly) {
|
||||
this.isOnly = isOnly;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DataScope func(DataScopeFuncEnum func) {
|
||||
this.func = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据权限函数类型
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2020-06-17
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataScopeFuncEnum {
|
||||
|
||||
/**
|
||||
* 查询全部数据 SELECT * FROM (originSql) temp_data_scope WHERE temp_data_scope.dept_id IN
|
||||
* (1)
|
||||
*/
|
||||
ALL("*", "全部"),
|
||||
|
||||
/**
|
||||
* 查询函数COUNT SELECT COUNT(1) FROM (originSql) temp_data_scope WHERE
|
||||
* temp_data_scope.dept_id IN (1)
|
||||
*/
|
||||
COUNT("COUNT(1)", "自定义");
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019-09-07
|
||||
* <p>
|
||||
* data scope 判断处理器,抽象服务扩展
|
||||
*/
|
||||
public interface DataScopeHandle {
|
||||
|
||||
/**
|
||||
* 计算用户数据权限
|
||||
* @param dataScope 数据权限设置
|
||||
* @return 返回true表示无需进行数据过滤处理,返回false表示需要进行数据过滤
|
||||
*/
|
||||
Boolean calcScope(DataScope dataScope);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import lombok.Setter;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/11/29
|
||||
*/
|
||||
public class DataScopeInnerInterceptor implements DataScopeInterceptor {
|
||||
|
||||
@Setter
|
||||
private DataScopeHandle dataScopeHandle;
|
||||
|
||||
@Override
|
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
|
||||
ResultHandler resultHandler, BoundSql boundSql) {
|
||||
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
|
||||
|
||||
String originalSql = boundSql.getSql();
|
||||
Object parameterObject = boundSql.getParameterObject();
|
||||
|
||||
// 查找参数中包含DataScope类型的参数
|
||||
DataScope dataScope = findDataScopeObject(parameterObject);
|
||||
if (dataScope == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 返回true 不拦截直接返回原始 SQL (只针对 * 查询)
|
||||
if (dataScopeHandle.calcScope(dataScope) && DataScopeFuncEnum.ALL.equals(dataScope.getFunc())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 返回true 不拦截直接返回原始 SQL (只针对 COUNT 查询)
|
||||
if (dataScopeHandle.calcScope(dataScope) && DataScopeFuncEnum.COUNT.equals(dataScope.getFunc())) {
|
||||
mpBs.sql(String.format("SELECT %s FROM (%s) temp_data_scope", dataScope.getFunc().getType(), originalSql));
|
||||
return;
|
||||
}
|
||||
|
||||
List<Long> deptIds = dataScope.getDeptList();
|
||||
|
||||
// 1.无数据权限限制,则直接返回 0 条数据
|
||||
if (CollUtil.isEmpty(deptIds) && StrUtil.isBlank(dataScope.getUsername())) {
|
||||
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE 1 = 2",
|
||||
dataScope.getFunc().getType(), originalSql);
|
||||
}
|
||||
// 2.如果为本人权限则走下面
|
||||
else if (StrUtil.isNotBlank(dataScope.getUsername())) {
|
||||
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s'",
|
||||
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername());
|
||||
}
|
||||
// 3.都没有,则是其他权限,走下面
|
||||
else {
|
||||
String join = CollectionUtil.join(deptIds, ",");
|
||||
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s IN (%s)",
|
||||
dataScope.getFunc().getType(), originalSql, dataScope.getScopeDeptName(), join);
|
||||
}
|
||||
|
||||
mpBs.sql(originalSql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找参数是否包括DataScope对象
|
||||
* @param parameterObj 参数列表
|
||||
* @return DataScope
|
||||
*/
|
||||
private DataScope findDataScopeObject(Object parameterObj) {
|
||||
if (parameterObj instanceof DataScope) {
|
||||
return (DataScope) parameterObj;
|
||||
}
|
||||
else if (parameterObj instanceof Map) {
|
||||
for (Object val : ((Map<?, ?>) parameterObj).values()) {
|
||||
if (val instanceof DataScope) {
|
||||
return (DataScope) val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
|
||||
/**
|
||||
* 数据权限抽象
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2022/8/9
|
||||
*/
|
||||
public interface DataScopeInterceptor extends InnerInterceptor {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.github.yulichang.injector.MPJSqlInjector;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支持自定义数据权限方法注入
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2020-06-17
|
||||
*/
|
||||
public class DataScopeSqlInjector extends MPJSqlInjector {
|
||||
|
||||
@Override
|
||||
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
|
||||
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
|
||||
methodList.add(new SelectListByScope());
|
||||
methodList.add(new SelectPageByScope());
|
||||
methodList.add(new SelectCountByScope());
|
||||
return methodList;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/12/26
|
||||
* <p>
|
||||
* 数据权限类型
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataScopeTypeEnum {
|
||||
|
||||
/**
|
||||
* 查询全部数据
|
||||
*/
|
||||
ALL(0, "全部"),
|
||||
|
||||
/**
|
||||
* 自定义
|
||||
*/
|
||||
CUSTOM(1, "自定义"),
|
||||
|
||||
/**
|
||||
* 本级及子级
|
||||
*/
|
||||
OWN_CHILD_LEVEL(2, "本级及子级"),
|
||||
|
||||
/**
|
||||
* 本级
|
||||
*/
|
||||
OWN_LEVEL(3, "本级"),
|
||||
|
||||
/**
|
||||
* 本人
|
||||
*/
|
||||
SELF_LEVEL(4, "本人");
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final int type;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 扩展通用 Mapper,支持数据权限 和批量插入
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2020-06-17
|
||||
*/
|
||||
public interface PigxBaseMapper<T> extends MPJBaseMapper<T> {
|
||||
|
||||
/**
|
||||
* 根据 entity 条件,查询全部记录
|
||||
* @param queryWrapper 实体对象封装操作类(可以为 null)
|
||||
* @param scope 数据权限范围
|
||||
* @return List<T>
|
||||
*/
|
||||
List<T> selectListByScope(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper, DataScope scope);
|
||||
|
||||
/**
|
||||
* 根据 entity 条件,查询全部记录(并翻页)
|
||||
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
|
||||
* @param queryWrapper 实体对象封装操作类(可以为 null)
|
||||
* @param scope 数据权限范围
|
||||
* @return Page
|
||||
*/
|
||||
<E extends IPage<T>> E selectPageByScope(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper,
|
||||
DataScope scope);
|
||||
|
||||
/**
|
||||
* 根据 Wrapper 条件,查询总记录数
|
||||
* @param queryWrapper 实体对象封装操作类(可以为 null)
|
||||
* @param scope 数据权限范围
|
||||
* @return Integer
|
||||
*/
|
||||
Long selectCountByScope(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper, DataScope scope);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysDept;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysRole;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteDataScopeService;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019-09-07
|
||||
* <p>
|
||||
* 默认data scope 判断处理器
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class PigxDefaultDatascopeHandle implements DataScopeHandle {
|
||||
|
||||
private final RemoteDataScopeService dataScopeService;
|
||||
|
||||
/**
|
||||
* 计算用户数据权限
|
||||
* @param dataScope 数据权限范围
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Boolean calcScope(DataScope dataScope) {
|
||||
PigxUser user = SecurityUtils.getUser();
|
||||
// toc 客户端不进行数据权限
|
||||
if (UserTypeEnum.TOC.getStatus().equals(user.getUserType())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> roleIdList = user.getAuthorities().stream().map(GrantedAuthority::getAuthority)
|
||||
.filter(authority -> authority.startsWith(SecurityConstants.ROLE))
|
||||
.map(authority -> authority.split(StrUtil.UNDERLINE)[1]).collect(Collectors.toList());
|
||||
|
||||
List<Long> deptList = dataScope.getDeptList();
|
||||
|
||||
// 当前用户的角色为空 , 返回false
|
||||
if (CollectionUtil.isEmpty(roleIdList)) {
|
||||
return false;
|
||||
}
|
||||
// @formatter:off
|
||||
SysRole role = RetOps.of(dataScopeService.getRoleList(roleIdList))
|
||||
.getData()
|
||||
.orElseGet(Collections::emptyList)
|
||||
.stream()
|
||||
.min(Comparator.comparingInt(SysRole::getDsType)).orElse(null);
|
||||
// @formatter:on
|
||||
// 角色有可能已经删除了
|
||||
if (role == null) {
|
||||
return false;
|
||||
}
|
||||
Integer dsType = role.getDsType();
|
||||
// 查询全部
|
||||
if (DataScopeTypeEnum.ALL.getType() == dsType) {
|
||||
return true;
|
||||
}
|
||||
// 自定义
|
||||
if (DataScopeTypeEnum.CUSTOM.getType() == dsType && StrUtil.isNotBlank(role.getDsScope())) {
|
||||
String dsScope = role.getDsScope();
|
||||
deptList.addAll(
|
||||
Arrays.stream(dsScope.split(StrUtil.COMMA)).map(Long::parseLong).collect(Collectors.toList()));
|
||||
}
|
||||
// 查询本级及其下级
|
||||
if (DataScopeTypeEnum.OWN_CHILD_LEVEL.getType() == dsType) {
|
||||
// @formatter:off
|
||||
List<Long> deptIdList = RetOps.of(dataScopeService.getDescendantList(user.getDeptId()))
|
||||
.getData()
|
||||
.orElseGet(Collections::emptyList)
|
||||
.stream()
|
||||
.map(SysDept::getDeptId).collect(Collectors.toList());
|
||||
// @formatter:on
|
||||
deptList.addAll(deptIdList);
|
||||
}
|
||||
// 只查询本级
|
||||
if (DataScopeTypeEnum.OWN_LEVEL.getType() == dsType) {
|
||||
deptList.add(user.getDeptId());
|
||||
}
|
||||
|
||||
// 只查询本人
|
||||
if (DataScopeTypeEnum.SELF_LEVEL.getType() == dsType) {
|
||||
dataScope.setUsername(user.getUsername());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlSource;
|
||||
|
||||
/**
|
||||
* 扩展支持COUNT查询数量
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2020/6/17
|
||||
*/
|
||||
public class SelectCountByScope extends AbstractMethod {
|
||||
|
||||
public SelectCountByScope() {
|
||||
super("selectCountByScope");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
|
||||
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
|
||||
|
||||
String sql = String.format(sqlMethod.getSql(), this.sqlFirst(), this.sqlSelectColumns(tableInfo, true),
|
||||
tableInfo.getTableName(), this.sqlWhereEntityWrapper(true, tableInfo), this.sqlOrderBy(tableInfo),
|
||||
this.sqlComment());
|
||||
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
|
||||
|
||||
return this.addSelectMappedStatementForOther(mapperClass, sqlSource, Long.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlSource;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/4/26
|
||||
*/
|
||||
public class SelectListByScope extends AbstractMethod {
|
||||
|
||||
public SelectListByScope() {
|
||||
super("selectListByScope");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
|
||||
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
|
||||
String sql = String.format(sqlMethod.getSql(), this.sqlFirst(), this.sqlSelectColumns(tableInfo, true),
|
||||
tableInfo.getTableName(), this.sqlWhereEntityWrapper(true, tableInfo), this.sqlOrderBy(tableInfo),
|
||||
this.sqlComment());
|
||||
SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
|
||||
return this.addSelectMappedStatementForTable(mapperClass, sqlSource, tableInfo);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.pig4cloud.pigx.common.data.datascope;
|
||||
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlSource;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/4/26
|
||||
*/
|
||||
public class SelectPageByScope extends AbstractMethod {
|
||||
|
||||
public SelectPageByScope() {
|
||||
super("selectPageByScope");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
|
||||
SqlMethod sqlMethod = SqlMethod.SELECT_PAGE;
|
||||
String sql = String.format(sqlMethod.getSql(), this.sqlFirst(), this.sqlSelectColumns(tableInfo, true),
|
||||
tableInfo.getTableName(), this.sqlWhereEntityWrapper(true, tableInfo), this.sqlOrderBy(tableInfo),
|
||||
this.sqlComment());
|
||||
SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
|
||||
return this.addSelectMappedStatementForTable(mapperClass, sqlSource, tableInfo);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.pig4cloud.pigx.common.data.handler;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Mybatis数组,符串互转
|
||||
* <p>
|
||||
* MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
|
||||
*
|
||||
* @author xuzihui
|
||||
* @date 2019-11-20
|
||||
*/
|
||||
@MappedTypes(value = { Long[].class })
|
||||
@MappedJdbcTypes(value = JdbcType.VARCHAR)
|
||||
public class JsonLongArrayTypeHandler extends BaseTypeHandler<Long[]> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, Long[] parameter, JdbcType jdbcType)
|
||||
throws SQLException {
|
||||
ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Long[] getNullableResult(ResultSet rs, String columnName) {
|
||||
String reString = rs.getString(columnName);
|
||||
return Convert.toLongArray(reString);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Long[] getNullableResult(ResultSet rs, int columnIndex) {
|
||||
String reString = rs.getString(columnIndex);
|
||||
return Convert.toLongArray(reString);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Long[] getNullableResult(CallableStatement cs, int columnIndex) {
|
||||
String reString = cs.getString(columnIndex);
|
||||
return Convert.toLongArray(reString);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.pig4cloud.pigx.common.data.handler;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Mybatis数组,符串互转
|
||||
* <p>
|
||||
* MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
|
||||
*
|
||||
* @author xuzihui
|
||||
* @date 2019-11-20
|
||||
*/
|
||||
@MappedTypes(value = { String[].class })
|
||||
@MappedJdbcTypes(value = JdbcType.VARCHAR)
|
||||
public class JsonStringArrayTypeHandler extends BaseTypeHandler<String[]> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, String[] parameter, JdbcType jdbcType)
|
||||
throws SQLException {
|
||||
ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String[] getNullableResult(ResultSet rs, String columnName) {
|
||||
String reString = rs.getString(columnName);
|
||||
return Convert.toStrArray(reString);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String[] getNullableResult(ResultSet rs, int columnIndex) {
|
||||
String reString = rs.getString(columnIndex);
|
||||
return Convert.toStrArray(reString);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String[] getNullableResult(CallableStatement cs, int columnIndex) {
|
||||
String reString = cs.getString(columnIndex);
|
||||
return Convert.toStrArray(reString);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.mybatis;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.druid.DbType;
|
||||
import com.alibaba.druid.filter.FilterChain;
|
||||
import com.alibaba.druid.filter.FilterEventAdapter;
|
||||
import com.alibaba.druid.proxy.jdbc.JdbcParameter;
|
||||
import com.alibaba.druid.proxy.jdbc.ResultSetProxy;
|
||||
import com.alibaba.druid.proxy.jdbc.StatementProxy;
|
||||
import com.alibaba.druid.sql.SQLUtils;
|
||||
import com.alibaba.druid.sql.ast.SQLStatement;
|
||||
import com.alibaba.druid.sql.visitor.SchemaStatVisitor;
|
||||
import com.alibaba.druid.stat.TableStat;
|
||||
import com.alibaba.druid.util.StringUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 打印可执行的 sql 日志
|
||||
*
|
||||
* <p>
|
||||
* 参考:https://jfinal.com/share/2204
|
||||
* </p>
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class DruidSqlLogFilter extends FilterEventAdapter {
|
||||
|
||||
private static final SQLUtils.FormatOption FORMAT_OPTION = new SQLUtils.FormatOption(false, false);
|
||||
|
||||
private final PigxMybatisProperties properties;
|
||||
|
||||
@Override
|
||||
protected void statementExecuteBefore(StatementProxy statement, String sql) {
|
||||
statement.setLastExecuteStartNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void statementExecuteBatchBefore(StatementProxy statement) {
|
||||
statement.setLastExecuteStartNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void statementExecuteUpdateBefore(StatementProxy statement, String sql) {
|
||||
statement.setLastExecuteStartNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void statementExecuteQueryBefore(StatementProxy statement, String sql) {
|
||||
statement.setLastExecuteStartNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) {
|
||||
statement.setLastExecuteTimeNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
|
||||
statement.setLastExecuteTimeNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {
|
||||
statement.setLastExecuteTimeNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {
|
||||
statement.setLastExecuteTimeNano();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void statement_close(FilterChain chain, StatementProxy statement) throws SQLException {
|
||||
// 先调用父类关闭 statement
|
||||
super.statement_close(chain, statement);
|
||||
// 支持动态开启
|
||||
if (!properties.isShowSql()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 是否开启调试
|
||||
if (!log.isInfoEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 打印可执行的 sql
|
||||
String sql = statement.getBatchSql();
|
||||
// sql 为空直接返回
|
||||
if (StringUtils.isEmpty(sql)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String dbType = statement.getConnectionProxy().getDirectDataSource().getDbType();
|
||||
|
||||
// 判断表名是配置了匹配过滤
|
||||
if (CollUtil.isNotEmpty(properties.getSkipTable())) {
|
||||
List<String> skipTableList = properties.getSkipTable();
|
||||
|
||||
List<String> tableNameList = getTablesBydruid(sql, dbType);
|
||||
if (tableNameList.stream().anyMatch(tableName -> StrUtil.containsAnyIgnoreCase(tableName,
|
||||
ArrayUtil.toArray(skipTableList, String.class)))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int parametersSize = statement.getParametersSize();
|
||||
List<Object> parameters = new ArrayList<>(parametersSize);
|
||||
for (int i = 0; i < parametersSize; ++i) {
|
||||
// 转换参数,处理 java8 时间
|
||||
parameters.add(getJdbcParameter(statement.getParameter(i)));
|
||||
}
|
||||
String formattedSql = SQLUtils.format(sql, DbType.of(dbType), parameters, FORMAT_OPTION);
|
||||
printSql(formattedSql, statement);
|
||||
}
|
||||
|
||||
private static Object getJdbcParameter(JdbcParameter jdbcParam) {
|
||||
if (jdbcParam == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = jdbcParam.getValue();
|
||||
// 处理 java8 时间
|
||||
if (value instanceof TemporalAccessor) {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void printSql(String sql, StatementProxy statement) {
|
||||
// 打印 sql
|
||||
String sqlLogger = "\n\n======= Sql Logger ======================" + "\n{}"
|
||||
+ "\n======= Sql Execute Time: {} =======\n";
|
||||
log.info(sqlLogger, sql.trim(), format(statement.getLastExecuteTimeNano()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化执行时间,单位为 ms 和 s,保留三位小数
|
||||
* @param nanos 纳秒
|
||||
* @return 格式化后的时间
|
||||
*/
|
||||
private static String format(long nanos) {
|
||||
if (nanos < 1) {
|
||||
return "0ms";
|
||||
}
|
||||
double millis = (double) nanos / (1000 * 1000);
|
||||
// 不够 1 ms,最小单位为 ms
|
||||
if (millis > 1000) {
|
||||
return String.format("%.3fs", millis / 1000);
|
||||
}
|
||||
else {
|
||||
return String.format("%.3fms", millis);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SQL中提取表名(sql中出现的所有表)
|
||||
* @param sql sql语句
|
||||
* @param dbType dbType
|
||||
* @return List<String>
|
||||
*/
|
||||
public static List<String> getTablesBydruid(String sql, String dbType) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
|
||||
for (SQLStatement stmt : stmtList) {
|
||||
// 也可以用更精确的解析器,如MySqlSchemaStatVisitor
|
||||
SchemaStatVisitor visitor = new SchemaStatVisitor();
|
||||
stmt.accept(visitor);
|
||||
Map<TableStat.Name, TableStat> tables = visitor.getTables();
|
||||
for (TableStat.Name name : tables.keySet()) {
|
||||
result.add(name.getName());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteDataScopeService;
|
||||
import com.pig4cloud.pigx.common.data.datascope.DataScopeInnerInterceptor;
|
||||
import com.pig4cloud.pigx.common.data.datascope.DataScopeInterceptor;
|
||||
import com.pig4cloud.pigx.common.data.datascope.DataScopeSqlInjector;
|
||||
import com.pig4cloud.pigx.common.data.datascope.PigxDefaultDatascopeHandle;
|
||||
import com.pig4cloud.pigx.common.data.resolver.SqlFilterArgumentResolver;
|
||||
import com.pig4cloud.pigx.common.data.tenant.PigxTenantConfigProperties;
|
||||
import com.pig4cloud.pigx.common.data.tenant.PigxTenantHandler;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020-02-08
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnBean(DataSource.class)
|
||||
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(PigxMybatisProperties.class)
|
||||
public class MybatisPlusConfiguration implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 增加请求参数解析器,对请求中的参数注入SQL 检查
|
||||
* @param resolverList
|
||||
*/
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolverList) {
|
||||
resolverList.add(new SqlFilterArgumentResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* mybatis plus 拦截器配置
|
||||
* @return PigxDefaultDatascopeHandle
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor(TenantLineInnerInterceptor tenantLineInnerInterceptor,
|
||||
DataScopeInterceptor dataScopeInterceptor) {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 注入多租户支持
|
||||
interceptor.addInnerInterceptor(tenantLineInnerInterceptor);
|
||||
// 数据权限
|
||||
interceptor.addInnerInterceptor(dataScopeInterceptor);
|
||||
// 分页支持
|
||||
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
|
||||
paginationInnerInterceptor.setMaxLimit(1000L);
|
||||
interceptor.addInnerInterceptor(paginationInnerInterceptor);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户维护处理器对象
|
||||
* @return 处理后的租户维护处理器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TenantLineInnerInterceptor tenantLineInnerInterceptor(PigxTenantConfigProperties tenantConfigProperties) {
|
||||
TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor();
|
||||
tenantLineInnerInterceptor.setTenantLineHandler(new PigxTenantHandler(tenantConfigProperties));
|
||||
return tenantLineInnerInterceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限拦截器
|
||||
* @return DataScopeInterceptor
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(PigxUser.class)
|
||||
public DataScopeInterceptor dataScopeInterceptor(RemoteDataScopeService dataScopeService) {
|
||||
DataScopeInnerInterceptor dataScopeInnerInterceptor = new DataScopeInnerInterceptor();
|
||||
dataScopeInnerInterceptor.setDataScopeHandle(new PigxDefaultDatascopeHandle(dataScopeService));
|
||||
return dataScopeInnerInterceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展 mybatis-plus baseMapper 支持数据权限
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnBean(DataScopeInterceptor.class)
|
||||
public DataScopeSqlInjector dataScopeSqlInjector() {
|
||||
return new DataScopeSqlInjector();
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 日志格式化
|
||||
* @return DruidSqlLogFilter
|
||||
*/
|
||||
@Bean
|
||||
public DruidSqlLogFilter sqlLogFilter(PigxMybatisProperties properties) {
|
||||
return new DruidSqlLogFilter(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计字段自动填充
|
||||
* @return {@link MetaObjectHandler}
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() {
|
||||
return new MybatisPlusMetaObjectHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库方言配置
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public DatabaseIdProvider databaseIdProvider() {
|
||||
VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("SQL Server", "mssql");
|
||||
databaseIdProvider.setProperties(properties);
|
||||
return databaseIdProvider;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.pig4cloud.pigx.common.data.mybatis;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* MybatisPlus 自动填充配置
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Slf4j
|
||||
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
log.debug("mybatis plus start insert fill ....");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 审计字段自动填充,覆盖用户输入
|
||||
fillValIfNullByName("createTime", now, metaObject, true);
|
||||
fillValIfNullByName("updateTime", now, metaObject, true);
|
||||
fillValIfNullByName("createBy", getUserName(), metaObject, true);
|
||||
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
|
||||
|
||||
// 删除标记自动填充
|
||||
fillValIfNullByName("delFlag", CommonConstants.STATUS_NORMAL, metaObject, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
log.debug("mybatis plus start update fill ....");
|
||||
fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
|
||||
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置
|
||||
* @param fieldName 属性名
|
||||
* @param fieldVal 属性值
|
||||
* @param metaObject MetaObject
|
||||
* @param isCover 是否覆盖原有值,避免更新操作手动入参
|
||||
*/
|
||||
private static void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) {
|
||||
// 0. 如果填充值为空
|
||||
if (fieldVal == null) {
|
||||
return;
|
||||
}
|
||||
// 1. 没有 get 方法
|
||||
if (!metaObject.hasSetter(fieldName)) {
|
||||
return;
|
||||
}
|
||||
// 2. 如果用户有手动设置的值
|
||||
Object userSetValue = metaObject.getValue(fieldName);
|
||||
String setValueStr = StrUtil.str(userSetValue, Charset.defaultCharset());
|
||||
if (StrUtil.isNotBlank(setValueStr) && !isCover) {
|
||||
return;
|
||||
}
|
||||
// 3. field 类型相同时设置
|
||||
Class<?> getterType = metaObject.getGetterType(fieldName);
|
||||
if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
|
||||
metaObject.setValue(fieldName, fieldVal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 spring security 当前的用户名
|
||||
* @return 当前用户名
|
||||
*/
|
||||
private String getUserName() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
// 匿名接口直接返回
|
||||
if (authentication instanceof AnonymousAuthenticationToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Optional.ofNullable(authentication).isPresent()) {
|
||||
return authentication.getName();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.pig4cloud.pigx.common.data.mybatis;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mybatis 配置
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2021/6/3
|
||||
*/
|
||||
@Data
|
||||
@RefreshScope
|
||||
@ConfigurationProperties("pigx.mybatis")
|
||||
public class PigxMybatisProperties {
|
||||
|
||||
/**
|
||||
* 是否打印可执行 sql
|
||||
*/
|
||||
private boolean showSql = true;
|
||||
|
||||
/**
|
||||
* 跳过表
|
||||
*/
|
||||
private List<String> skipTable = new ArrayList<>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.pig4cloud.pigx.common.data.resolver;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysDictItem;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteDictService;
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author fxz
|
||||
* @date 2022/3/24 字典解析器
|
||||
*/
|
||||
@UtilityClass
|
||||
public class DictResolver {
|
||||
|
||||
/**
|
||||
* 根据字典类型获取所有字典项
|
||||
* @param type 字典类型
|
||||
* @return 字典数据项集合
|
||||
*/
|
||||
public List<SysDictItem> getDictItemsByType(String type) {
|
||||
Assert.isTrue(StringUtils.isNotBlank(type), "参数不合法");
|
||||
|
||||
RemoteDictService remoteDictService = SpringContextHolder.getBean(RemoteDictService.class);
|
||||
|
||||
return remoteDictService.getDictByType(type).getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型以及字典项字典值获取字典标签
|
||||
* @param type 字典类型
|
||||
* @param itemValue 字典项字典值
|
||||
* @return 字典项标签值
|
||||
*/
|
||||
public String getDictItemLabel(String type, String itemValue) {
|
||||
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemValue), "参数不合法");
|
||||
|
||||
SysDictItem sysDictItem = getDictItemByItemValue(type, itemValue);
|
||||
|
||||
return ObjectUtils.isNotEmpty(sysDictItem) ? sysDictItem.getLabel() : StringPool.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型以及字典标签获取字典值
|
||||
* @param type 字典类型
|
||||
* @param itemLabel 字典数据标签
|
||||
* @return 字典数据项值
|
||||
*/
|
||||
public String getDictItemValue(String type, String itemLabel) {
|
||||
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemLabel), "参数不合法");
|
||||
|
||||
SysDictItem sysDictItem = getDictItemByItemLabel(type, itemLabel);
|
||||
|
||||
return ObjectUtils.isNotEmpty(sysDictItem) ? sysDictItem.getItemValue() : StringPool.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型以及字典值获取字典项
|
||||
* @param type 字典类型
|
||||
* @param itemValue 字典数据值
|
||||
* @return 字典数据项
|
||||
*/
|
||||
public SysDictItem getDictItemByItemValue(String type, String itemValue) {
|
||||
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemValue), "参数不合法");
|
||||
|
||||
List<SysDictItem> dictItemList = getDictItemsByType(type);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(dictItemList)) {
|
||||
return dictItemList.stream().filter(item -> itemValue.equals(item.getItemValue())).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型以及字典标签获取字典项
|
||||
* @param type 字典类型
|
||||
* @param itemLabel 字典数据项标签
|
||||
* @return 字典数据项
|
||||
*/
|
||||
public SysDictItem getDictItemByItemLabel(String type, String itemLabel) {
|
||||
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemLabel), "参数不合法");
|
||||
|
||||
List<SysDictItem> dictItemList = getDictItemsByType(type);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(dictItemList)) {
|
||||
return dictItemList.stream().filter(item -> itemLabel.equals(item.getLabel())).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.pig4cloud.pigx.common.data.resolver;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteParamService;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/5/12
|
||||
* <p>
|
||||
* 系统参数配置解析器
|
||||
*/
|
||||
@UtilityClass
|
||||
public class ParamResolver {
|
||||
|
||||
/**
|
||||
* 根据多个key 查询value 配置 结果使用hutool 的maputil 进行包装处理 MapUtil.getBool(result,key)
|
||||
* @param key key
|
||||
* @return Map<String,Object>
|
||||
*/
|
||||
public Map<String, Object> getMap(String... key) {
|
||||
// 校验入参是否合法
|
||||
if (Objects.isNull(key)) {
|
||||
throw new IllegalArgumentException("参数不合法");
|
||||
}
|
||||
|
||||
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
|
||||
return remoteParamService.getByKeys(key, SecurityConstants.FROM_IN).getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据key 查询value 配置
|
||||
* @param key key
|
||||
* @param defaultVal 默认值
|
||||
* @return value
|
||||
*/
|
||||
public Long getLong(String key, Long... defaultVal) {
|
||||
return checkAndGet(key, Long.class, defaultVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据key 查询value 配置
|
||||
* @param key key
|
||||
* @param defaultVal 默认值
|
||||
* @return value
|
||||
*/
|
||||
public String getStr(String key, String... defaultVal) {
|
||||
return checkAndGet(key, String.class, defaultVal);
|
||||
}
|
||||
|
||||
private <T> T checkAndGet(String key, Class<T> clazz, T... defaultVal) {
|
||||
// 校验入参是否合法
|
||||
if (StrUtil.isBlank(key) || defaultVal.length > 1) {
|
||||
throw new IllegalArgumentException("参数不合法");
|
||||
}
|
||||
|
||||
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
|
||||
|
||||
String result = remoteParamService.getByKey(key, SecurityConstants.FROM_IN).getData();
|
||||
|
||||
if (StrUtil.isNotBlank(result)) {
|
||||
return Convert.convert(clazz, result);
|
||||
}
|
||||
|
||||
if (defaultVal.length == 1) {
|
||||
return Convert.convert(clazz, defaultVal.clone()[0]);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.resolver;
|
||||
|
||||
import cn.hutool.core.comparator.CompareUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pig4cloud.pigx.common.core.exception.CheckedException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019-06-24
|
||||
* <p>
|
||||
* 解决Mybatis Plus Order By SQL注入问题
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final static String[] KEYWORDS = { "master", "truncate", "insert", "select", "delete", "update", "declare",
|
||||
"alter", "drop", "sleep", "extractvalue", "concat" };
|
||||
|
||||
/**
|
||||
* 判断Controller是否包含page 参数
|
||||
* @param parameter 参数
|
||||
* @return 是否过滤
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterType().equals(Page.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parameter 入参集合
|
||||
* @param mavContainer model 和 view
|
||||
* @param webRequest web相关
|
||||
* @param binderFactory 入参解析
|
||||
* @return 检查后新的page对象
|
||||
* <p>
|
||||
* page 只支持查询 GET .如需解析POST获取请求报文体处理
|
||||
*/
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
|
||||
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
|
||||
String ascs = request.getParameter("ascs");
|
||||
String descs = request.getParameter("descs");
|
||||
|
||||
String current = request.getParameter("current");
|
||||
String size = request.getParameter("size");
|
||||
|
||||
Page page = new Page();
|
||||
if (StrUtil.isNotBlank(current)) {
|
||||
// 如果current page 小于零 视为不合法数据
|
||||
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
|
||||
throw new CheckedException("current page error");
|
||||
}
|
||||
page.setCurrent(Long.parseLong(current));
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(size)) {
|
||||
page.setSize(Long.parseLong(size));
|
||||
}
|
||||
|
||||
List<OrderItem> orderItemList = new ArrayList<>();
|
||||
Optional.ofNullable(ascs).ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.filter(sqlInjectPredicate()).map(OrderItem::asc).collect(Collectors.toList())));
|
||||
Optional.ofNullable(descs).ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.filter(sqlInjectPredicate()).map(OrderItem::desc).collect(Collectors.toList())));
|
||||
page.addOrder(orderItemList);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户输入里面有没有关键字
|
||||
* @return Predicate
|
||||
*/
|
||||
private Predicate<String> sqlInjectPredicate() {
|
||||
return sql -> Arrays.stream(KEYWORDS).noneMatch(keyword -> StrUtil.containsIgnoreCase(sql, keyword));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.pig4cloud.pigx.common.data.resolver;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/9/29
|
||||
* <p>
|
||||
* 租户字符串处理(方便其他模块获取)
|
||||
*/
|
||||
public class TenantKeyStrResolver implements KeyStrResolver {
|
||||
|
||||
/**
|
||||
* 传入字符串增加 租户编号:in
|
||||
* @param in 输入字符串
|
||||
* @param split 分割符
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String extract(String in, String split) {
|
||||
return TenantContextHolder.getTenantId() + split + in;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前租户ID
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String key() {
|
||||
return String.valueOf(TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/9/14
|
||||
*/
|
||||
@Slf4j
|
||||
public class PigxFeignTenantInterceptor implements RequestInterceptor {
|
||||
|
||||
@Override
|
||||
public void apply(RequestTemplate requestTemplate) {
|
||||
if (TenantContextHolder.getTenantId() == null) {
|
||||
log.debug("TTL 中的 租户ID为空,feign拦截器 >> 跳过");
|
||||
return;
|
||||
}
|
||||
requestTemplate.header(CommonConstants.TENANT_ID, TenantContextHolder.getTenantId().toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多租户配置
|
||||
*
|
||||
* @author oathsign
|
||||
*/
|
||||
@Data
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "pigx.tenant")
|
||||
public class PigxTenantConfigProperties {
|
||||
|
||||
/**
|
||||
* 维护租户列名称
|
||||
*/
|
||||
private String column = "tenant_id";
|
||||
|
||||
/**
|
||||
* 多租户的数据表集合
|
||||
*/
|
||||
private List<String> tables = new ArrayList<>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/4/29
|
||||
* <p>
|
||||
* 租户信息拦截
|
||||
*/
|
||||
@Configuration
|
||||
public class PigxTenantConfiguration {
|
||||
|
||||
@Bean
|
||||
public RequestInterceptor pigxFeignTenantInterceptor() {
|
||||
return new PigxFeignTenantInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClientHttpRequestInterceptor pigxTenantRequestInterceptor() {
|
||||
return new TenantRequestInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.NullValue;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018-12-26
|
||||
* <p>
|
||||
* 租户维护处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PigxTenantHandler implements TenantLineHandler {
|
||||
|
||||
private final PigxTenantConfigProperties properties;
|
||||
|
||||
/**
|
||||
* 获取租户 ID 值表达式,只支持单个 ID 值
|
||||
* <p>
|
||||
* @return 租户 ID 值表达式
|
||||
*/
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
log.debug("当前租户为 >> {}", tenantId);
|
||||
|
||||
if (tenantId == null) {
|
||||
return new NullValue();
|
||||
}
|
||||
return new LongValue(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户字段名
|
||||
* @return 租户字段名
|
||||
*/
|
||||
@Override
|
||||
public String getTenantIdColumn() {
|
||||
return properties.getColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表名判断是否忽略拼接多租户条件
|
||||
* <p>
|
||||
* 默认都要进行解析并拼接多租户条件
|
||||
* @param tableName 表名
|
||||
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
|
||||
*/
|
||||
@Override
|
||||
public boolean ignoreTable(String tableName) {
|
||||
// 判断是否跳过当前查询的租户过滤
|
||||
if (TenantContextHolder.getTenantSkip()) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
// 租户中ID 为空,查询全部,不进行过滤
|
||||
if (tenantId == null) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
return !properties.getTables().contains(tableName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 租户运行时代理<br/>
|
||||
* 这是一个工具类,用于切换租户运行时,保护租户ID上下文<br/>
|
||||
* 下面这段代码演示问题所在 <pre>
|
||||
* void methodA(){
|
||||
* // 因为某些特殊原因,需要手动指定租户
|
||||
* TenantContextHolder.setTenantId(1);
|
||||
* // do something ...
|
||||
* }
|
||||
* void methodB(){
|
||||
* // 因为某些特殊原因,需要手动指定租户
|
||||
* TenantContextHolder.setTenantId(2);
|
||||
* methodA();
|
||||
* // 此时租户ID已经变成 1
|
||||
* // do something ...
|
||||
* }
|
||||
* </pre> 嵌套设置租户ID会导致租户上下文难以维护,并且很难察觉,容易导致数据错乱。 推荐的写法: <pre>
|
||||
* void methodA(){
|
||||
* TenantBroker.RunAs(1,() -> {
|
||||
* // do something ...
|
||||
* });
|
||||
* }
|
||||
* void methodB(){
|
||||
* TenantBroker.RunAs(2,() -> {
|
||||
* methodA();
|
||||
* // do something ...
|
||||
* });
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author CJ (jclazz@outlook.com)
|
||||
* @date 2020/6/12
|
||||
* @since 3.9
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class TenantBroker {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RunAs<T> {
|
||||
|
||||
/**
|
||||
* 执行业务逻辑
|
||||
* @param tenantId
|
||||
* @throws Exception
|
||||
*/
|
||||
void run(T tenantId) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ApplyAs<T, R> {
|
||||
|
||||
/**
|
||||
* 执行业务逻辑,返回一个值
|
||||
* @param tenantId
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
R apply(T tenantId) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 以某个租户的身份运行
|
||||
* @param tenant 租户ID
|
||||
* @param func
|
||||
*/
|
||||
public void runAs(Long tenant, RunAs<Long> func) {
|
||||
final Long pre = TenantContextHolder.getTenantId();
|
||||
try {
|
||||
log.trace("TenantBroker 切换租户{} -> {}", pre, tenant);
|
||||
TenantContextHolder.setTenantId(tenant);
|
||||
func.run(tenant);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new TenantBrokerExceptionWrapper(e.getMessage(), e);
|
||||
}
|
||||
finally {
|
||||
log.trace("TenantBroker 还原租户{} <- {}", pre, tenant);
|
||||
TenantContextHolder.setTenantId(pre);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以某个租户的身份运行
|
||||
* @param tenant 租户ID
|
||||
* @param func
|
||||
* @param <T> 返回数据类型
|
||||
* @return
|
||||
*/
|
||||
public <T> T applyAs(Long tenant, ApplyAs<Long, T> func) {
|
||||
final Long pre = TenantContextHolder.getTenantId();
|
||||
try {
|
||||
log.trace("TenantBroker 切换租户{} -> {}", pre, tenant);
|
||||
TenantContextHolder.setTenantId(tenant);
|
||||
return func.apply(tenant);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new TenantBrokerExceptionWrapper(e.getMessage(), e);
|
||||
}
|
||||
finally {
|
||||
log.trace("TenantBroker 还原租户{} <- {}", pre, tenant);
|
||||
TenantContextHolder.setTenantId(pre);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以某个租户的身份运行
|
||||
* @param supplier
|
||||
* @param func
|
||||
*/
|
||||
public void runAs(Supplier<Long> supplier, RunAs<Long> func) {
|
||||
runAs(supplier.get(), func);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以某个租户的身份运行
|
||||
* @param supplier
|
||||
* @param func
|
||||
* @param <T> 返回数据类型
|
||||
* @return
|
||||
*/
|
||||
public <T> T applyAs(Supplier<Long> supplier, ApplyAs<Long, T> func) {
|
||||
return applyAs(supplier.get(), func);
|
||||
}
|
||||
|
||||
public static class TenantBrokerExceptionWrapper extends RuntimeException {
|
||||
|
||||
public TenantBrokerExceptionWrapper(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public TenantBrokerExceptionWrapper(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/10/4 租户工具类
|
||||
*/
|
||||
@UtilityClass
|
||||
public class TenantContextHolder {
|
||||
|
||||
private final ThreadLocal<Long> THREAD_LOCAL_TENANT = new TransmittableThreadLocal<>();
|
||||
|
||||
private final ThreadLocal<Boolean> THREAD_LOCAL_TENANT_SKIP_FLAG = new TransmittableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* TTL 设置租户ID<br/>
|
||||
* <b>谨慎使用此方法,避免嵌套调用。尽量使用 {@code TenantBroker} </b>
|
||||
* @param tenantId
|
||||
* @see TenantBroker
|
||||
*/
|
||||
public void setTenantId(Long tenantId) {
|
||||
THREAD_LOCAL_TENANT.set(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否过滤的标识
|
||||
*/
|
||||
public void setTenantSkip() {
|
||||
THREAD_LOCAL_TENANT_SKIP_FLAG.set(Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取TTL中的租户ID
|
||||
* @return
|
||||
*/
|
||||
public Long getTenantId() {
|
||||
return THREAD_LOCAL_TENANT.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否跳过租户过滤的标识
|
||||
* @return
|
||||
*/
|
||||
public Boolean getTenantSkip() {
|
||||
return THREAD_LOCAL_TENANT_SKIP_FLAG.get() != null && THREAD_LOCAL_TENANT_SKIP_FLAG.get();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
THREAD_LOCAL_TENANT.remove();
|
||||
THREAD_LOCAL_TENANT_SKIP_FLAG.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/9/13
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class TenantContextHolderFilter extends GenericFilterBean {
|
||||
|
||||
private final static String UNDEFINED_STR = "undefined";
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
|
||||
String headerTenantId = request.getHeader(CommonConstants.TENANT_ID);
|
||||
String paramTenantId = request.getParameter(CommonConstants.TENANT_ID);
|
||||
|
||||
log.debug("获取header中的租户ID为:{}", headerTenantId);
|
||||
|
||||
if (StrUtil.isNotBlank(headerTenantId) && !StrUtil.equals(UNDEFINED_STR, headerTenantId)) {
|
||||
TenantContextHolder.setTenantId(Long.parseLong(headerTenantId));
|
||||
}
|
||||
else if (StrUtil.isNotBlank(paramTenantId) && !StrUtil.equals(UNDEFINED_STR, paramTenantId)) {
|
||||
TenantContextHolder.setTenantId(Long.parseLong(paramTenantId));
|
||||
}
|
||||
else {
|
||||
TenantContextHolder.setTenantId(CommonConstants.TENANT_ID_1);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.pig4cloud.pigx.common.data.tenant;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/4/29
|
||||
* <p>
|
||||
* 传递 RestTemplate 请求的租户ID
|
||||
*/
|
||||
public class TenantRequestInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
|
||||
throws IOException {
|
||||
|
||||
if (TenantContextHolder.getTenantId() != null) {
|
||||
request.getHeaders().set(CommonConstants.TENANT_ID, String.valueOf(TenantContextHolder.getTenantId()));
|
||||
}
|
||||
|
||||
return execution.execute(request, body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
com.pig4cloud.pigx.common.data.cache.RedisTemplateConfiguration
|
||||
com.pig4cloud.pigx.common.data.cache.RedisMessageConfiguration
|
||||
com.pig4cloud.pigx.common.data.cache.RedisCacheManagerConfiguration
|
||||
com.pig4cloud.pigx.common.data.cache.RedisCacheAutoConfiguration
|
||||
com.pig4cloud.pigx.common.data.tenant.PigxTenantConfigProperties
|
||||
com.pig4cloud.pigx.common.data.tenant.TenantContextHolderFilter
|
||||
com.pig4cloud.pigx.common.data.tenant.PigxTenantConfiguration
|
||||
com.pig4cloud.pigx.common.data.mybatis.MybatisPlusConfiguration
|
||||
com.pig4cloud.pigx.common.data.resolver.TenantKeyStrResolver
|
||||
Reference in New Issue
Block a user