基于rouyi框架的多租户改造

         基于rouyi框架的多租户改造,重点是实现权限管理数据隔离。权限管理相当于从原来的“顶级管理员admin-普通用户user”转变为“顶级管理员admin-租户管理员tanantAdmin-普通用户user”。数据隔离主要通过分库、分表、表内设置tenantId字段进行过滤三种方式。

       本文主要介绍了rouyi下(SpringBoot3+vue2)权限管理的改造方法思路以及数据隔离的分表、同表加字段过滤方法。同时介绍了:

多租户改造的重点:权限管理+数据隔离实现方法;

多租户优化功能:切换租户设置虚拟ID,实现免登录对应租户账号可查看下级租户数据;

前端请求头设置、后端请求头拦截器的使用;

Spring Security手动设置登录信息的方法;

子模块互相调用时避免相互依赖解决方法;

mybatisPlus的动态表名插件拦截器使用(手动在需要的sql过滤,非全自动);

手动tenantId过滤的方法和注意事项;

目录

一、权限管理

1、顶级租户用户

2、子集租户用户

3、菜单、角色分配、部门分配、租户实现角色控制

​二、数据隔离

三、实现方案

1、租户切换(请求头设置与拦截方法、Spring Security框架下手动更改登录信息方法)

(1)实现功能

(2)实现方式

(3)实现案例

(3.1)前端请求头设置及请求封装

         (3.2)请求拦截器获取tenantId,Spring Security框架下更新登录用户信息

            (3.3)拦截器配置文件:resourcesConfig.java

2、权限管理控制与租户管理

(1)权限管理

(2)租户管理

3、Mybatis拦截器实现动态表名

(3.1)Mybatis动态表名拦截器

(3.2)MyBatisConfig.java添加插件DynamicTableNameInnerInterceptor

(3.3)自定义使用

4、分表隔离(子模块之间相互调用且避免循环依赖方法)

5、过滤字段隔离

6、分表和过滤字段隔离 sql修改注意

7、定时任务


一、权限管理

1、顶级租户用户

用户admin不属于任何租户,唯一账号且最高权限,唯一可以管理租户的账号。

新建菜单==>新建租户并为租户分配菜单==>新建人员(带归属租户)

2、子集租户用户

租户管理员角色tenantAdmin,新建人员(租户归属只能是自己租户),新建角色并为之分配菜单,菜单最多分配到 本租户分配到的菜单。

3、菜单、角色分配、部门分配、租户实现角色控制

二、数据隔离

       Saas数据隔离主要通过:分库、分表、字段过滤三种方式。分库可以通过动态数据源实现,分表可以通过动态表名实现(手动设置或mybatisPlus的DynamicTableNameInnerInterceptor),字段过滤可以通过where条件手动或mybatisPlus的TenantLineInnerInterceptor实现。

三、实现方案

1、租户切换(请求头设置与拦截方法、Spring Security框架下手动更改登录信息方法)

(1)实现功能

       租户切换为优化功能,admin超级管理员可以不用创建和登录租户下用户的账号,就可以以该租户的最高权限操作该租户的数据,查看该租户的数据。

(2)实现方式

前端:切换租户时,将切换的租户id进行缓存,在request请求头加上tenantId信息;后续每个请求进行封装时请求头都会加上tenantId信息。

后端:请求头拦截器做改造,请求头拦截器可以拦截过滤每一个访问后端的请求。设置虚拟mockTenantId(切换租户id,区分实际登录用户的tenantId), 将虚拟mockTenantId存到登录用户信息里。登录用户信息采用Spring Security框架,人为手动更改登录用户信息后,需要调用tokenService更新;也可以将登录用户信息存储在redis,但在使用Spring Security框架时采用此种方法,容易造成登录信息不同步(框架操作了登录用户信息,但redis未人工加代码同步更新)。

(3)实现案例
  (3.1)前端请求头设置及请求封装

request.js文件:

config.headers['tenantId']请求头设置;encodeURIComponent要加,否则会乱码;localStore为本地缓存,也可通过其他方式。

// request拦截器
service.interceptors.request.use(config => {// 是否需要设置 tokenconst isToken = (config.headers || {}).isToken === falseif (getToken() && !isToken) {config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改config.headers['tenantId'] = localStore.get('tenantId') === undefined ? '': encodeURIComponent(localStore.get('tenantId')) // 让每个请求携带自定义token 请根据实际情况自行修改}
(3.2)请求拦截器获取tenantId,Spring Security框架下更新登录用户信息

headerInterceptor.java文件:

Spring Security框架鉴权方式下,登录信息由框架代码处理,获取方法为

LoginUser loginUser = tokenService.getLoginUser(token);

或者

LoginUser loginUser =  SecurityUtils.getLoginUser();

要想手动修改登录信息需要调用:

tokenService.refreshToken(loginUser)。

也可以通过redis存储同步。

package com.inspur.framework.interceptor;

import com.inspur.common.constant.SecurityConstants;

import com.inspur.common.core.domain.model.LoginUser;

import com.inspur.common.service.TokenService;
import com.inspur.common.utils.RSAUtils;
import com.inspur.common.utils.ServletUtils;
import com.inspur.common.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * 自定义请求头拦截器,将Header数据封装到线程变量中方便获取
 * 注意:此拦截器会同时验证当前用户有效期自动刷新有效期
 *
 * @author inspur
 */
@Component
public class HeaderInterceptor implements HandlerInterceptor
{
    private final Logger logger = LoggerFactory.getLogger(HeaderInterceptor.class);
    @Autowired
    private TokenService tokenService;
    @Override
    public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception
    {
        if (!(handler instanceof HandlerMethod))
        {
            return true;
        }
        String token = request.getHeader(SecurityConstants.AUTHORIZATION_HEADER).substring(7);
        if (StringUtils.isNotEmpty(token)) {
            LoginUser loginUser = tokenService.getLoginUser(token);
            if (StringUtils.isNotNull(loginUser)) {

                // 顶级租户管理员可以切换租户
                if (loginUser.isSuperAdmin()) {
                    String mockTenantId = request.getHeader(SecurityConstants.MOCK_TENANT_ID);
                    if (StringUtils.isBlank(mockTenantId)) {
                        loginUser.setMockTenantId(null);
                    } else {



                        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                        loginUser = (LoginUser) authentication.getPrincipal();
                        loginUser.setMockTenantId(Long.valueOf(mockTenantId));
                    }
                }
                    tokenService.refreshToken(loginUser);
//        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());

//        SecurityContextHolder.getContext().setAuthentication(authentication);

            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception
    {
        String mockTenantId = ServletUtils.getRequest().getHeader(SecurityConstants.MOCK_TENANT_ID);

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        loginUser.setMockTenantId(null);
        tokenService.refreshToken(loginUser);
//        SecurityContextHolder.remove();
    }


        public static boolean isStringNumeric(String str) {
            // 获取待验证的字符串
            if (str == null || str.length() == 0) {
                // 判断字符串是否为空
                return false;
            }
            for (char c : str.toCharArray()) {
                // 遍历字符串的每个字符
                if (!Character.isDigit(c)) {
                    // 检查每个字符是否是数字
                    return false;
                }
            }
            return true;
        }


}

(3.3)拦截器配置文件:resourcesConfig.java

在带有@Configuration的拦截器配置文件,添加tenantId拦截器。

在addInterceptors方法添加excludePathPatterns为不进行拦截过滤的,由于tenantId从登录信息获取,对于不需要登录的网址,如登录、验证码等需要排除。

registry.addInterceptor(headerInterceptor).addPathPatterns("/**").excludePathPatterns("/login","/loginApp", "/appLogin","/register", "/captchaImage","/factory/getPublicKey").excludePathPatterns("/system/sysAppManagement/getActiveAppInfo");

/*** 自定义拦截规则*/
@Override
public void addInterceptors(InterceptorRegistry registry)
{registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");registry.addInterceptor(headerInterceptor).addPathPatterns("/**").excludePathPatterns("/login","/loginApp", "/appLogin","/register", "/captchaImage","/factory/getPublicKey").excludePathPatterns("/system/sysAppManagement/getActiveAppInfo");
}

附:

package com.inspur.framework.config;

import com.inspur.framework.interceptor.HeaderInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.inspur.common.config.InspurConfig;
import com.inspur.common.constant.Constants;
import com.inspur.framework.interceptor.RepeatSubmitInterceptor;

/**
 * 通用配置
 *
 * @author Inspur
 */
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
    @Autowired
    private RepeatSubmitInterceptor repeatSubmitInterceptor;
    @Autowired
    private HeaderInterceptor headerInterceptor;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
    {
        /** 本地文件上传路径 */
        registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + InspurConfig.getProfile() + "/");

        /** swagger配置 */
        registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
    }

    /**
     * 自定义拦截规则
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
        registry.addInterceptor(headerInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login","/loginApp", "/appLogin","/register", "/captchaImage","/factory/getPublicKey")
                .excludePathPatterns("/system/sysAppManagement/getActiveAppInfo");
    }

    /**
     * 跨域配置
     */
    @Bean
    public CorsFilter corsFilter()
    {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置访问源地址

        config.addAllowedOriginPattern("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

2、权限管理控制与租户管理

(1)权限管理

     主要在原有基础上添加sys_tenant、sys_tenant_menu(tenant_id, menu_id)表,进行控制。并对菜单、部门、角色进行tenant_id过滤。

        引入虚拟mockTenantId,用于切换租户功能,优先获取mockTenantId作为tenantId,表明用户当前进行切换租户操作;如果mockTenantId为空,表名未进行切换租户操作,获取实际登录用户的所属租户tenantId。

        租户管理员角色为全局固定“tenantAdmin”,顶级管理员admin只能有一个用户,不能重名;租户管理员可以有多个,赋予角色tenantAdmin,菜单权限为“*:*:*”

public static Long getTenantId() {try {LoginUser loginUser = SecurityUtils.getLoginUser();Long tenantId1 = loginUser.getTenantId();if (loginUser.getMockTenantId() != null && loginUser.isSuperAdmin()) {tenantId1 = loginUser.getMockTenantId();}return tenantId1;} catch (Exception e) {logger.error("获取租户ID异常",e);return -1L;}}

public static boolean isSuperAdmin(LoginUser loginUser)
{if (loginUser.getUsername() != null && loginUser.getUsername().equals("admin")) {return true;}return false;
}

// 子集租户 - 租户管理员角色tenantAdmin
public static boolean hasTenantAdminRole(SysUser user) {if (user == null) {return false;}List<SysRole> roles = user.getRoles();if (roles == null || roles.isEmpty()) {return false;}for (SysRole sysRole : roles) {if (sysRole.getRoleKey().equals("tenantAdmin")) {return true;}}return false;
}
/*** 获取菜单数据权限** @param user 用户信息* @return 菜单权限信息*/
public Set<String> getMenuPermission(SysUser user)
{Set<String> perms = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin() || LoginUser.hasTenantAdminRole(user)){perms.add("*:*:*");}else{perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));}return perms;
}
(2)租户管理

租户管理:admin唯一账号能管理租户、切换租户,为租户分配菜单。

用户管理:admin可以创建用户并选择所属租户,通常用于创建租户管理员。租户管理员角色只有admin能赋予。

其他用户(所属角色有”用户管理“菜单)可以创建用户,用户的租户默认为创建人所属租户。

3、Mybatis拦截器实现动态表名

          主要介绍动态表名拦截器的使用DynamicTableNameInnerInterceptor,且非全自动给所有语句添加,在需要使用的查询中设置使用,更加灵活。

Mybatis拦截器的应用有

·  自动分页: PaginationInnerInterceptor

·  多租户: TenantLineInnerInterceptor

·  动态表名: DynamicTableNameInnerInterceptor

·  乐观锁: OptimisticLockerInnerInterceptor

·  sql 性能规范: IllegalSQLInnerInterceptor

·  防止全表更新与删除: BlockAttackInnerInterceptor

    在多租户中,DynamicTableNameInnerInterceptor可用于分表隔离使用,TenantLineInnerInterceptor可用于同一表中tenant_id过滤隔离。

(3.1)Mybatis动态表名拦截器

TenantTableNameHandler.java

package com.inspur.framework.datasource;import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;import java.util.Arrays;
import java.util.List;/*** 按天参数,组成动态表名*/
public class TenantTableNameHandler implements TableNameHandler {//用于记录哪些表可以使用该动态表名处理器(即哪些表需要分表)private List<String> tableNames;//构造函数,构造动态表名处理器的时候,传递tableNames参数public TenantTableNameHandler(String ...tableNames) {this.tableNames = Arrays.asList(tableNames);}//每个请求线程维护一个tenantId数据,避免多线程数据冲突。所以使用ThreadLocalprivate static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();//设置请求线程的TenantId数据public static void setTenantId(String day) {TENANT_ID.set(day);}//删除当前请求线程的数据public static void removeTenantId() {TENANT_ID.remove();}//动态表名接口实现方法@Overridepublic String dynamicTableName(String sql, String tableName) {if (this.tableNames.contains(tableName)){return tableName + "_" + TENANT_ID.get();  //表名增加后缀}else{return tableName;   //表名原样返回}}
}

(3.2)MyBatisConfig.java添加插件DynamicTableNameInnerInterceptor

MyBatisConfig.java文件:

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();//动态表名DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();//可以传多个表名参数,指定哪些表使用DayTableNameHandler处理表名称dynamicTableNameInnerInterceptor.setTableNameHandler(new TenantTableNameHandler("factory_check_plan","factory_check_plan_item","factory_check_item","factory_check_task","factory_check_task_item","factory_check_task_user","factory_check_task_approve","factory_inspection_plan","factory_inspection_plan_item","factory_inspection_item","factory_inspection_task","factory_inspection_task_item","factory_inspection_task_user"));//以拦截器的方式处理表名称//可以传递多个拦截器,即:可以传递多个表名处理器TableNameHandlermybatisPlusInterceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);return mybatisPlusInterceptor;
}

(3.3)自定义使用

拦截器采取手动设置方法,在需要的地方使用动态表名,而不是所有语句,此种方式更为灵活。

//查询时手动设置,更为灵活,不会自动设置Long tenantId = SecurityUtils.getTenantId();
TenantTableNameHandler.setTenantId(tenantId.toString());//  sql语句// getById、list、updateById、removeByIds等//例如: FactoryCheckItem factoryCheckItem = factoryCheckItemService.getById(id);// 移除不影响其他语句
TenantTableNameHandler.removeTenantId();

效果:

分表直接使用selectById

设置拦截器后,再使用selectById

4、分表隔离(子模块之间相互调用且避免循环依赖方法)

        分表隔离,表名设置通常为table_name_${tenantId},一个租户一张表,在创建租户时就要创建分表。

        租户管理在system_module模块,业务表在其他子模块(eg:factory_module), 业务模块factory_module一般会依赖于系统模块system_module。如果创建租户(system_module模块中)直接调用业务表创建方法(factory_module模块中),会造成循环依赖。

        子模块之间互相调用的解决方案主要有以下两种方法:

一是新建公共api模块,类似于common_module,此时实体类需要重命名防止类名冲突;

二是调用接口地址,在微服务Springcloud中建立公共module采用@Fegin方式,实现子模块调用解决循环依赖;在springBoot也可以采用类似思路,HttpUtil、restTemplate等工具,此时需要封装请求头,较为繁琐。详见:

Java调用第三方http接口的4种方式:restTemplate,HttpURLConnection,HttpClient,hutool的HttpUtil,实例直接干,以防忘记_resttemplate hutool-CSDN博客

        本文主要介绍第一种,提取公共模块并修改类名。

分表设置时,实体类加上tablePrefix(默认前缀)、 stableName(最终实际表名)、tenantId。查询时mapper映射xml文件的表名可以用 ${stableName}或者table_name_${tenantId}取代。

@TableField(exist = false)
private final String tablePrefix = "factory_check_task_item";public String getStableName() {return tablePrefix + "_" + getTenantId();
}@TableField(exist = false)
private String stableName;
@TableField(exist = false)
private Long tenantId;public String getTablePrefix() {return tablePrefix;
}public Long getTenantId() {if(tenantId!=null){return tenantId;}return SecurityUtils.getTenantId();
}

# 创建分表语句create Table IF NOT EXISTS ${stableName} like ${tablePrefix};

# 查询语句Select * from ${stableName};

5、过滤字段隔离

实体类加tenantId, 查询时mapper映射xml文件在where后加上tenantId过滤。

@TableField(exist = false)
private Long tenantId;public Long getTenantId() {if(tenantId!=null){return tenantId;}return SecurityUtils.getTenantId();
}

#查询语句Select * from table_name where<if test="tenantId != null"> and tenant_id = #{tenantId}</if>

6、分表和过滤字段隔离 sql修改注意

为实现同分库级别同样效果的隔离:

(1)在涉及多表连接查询(left join等)时,每一张表都需要进行tenant_id过滤;

(2)在涉及多层嵌套子查询时,每一层都需要进行过滤

(3)在涉及传参为非实体类时,需要增加参数个数,即tenantId

7、定时任务

        定时任务无法获取登录用户,故无法获取tenant_id。

        此时可以在原有业务逻辑最外层嵌套for循环遍历所有租户,在查询语句前设置tenantId。

        此时实体类的getTenantId优先判断是否人工设置,如果已经设置就优先按照人工设置的tenantId,如果没有再自动获取登录用户tenantId.

定时任务:

List<SysTenant> tenantList = sysTenantService.selectSysTenantList(sysTenant);for(SysTenant tenant : tenantList) {Long tenantId = tenant.getId();//查询前:QueryDao.setTenantId(tenantId);Mapper.select***(QueryDao);}

实体类:

public Long getTenantId() {if(tenantId!=null){return tenantId;}return SecurityUtils.getTenantId();
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

由于bug造成truncate table卡住问题

客户反应truncate table卡主&#xff0c;检查awr发现多个truncate在awr报告期内一直没执行完&#xff0c;如下&#xff1a; 检查ash&#xff0c;truncate table表的等待事件都是“enq: RO - fast object reuse”和“local write wait” 查找“enq: RO - fast object reuse”&am…

爬虫笔记15——爬取网页数据并使用redis数据库set类型去重存入,以爬取芒果踢V为例

下载redis数据库 首先需要下载redis数据库&#xff0c;可以直接去Redis官网下载。或者可以看这里下载过程。 pycharm项目文件下载redis库 > pip install redis 然后在程序中连接redis服务&#xff1a; from redis import RedisredisObj Redis(host127.0.0.1, port6379)…

Django模板

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 Django指定的模板引擎在settings.py文件中定义&#xff0c;代码如下&#xff1a; TEMPLATES [{ # 模板引擎&#xff0c;默认为Django模板 BACKEND:…

SpringMVC框架中常用的几种切面Fliter、Aspect、Interceptor、Advice功能对比和应用场景

1.过滤器&#xff1a;Filter接口 参数校验&#xff1a;用户输入的参数可能包含恶意字符或参数格式错误&#xff0c;通过使用Filter可以拦截并进行参数校验&#xff0c;以保证应用安全。 多语言选择&#xff1a;通过获取请求头的语言参数&#xff0c;Filter可以根据用户的语言…

MySQL连接

MySQL工具包 MySQL实现简单链接 一 引入工具包 JBDCUtils&#xff0c;无需更改&#xff0c;直接使用即可。 import java.io.IOException; import java.io.InputStream; import java.sql.*; import java.util.Properties;public class JDBCUtil {private static String URL;p…

国标GB28181视频汇聚平台EasyCVR设备展示数量和显示条数不符的原因排查与解决

国标GB28181/GA/T1400协议/安防综合管理系统EasyCVR视频汇聚平台能在复杂的网络环境中&#xff0c;将前端设备统一集中接入与汇聚管理。智慧安防/视频存储/视频监控/视频汇聚EasyCVR平台可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级…

Kotlin设计模式:深入解析Facade模式

Kotlin设计模式&#xff1a;深入解析Facade模式 在软件开发中&#xff0c;随着系统复杂度的增加&#xff0c;管理和使用多个相关接口变得越来越困难。这时候&#xff0c;Facade模式&#xff08;外观模式&#xff09;就显得尤为重要。本文将深入探讨Kotlin中的Facade模式&#…

Linux CentoS安装RabbitMQ:一键安装指南

有两种安装方法&#xff0c;官方推荐使用 docker安装RabbitMQ 一、Docker安装RabbitMQ 1、安装docker 参考我之前的文章&#xff1a;Centos7.5搭建docker并且部署Lnmp环境&#xff08;小白入门docoker&#xff09;_centos7.5安装docker和docker-compose-CSDN博客 2、安装Ra…

美食解压视频素材无水印无字幕的在哪找?海外美食解压网站分享

在如今快节奏的生活中&#xff0c;观看美食视频已成为许多人缓解压力的一种方式。这些视频不仅唤醒人们的味觉记忆&#xff0c;还能在繁忙中带来片刻的放松。然而&#xff0c;对于视频创作者来说&#xff0c;寻找高品质的美食视频素材&#xff0c;特别是那些无水印、无字幕、可…

利用SHAP算法解释BERT模型的输出

1 何为SHAP? 传统的 feature importance 只告诉哪个特征重要&#xff0c;但并不清楚该特征如何影响预测结果。SHAP 算法的最大优势是能反应每一个样本中特征的影响力&#xff0c;且可表现出影响的正负性。SHAP算法的主要思想为&#xff1a;控制变量法&#xff0c;如果某个特征…

养殖自动化温控系统:现代养殖场的智能守护神

现代农业养殖业中&#xff0c;养殖自动化温控系统已经成为提高生产效率和保障动物福利的关键技术之一。本篇文章将深入介绍养殖自动化温控系统的原理、组成、优势及其在不同类型养殖场中的应用实例&#xff0c;并展望该技术的未来发展。 一、养殖自动化温控系统概述 养殖自动…

数据结构——优先级队列(堆)Priority Queue详解

1. 优先级队列 队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该场景下&#xff0c;使用队列不合适 在这种情况下&#xff0c;数据结构应…

[笔记] CCD相机测距相关的一些基础知识

1.35mm胶片相机等效焦距 https://zhuanlan.zhihu.com/p/419616729 拿到摄像头拍摄的数码照片后&#xff0c;我们会看到这样的信息&#xff1a; 这里显示出了两个焦距&#xff1a;一个是实际焦距&#xff1a;5mm&#xff0c;一个是等效焦距&#xff1a;25mm。 实际焦距很容易…

HarmonyOS Next 系列之可移动悬浮按钮实现(六)

系列文章目录 HarmonyOS Next 系列之省市区弹窗选择器实现&#xff08;一&#xff09; HarmonyOS Next 系列之验证码输入组件实现&#xff08;二&#xff09; HarmonyOS Next 系列之底部标签栏TabBar实现&#xff08;三&#xff09; HarmonyOS Next 系列之HTTP请求封装和Token…

Pytorch Geometric(PyG)入门

PyG (PyTorch Geometric) 是建立在 PyTorch 基础上的一个库&#xff0c;用于轻松编写和训练图形神经网络 (GNN)&#xff0c;适用于与结构化数据相关的各种应用。官方文档 Install PyG PyG适用于python3.8-3.12 一般使用场景&#xff1a;pip install torch_geometric 或conda …

百度地图3d区域掩膜,最常见通用的大屏地图展现形式

需求及效果 原本项目使用的是百度地图3.0,也就是2d版本的那个地图,客户不满意觉得不够好看,让把地图改成3d的,但是我们因为另外的系统用的都是百度地图,为了保持统一只能用百度地图做 经过3天的努力,最后我终于把这个效果实现了,效果如下: 如何引用GL版本 为了实现…

前端项目外包出去,是我痛苦的开始。如何破?

不止一个老铁给我反馈&#xff0c;他们把其前端项目外包出去&#xff0c;非常的痛苦&#xff0c;远不如用自己的员工省心。明面上钱省了&#xff0c;实际精力大量耗费在上面&#xff0c;一算账并没省&#xff0c;反而闹了一肚子气&#xff0c;问我这事该如何破&#xff1f;其实…

C#的无边框窗体项目模板 - 开源研究系列文章

继续整理和编写代码及博文。 这次将笔者自己整理的C#的无边框窗体项目的基本模板进行总结&#xff0c;得出了基于C#的.net framework的Winform的4个项目模板&#xff0c;这些模板具有基本的功能&#xff0c;即已经初步将代码写了&#xff0c;直接在其基础上添加业务代码即可&am…

Servlet组件

目录 1 我们为什么需要Servlet&#xff1f; 1.1 Web应用基本运行模式 1.2 Web服务器中Servlet作用举例 2 什么是Servlet&#xff1f; 3 如何使用Servlet&#xff1f; 3.1 操作步骤 3.2 运行分析(执行原理) 3.3 Servlet作用总结 4 Servlet生命周期 4.1 Servlet生命周期…

CRMEB 多门店后台登录入口地址修改(默认admin)

一、>2.4版本 1、修改后端 config/admin.php 配置文件,为自定义的后缀 2、修改 平台后台前端源码中 view/admin/src/settings.js 文件,修改为和上面一样的配置 3、修改后重新打包前端代码,并且覆盖到后端的 public 目录下&#xff1a;打包方法 4、重启swoole 二、<2.4版…