ruoyi-nbcio-plus基于vue3的多租户机制

news2024/11/25 19:35:10

更多ruoyi-nbcio功能请看演示系统

gitee源代码地址

前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio

演示地址:RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/

更多nbcio-boot功能请看演示系统 

gitee源代码地址

后端代码: https://gitee.com/nbacheng/nbcio-boot

前端代码:https://gitee.com/nbacheng/nbcio-vue.git

在线演示(包括H5) : http://122.227.135.243:9888

       因为基于ruoyi-vue-plus的框架,所以多租户总体基于使用了 MyBatis-Plus (简称 MP)的多租户插件功能

      可以参考

  • MP官方文档 - 多租户插件
  • MP官方 Demo

    实现主要有以下步骤:

    在相关表添加多租户字段
    在多租户配置TenantConfig 里中添加多租户插件拦截器 TenantLineInnerInterceptor
根据业务对多租户插件拦截器 TenantLineInnerInterceptor 进行配置(多租户字段、需要进行过滤的表等)
    在数据库相关表中加入租户id字段 tenant_id(别忘了相关实体类也要加上)

具体代码如下:

@EnableConfigurationProperties(TenantProperties.class)
@AutoConfiguration(after = {RedisConfig.class, MybatisPlusConfig.class})
@ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
public class TenantConfig {

    /**
     * 初始化租户配置
     */
    @Bean
    public boolean tenantInit(MybatisPlusInterceptor mybatisPlusInterceptor,
                              TenantProperties tenantProperties) {
        List<InnerInterceptor> interceptors = new ArrayList<>();
        // 多租户插件 必须放到第一位
        interceptors.add(tenantLineInnerInterceptor(tenantProperties));
        interceptors.addAll(mybatisPlusInterceptor.getInterceptors());
        mybatisPlusInterceptor.setInterceptors(interceptors);
        return true;
    }

    /**
     * 多租户插件
     */
    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties tenantProperties) {
        return new TenantLineInnerInterceptor(new PlusTenantLineHandler(tenantProperties));
    }

    @Bean
    public RedissonAutoConfigurationCustomizer tenantRedissonCustomizer(RedissonProperties redissonProperties) {
        return config -> {
            TenantKeyPrefixHandler nameMapper = new TenantKeyPrefixHandler(redissonProperties.getKeyPrefix());
            SingleServerConfig singleServerConfig = ReflectUtils.invokeGetter(config, "singleServerConfig");
            if (ObjectUtil.isNotNull(singleServerConfig)) {
                // 使用单机模式
                // 设置多租户 redis key前缀
                singleServerConfig.setNameMapper(nameMapper);
                ReflectUtils.invokeSetter(config, "singleServerConfig", singleServerConfig);
            }
            ClusterServersConfig clusterServersConfig = ReflectUtils.invokeGetter(config, "clusterServersConfig");
            // 集群配置方式 参考下方注释
            if (ObjectUtil.isNotNull(clusterServersConfig)) {
                // 设置多租户 redis key前缀
                clusterServersConfig.setNameMapper(nameMapper);
                ReflectUtils.invokeSetter(config, "clusterServersConfig", clusterServersConfig);
            }
        };
    }

    /**
     * 多租户缓存管理器
     */
    @Primary
    @Bean
    public CacheManager tenantCacheManager() {
        return new TenantSpringCacheManager();
    }

    /**
     * 多租户鉴权dao实现
     */
    @Primary
    @Bean
    public SaTokenDao tenantSaTokenDao() {
        return new TenantSaTokenDao();
    }

}

其中 自定义租户处理器代码如下:

/**
 * 自定义租户处理器
 *
 * @author nbacheng
 */
@Slf4j
@AllArgsConstructor
public class PlusTenantLineHandler implements TenantLineHandler {

    private final TenantProperties tenantProperties;

    @Override
    public Expression getTenantId() {
        String tenantId = TenantHelper.getTenantId();
        if (StringUtils.isBlank(tenantId)) {
            log.error("无法获取有效的租户id -> Null");
            return new NullValue();
        }
        String dynamicTenantId = TenantHelper.getDynamic();
        if (StringUtils.isNotBlank(dynamicTenantId)) {
            // 返回动态租户
            return new StringValue(dynamicTenantId);
        }
        // 返回固定租户
        return new StringValue(tenantId);
    }

    @Override
    public boolean ignoreTable(String tableName) {
        String tenantId = TenantHelper.getTenantId();
        // 判断是否有租户
        if (StringUtils.isNotBlank(tenantId)) {
            // 不需要过滤租户的表
            List<String> excludes = tenantProperties.getExcludes();
            // 非业务表
            List<String> tables = ListUtil.toList(
                "gen_table",
                "gen_table_column"
            );
            tables.addAll(excludes);
            return tables.contains(tableName);
        }
        return true;
    }

}

上面就是重载了mybasisplus的TenantLineHandler 

/**
 * 租户处理器( TenantId 行级 )
 *
 * @author hubin
 * @since 3.4.0
 */
public interface TenantLineHandler {

    /**
     * 获取租户 ID 值表达式,只支持单个 ID 值
     * <p>
     *
     * @return 租户 ID 值表达式
     */
    Expression getTenantId();

    /**
     * 获取租户字段名
     * <p>
     * 默认字段名叫: tenant_id
     *
     * @return 租户字段名
     */
    default String getTenantIdColumn() {
        return "tenant_id";
    }

    /**
     * 根据表名判断是否忽略拼接多租户条件
     * <p>
     * 默认都要进行解析并拼接多租户条件
     *
     * @param tableName 表名
     * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
     */
    default boolean ignoreTable(String tableName) {
        return false;
    }

    /**
     * 忽略插入租户字段逻辑
     *
     * @param columns        插入字段
     * @param tenantIdColumn 租户 ID 字段
     * @return
     */
    default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
        return columns.stream().map(Column::getColumnName).anyMatch(i -> i.equalsIgnoreCase(tenantIdColumn));
    }
}

多租户插件的调用流程如下图:

上面主要是用到了mybatisPlusInterceptor,

public class MybatisPlusInterceptor implements Interceptor {

    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
            Object parameter = args[1];
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    // 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
                    boundSql = (BoundSql) args[5];
                }
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {

上面进入beforeQuery

public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
            return;
        }
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), null));
    }

通过parserSingle的processParser

public String parserSingle(String sql, Object obj) {
        if (logger.isDebugEnabled()) {
            logger.debug("original SQL: " + sql);
        }
        try {
            Statement statement = JsqlParserGlobal.parse(sql);
            return processParser(statement, 0, sql, obj);
        } catch (JSQLParserException e) {
            throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e.getCause(), sql);
        }
    }
protected String processParser(Statement statement, int index, String sql, Object obj) {
        if (logger.isDebugEnabled()) {
            logger.debug("SQL to parse, SQL: " + sql);
        }
        if (statement instanceof Insert) {
            this.processInsert((Insert) statement, index, sql, obj);
        } else if (statement instanceof Select) {
            this.processSelect((Select) statement, index, sql, obj);
        } else if (statement instanceof Update) {
            this.processUpdate((Update) statement, index, sql, obj);
        } else if (statement instanceof Delete) {
            this.processDelete((Delete) statement, index, sql, obj);
        }
        sql = statement.toString();
        if (logger.isDebugEnabled()) {
            logger.debug("parse the finished SQL: " + sql);
        }
        return sql;
    }

通过这个processSelect的进入select


@Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        final String whereSegment = (String) obj;
        processSelectBody(select.getSelectBody(), whereSegment);
        List<WithItem> withItemsList = select.getWithItemsList();
        if (!CollectionUtils.isEmpty(withItemsList)) {
            withItemsList.forEach(withItem -> processSelectBody(withItem, whereSegment));
        }
    }

其中进入processSelectBody处理

protected void processSelectBody(SelectBody selectBody, final String whereSegment) {
        if (selectBody == null) {
            return;
        }
        if (selectBody instanceof PlainSelect) {
            processPlainSelect((PlainSelect) selectBody, whereSegment);
        } else if (selectBody instanceof WithItem) {
            WithItem withItem = (WithItem) selectBody;
            processSelectBody(withItem.getSubSelect().getSelectBody(), whereSegment);
        } else {
            SetOperationList operationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = operationList.getSelects();
            if (CollectionUtils.isNotEmpty(selectBodyList)) {
                selectBodyList.forEach(body -> processSelectBody(body, whereSegment));
            }
        }
    }

之后进入processPlainSelect

protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) {
        //#3087 github
        List<SelectItem> selectItems = plainSelect.getSelectItems();
        if (CollectionUtils.isNotEmpty(selectItems)) {
            selectItems.forEach(selectItem -> processSelectItem(selectItem, whereSegment));
        }

        // 处理 where 中的子查询
        Expression where = plainSelect.getWhere();
        processWhereSubSelect(where, whereSegment);

        // 处理 fromItem
        FromItem fromItem = plainSelect.getFromItem();
        List<Table> list = processFromItem(fromItem, whereSegment);
        List<Table> mainTables = new ArrayList<>(list);

        // 处理 join
        List<Join> joins = plainSelect.getJoins();
        if (CollectionUtils.isNotEmpty(joins)) {
            mainTables = processJoins(mainTables, joins, whereSegment);
        }

        // 当有 mainTable 时,进行 where 条件追加
        if (CollectionUtils.isNotEmpty(mainTables)) {
            plainSelect.setWhere(builderExpression(where, mainTables, whereSegment));
        }
    }

上面进入builderExpression 构造表达式

protected Expression builderExpression(Expression currentExpression, List<Table> tables, final String whereSegment) {
        // 没有表需要处理直接返回
        if (CollectionUtils.isEmpty(tables)) {
            return currentExpression;
        }
        // 构造每张表的条件
        List<Expression> expressions = tables.stream()
            .map(item -> buildTableExpression(item, currentExpression, whereSegment))
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

        // 没有表需要处理直接返回
        if (CollectionUtils.isEmpty(expressions)) {
            return currentExpression;
        }

        // 注入的表达式
        Expression injectExpression = expressions.get(0);
        // 如果有多表,则用 and 连接
        if (expressions.size() > 1) {
            for (int i = 1; i < expressions.size(); i++) {
                injectExpression = new AndExpression(injectExpression, expressions.get(i));
            }
        }

上面的buildTableExpression加入了租户的条件

public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
        if (tenantLineHandler.ignoreTable(table.getName())) {
            return null;
        }
        return new EqualsTo(getAliasColumn(table), tenantLineHandler.getTenantId());
    }

最终通过前面的processParser获取select的sql表达式,加入了多租户条件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1607123.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

饮料市场迎来“营养革命”?2024年饮料行业销售数据分析已出炉

随着健康意识的日益增强&#xff0c;消费者对于饮料的需求已经不再是单纯追求口感和美味&#xff0c;而是更加关注产品的营养价值和健康属性。在这种背景下&#xff0c;上海市卫生健康委近期启动了“首批营养健康指导试点项目”&#xff0c;其中饮料“营养选择”分级标识试点的…

DFS专题:力扣岛屿问题(持续更新)

DFS专题&#xff1a;力扣岛屿问题 开篇 每次做到DFS相关的题目都是直接跳过。蓝桥杯过后痛定思痛&#xff0c;好好学习一下DFS和BFS。先从DFS开始吧。 参考题解&#xff1a;nettee&#xff1a;岛屿类问题的通用解法、DFS 遍历框架 题目链接&#xff1a; 200.岛屿数量    …

机器学习波士顿房价

流程 数据获取导入需要的包引入文件,查看内容划分训练集和测试集调用模型查看准确率 数据获取 链接&#xff1a;https://pan.baidu.com/s/1deECYRPQFx8h28BvoZcbWw?pwdft5a 提取码&#xff1a;ft5a --来自百度网盘超级会员V1的分享导入需要的包 import pandas as pd imp…

FreeRTOS之动态创建任务与删除任务

1.本文是利用FreeRTOS来动态创建任务和删除任务。主要是使用FreeRTOS的两个API函数&#xff1a;xTaskCreate()和vTaskDelete()。 任务1和任务2是让LED0、LED1闪烁。任务3是当按键按下时删除任务1。 使用动态创建任务时&#xff0c;需要动态的堆中申请任务所需的内存空间&…

Eagle for Mac v1.9.13注册版:强大的图片管理工具

Eagle for Mac是一款专为Mac用户设计的图片管理工具&#xff0c;旨在帮助用户更高效、有序地管理和查找图片资源。 Eagle for Mac v1.9.13注册版下载 Eagle支持多种图片格式&#xff0c;包括JPG、PNG、GIF、SVG、PSD、AI等&#xff0c;无论是矢量图还是位图&#xff0c;都能以清…

软考 系统架构设计师系列知识点之大数据设计理论与实践(11)

接前一篇文章&#xff1a;软考 系统架构设计师系列知识点之大数据设计理论与实践&#xff08;10&#xff09; 所属章节&#xff1a; 第19章. 大数据架构设计理论与实践 第3节 Lambda架构 19.3.6 Lambda与其它架构模式对比 Lambda架构的诞生离不开很多现有设计思想和架构的铺垫…

ctfhub-ssrf(2)

1.URL Bypass 题目提示:请求的URL中必须包含http://notfound.ctfhub.com&#xff0c;来尝试利用URL的一些特殊地方绕过这个限制吧 打开环境发现URL中必须包含http://notfound.ctfhub.com&#xff0c;先按照之前的经验查看127.0.0.1/flag.php,发现没什么反应&#xff0c;按照题…

excel表格如何筛选重复的内容并单独显示

在处理Excel数据时&#xff0c;遇到大量数据时需要筛选数据中的重复值并单独显示出来&#xff0c;那么此时该如何处理呢&#xff1f;事实上在Excel表格中筛选出重复的内容并单独显示的方法有很多种&#xff0c;以下是其中常用的3种&#xff1a; 方法一&#xff1a;使用条件格式…

每日OJ题_多源BFS①_力扣542. 01 矩阵(多源BFS解决最短路原理)

目录 多源BFS解决最短路算法原理 力扣542. 01 矩阵 解析代码 多源BFS解决最短路算法原理 什么是单源最短路 / 多源最短路&#xff1f; 之前的BFS解决最短路都是解决的单源最短路。 画图来说&#xff0c;单源最短路问题即为&#xff1a; 而对于多源最短路问题: 如何解决此…

全球排名前十的搜索引擎,你猜百度排名在第几位?bing稳居二位!

通常情况下&#xff0c;营销人员在争夺其在线业务的流量时会非常关注Google&#xff0c;无论是通过他们的网站&#xff0c;博客文章还是其他形式的内容。考虑到谷歌无疑是最受欢迎的搜索引擎&#xff0c;拥有超过85%的搜索市场份额&#xff0c;这是有道理的。 但这种受欢迎程度…

STM32使用HAL库解码433遥控芯片EV1527

1、首先了解一下433遥控芯片ev1527的基本资料&#xff1a; 这是他编码的关键信息&#xff1a; 也就是说&#xff0c;一帧数据是&#xff1a;一个同步码20位内码4位按键码。 内码20位2^201048576个地址。 发送就是一帧数据接一帧数据不间断发送。 2、解码思路 从上面的帧结构…

node-mysql数据库的下载与安装

01 mysql数据库的安装 网址&#xff1a;mysql.com/downloads/ 打开之后往下翻 点击 MySQL Community (GPL) Downloads 》 点击 MySRL Community Server 再点击 No thanks,just stant my download. 02 安装mysql 03 安装完成之后检查mysql服务是否开启 services.msc 04 启动…

vue3【详解】 vue3 比 vue2 快的原因

使用 Proxy 实现响应式 vue3使用的 Proxy 在处理属性的读取和写入时&#xff0c;比vue2使用的defineProperty 有更好的性能&#xff08;速度加倍的同时&#xff0c;内存还能减半&#xff01;&#xff09; 更新类型标记 Patch Flag 在编译模板时&#xff08;将vue语法转换为js描…

Seal^_^【送书活动第一期】——《Vue.js+Node.js全栈开发实战(第2版)》

Seal^_^【送书活动第一期】——《Vue.jsNode.js全栈开发实战&#xff08;第2版&#xff09;》 一、参与方式二、本期推荐图书2.1 前 言2.2 作者简介2.3 图书简介2.4 本书特色2.5 编辑推荐2.6 书籍目录 三、正版购买 一、参与方式 1、关注博主的账号。 2、点赞、收藏、评论博主的…

如何判断两个IP地址是否在同一网段?

要判断两个IP地址是否在同一网段&#xff0c;首先需要对IP地址和子网掩码有深入的理解。IP地址是互联网协议地址&#xff0c;用于在IP通信中标识和定位每台设备的逻辑地址。而子网掩码则是一个32位的地址掩码&#xff0c;用于将IP地址划分为网络地址和主机地址两部分。通过比较…

9月BTE第8届广州国际生物技术大会暨展览会,全媒体聚焦下的高精尖行业盛会

政策春风助力&#xff0c;共迎大湾区生物医药行业50亿红利 今年3月“创新药”首次写入国务院政府工作报告之后&#xff0c;广州、珠海、北京多地政府纷纷同步出台了多项细化政策&#xff0c;广州最高支持额度高达50亿元&#xff0c;全链条为生物医药产业提供资金支持&#xff…

【C++】开始了解反向迭代器

送给大家一句话&#xff1a; 重要的东西眼睛是看不到的 — 《小王子》 反向迭代器 1 前言2 反向迭代器3 复刻反向迭代器3.1 加减操作3.2 判断操作3.3 访问操作 4 链表的反向迭代器Thanks♪(&#xff65;ω&#xff65;)&#xff89;谢谢阅读&#xff01;&#xff01;&#xff0…

SQVI创建以及生成程序

SAP数据快速查询工具&#xff1a;Sqvi-QuickView 项目实施&运维阶段&#xff0c;为了快速获取一些透明表数据&#xff0c;一开始接触项目肯定会通过大量的数据表查找&#xff0c;然后线下通过EXCEL通过VLOOKUP进行数据关联&#xff0c;这种方式在关联数据较少的情况比较适应…

【源码】2024新版二开版抢单刷单系统,前端简体、繁体双语言-支持倒计时抢单,后台指定派单连单卡单

CD&#xff1a;获取方式联系小编 微信&#xff1a;uucodes 公众号&#xff1a;资源猿 小编提供资源代找&#xff0c;环境搭建&#xff0c;源码部署调试等业务&#xff0c;需要的可以联系

APP广告变现项目要怎么去做,需要考虑哪些方面!!

要开始一个APP广告变现项目&#xff0c;您可以按照以下步骤进行操作&#xff1a; 制定商业计划&#xff1a;确定您的目标市场、目标受众和变现方式。了解竞争对手和市场趋势&#xff0c;并制定相应的推广策略。 开发APP&#xff1a;找到合适的开发团队或开发者来设计和开发您…