Browse Source

change: 添加店铺租户模块

gaohp 1 year ago
parent
commit
cc24a97873
30 changed files with 1583 additions and 0 deletions
  1. 83 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/pom.xml
  2. 132 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/FeifanTenantAutoConfiguration.java
  3. 42 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/TenantProperties.java
  4. 18 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnore.java
  5. 36 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnoreAspect.java
  6. 79 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/ShopTenantContextHolder.java
  7. 79 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/TenantContextHolder.java
  8. 26 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantBaseDO.java
  9. 49 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantDatabaseInterceptor.java
  10. 14 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJob.java
  11. 56 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJobAspect.java
  12. 37 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java
  13. 48 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java
  14. 23 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
  15. 32 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java
  16. 43 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/redis/TenantRedisMessageInterceptor.java
  17. 47 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java
  18. 53 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
  19. 37 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java
  20. 39 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/redis/TenantRedisCacheManager.java
  21. 118 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/security/TenantSecurityWebFilter.java
  22. 26 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkService.java
  23. 73 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkServiceImpl.java
  24. 94 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/util/TenantUtils.java
  25. 38 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/web/TenantContextWebFilter.java
  26. 255 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java
  27. 2 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring.factories
  28. 1 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  29. 1 0
      feifan-framework/pom.xml
  30. 2 0
      feifan-server/src/main/resources/application.yaml

+ 83 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/pom.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         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>cn.newfeifan.zx</groupId>
+        <artifactId>feifan-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>feifan-spring-boot-starter-biz-shop-tenant</artifactId>
+
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>多租户</description>
+    <dependencies>
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-redis</artifactId>
+        </dependency>
+
+        <!-- Job 定时任务相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-job</artifactId>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-mq</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.amqp</groupId>
+            <artifactId>spring-rabbit</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 132 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/FeifanTenantAutoConfiguration.java

@@ -0,0 +1,132 @@
+package cn.newfeifan.mall.framework.shop.tenant.config;
+
+import cn.newfeifan.mall.framework.common.enums.WebFilterOrderEnum;
+import cn.newfeifan.mall.framework.mybatis.core.util.MyBatisUtils;
+import cn.newfeifan.mall.framework.redis.config.FeifanCacheProperties;
+import cn.newfeifan.mall.framework.shop.tenant.core.aop.TenantIgnoreAspect;
+import cn.newfeifan.mall.framework.shop.tenant.core.db.TenantDatabaseInterceptor;
+import cn.newfeifan.mall.framework.shop.tenant.core.job.TenantJobAspect;
+import cn.newfeifan.mall.framework.shop.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
+import cn.newfeifan.mall.framework.shop.tenant.core.mq.redis.TenantRedisMessageInterceptor;
+import cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
+import cn.newfeifan.mall.framework.shop.tenant.core.redis.TenantRedisCacheManager;
+import cn.newfeifan.mall.framework.shop.tenant.core.security.TenantSecurityWebFilter;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkService;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkServiceImpl;
+import cn.newfeifan.mall.framework.shop.tenant.core.web.TenantContextWebFilter;
+import cn.newfeifan.mall.framework.web.config.WebProperties;
+import cn.newfeifan.mall.framework.web.core.handler.GlobalExceptionHandler;
+import cn.newfeifan.mall.module.system.api.tenant.TenantApi;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.cache.BatchStrategies;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.Objects;
+
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "feifan.tenant.shop", value = "enable", matchIfMissing = true) // 允许使用 feifan.tenant.enable=false 禁用多租户
+@EnableConfigurationProperties(TenantProperties.class)
+public class FeifanTenantAutoConfiguration {
+
+    @Bean
+    public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) {
+        return new TenantFrameworkServiceImpl(tenantApi);
+    }
+
+    // ========== AOP ==========
+
+    @Bean
+    public TenantIgnoreAspect tenantIgnoreAspect() {
+        return new TenantIgnoreAspect();
+    }
+
+    // ========== DB ==========
+
+    @Bean
+    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
+                                                                 MybatisPlusInterceptor interceptor) {
+        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
+        // 添加到 interceptor 中
+        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
+        MyBatisUtils.addInterceptor(interceptor, inner, 0);
+        return inner;
+    }
+
+    // ========== WEB ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
+        FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantContextWebFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
+        return registrationBean;
+    }
+
+    // ========== Security ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
+                                                                                   WebProperties webProperties,
+                                                                                   GlobalExceptionHandler globalExceptionHandler,
+                                                                                   TenantFrameworkService tenantFrameworkService) {
+        FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties,
+                globalExceptionHandler, tenantFrameworkService));
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
+        return registrationBean;
+    }
+
+    // ========== MQ ==========
+
+    @Bean
+    public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
+        return new TenantRedisMessageInterceptor();
+    }
+
+    @Bean
+    @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
+    public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
+        return new TenantRabbitMQInitializer();
+    }
+
+    @Bean
+    @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
+    public TenantRocketMQInitializer tenantRocketMQInitializer() {
+        return new TenantRocketMQInitializer();
+    }
+
+    // ========== Job ==========
+
+    @Bean
+    public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
+        return new TenantJobAspect(tenantFrameworkService);
+    }
+
+    // ========== Redis ==========
+
+    @Bean
+    @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
+    public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
+                                                     RedisCacheConfiguration redisCacheConfiguration,
+                                                     FeifanCacheProperties feifanCacheProperties) {
+        // 创建 RedisCacheWriter 对象
+        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
+        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
+                BatchStrategies.scan(feifanCacheProperties.getRedisScanBatchSize()));
+        // 创建 TenantRedisCacheManager 对象
+        return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration);
+    }
+
+}

+ 42 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/TenantProperties.java

@@ -0,0 +1,42 @@
+package cn.newfeifan.mall.framework.shop.tenant.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 多租户配置
+ *
+ * @author 非繁源码
+ */
+@ConfigurationProperties(prefix = "feifan.tenant.shop")
+@Data
+public class TenantProperties {
+
+    /**
+     * 租户是否开启
+     */
+    private static final Boolean ENABLE_DEFAULT = true;
+
+    /**
+     * 是否开启
+     */
+    private Boolean enable = ENABLE_DEFAULT;
+
+    /**
+     * 需要忽略多租户的请求
+     *
+     * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
+     */
+    private Set<String> ignoreUrls = Collections.emptySet();
+
+    /**
+     * 需要忽略多租户的表
+     *
+     * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
+     */
+    private Set<String> ignoreTables = Collections.emptySet();
+
+}

+ 18 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnore.java

@@ -0,0 +1,18 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.aop;
+
+import java.lang.annotation.*;
+
+/**
+ * 忽略租户,标记指定方法不进行租户的自动过滤
+ *
+ * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
+ * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
+ * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
+ *
+ * @author 非繁源码
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TenantIgnore {
+}

+ 36 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnoreAspect.java

@@ -0,0 +1,36 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.aop;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.util.TenantUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+/**
+ * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
+ * 例如说,一个定时任务,读取所有数据,进行处理。
+ * 又例如说,读取所有数据,进行缓存。
+ *
+ * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致
+ *
+ * @author 非繁源码
+ */
+@Aspect
+@Slf4j
+public class TenantIgnoreAspect {
+
+    @Around("@annotation(tenantIgnore)")
+    public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            return joinPoint.proceed();
+        } finally {
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+}

+ 79 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/ShopTenantContextHolder.java

@@ -0,0 +1,79 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.context;
+
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.common.enums.DocumentEnum;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author 非繁源码
+ */
+public class ShopTenantContextHolder {
+
+    /**
+     * 当前租户编号
+     */
+    private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
+
+    /**
+     * 是否忽略租户
+     */
+    private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
+
+    /**
+     * 获得租户编号
+     *
+     * @return 租户编号
+     */
+    public static Long getTenantId() {
+        return TENANT_ID.get();
+    }
+
+    /**
+     * 获得租户编号 String
+     *
+     * @return 租户编号
+     */
+    public static String getTenantIdStr() {
+        Long tenantId = getTenantId();
+        return StrUtil.toStringOrNull(tenantId);
+    }
+
+    /**
+     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static Long getRequiredTenantId() {
+        Long tenantId = getTenantId();
+        if (tenantId == null) {
+            throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
+                + DocumentEnum.TENANT.getUrl());
+        }
+        return tenantId;
+    }
+
+    public static void setTenantId(Long tenantId) {
+        TENANT_ID.set(tenantId);
+    }
+
+    public static void setIgnore(Boolean ignore) {
+        IGNORE.set(ignore);
+    }
+
+    /**
+     * 当前是否忽略租户
+     *
+     * @return 是否忽略
+     */
+    public static boolean isIgnore() {
+        return Boolean.TRUE.equals(IGNORE.get());
+    }
+
+    public static void clear() {
+        TENANT_ID.remove();
+        IGNORE.remove();
+    }
+
+}

+ 79 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/TenantContextHolder.java

@@ -0,0 +1,79 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.context;
+
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.common.enums.DocumentEnum;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author 非繁源码
+ */
+public class TenantContextHolder {
+
+    /**
+     * 当前租户编号
+     */
+    private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
+
+    /**
+     * 是否忽略租户
+     */
+    private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
+
+    /**
+     * 获得租户编号
+     *
+     * @return 租户编号
+     */
+    public static Long getTenantId() {
+        return TENANT_ID.get();
+    }
+
+    /**
+     * 获得租户编号 String
+     *
+     * @return 租户编号
+     */
+    public static String getTenantIdStr() {
+        Long tenantId = getTenantId();
+        return StrUtil.toStringOrNull(tenantId);
+    }
+
+    /**
+     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static Long getRequiredTenantId() {
+        Long tenantId = getTenantId();
+        if (tenantId == null) {
+            throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
+                + DocumentEnum.TENANT.getUrl());
+        }
+        return tenantId;
+    }
+
+    public static void setTenantId(Long tenantId) {
+        TENANT_ID.set(tenantId);
+    }
+
+    public static void setIgnore(Boolean ignore) {
+        IGNORE.set(ignore);
+    }
+
+    /**
+     * 当前是否忽略租户
+     *
+     * @return 是否忽略
+     */
+    public static boolean isIgnore() {
+        return Boolean.TRUE.equals(IGNORE.get());
+    }
+
+    public static void clear() {
+        TENANT_ID.remove();
+        IGNORE.remove();
+    }
+
+}

+ 26 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantBaseDO.java

@@ -0,0 +1,26 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.db;
+
+import cn.newfeifan.mall.framework.mybatis.core.dataobject.BaseDO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 拓展多租户的 BaseDO 基类
+ *
+ * @author 非繁源码
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class TenantBaseDO extends BaseDO {
+
+    /**
+     * 多租户编号
+     */
+    private Long tenantId;
+
+    /**
+     * 多店铺编号
+     */
+    private Long shopId;
+
+}

+ 49 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantDatabaseInterceptor.java

@@ -0,0 +1,49 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.newfeifan.mall.framework.shop.tenant.config.TenantProperties;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
+ *
+ * @author 非繁源码
+ */
+public class TenantDatabaseInterceptor implements TenantLineHandler {
+
+    @Override
+    public String getTenantIdColumn() {
+        return TenantLineHandler.super.getTenantIdColumn();
+    }
+
+    private final Set<String> ignoreTables = new HashSet<>();
+
+    public TenantDatabaseInterceptor(TenantProperties properties) {
+        // 不同 DB 下,大小写的习惯不同,所以需要都添加进去
+        properties.getIgnoreTables().forEach(table -> {
+            ignoreTables.add(table.toLowerCase());
+            ignoreTables.add(table.toUpperCase());
+        });
+        // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
+        ignoreTables.add("DUAL");
+    }
+
+    @Override
+    public Expression getTenantId() {
+        return new LongValue(ShopTenantContextHolder.getRequiredTenantId());
+    }
+
+    @Override
+    public boolean ignoreTable(String tableName) {
+        return ShopTenantContextHolder.isIgnore() // 情况一,全局忽略多租户
+            || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表
+    }
+
+}

+ 14 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJob.java

@@ -0,0 +1,14 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.job;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 多租户 Job 注解
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TenantJob {
+}

+ 56 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJobAspect.java

@@ -0,0 +1,56 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.newfeifan.mall.framework.common.util.json.JsonUtils;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkService;
+import cn.newfeifan.mall.framework.shop.tenant.core.util.TenantUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 多租户 JobHandler AOP
+ * 任务执行时,会按照租户逐个执行 Job 的逻辑
+ *
+ * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
+ *
+ * @author 非繁源码
+ */
+@Aspect
+@RequiredArgsConstructor
+@Slf4j
+public class TenantJobAspect {
+
+    private final TenantFrameworkService tenantFrameworkService;
+
+    @Around("@annotation(tenantJob)")
+    public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) {
+        // 获得租户列表
+        List<Long> tenantIds = tenantFrameworkService.getTenantIds();
+        if (CollUtil.isEmpty(tenantIds)) {
+            return null;
+        }
+
+        // 逐个租户,执行 Job
+        Map<Long, String> results = new ConcurrentHashMap<>();
+        tenantIds.parallelStream().forEach(tenantId -> {
+            // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
+            TenantUtils.execute(tenantId, () -> {
+                try {
+                    joinPoint.proceed();
+                } catch (Throwable e) {
+                    results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
+                }
+            });
+        });
+        return JsonUtils.toJsonString(results);
+    }
+
+}

+ 37 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java

@@ -0,0 +1,37 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.kafka;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类
+ *
+ * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器
+ *
+ * @author 非繁源码
+ */
+@Slf4j
+public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor {
+
+    private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes";
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+        // 添加 TenantKafkaProducerInterceptor 拦截器
+        try {
+            String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES);
+            if (StrUtil.isEmpty(value)) {
+                value = TenantKafkaProducerInterceptor.class.getName();
+            } else {
+                value += "," + TenantKafkaProducerInterceptor.class.getName();
+            }
+            environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value);
+        } catch (NoClassDefFoundError ignore) {
+            // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
+        }
+    }
+
+}

+ 48 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java

@@ -0,0 +1,48 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.kafka;
+
+import cn.hutool.core.util.ReflectUtil;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.kafka.clients.producer.ProducerInterceptor;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.clients.producer.RecordMetadata;
+import org.apache.kafka.common.header.Headers;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import java.util.Map;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author 非繁源码
+ */
+public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> {
+
+    @Override
+    public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
+            headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes());
+        }
+        return record;
+    }
+
+    @Override
+    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public void configure(Map<String, ?> configs) {
+    }
+
+}

+ 23 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java

@@ -0,0 +1,23 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rabbitmq;
+
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * 多租户的 RabbitMQ 初始化器
+ *
+ * @author 非繁源码
+ */
+public class TenantRabbitMQInitializer implements BeanPostProcessor {
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof RabbitTemplate) {
+            RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
+            rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor());
+        }
+        return bean;
+    }
+
+}

+ 32 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java

@@ -0,0 +1,32 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rabbitmq;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.kafka.clients.producer.ProducerInterceptor;
+import org.springframework.amqp.AmqpException;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.core.MessagePostProcessor;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author 非繁源码
+ */
+public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor {
+
+    @Override
+    public Message postProcessMessage(Message message) throws AmqpException {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId);
+        }
+        return message;
+    }
+
+}

+ 43 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/redis/TenantRedisMessageInterceptor.java

@@ -0,0 +1,43 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.redis;
+
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import cn.newfeifan.mall.framework.mq.redis.core.message.AbstractRedisMessage;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * 多租户 {@link AbstractRedisMessage} 拦截器
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
+ *
+ * @author 非繁源码
+ */
+public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
+
+    @Override
+    public void sendMessageBefore(AbstractRedisMessage message) {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            message.addHeader(HEADER_TENANT_ID, tenantId.toString());
+        }
+    }
+
+    @Override
+    public void consumeMessageBefore(AbstractRedisMessage message) {
+        String tenantIdStr = message.getHeader(HEADER_TENANT_ID);
+        if (StrUtil.isNotEmpty(tenantIdStr)) {
+            ShopTenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
+        }
+    }
+
+    @Override
+    public void consumeMessageAfter(AbstractRedisMessage message) {
+        // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
+        ShopTenantContextHolder.clear();
+    }
+
+}

+ 47 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java

@@ -0,0 +1,47 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.rocketmq.client.hook.ConsumeMessageContext;
+import org.apache.rocketmq.client.hook.ConsumeMessageHook;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import java.util.List;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类
+ *
+ * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author 非繁源码
+ */
+public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook {
+
+    @Override
+    public String hookName() {
+        return getClass().getSimpleName();
+    }
+
+    @Override
+    public void consumeMessageBefore(ConsumeMessageContext context) {
+        // 校验,消息必须是单条,不然设置租户可能不正确
+        List<MessageExt> messages = context.getMsgList();
+        Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size());
+        // 设置租户编号
+        String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID);
+        if (StrUtil.isNotEmpty(tenantId)) {
+            ShopTenantContextHolder.setTenantId(Long.parseLong(tenantId));
+        }
+    }
+
+    @Override
+    public void consumeMessageAfter(ConsumeMessageContext context) {
+        ShopTenantContextHolder.clear();
+    }
+
+}

+ 53 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java

@@ -0,0 +1,53 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq;
+
+import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
+import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl;
+import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl;
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * 多租户的 RocketMQ 初始化器
+ *
+ * @author 非繁源码
+ */
+public class TenantRocketMQInitializer implements BeanPostProcessor {
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof DefaultRocketMQListenerContainer) {
+            DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
+            initTenantConsumer(container.getConsumer());
+        } else if (bean instanceof RocketMQTemplate) {
+            RocketMQTemplate template = (RocketMQTemplate) bean;
+            initTenantProducer(template.getProducer());
+        }
+        return bean;
+    }
+
+    private void initTenantProducer(DefaultMQProducer producer) {
+        if (producer == null) {
+            return;
+        }
+        DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl();
+        if (producerImpl == null) {
+            return;
+        }
+        producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook());
+    }
+
+    private void initTenantConsumer(DefaultMQPushConsumer consumer) {
+        if (consumer == null) {
+            return;
+        }
+        DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl();
+        if (consumerImpl == null) {
+            return;
+        }
+        consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
+    }
+
+}

+ 37 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java

@@ -0,0 +1,37 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.rocketmq.client.hook.SendMessageContext;
+import org.apache.rocketmq.client.hook.SendMessageHook;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类
+ *
+ * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ *
+ * @author 非繁源码
+ */
+public class TenantRocketMQSendMessageHook implements SendMessageHook {
+
+    @Override
+    public String hookName() {
+        return getClass().getSimpleName();
+    }
+
+    @Override
+    public void sendMessageBefore(SendMessageContext sendMessageContext) {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId == null) {
+            return;
+        }
+        sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString());
+    }
+
+    @Override
+    public void sendMessageAfter(SendMessageContext sendMessageContext) {
+    }
+
+}

+ 39 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/redis/TenantRedisCacheManager.java

@@ -0,0 +1,39 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.redis;
+
+import cn.newfeifan.mall.framework.redis.core.TimeoutRedisCacheManager;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.Cache;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+
+/**
+ * 多租户的 {@link RedisCacheManager} 实现类
+ *
+ * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀
+ *
+ * @author airhead
+ */
+@Slf4j
+public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
+
+    public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
+                                   RedisCacheConfiguration defaultCacheConfiguration) {
+        super(cacheWriter, defaultCacheConfiguration);
+    }
+
+    @Override
+    public Cache getCache(String name) {
+        // 如果开启多租户,则 name 拼接租户后缀
+        if (!ShopTenantContextHolder.isIgnore()
+            && ShopTenantContextHolder.getTenantId() != null) {
+            name = name + ":" + ShopTenantContextHolder.getTenantId();
+        }
+
+        // 继续基于父方法
+        return super.getCache(name);
+    }
+
+}

+ 118 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/security/TenantSecurityWebFilter.java

@@ -0,0 +1,118 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.security;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.newfeifan.mall.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.newfeifan.mall.framework.common.pojo.CommonResult;
+import cn.newfeifan.mall.framework.common.util.servlet.ServletUtils;
+import cn.newfeifan.mall.framework.security.core.LoginUser;
+import cn.newfeifan.mall.framework.security.core.util.SecurityFrameworkUtils;
+import cn.newfeifan.mall.framework.shop.tenant.config.TenantProperties;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkService;
+import cn.newfeifan.mall.framework.web.config.WebProperties;
+import cn.newfeifan.mall.framework.web.core.filter.ApiRequestFilter;
+import cn.newfeifan.mall.framework.web.core.handler.GlobalExceptionHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.AntPathMatcher;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * 多租户 Security Web 过滤器
+ * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
+ * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
+ * 3. 校验租户是合法,例如说被禁用、到期
+ *
+ * @author 非繁源码
+ */
+@Slf4j
+public class TenantSecurityWebFilter extends ApiRequestFilter {
+
+    private final TenantProperties tenantProperties;
+
+    private final AntPathMatcher pathMatcher;
+
+    private final GlobalExceptionHandler globalExceptionHandler;
+    private final TenantFrameworkService tenantFrameworkService;
+
+    public TenantSecurityWebFilter(TenantProperties tenantProperties,
+                                   WebProperties webProperties,
+                                   GlobalExceptionHandler globalExceptionHandler,
+                                   TenantFrameworkService tenantFrameworkService) {
+        super(webProperties);
+        this.tenantProperties = tenantProperties;
+        this.pathMatcher = new AntPathMatcher();
+        this.globalExceptionHandler = globalExceptionHandler;
+        this.tenantFrameworkService = tenantFrameworkService;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
+        LoginUser user = SecurityFrameworkUtils.getLoginUser();
+        if (user != null) {
+            // 如果获取不到租户编号,则尝试使用登陆用户的租户编号
+            if (tenantId == null) {
+                tenantId = user.getTenantId();
+                ShopTenantContextHolder.setTenantId(tenantId);
+            // 如果传递了租户编号,则进行比对租户编号,避免越权问题
+            } else if (!Objects.equals(user.getTenantId(), ShopTenantContextHolder.getTenantId())) {
+                log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
+                        user.getTenantId(), user.getId(), user.getUserType(),
+                        ShopTenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
+                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
+                        "您无权访问该租户的数据"));
+                return;
+            }
+        }
+
+        // 如果非允许忽略租户的 URL,则校验租户是否合法
+        if (!isIgnoreUrl(request)) {
+            // 2. 如果请求未带租户的编号,不允许访问。
+            if (tenantId == null) {
+                log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
+                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
+                        "请求的租户标识未传递,请进行排查"));
+                return;
+            }
+            // 3. 校验租户是合法,例如说被禁用、到期
+            try {
+                tenantFrameworkService.validTenant(tenantId);
+            } catch (Throwable ex) {
+                CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
+                ServletUtils.writeJSON(response, result);
+                return;
+            }
+        } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
+            if (tenantId == null) {
+                ShopTenantContextHolder.setIgnore(true);
+            }
+        }
+
+        // 继续过滤
+        chain.doFilter(request, response);
+    }
+
+    private boolean isIgnoreUrl(HttpServletRequest request) {
+        // 快速匹配,保证性能
+        if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
+            return true;
+        }
+        // 逐个 Ant 路径匹配
+        for (String url : tenantProperties.getIgnoreUrls()) {
+            if (pathMatcher.match(url, request.getRequestURI())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

+ 26 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkService.java

@@ -0,0 +1,26 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.service;
+
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 接口,定义获取租户信息
+ *
+ * @author 非繁源码
+ */
+public interface TenantFrameworkService {
+
+    /**
+     * 获得所有租户
+     *
+     * @return 租户编号数组
+     */
+    List<Long> getTenantIds();
+
+    /**
+     * 校验租户是否合法
+     *
+     * @param id 租户编号
+     */
+    void validTenant(Long id);
+
+}

+ 73 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkServiceImpl.java

@@ -0,0 +1,73 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.service;
+
+import cn.newfeifan.mall.framework.common.exception.ServiceException;
+import cn.newfeifan.mall.framework.common.util.cache.CacheUtils;
+import cn.newfeifan.mall.module.system.api.tenant.TenantApi;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 实现类
+ *
+ * @author 非繁源码
+ */
+@RequiredArgsConstructor
+public class TenantFrameworkServiceImpl implements TenantFrameworkService {
+
+    private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException();
+
+    private final TenantApi tenantApi;
+
+    /**
+     * 针对 {@link #getTenantIds()} 的缓存
+     */
+    private final LoadingCache<Object, List<Long>> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Object, List<Long>>() {
+
+                @Override
+                public List<Long> load(Object key) {
+                    return tenantApi.getTenantIdList();
+                }
+
+            });
+
+    /**
+     * 针对 {@link #validTenant(Long)} 的缓存
+     */
+    private final LoadingCache<Long, ServiceException> validTenantCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Long, ServiceException>() {
+
+                @Override
+                public ServiceException load(Long id) {
+                    try {
+                        tenantApi.validateTenant(id);
+                        return SERVICE_EXCEPTION_NULL;
+                    } catch (ServiceException ex) {
+                        return ex;
+                    }
+                }
+
+            });
+
+    @Override
+    @SneakyThrows
+    public List<Long> getTenantIds() {
+        return getTenantIdsCache.get(Boolean.TRUE);
+    }
+
+    @Override
+    public void validTenant(Long id) {
+        ServiceException serviceException = validTenantCache.getUnchecked(id);
+        if (serviceException != SERVICE_EXCEPTION_NULL) {
+            throw serviceException;
+        }
+    }
+
+}

+ 94 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/util/TenantUtils.java

@@ -0,0 +1,94 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.util;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * 多租户 Util
+ *
+ * @author 非繁源码
+ */
+public class TenantUtils {
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param runnable 逻辑
+     */
+    public static void execute(Long tenantId, Runnable runnable) {
+        Long oldTenantId = ShopTenantContextHolder.getTenantId();
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setTenantId(tenantId);
+            ShopTenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            ShopTenantContextHolder.setTenantId(oldTenantId);
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param callable 逻辑
+     */
+    public static <V> V execute(Long tenantId, Callable<V> callable) {
+        Long oldTenantId = ShopTenantContextHolder.getTenantId();
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setTenantId(tenantId);
+            ShopTenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            return callable.call();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            ShopTenantContextHolder.setTenantId(oldTenantId);
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 忽略租户,执行对应的逻辑
+     *
+     * @param runnable 逻辑
+     */
+    public static void executeIgnore(Runnable runnable) {
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 将多租户编号,添加到 header 中
+     *
+     * @param headers HTTP 请求 headers
+     * @param tenantId 租户编号
+     */
+    public static void addTenantHeader(Map<String, String> headers, Long tenantId) {
+        if (tenantId != null) {
+            headers.put(HEADER_TENANT_ID, tenantId.toString());
+        }
+    }
+
+}

+ 38 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/web/TenantContextWebFilter.java

@@ -0,0 +1,38 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.web;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 多租户 Context Web 过滤器
+ * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
+ *
+ * @author 非繁源码
+ */
+public class TenantContextWebFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 设置
+        Long tenantId = WebFrameworkUtils.getTenantId(request);
+        if (tenantId != null) {
+            ShopTenantContextHolder.setTenantId(tenantId);
+        }
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            // 清理
+            ShopTenantContextHolder.clear();
+        }
+    }
+
+}

+ 255 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java

@@ -0,0 +1,255 @@
+
+
+package org.springframework.messaging.handler.invocation;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.util.TenantUtils;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.ResolvableType;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.handler.HandlerMethod;
+import org.springframework.util.ObjectUtils;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Extension of {@link HandlerMethod} that invokes the underlying method with
+ * argument values resolved from the current HTTP request through a list of
+ * {@link HandlerMethodArgumentResolver}.
+ *
+ * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中
+ * TODO 芋艿:持续跟进,看看有没新的拓展点
+ *
+ * @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @since 4.0
+ */
+public class InvocableHandlerMethod extends HandlerMethod {
+
+    private static final Object[] EMPTY_ARGS = new Object[0];
+
+    private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
+
+    private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
+
+    /**
+     * Create an instance from a {@code HandlerMethod}.
+     */
+    public InvocableHandlerMethod(HandlerMethod handlerMethod) {
+        super(handlerMethod);
+    }
+
+    /**
+     * Create an instance from a bean instance and a method.
+     */
+    public InvocableHandlerMethod(Object bean, Method method) {
+        super(bean, method);
+    }
+
+    /**
+     * Construct a new handler method with the given bean instance, method name and parameters.
+     * @param bean the object bean
+     * @param methodName the method name
+     * @param parameterTypes the method parameter types
+     * @throws NoSuchMethodException when the method cannot be found
+     */
+    public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
+            throws NoSuchMethodException {
+
+        super(bean, methodName, parameterTypes);
+    }
+
+    /**
+     * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values.
+     */
+    public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
+        this.resolvers = argumentResolvers;
+    }
+
+    /**
+     * Set the ParameterNameDiscoverer for resolving parameter names when needed
+     * (e.g. default request attribute name).
+     * <p>Default is a {@link DefaultParameterNameDiscoverer}.
+     */
+    public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
+        this.parameterNameDiscoverer = parameterNameDiscoverer;
+    }
+
+    /**
+     * Invoke the method after resolving its argument values in the context of the given message.
+     * <p>Argument values are commonly resolved through
+     * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
+     * The {@code providedArgs} parameter however may supply argument values to be used directly,
+     * i.e. without argument resolution.
+     * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
+     * resolved arguments.
+     * @param message the current message being processed
+     * @param providedArgs "given" arguments matched by type, not resolved
+     * @return the raw value returned by the invoked method
+     * @throws Exception raised if no suitable argument resolver can be found,
+     * or if the method raised an exception
+     * @see #getMethodArgumentValues
+     * @see #doInvoke
+     */
+    @Nullable
+    public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
+        Object[] args = getMethodArgumentValues(message, providedArgs);
+        if (logger.isTraceEnabled()) {
+            logger.trace("Arguments: " + Arrays.toString(args));
+        }
+        // 注意:如下是本类的改动点!!!
+        // 情况一:无租户编号的情况
+        Long tenantId= parseTenantId(message);
+        if (tenantId == null) {
+            return doInvoke(args);
+        }
+        // 情况二:有租户的情况下
+        return TenantUtils.execute(tenantId, () -> doInvoke(args));
+    }
+
+    private Long parseTenantId(Message<?> message) {
+        Object tenantId = message.getHeaders().get(HEADER_TENANT_ID);
+        if (tenantId == null) {
+            return null;
+        }
+        if (tenantId instanceof Long) {
+            return (Long) tenantId;
+        }
+        if (tenantId instanceof Number) {
+            return ((Number) tenantId).longValue();
+        }
+        if (tenantId instanceof String) {
+            return Long.parseLong((String) tenantId);
+        }
+        if (tenantId instanceof byte[]) {
+            return Long.parseLong(new String((byte[]) tenantId));
+        }
+        throw new IllegalArgumentException("未知的数据类型:" + tenantId);
+    }
+
+    /**
+     * Get the method argument values for the current message, checking the provided
+     * argument values and falling back to the configured argument resolvers.
+     * <p>The resulting array will be passed into {@link #doInvoke}.
+     * @since 5.1.2
+     */
+    protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
+        MethodParameter[] parameters = getMethodParameters();
+        if (ObjectUtils.isEmpty(parameters)) {
+            return EMPTY_ARGS;
+        }
+
+        Object[] args = new Object[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            MethodParameter parameter = parameters[i];
+            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
+            args[i] = findProvidedArgument(parameter, providedArgs);
+            if (args[i] != null) {
+                continue;
+            }
+            if (!this.resolvers.supportsParameter(parameter)) {
+                throw new MethodArgumentResolutionException(
+                        message, parameter, formatArgumentError(parameter, "No suitable resolver"));
+            }
+            try {
+                args[i] = this.resolvers.resolveArgument(parameter, message);
+            }
+            catch (Exception ex) {
+                // Leave stack trace for later, exception may actually be resolved and handled...
+                if (logger.isDebugEnabled()) {
+                    String exMsg = ex.getMessage();
+                    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
+                        logger.debug(formatArgumentError(parameter, exMsg));
+                    }
+                }
+                throw ex;
+            }
+        }
+        return args;
+    }
+
+    /**
+     * Invoke the handler method with the given argument values.
+     */
+    @Nullable
+    protected Object doInvoke(Object... args) throws Exception {
+        try {
+            return getBridgedMethod().invoke(getBean(), args);
+        }
+        catch (IllegalArgumentException ex) {
+            assertTargetBean(getBridgedMethod(), getBean(), args);
+            String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+            throw new IllegalStateException(formatInvokeError(text, args), ex);
+        }
+        catch (InvocationTargetException ex) {
+            // Unwrap for HandlerExceptionResolvers ...
+            Throwable targetException = ex.getTargetException();
+            if (targetException instanceof RuntimeException) {
+                throw (RuntimeException) targetException;
+            }
+            else if (targetException instanceof Error) {
+                throw (Error) targetException;
+            }
+            else if (targetException instanceof Exception) {
+                throw (Exception) targetException;
+            }
+            else {
+                throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
+            }
+        }
+    }
+
+    MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
+        return new AsyncResultMethodParameter(returnValue);
+    }
+
+    private class AsyncResultMethodParameter extends HandlerMethodParameter {
+
+        @Nullable
+        private final Object returnValue;
+
+        private final ResolvableType returnType;
+
+        public AsyncResultMethodParameter(@Nullable Object returnValue) {
+            super(-1);
+            this.returnValue = returnValue;
+            this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
+        }
+
+        protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
+            super(original);
+            this.returnValue = original.returnValue;
+            this.returnType = original.returnType;
+        }
+
+        @Override
+        public Class<?> getParameterType() {
+            if (this.returnValue != null) {
+                return this.returnValue.getClass();
+            }
+            if (!ResolvableType.NONE.equals(this.returnType)) {
+                return this.returnType.toClass();
+            }
+            return super.getParameterType();
+        }
+
+        @Override
+        public Type getGenericParameterType() {
+            return this.returnType.getType();
+        }
+
+        @Override
+        public AsyncResultMethodParameter clone() {
+            return new AsyncResultMethodParameter(this);
+        }
+    }
+
+}

+ 2 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.env.EnvironmentPostProcessor=\
+  cn.newfeifan.mall.framework.shop.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor

+ 1 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.newfeifan.mall.framework.shop.tenant.config.FeifanTenantAutoConfiguration

+ 1 - 0
feifan-framework/pom.xml

@@ -36,6 +36,7 @@
         <module>feifan-spring-boot-starter-flowable</module>
         <module>feifan-spring-boot-starter-captcha</module>
         <module>feifan-spring-boot-starter-websocket</module>
+        <module>feifan-spring-boot-starter-biz-shop-tenant</module>
     </modules>
 
     <artifactId>feifan-framework</artifactId>

+ 2 - 0
feifan-server/src/main/resources/application.yaml

@@ -184,6 +184,8 @@ feifan:
       - cn.newfeifan.mall.module.system.enums.ErrorCodeConstants
       - cn.newfeifan.mall.module.mp.enums.ErrorCodeConstants
   tenant: # 多租户相关配置项
+    shop:
+      enable: false
     enable: false
     ignore-urls:
       - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号