1.初始化版本,2.引入thirdpart模块,引入canteen模块,初始化菜品分类,spu

This commit is contained in:
zjf committed 2024-12-06 16:54:30 +08:00
commit aff77150d6
1595 files changed
+168833

No files matched your search

@@ -0,0 +1,90 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.cache;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.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 pjcloud
* <p>
* cachename = xx#ttl
*/
@Slf4j
public class RedisAutoCacheManager extends RedisCacheManager {
private static final String SPLIT_FLAG = "#";
private static final int CACHE_LENGTH = 2;
public RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
boolean allowInFlightCacheCreation,
Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheConfigurations);
}
@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/pjcloud/issues/613
if (name.startsWith(CacheConstants.GLOBALLY)) {
return super.getCache(name);
}
return super.getCache(TenantContextHolder.getTenantId() + StrUtil.COLON + name);
}
}
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.cache;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bsd.pjcloud.common.core.jackson.PjcloudJavaTimeModule;
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.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 扩展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();
}
@Primary
@Bean("cacheResolver")
public CacheManager redisCacheManager(ObjectProvider<RedisConnectionFactory> connectionFactoryObjectProvider) {
RedisConnectionFactory connectionFactory = connectionFactoryObjectProvider.getIfAvailable();
Objects.requireNonNull(connectionFactory, "Bean RedisConnectionFactory is null.");
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration();
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);
}
boolean allowInFlightCacheCreation = true;
boolean enableTransactions = false;
RedisAutoCacheManager cacheManager = new RedisAutoCacheManager(
redisCacheWriter, cacheConfiguration, allowInFlightCacheCreation, initialCaches
);
cacheManager.setTransactionAware(enableTransactions);
return this.customizerInvoker.customize(cacheManager);
}
private RedisCacheConfiguration determineConfiguration() {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
RedisSerializer<Object> jsonRedisSerializer = RedisSerializer.json();
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(jsonRedisSerializer, "mapper");
objectMapper.registerModules(new PjcloudJavaTimeModule());
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jsonRedisSerializer));
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, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.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.bsd.pjcloud.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 pjcloud
* @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,61 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.cache;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bsd.pjcloud.common.core.jackson.PjcloudJavaTimeModule;
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.RedisSerializer;
/**
* RedisTemplate 配置
*
* @author L.cm
*/
@EnableCaching
@Configuration
@AutoConfigureBefore(name = { "org.redisson.spring.starter.RedissonAutoConfigurationV2",
"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(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置 redisTemplate 的序列化器为 json
RedisSerializer<Object> jsonRedisSerializer = RedisSerializer.json();
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(jsonRedisSerializer, "mapper");
objectMapper.registerModules(new PjcloudJavaTimeModule());
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* @author pjcloud
* @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, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据权限函数类型
*
* @author pjcloud
* @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, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
/**
* @author pjcloud
* @date 2019-09-07
* <p>
* data scope 判断处理器,抽象服务扩展
*/
public interface DataScopeHandle {
/**
* 计算用户数据权限
* @param dataScope 数据权限设置
* @return 返回true表示无需进行数据过滤处理,返回false表示需要进行数据过滤
*/
Boolean calcScope(DataScope dataScope);
}
@@ -0,0 +1,98 @@
package com.bsd.pjcloud.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 pjcloud
* @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 (DataScopeFuncEnum.ALL.equals(dataScope.getFunc()) && dataScopeHandle.calcScope(dataScope)) {
return;
}
// 返回true 不拦截直接返回原始 SQL (只针对 COUNT 查询)
if (DataScopeFuncEnum.COUNT.equals(dataScope.getFunc()) && dataScopeHandle.calcScope(dataScope)) {
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()) && CollUtil.isNotEmpty(deptIds)) {
String join = CollectionUtil.join(deptIds, ",");
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s' OR temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername(), dataScope.getScopeDeptName(), join);
}
// 3. 如果为本人
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());
}
// 4.部门权限控制
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.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
/**
* 数据权限抽象
*
* @author pjcloud
* @date 2022/8/9
*/
public interface DataScopeInterceptor extends InnerInterceptor {
}
@@ -0,0 +1,27 @@
package com.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.github.yulichang.injector.MPJSqlInjector;
import java.util.List;
/**
* 支持自定义数据权限方法注入
*
* @author pjcloud
* @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,82 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @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;
/**
* 按类型获取
*
* @param dsType DS 型
* @return {@link DataScopeTypeEnum }
*/
public static DataScopeTypeEnum getByType(Integer dsType) {
for (DataScopeTypeEnum value : DataScopeTypeEnum.values()) {
if (value.getType() == dsType) {
return value;
}
}
return null;
}
}
@@ -0,0 +1,48 @@
package com.bsd.pjcloud.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 pjcloud
* @date 2020-06-17
*/
public interface PjcloudBaseMapper<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,157 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.entity.SysDept;
import com.bsd.pjcloud.admin.api.entity.SysRole;
import com.bsd.pjcloud.admin.api.feign.RemoteDataScopeService;
import com.bsd.pjcloud.common.core.constant.enums.UserTypeEnum;
import com.bsd.pjcloud.common.core.util.RetOps;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author pjcloud
* @date 2019-09-07
* <p>
* 默认data scope 判断处理器
*/
/**
* @author pjcloud
* @date 2019-09-07
* <p>
* 默认data scope 判断处理器
*/
@RequiredArgsConstructor
public class PjcloudDefaultDataScopeHandle implements DataScopeHandle {
private final RemoteDataScopeService dataScopeService;
@Override
public Boolean calcScope(DataScope dataScope) {
// 对于TOC客户端,不进行数据权限校验
PjcloudUser user = SecurityUtils.getUser();
if (UserTypeEnum.TOC.getStatus().equals(user.getUserType())) {
return true;
}
// 业务代码里的规则,覆盖计算规则
if (StrUtil.isNotBlank(dataScope.getUsername()) || CollUtil.isNotEmpty(dataScope.getDeptList())) {
return false;
}
// 获取用户角色ID列表
List<Long> roleIdList = SecurityUtils.getRoleIds();
if (CollUtil.isEmpty(roleIdList)) {
return false;
}
// 获取角色列表
List<SysRole> roleList = RetOps.of(dataScopeService.getRoleList(roleIdList))
.getData()
.orElseGet(Collections::emptyList);
if (CollUtil.isEmpty(roleList)) {
return false;
}
// 处理数据权限
return processDataScope(user, dataScope, roleList);
}
/**
* 处理数据权限
*/
private boolean processDataScope(PjcloudUser user, DataScope dataScope, List<SysRole> roleList) {
List<Long> deptList = dataScope.getDeptList();
for (SysRole role : roleList) {
Integer dsType = role.getDsType();
// 处理不同数据权限类型
switch (Objects.requireNonNull(DataScopeTypeEnum.getByType(dsType))) {
case ALL:
return true;
case CUSTOM:
handleCustomScope(role, deptList);
break;
case OWN_CHILD_LEVEL:
handleOwnChildLevelScope(user, deptList);
break;
case OWN_LEVEL:
handleOwnLevelScope(user, deptList);
break;
case SELF_LEVEL:
handleSelfLevelScope(user, dataScope);
break;
default:
break;
}
}
return false;
}
/**
* 处理自定义数据权限
*/
private void handleCustomScope(SysRole role, List<Long> deptList) {
if (StrUtil.isNotBlank(role.getDsScope())) {
deptList.addAll(Arrays.stream(role.getDsScope().split(StrUtil.COMMA))
.map(Long::parseLong)
.collect(Collectors.toList()));
}
}
/**
* 处理本级及下级数据权限
*/
private void handleOwnChildLevelScope(PjcloudUser user, List<Long> deptList) {
List<Long> descendantDeptIds = RetOps.of(dataScopeService.getDescendantList(user.getDeptId()))
.getData()
.orElseGet(Collections::emptyList)
.stream()
.map(SysDept::getDeptId)
.collect(Collectors.toList());
deptList.addAll(descendantDeptIds);
}
/**
* 处理本级数据权限
*/
private void handleOwnLevelScope(PjcloudUser user, List<Long> deptList) {
deptList.add(user.getDeptId());
}
/**
* 处理个人数据权限
*/
private void handleSelfLevelScope(PjcloudUser user, DataScope dataScope) {
dataScope.setUsername(user.getUsername());
}
}
@@ -0,0 +1,33 @@
package com.bsd.pjcloud.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 pjcloud
* @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.bsd.pjcloud.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 pjcloud
* @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.bsd.pjcloud.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 pjcloud
* @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,59 @@
package com.bsd.pjcloud.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);
if (StrUtil.isBlank(reString)) return new Long[]{};
return Convert.toLongArray(reString);
}
@Override
@SneakyThrows
public Long[] getNullableResult(ResultSet rs, int columnIndex) {
String reString = rs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new Long[]{};
return Convert.toLongArray(reString);
}
@Override
@SneakyThrows
public Long[] getNullableResult(CallableStatement cs, int columnIndex) {
String reString = cs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new Long[]{};
return Convert.toLongArray(reString);
}
}
@@ -0,0 +1,59 @@
package com.bsd.pjcloud.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);
if (StrUtil.isBlank(reString)) return new String[]{};
return Convert.toStrArray(reString);
}
@Override
@SneakyThrows
public String[] getNullableResult(ResultSet rs, int columnIndex) {
String reString = rs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new String[]{};
return Convert.toStrArray(reString);
}
@Override
@SneakyThrows
public String[] getNullableResult(CallableStatement cs, int columnIndex) {
String reString = cs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new String[]{};
return Convert.toStrArray(reString);
}
}
@@ -0,0 +1,211 @@
/*
* 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.bsd.pjcloud.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.dialect.sqlserver.visitor.SQLServerSchemaStatVisitor;
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 PjcloudMybatisProperties 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;
// SQL server 数据库特殊处理
if (DbType.sqlserver.name().equalsIgnoreCase(dbType)) {
visitor = new SQLServerSchemaStatVisitor();
}
else {
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,168 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.bsd.pjcloud.admin.api.feign.RemoteDataScopeService;
import com.bsd.pjcloud.common.data.datascope.DataScopeInnerInterceptor;
import com.bsd.pjcloud.common.data.datascope.DataScopeInterceptor;
import com.bsd.pjcloud.common.data.datascope.DataScopeSqlInjector;
import com.bsd.pjcloud.common.data.datascope.PjcloudDefaultDataScopeHandle;
import com.bsd.pjcloud.common.data.resolver.SqlFilterArgumentResolver;
import com.bsd.pjcloud.common.data.tenant.PjcloudTenantConfigProperties;
import com.bsd.pjcloud.common.data.tenant.PjcloudTenantHandler;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
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 pjcloud
* @date 2020-02-08
*/
@Configuration
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(PjcloudMybatisProperties.class)
public class MybatisPlusConfiguration implements WebMvcConfigurer {
/**
* 增加请求参数解析器,对请求中的参数注入SQL 检查
*
* @param resolverList
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolverList) {
resolverList.add(new SqlFilterArgumentResolver());
}
/**
* mybatis plus 拦截器配置
*
* @return PjcloudDefaultDatascopeHandle
*/
@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);
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/**
* 创建租户维护处理器对象
*
* @return 处理后的租户维护处理器
*/
@Bean
@ConditionalOnMissingBean
public TenantLineInnerInterceptor tenantLineInnerInterceptor(PjcloudTenantConfigProperties tenantConfigProperties) {
TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor();
tenantLineInnerInterceptor.setTenantLineHandler(new PjcloudTenantHandler(tenantConfigProperties));
return tenantLineInnerInterceptor;
}
/**
* 数据权限拦截器
*
* @return DataScopeInterceptor
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(PjcloudUser.class)
public DataScopeInterceptor dataScopeInterceptor(RemoteDataScopeService dataScopeService) {
DataScopeInnerInterceptor dataScopeInnerInterceptor = new DataScopeInnerInterceptor();
dataScopeInnerInterceptor.setDataScopeHandle(new PjcloudDefaultDataScopeHandle(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(PjcloudMybatisProperties 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,95 @@
package com.bsd.pjcloud.common.data.mybatis;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
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.bsd.pjcloud.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 pjcloud
* @date 2021/6/3
*/
@Data
@RefreshScope
@ConfigurationProperties("pjcloud.mybatis")
public class PjcloudMybatisProperties {
/**
* 是否打印可执行 sql
*/
private boolean showSql = true;
/**
* 跳过表
*/
private List<String> skipTable = new ArrayList<>();
}
@@ -0,0 +1,99 @@
package com.bsd.pjcloud.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.bsd.pjcloud.admin.api.entity.SysDictItem;
import com.bsd.pjcloud.admin.api.feign.RemoteDictService;
import com.bsd.pjcloud.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,77 @@
package com.bsd.pjcloud.common.data.resolver;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.feign.RemoteParamService;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import lombok.experimental.UtilityClass;
import java.util.Map;
import java.util.Objects;
/**
* @author pjcloud
* @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).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).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,110 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.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.core.toolkit.sql.SqlInjectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import jakarta.servlet.http.HttpServletRequest;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @author pjcloud
* @date 2019-06-24
* <p>
* 解决Mybatis Plus Order By SQL注入问题
*/
@Slf4j
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 判断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(asc -> !SqlInjectionUtils.check(asc))
.map(OrderItem::asc)
.collect(Collectors.toList())));
Optional.ofNullable(descs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(desc -> !SqlInjectionUtils.check(desc))
.map(OrderItem::desc)
.collect(Collectors.toList())));
page.addOrder(orderItemList);
return page;
}
}
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.common.data.resolver;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
/**
* @author pjcloud
* @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, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
/**
* @author pjcloud
* @date 2018/9/14
*/
@Slf4j
public class PjcloudFeignTenantInterceptor 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.bsd.pjcloud.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 = "pjcloud.tenant")
public class PjcloudTenantConfigProperties {
/**
* 维护租户列名称
*/
private String column = "tenant_id";
/**
* 多租户的数据表集合
*/
private List<String> tables = new ArrayList<>();
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.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 pjcloud
* @date 2020/4/29
* <p>
* 租户信息拦截
*/
@Configuration
public class PjcloudTenantConfiguration {
@Bean
public RequestInterceptor pjcloudFeignTenantInterceptor() {
return new PjcloudFeignTenantInterceptor();
}
@Bean
public ClientHttpRequestInterceptor pjcloudTenantRequestInterceptor() {
return new TenantRequestInterceptor();
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.bsd.pjcloud.common.core.util.TenantTable;
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;
import org.springframework.core.annotation.AnnotationUtils;
import java.util.Objects;
/**
* @author pjcloud
* @date 2018-12-26
* <p>
* 租户维护处理器
*/
@Slf4j
@RequiredArgsConstructor
public class PjcloudTenantHandler implements TenantLineHandler {
private final PjcloudTenantConfigProperties 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;
}
// 判断实体类是否标记为多租户隔离
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
TenantTable annotation = null;
if (Objects.nonNull(tableInfo)) {
annotation = AnnotationUtils.findAnnotation(tableInfo.getEntityType(), TenantTable.class);
}
return !properties.getTables().contains(tableName) && annotation == null;
}
}
@@ -0,0 +1,147 @@
package com.bsd.pjcloud.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, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.experimental.UtilityClass;
/**
* @author pjcloud
* @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,123 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.entity.SysTenant;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import java.util.List;
/**
* @author pjcloud
* @date 2018/9/13
*/
@Slf4j
@Component
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TenantContextHolderFilter extends GenericFilterBean {
private final static String UNDEFINED_STR = "undefined";
private final CacheManager cacheManager;
@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);
}
if (!checkTenantStatus(request, response)) {
TenantContextHolder.clear();
return;
}
try {
filterChain.doFilter(request, response);
} finally {
TenantContextHolder.clear();
}
}
/**
* 检查租户状态
*
* @param request 请求
* @param response 响应
* @return boolean
*/
private boolean checkTenantStatus(HttpServletRequest request, HttpServletResponse response) {
// 如果是获取租户列表请求跳过
if (StrUtil.containsAnyIgnoreCase(request.getRequestURI(), "/tenant/list")) {
return true;
}
// 判断租户状态
Cache cache = cacheManager.getCache(CacheConstants.TENANT_DETAILS);
if (cache == null) {
return true;
}
List<SysTenant> tenantList = cache.get(SimpleKey.EMPTY, List.class);
if (CollUtil.isEmpty(tenantList)) {
return true;
}
boolean exist = tenantList.stream().anyMatch(tenant -> NumberUtil.equals(tenant.getId(), TenantContextHolder.getTenantId()));
if (exist) {
return true;
}
response.setStatus(HttpStatus.UPGRADE_REQUIRED.value());
return false;
}
}
@@ -0,0 +1,30 @@
package com.bsd.pjcloud.common.data.tenant;
import com.bsd.pjcloud.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 pjcloud
* @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.bsd.pjcloud.common.data.cache.RedisTemplateConfiguration
com.bsd.pjcloud.common.data.cache.RedisMessageConfiguration
com.bsd.pjcloud.common.data.cache.RedisCacheManagerConfiguration
com.bsd.pjcloud.common.data.cache.RedisCacheAutoConfiguration
com.bsd.pjcloud.common.data.tenant.PjcloudTenantConfigProperties
com.bsd.pjcloud.common.data.tenant.TenantContextHolderFilter
com.bsd.pjcloud.common.data.tenant.PjcloudTenantConfiguration
com.bsd.pjcloud.common.data.mybatis.MybatisPlusConfiguration
com.bsd.pjcloud.common.data.resolver.TenantKeyStrResolver