Pārlūkot izejas kodu

1、租户配置通用租户,不可修改非本租户数据、租户id设置mybatsplus
2、超级管理员可以夸租户更改套餐

liyanbo 7 mēneši atpakaļ
vecāks
revīzija
173d7480e6
36 mainītis faili ar 382 papildinājumiem un 25 dzēšanām
  1. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CourseRespVO.java
  2. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/courseconfig/vo/CourseConfigRespVO.java
  3. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/courselabel/vo/CourseLabelRespVO.java
  4. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/coursequestion/vo/CourseQuestionRespVO.java
  5. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/coursequestoption/vo/CourseQuestOptionRespVO.java
  6. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/coursetype/vo/CourseTypeRespVO.java
  7. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/questionnaire/vo/QuestionnaireRespVO.java
  8. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/questionnaireresultdetails/vo/QuestionnaireResultDetailsRespVO.java
  9. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/reportmanage/vo/ReportManageRespVO.java
  10. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/course/CourseDO.java
  11. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/courseconfig/CourseConfigDO.java
  12. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/courselabel/CourseLabelDO.java
  13. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/coursequestion/CourseQuestionDO.java
  14. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/coursequestoption/CourseQuestOptionDO.java
  15. 4 1
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/coursetype/CourseTypeDO.java
  16. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/questionnaire/QuestionnaireDO.java
  17. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/questionnaireresult/QuestionnaireResultDO.java
  18. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/questionnaireresultdetails/QuestionnaireResultDetailsDO.java
  19. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/reportmanage/ReportManageDO.java
  20. 2 2
      byzs-framework/byzs-common/src/main/java/cn/iocoder/byzs/framework/common/enums/DocumentEnum.java
  21. 13 0
      byzs-framework/byzs-spring-boot-starter-biz-tenant/pom.xml
  22. 5 0
      byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/config/TenantProperties.java
  23. 15 1
      byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/config/YudaoTenantAutoConfiguration.java
  24. 19 0
      byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/context/TenantContextHolder.java
  25. 14 0
      byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/db/TenantDatabaseInterceptor.java
  26. 59 0
      byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/security/CustomTenantLineInnerInterceptor.java
  27. 99 0
      byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/security/TenantDataSecurityFilter.java
  28. 9 0
      byzs-framework/byzs-spring-boot-starter-web/src/main/java/cn/iocoder/byzs/framework/web/core/util/WebFrameworkUtils.java
  29. 2 0
      byzs-module-infra/src/main/java/cn/iocoder/byzs/module/infra/service/codegen/inner/CodegenBuilder.java
  30. 11 0
      byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/dal/mysql/permission/RoleMapper.java
  31. 7 0
      byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/service/permission/RoleService.java
  32. 4 0
      byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/service/permission/RoleServiceImpl.java
  33. 42 18
      byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/service/tenant/TenantServiceImpl.java
  34. 2 2
      byzs-server/src/main/resources/application-localProd.yaml
  35. 2 0
      byzs-server/src/main/resources/application.yaml
  36. 1 1
      byzs-server/src/main/resources/logback-spring.xml

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CourseRespVO.java

@@ -11,6 +11,9 @@ import cn.iocoder.byzs.framework.excel.core.convert.DictConvert;
 @ExcelIgnoreUnannotated
 public class CourseRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "课程d", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("课程d")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/courseconfig/vo/CourseConfigRespVO.java

@@ -14,6 +14,9 @@ import lombok.Data;
 @ExcelIgnoreUnannotated
 public class CourseConfigRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "课程配置id", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("课程配置id")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/courselabel/vo/CourseLabelRespVO.java

@@ -10,6 +10,9 @@ import com.alibaba.excel.annotation.*;
 @ExcelIgnoreUnannotated
 public class CourseLabelRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "课程标签关联id", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("课程标签关联id")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/coursequestion/vo/CourseQuestionRespVO.java

@@ -11,6 +11,9 @@ import com.alibaba.excel.annotation.*;
 @ExcelIgnoreUnannotated
 public class CourseQuestionRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "课程试题id", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("课程试题id")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/coursequestoption/vo/CourseQuestOptionRespVO.java

@@ -11,6 +11,9 @@ import cn.iocoder.byzs.framework.excel.core.convert.DictConvert;
 @ExcelIgnoreUnannotated
 public class CourseQuestOptionRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "课程试题选项id", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("课程试题选项id")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/coursetype/vo/CourseTypeRespVO.java

@@ -10,6 +10,9 @@ import com.alibaba.excel.annotation.*;
 @ExcelIgnoreUnannotated
 public class CourseTypeRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "课程类型id", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("课程类型id")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/questionnaire/vo/QuestionnaireRespVO.java

@@ -12,6 +12,9 @@ import java.time.LocalDateTime;
 @ExcelIgnoreUnannotated
 public class QuestionnaireRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "3832")
     @ExcelProperty("id")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/questionnaireresultdetails/vo/QuestionnaireResultDetailsRespVO.java

@@ -14,6 +14,9 @@ import cn.iocoder.byzs.framework.excel.core.convert.DictConvert;
 @ExcelIgnoreUnannotated
 public class QuestionnaireResultDetailsRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("id")
     private Long id;

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/reportmanage/vo/ReportManageRespVO.java

@@ -12,6 +12,9 @@ import java.time.LocalDateTime;
 @ExcelIgnoreUnannotated
 public class ReportManageRespVO {
 
+    @Schema(description = "租户编号")
+    private Long tenantId;
+
     @Schema(description = "主键id", requiredMode = Schema.RequiredMode.REQUIRED, example = "25358")
     @ExcelProperty("主键id")
     private Long brcId;

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/course/CourseDO.java

@@ -20,6 +20,11 @@ import cn.iocoder.byzs.framework.mybatis.core.dataobject.BaseDO;
 @AllArgsConstructor
 public class CourseDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * 课程d
      */

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/courseconfig/CourseConfigDO.java

@@ -22,6 +22,11 @@ import lombok.*;
 @AllArgsConstructor
 public class CourseConfigDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * 课程配置id
      */

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/courselabel/CourseLabelDO.java

@@ -19,6 +19,11 @@ import cn.iocoder.byzs.framework.mybatis.core.dataobject.BaseDO;
 @AllArgsConstructor
 public class CourseLabelDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * 课程标签关联id
      */

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/coursequestion/CourseQuestionDO.java

@@ -20,6 +20,11 @@ import cn.iocoder.byzs.framework.mybatis.core.dataobject.BaseDO;
 @AllArgsConstructor
 public class CourseQuestionDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * 课程试题id
      */

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/coursequestoption/CourseQuestOptionDO.java

@@ -19,6 +19,11 @@ import cn.iocoder.byzs.framework.mybatis.core.dataobject.BaseDO;
 @AllArgsConstructor
 public class CourseQuestOptionDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * 课程试题选项id
      */

+ 4 - 1
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/coursetype/CourseTypeDO.java

@@ -51,6 +51,9 @@ public class CourseTypeDO extends BaseDO {
      * 课程类型描述
      */
     private String ctTypeDescribe;
-
+    /**
+     * 租户id
+     */
+    private Long tenantId;
 
 }

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/questionnaire/QuestionnaireDO.java

@@ -21,6 +21,11 @@ import lombok.*;
 @AllArgsConstructor
 public class QuestionnaireDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * id
      */

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/questionnaireresult/QuestionnaireResultDO.java

@@ -25,6 +25,11 @@ import cn.iocoder.byzs.framework.mybatis.core.dataobject.BaseDO;
 @AllArgsConstructor
 public class QuestionnaireResultDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * id
      */

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/questionnaireresultdetails/QuestionnaireResultDetailsDO.java

@@ -22,6 +22,11 @@ import cn.iocoder.byzs.framework.mybatis.core.dataobject.BaseDO;
 @AllArgsConstructor
 public class QuestionnaireResultDetailsDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * id
      */

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/reportmanage/ReportManageDO.java

@@ -21,6 +21,11 @@ import lombok.*;
 @AllArgsConstructor
 public class ReportManageDO extends BaseDO {
 
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+
     /**
      * 主键id
      */

+ 2 - 2
byzs-framework/byzs-common/src/main/java/cn/iocoder/byzs/framework/common/enums/DocumentEnum.java

@@ -12,8 +12,8 @@ import lombok.Getter;
 @AllArgsConstructor
 public enum DocumentEnum {
 
-    REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"),
-    TENANT("https://doc.iocoder.cn", "SaaS 多租户文档");
+    REDIS_INSTALL("未提供", "Redis 安装文档"),
+    TENANT("未提供", "SaaS 多租户文档");
 
     private final String url;
     private final String memo;

+ 13 - 0
byzs-framework/byzs-spring-boot-starter-biz-tenant/pom.xml

@@ -31,6 +31,19 @@
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>byzs-spring-boot-starter-mybatis</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.github.jsqlparser</groupId>
+                    <artifactId>jsqlparser</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- 确保 JSQLParser 4.8 版本优先生效 -->
+        <dependency>
+            <groupId>com.github.jsqlparser</groupId>
+            <artifactId>jsqlparser</artifactId>
+            <version>4.8</version>
         </dependency>
 
         <dependency>

+ 5 - 0
byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/config/TenantProperties.java

@@ -26,6 +26,11 @@ public class TenantProperties {
      */
     private Boolean enable = ENABLE_DEFAULT;
 
+    /**
+     * 需要拼接的路径
+     */
+    private Set<String> spliceUrls = new HashSet<>();
+
     /**
      * 需要忽略多租户的请求
      *

+ 15 - 1
byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/config/YudaoTenantAutoConfiguration.java

@@ -13,6 +13,8 @@ import cn.iocoder.byzs.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializ
 import cn.iocoder.byzs.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor;
 import cn.iocoder.byzs.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
 import cn.iocoder.byzs.framework.tenant.core.redis.TenantRedisCacheManager;
+import cn.iocoder.byzs.framework.tenant.core.security.CustomTenantLineInnerInterceptor;
+import cn.iocoder.byzs.framework.tenant.core.security.TenantDataSecurityFilter;
 import cn.iocoder.byzs.framework.tenant.core.security.TenantSecurityWebFilter;
 import cn.iocoder.byzs.framework.tenant.core.service.TenantFrameworkService;
 import cn.iocoder.byzs.framework.tenant.core.service.TenantFrameworkServiceImpl;
@@ -74,13 +76,25 @@ public class YudaoTenantAutoConfiguration {
     @Bean
     public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
                                                                  MybatisPlusInterceptor interceptor) {
-        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
+        // 使用自定义的拦截器替代默认的
+        TenantDatabaseInterceptor tenantHandler = new TenantDatabaseInterceptor(properties);
+        TenantLineInnerInterceptor inner = new CustomTenantLineInnerInterceptor(tenantHandler);
+
+//        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
         // 添加到 interceptor 中
         // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
         MyBatisUtils.addInterceptor(interceptor, inner, 0);
         return inner;
     }
 
+    @Bean
+    public FilterRegistrationBean<TenantDataSecurityFilter> tenantDataSecurityFilter() {
+        FilterRegistrationBean<TenantDataSecurityFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantDataSecurityFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER + 1); // 放在租户安全过滤器之后
+        return registrationBean;
+    }
+
     // ========== WEB ==========
 
     @Bean

+ 19 - 0
byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/context/TenantContextHolder.java

@@ -20,6 +20,11 @@ public class TenantContextHolder {
      */
     private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
 
+    /**
+     * 是否拼接租户
+     */
+    private static final ThreadLocal<Boolean> SPLICE = new TransmittableThreadLocal<>(true);
+
     /**
      * 获得租户编号
      *
@@ -51,6 +56,10 @@ public class TenantContextHolder {
         IGNORE.set(ignore);
     }
 
+    public static void setSplice(Boolean splice) {
+        SPLICE.set(splice);
+    }
+
     /**
      * 当前是否忽略租户
      *
@@ -60,9 +69,19 @@ public class TenantContextHolder {
         return Boolean.TRUE.equals(IGNORE.get());
     }
 
+    /**
+     * 当前是否拼接租户
+     *
+     * @return 是否拼接
+     */
+    public static boolean isSplice() {
+        return Boolean.TRUE.equals(SPLICE.get());
+    }
+
     public static void clear() {
         TENANT_ID.remove();
         IGNORE.remove();
+        SPLICE.remove();
     }
 
 }

+ 14 - 0
byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/db/TenantDatabaseInterceptor.java

@@ -7,8 +7,14 @@ import com.baomidou.mybatisplus.core.metadata.TableInfo;
 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
 import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils;
+import lombok.extern.slf4j.Slf4j;
 import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+
+import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
+import net.sf.jsqlparser.schema.Column;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -18,6 +24,7 @@ import java.util.Map;
  *
  * @author 博雅智算源码
  */
+@Slf4j
 public class TenantDatabaseInterceptor implements TenantLineHandler {
 
     /**
@@ -80,4 +87,11 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
         return tenantIgnore != null;
     }
 
+    // 更新和删除时进行租户检查
+    @Override
+    public String getTenantIdColumn() {
+        // 默认实现,返回表中存储租户ID的列名
+        return "tenant_id";
+    }
+
 }

+ 59 - 0
byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/security/CustomTenantLineInnerInterceptor.java

@@ -0,0 +1,59 @@
+package cn.iocoder.byzs.framework.tenant.core.security;
+
+import cn.iocoder.byzs.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.byzs.framework.web.core.util.WebFrameworkUtils;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+
+/**
+ * 自定义租户行拦截器,解决多表联查时tenant_id字段歧义问题
+ */
+@Slf4j
+public class CustomTenantLineInnerInterceptor extends TenantLineInnerInterceptor {
+
+    public CustomTenantLineInnerInterceptor(TenantLineHandler tenantLineHandler) {
+        super(tenantLineHandler);
+    }
+
+
+    /**
+     * 重写buildTableExpression方法,解决列歧义问题
+     */
+    @Override
+    public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
+        log.info("table:{}, where:{}, whereSegment:{}", table.getName(), where, whereSegment);
+        Expression expression = super.buildTableExpression(table, where, whereSegment);
+
+        // 不拼接sql
+        if (expression == null) {
+            return expression;
+        }
+        if (TenantContextHolder.isSplice()) {
+            return new EqualsTo(this.getAliasColumn(table), this.getTenantLineHandler().getTenantId());
+        }
+
+        String column = "";
+        //去除表明的别名
+        if (table.getAlias() != null) {
+            column = table.getAlias().getName() + "." ;
+        }
+        column += WebFrameworkUtils.HEADER_DATA_TENANT_ID;
+        Long currentTenantId = TenantContextHolder.getRequiredTenantId();
+        // 构造 JSQLParser 兼容的列表达式
+        Column tenantIdColumn = new Column(column);
+        EqualsTo condition1 = new EqualsTo(tenantIdColumn, new LongValue(currentTenantId));
+        EqualsTo condition2 = new EqualsTo(tenantIdColumn, new LongValue(1L));
+
+        // 用 ParenthesedExpressionList 包裹 OR 表达式添加括号
+        OrExpression orExpression = new OrExpression(condition1, condition2);
+        return new ParenthesedExpressionList<>(orExpression); // 生成 (A OR B)
+    }
+}

+ 99 - 0
byzs-framework/byzs-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/byzs/framework/tenant/core/security/TenantDataSecurityFilter.java

@@ -0,0 +1,99 @@
+package cn.iocoder.byzs.framework.tenant.core.security;
+
+import cn.iocoder.byzs.framework.common.pojo.CommonResult;
+import cn.iocoder.byzs.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.byzs.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.byzs.framework.web.core.util.WebFrameworkUtils;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static cn.iocoder.byzs.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
+
+/**
+ * 租户数据安全过滤器
+ * 用于防止用户修改不属于自己租户的数据
+ */
+public class TenantDataSecurityFilter extends OncePerRequestFilter {
+
+    // 修改操作的HTTP方法
+    private static final Set<String> MODIFY_METHODS = new HashSet<>(Arrays.asList("POST", "PUT", "DELETE", "PATCH"));
+
+    // 不参与SQL拼接的特定方法路径
+    private static final Set<String> EXCLUDED_METHOD_PATHS = new HashSet<>(Arrays.asList(
+            "/system/tenant/get-id-by-name",
+            "/system/auth/login",
+            "/system/auth/logout",
+            "system/captcha/get",
+            "system/auth/get-permission-info",
+            "system/dict-data/simple-list",
+            "/system/user/page",
+            "system/dept/page",
+            "system/role/page",
+            "system/menu/page",
+            "system/param/page",
+            "system/param/simple-list",
+            "system/tenant/page",
+            "system/tenant/page",
+            "system/tenant/create",
+            "/ai/chat/conversation/my-list",
+            "/ai/image/my-page"
+    ));
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+//        // 首先检查是否isExcludedPath(request)为需要排除的路径
+//        if (request.getMethod().equalsIgnoreCase("GET")) {
+//
+//            return;
+//        }
+        if (isExcludedPath(request)) {
+            // 对于排除的路径,完全忽略租户过滤,确保只返回单条记录
+            boolean splice = TenantContextHolder.isSplice();
+            try {
+                TenantContextHolder.setSplice(true);
+                chain.doFilter(request, response);
+
+                return; // 直接返回,不再执行后续的过滤逻辑
+            } finally {
+                // 恢复原始状态
+                TenantContextHolder.setSplice(splice);
+            }
+        }
+
+        // 请求数据的租户id
+        String dataTenantId = request.getHeader(WebFrameworkUtils.HEADER_DATA_TENANT_ID);
+
+        // 获取当前请求的租户ID
+        Long currentTenantId = TenantContextHolder.getTenantId();
+        if (dataTenantId != null && currentTenantId != null && !dataTenantId.equals(currentTenantId.toString())) {
+            ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN.getCode(),
+                    "不允许修改非本租户的数据"));
+            return;
+        }
+        // 如果是默认租户1,直接放行
+        if (WebFrameworkUtils.DEFAULT_TENANT_ID.equals(currentTenantId)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        // 在数据访问层,会根据TenantContextHolder.getTenantId()获取当前租户ID,并拼接待查询的租户ID列表
+        chain.doFilter(request, response);
+    }
+
+    /**
+     * 判断当前请求路径是否为不参与SQL拼接的特定方法路径
+     */
+    private boolean isExcludedPath(HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        return EXCLUDED_METHOD_PATHS.stream().anyMatch(path -> requestURI.contains(path));
+    }
+}

+ 9 - 0
byzs-framework/byzs-spring-boot-starter-web/src/main/java/cn/iocoder/byzs/framework/web/core/util/WebFrameworkUtils.java

@@ -23,9 +23,18 @@ public class WebFrameworkUtils {
 
     private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
 
+    // 默认租户ID
+    public static final Long DEFAULT_TENANT_ID = 1L;
+    //租户id
     public static final String HEADER_TENANT_ID = "tenant-id";
+    //数据租户id
+    public static final String HEADER_DATA_TENANT_ID = "tenant_id";
+    //访问租户id
     public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id";
 
+    // 超级管理员角色编码
+    public static final String SUPER_ADMIN_ROLE_CODE = "super_admin";
+
     /**
      * 终端的 Header
      *

+ 2 - 0
byzs-module-infra/src/main/java/cn/iocoder/byzs/module/infra/service/codegen/inner/CodegenBuilder.java

@@ -92,8 +92,10 @@ public class CodegenBuilder {
         UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS);
         LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS);
         LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是可能需要传递的
+        LIST_OPERATION_EXCLUDE_COLUMN.remove("tenantId"); // 租户id,还是可能需要传递的
         LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS);
         LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是需要返回的
+        LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("tenantId"); // 租户id,还是需要返回的
     }
 
     public CodegenTableDO buildTable(TableInfo tableInfo) {

+ 11 - 0
byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/dal/mysql/permission/RoleMapper.java

@@ -36,4 +36,15 @@ public interface RoleMapper extends BaseMapperX<RoleDO> {
         return selectList(RoleDO::getStatus, statuses);
     }
 
+    /**
+     * 查询指定租户的角色列表
+     *
+     * @param tenantId 租户编号
+     * @return 角色列表
+     */
+    default List<RoleDO> selectListByTenantId(Long tenantId) {
+        return selectList(new LambdaQueryWrapperX<RoleDO>()
+                .eq(RoleDO::getTenantId, tenantId)
+                .orderByAsc(RoleDO::getSort));
+    }
 }

+ 7 - 0
byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/service/permission/RoleService.java

@@ -121,4 +121,11 @@ public interface RoleService {
      */
     void validateRoleList(Collection<Long> ids);
 
+    /**
+     * 获得指定租户的角色列表
+     *
+     * @param tenantId 租户编号
+     * @return 角色列表
+     */
+    List<RoleDO> getRoleListByTenantId(Long tenantId);
 }

+ 4 - 0
byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/service/permission/RoleServiceImpl.java

@@ -259,4 +259,8 @@ public class RoleServiceImpl implements RoleService {
         return SpringUtil.getBean(getClass());
     }
 
+    @Override
+    public List<RoleDO> getRoleListByTenantId(Long tenantId) {
+        return roleMapper.selectListByTenantId(tenantId);
+    }
 }

+ 42 - 18
byzs-module-system/src/main/java/cn/iocoder/byzs/module/system/service/tenant/TenantServiceImpl.java

@@ -10,9 +10,11 @@ import cn.iocoder.byzs.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.byzs.framework.common.util.date.DateUtils;
 import cn.iocoder.byzs.framework.common.util.object.BeanUtils;
 import cn.iocoder.byzs.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.byzs.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.byzs.framework.tenant.config.TenantProperties;
 import cn.iocoder.byzs.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.byzs.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.byzs.module.system.api.permission.PermissionApi;
 import cn.iocoder.byzs.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
 import cn.iocoder.byzs.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
 import cn.iocoder.byzs.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
@@ -74,6 +76,8 @@ public class TenantServiceImpl implements TenantService {
     private MenuService menuService;
     @Resource
     private PermissionService permissionService;
+    @Resource
+    private PermissionApi permissionApi;
 
     @Override
     public List<Long> getTenantIdList() {
@@ -195,25 +199,45 @@ public class TenantServiceImpl implements TenantService {
     @Override
     @DSTransactional
     public void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds) {
-        TenantUtils.execute(tenantId, () -> {
-            // 获得所有角色
-            List<RoleDO> roles = roleService.getRoleList();
-            roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配",
-                    role.getId(), role.getTenantId(), tenantId)); // 兜底校验
-            // 重新分配每个角色的权限
-            roles.forEach(role -> {
-                // 如果是租户管理员,重新分配其权限为租户套餐的权限
-                if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) {
-                    permissionService.assignRoleMenu(role.getId(), menuIds);
-                    log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds);
-                    return;
-                }
-                // 如果是其他角色,则去掉超过套餐的权限
-                Set<Long> roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId());
-                roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds);
-                permissionService.assignRoleMenu(role.getId(), roleMenuIds);
-                log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds);
+        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
+        boolean isSuperAdmin = permissionApi.hasAnyRoles(loginUserId, RoleCodeEnum.SUPER_ADMIN.getCode());
+
+        if (isSuperAdmin) {
+            // 超级管理员:忽略租户隔离,直接操作
+            TenantUtils.executeIgnore(() -> {
+                List<RoleDO> roles = roleService.getRoleListByTenantId(tenantId);
+                updateRoleMenus(roles, menuIds, tenantId);
+            });
+        } else {
+            // 普通用户:使用租户切换
+            TenantUtils.execute(tenantId, () -> {
+                List<RoleDO> roles = roleService.getRoleList();
+                updateRoleMenus(roles, menuIds, tenantId);
             });
+        }
+    }
+
+    /**
+     * 更新角色菜单权限的通用方法
+     */
+    private void updateRoleMenus(List<RoleDO> roles, Set<Long> menuIds, Long tenantId) {
+        roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配",
+                role.getId(), role.getTenantId(), tenantId)); // 兜底校验
+
+        // 重新分配每个角色的权限
+        roles.forEach(role -> {
+            // 如果是租户管理员,重新分配其权限为租户套餐的权限
+            if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) {
+                permissionService.assignRoleMenu(role.getId(), menuIds);
+                log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds);
+                return;
+            }
+
+            // 如果是其他角色,则去掉超过套餐的权限
+            Set<Long> roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId());
+            roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds);
+            permissionService.assignRoleMenu(role.getId(), roleMenuIds);
+            log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds);
         });
     }
 

+ 2 - 2
byzs-server/src/main/resources/application-localProd.yaml

@@ -48,7 +48,7 @@ spring:
       primary: master
       datasource:
         master:
-          url: jdbc:mysql://127.0.0.1:3306/byzs-bjdx-59?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true&allowMultiQueries=true # MySQL Connector/J 8.X 连接的示例
+          url: jdbc:mysql://127.0.0.1:3306/byzs-bjdx-59-local?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true&allowMultiQueries=true # MySQL Connector/J 8.X 连接的示例
           #          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-all?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
           #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
           #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
@@ -89,7 +89,7 @@ spring:
     redis:
       host: 127.0.0.1 # 地址
       port: 6379 # 端口
-      database: 0 # 数据库索引
+      database: 1 # 数据库索引
       password: root # 密码,建议生产环境开启
 
 --- #################### 定时任务相关配置 ####################

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

@@ -285,6 +285,8 @@ byzs:
     unit-test-enable: false # 是否生成单元测试
   tenant: # 多租户相关配置项
     enable: true
+    splice-urls:
+      - /system/
     ignore-urls:
       - /jmreport/* # 积木报表,无法携带租户编号
     ignore-visit-urls:

+ 1 - 1
byzs-server/src/main/resources/logback-spring.xml

@@ -57,7 +57,7 @@
     </appender>
 
     <!-- 本地环境 -->
-    <springProfile name="local">
+    <springProfile name="local,localProd">
         <root level="INFO">
             <appender-ref ref="STDOUT"/>
             <appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->