基于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.mzph.cn/bicheng/34511.shtml

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

相关文章

[word] word 如何在文档中进行分栏排版? #媒体#其他#媒体

word 如何在文档中进行分栏排版&#xff1f; 目标效果 将唐代诗人李白的组诗作品《清平调词》进行分栏排版&#xff0c;共分三栏&#xff0c;每一首诗作为一栏&#xff0c;参考效果如下图。

计算机图形学入门16:阴影映射

1.前言 前面几篇关于光栅化的文章中介绍了如何计算物体表面的光照&#xff0c;但是着色并不会进行阴影的计算&#xff0c;阴影需要单独进行处理&#xff0c;目前最常用的阴影计算技术之一就是Shadow Mapping技术&#xff0c;也就是俗称的阴影映射技术。 2.阴影映射 Shadow Map…

C++在VS2022开发Windows窗口程序2:API式的Windows窗口程序设计模式

函数API式的Windows GUI程序设计模式是一种基于Windows API函数的方式来设计和开发Windows图形用户界面&#xff08;GUI&#xff09;应用程序的模式。在这种模式下&#xff0c;开发者通过调用Windows API函数来创建窗口、处理消息、绘制图形等&#xff0c;而不依赖于特定的GUI库…

mass storage:RAID Structure , Error Detection and Correction

RAID Structure RAID – redundant array of inexpensive disks multiple disk drives provides reliability via redundancyIncreases the mean time to failureMean time to repair – exposure time when another failure could cause data lossMean time to data loss bas…

【TB作品】stm32单片机,红外遥控器,温控风扇,模拟空调,PWM风扇

空调机 硬件&#xff1a;stm32、oled显示器、ds18b20温度传感器、风扇驱动和风扇、红外接收器、遥控器 软件功能&#xff1a; &#xff08;1&#xff09;显示室内温度 &#xff08;2&#xff09;显示当前模式&#xff1a;常态、除湿、通风 &#xff08;3&#xff09;显示当前风…

Ubuntu Apache2 搭建Gerrit 环境

一、前言 时隔多年&#xff0c;好久没有更新CSDN 博客了&#xff0c;主要原因有如下两点&#xff1a; 1、平时工作繁忙&#xff0c;无暇更新。 2、工作内容涉及信息安全&#xff0c;一些工作经验积累不便更新到互联网上。 最近一直在折腾搭建Gerrit 环境&#xff0c;最开始…

win11安装VMware虚拟机,启动系统后蓝屏,安装虚拟机卡在虚拟网卡界面的解决办法

机缘和遇到的问题 由于最近618换了台新笔记本电脑&#xff0c;然后系统自带的操作系统是windows 11 家庭版本&#xff0c;由于工作需要用到window10的环境&#xff0c;不得不安装一个虚拟机来解决问题&#xff0c;然后就把这次安装VMware虚拟机遇到的坑给大家分享一下&#xf…

研二自学嵌入式开发,就业导向,学习路线该如何规划?

研二才来问这个问题&#xff0c;有点晚&#xff0c;离你开始找工作还有大概8&#xff5e;9个月&#xff0c;你应该用应试思维来应对找工作这个事&#xff0c;尤其当前这个经济形势下。 刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「嵌入式的资料从专业入…

基于Openmv的追小球的云台

介绍 在这篇文章&#xff0c;我会先介绍需要用到且需要注意的函数&#xff0c;之后再给出整体代码 在追小球的云台中&#xff0c;比较重要的部分就是云台&#xff08;实质上就是舵机&#xff09;的控制以及对识别的色块位置进行处理得到相应信息后控制云台进行运动 1、舵机模…

asp.net core反向代理

新建项目 新建空白的asp.net core web项目 安装Yarp.ReverseProxy包版本为2.2.0-preview.1.24266.1 编写代码 namespace YarpStu01;public class Program {public static void Main(string[] args){var builder WebApplication.CreateBuilder(args);builder.Services.AddRev…

JavaWeb——MySQL:DQL

3. DQL:查询 查询是使用最多、最频繁的操作&#xff0c;因为前面的修改以及删除&#xff0c;一般会交给数据库专业的人员&#xff0c;对于非数据库专业人员来说&#xff0c;老板一般会放心的让你对数据库只进行查询操作&#xff1b; 3.2 条件查询&#xff08;where&#xff09…

浏览器自带的IndexDB的简单使用示例--小型学生管理系统

浏览器自带的IndexDB的简单使用示例--小型学生管理系统 文章说明代码效果展示 文章说明 本文主要为了简单学习IndexDB数据库的使用&#xff0c;写了一个简单的增删改查功能 代码 App.vue&#xff08;界面的源码&#xff09; <template><div style"padding: 30px&…

2024年通信技术与计算机科学国际学术会议(ICCTCS 2024)

2024年通信技术与计算机科学国际学术会议&#xff08;ICCTCS 2024&#xff09; 2024 International Academic Conference on Communication Technology and Computer Science&#xff08;ICCTCS 2024&#xff09; 会议简介&#xff1a; 2024年通信技术与计算机科学国际学术会议…

Leetcode.1735 生成乘积数组的方案数

题目链接 Leetcode.1735 生成乘积数组的方案数 rating : 2500 题目描述 给你一个二维整数数组 q u e r i e s queries queries &#xff0c;其中 q u e r i e s [ i ] [ n i , k i ] queries[i] [n_i, k_i] queries[i][ni​,ki​] 。第 i i i 个查询 q u e r i e s [ i …

JAVA SDK 整合 AI 大语言模型

目前主流模型厂商的 SDK 并没有很好的支持 JAVA 环境&#xff0c;主流还是使用的 Python &#xff0c;如果希望将 AI 功能集成到业务中来&#xff0c;则需要找找有没有一些现成的开源项目&#xff0c;但是这种项目一般需要谨慎使用&#xff0c;以防有偷取 app_key 等风险问题 前…

如何在Linux下使用git(几步把你教会)

目录 一、注册github账号 二、新建项目 1.点击右上角自己的头像&#xff0c;然后点击Your repositories。 2.点击New。 3.配置新项目信息。 4.点击Create repository即可成功创建。 三、安装git 四、配置git 五、初始化git仓库 1.先进入想要使用git的目录。 2.初始化…

数据时代的数字企业

1.写在前面 讨论数据治理在数字企业中的影响和必要性&#xff0c;并介绍数据治理的核心内容和实践方法。作者强调了数据质量、数据安全、数据隐私和数据合规等方面是数据治理的核心内容&#xff0c;并介绍了具体的实践措施和案例分析。企业需要重视这些方面以实现数字化转型和…

多孔散热器简介

今天给大家分享关于多孔散热器的一些构造、散热情况。 更多资讯&#xff0c;请关注B站【莱歌数字】&#xff0c;有视频教程~~ 常见的散热器通常由不渗透水、空气和其他液体的无孔材料制成。固体铝和铜是行业标准。 但散热器也可以作为半多孔材料或多孔涂层。研究和应用表明&…

防静电监控系统全方位防静电监测,保障产品质量

在当今高度精密的电子制造领域&#xff0c;产品质量的保障至关重要。哪怕是微小的静电干扰&#xff0c;都可能导致电子元件损坏、性能下降&#xff0c;从而影响整个产品的质量和可靠性。为了应对这一挑战&#xff0c;某电子工厂车间引入了先进的防静电监控系统&#xff0c;实现…

11g rac db安装软件时找不到 节点的问题处理

问题 在安装11.2.0.4db软件时数据库软件无法识别集群的两个主机 处理方法 [oracleracdg1-1 database]$ cd /u01/app/oraInventory/ [oracleracdg1-1 oraInventory]$ ls ContentsXML logs oraInst.loc orainstRoot.sh oui [oracleracdg1-1 oraInventory]$ cd ContentsXML/…