基于rouyi框架的多租户改造

news2024/11/23 15:33:16

         基于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 => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if (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数据,避免多线程数据冲突。所以使用ThreadLocal
    private 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();
    }
    //动态表名接口实现方法
    @Override
    public 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"
            ));
    //以拦截器的方式处理表名称
    //可以传递多个拦截器,即:可以传递多个表名处理器TableNameHandler
    mybatisPlusInterceptor.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.coloradmin.cn/o/1860163.html

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

相关文章

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

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

IEEE Signal Processing Letters投稿记录

0.写在前面&#xff1a; 该期刊4页正文&#xff0c;第5页只能是参考文献&#xff0c;篇幅紧凑。 最多只有一次小修机会&#xff0c;或者直接接收。网上说平均审稿周期是2.7个月。 祝愿大家都能如愿&#xff0c;一次接收&#xff01; 期刊网址&#xff1a;投稿 投稿网址&…

被忽视的商机:全民拼购模式如何助力企业实现惊人业绩

在当今的商业环境中&#xff0c;一个被大多数人忽视但实则蕴藏着巨大潜力的模式正在悄然崭露头角。不同于传统商业模式的惯性思维&#xff0c;这一模式在细节上进行了巧妙的调整&#xff0c;带来了意想不到的效果。我的一位客户便凭借这一策略&#xff0c;实现了令人瞩目的业绩…

Java项目毕业设计:基于springboot+vue的幼儿园管理系统

数据库:MYSQL5.7 **应用服务:Tomcat7/Tomcat8 使用框架springbootvue** 项目介绍 管理员&#xff1b;首页、个人中心、用户管理、教师管理、幼儿信息管理、班级信息管理、工作日志管理、会议记录管理、待办事项管理、职工考核管理、请假信息管理、缴费信息管理、幼儿请假管理…

轻松驾驭多云存储,不再为文件流转烦恼,可道云teamOS带你走进便捷的多云时代

不知道大家有没有遇到过这样的问题&#xff1a;使用了不同云平台的存储&#xff0c;但不同的平台存储就存在文件不流通、共享困难、管理困难等问题。 对于这种情况&#xff0c;我们该如何进行资源整合&#xff0c;实现不同挂载存储之间的文件管理与流转&#xff1f; 这些问题…

Linux基础 - shell基础

目录 零. 简介 一、常见的 Shell 类型 二、Shell 命令格式 三、基本命令 四、通配符 五、重定向 六、管道 七、变量 八、条件判断和流程控制 零. 简介 Shell 是一种命令解释器&#xff0c;在 Ubuntu 系统中&#xff0c;它负责接收用户在命令行中输入的命令&#xff0c…

AI大神 Sebastian Raschka 发布新书《从零开始构建大语言模型》

Sebastian 热衷于开源软件&#xff0c;还喜欢写作&#xff0c;撰写了畅销书《Python Machine Learning》&#xff08;《Python 机器学习》&#xff09;和《Machine Learning with PyTorch and ScikitLearn》。 最近&#xff0c;Sebastian Raschka 发布了新书《Build a Large L…

pytorch神经网络训练(LeNet-5)

LeNet-5 导包 import osimport torchimport torch.nn as nnimport torch.optim as optimfrom torch.utils.data import Dataset, DataLoaderfrom PIL import Imagefrom torchvision import transforms 定义自定义图像数据集 class CustomImageDataset(Dataset):def __init_…

Git的安装以及使用

一.简单介绍 1.1版本控制 版本控制是指对软件开发过程中各种程序代码,配置文件及说明文档等文件变更管理&#xff0c;是软件配置管理的核心思想之一。 版本控制最重要的内容是追踪文件的变更&#xff0c;它将什么时候&#xff0c;什么人更改了文件的什么内容等信息忠实的记录…

社交小心机:特别的动态给特别的她/他

在社交媒体盛行的今天&#xff0c;微信朋友圈成了我们分享生活点滴的重要平台。 但是&#xff0c;你是否有过这样的烦恼——有些动态只想和特定的人分享&#xff0c;而不是所有人&#xff1f;别担心&#xff0c;今天我就来教大家如何巧妙地设置朋友圈权限&#xff0c;让你的分…

【2024.6.25】今日 IT之家精选新闻

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

C语言 循环语句while 2

应用实例 int main() {char password[20] { 0 };printf("输入密码&#xff1a;>");scanf("%s", password);printf("请确认输入密码(Y/N):>");//清理缓存区int tmp 0;while ((tmp getchar()) ! \n){;}int ch getchar();if (ch Y){pri…

RAG实践 - 搭建本地知识库 - Ollama + AnythingLLM

0&#xff0c;什么是RAG&#xff1f; RAG&#xff0c;即检索增强生成&#xff08;Retrieval-Augmented Generation&#xff09;&#xff0c;是一种先进的自然语言处理技术架构&#xff0c;旨在克服传统大型语言模型&#xff08;LLM&#xff09;在处理开放域问题时的信息容量限…

java 多线程入门

对于 Java 初学者来说&#xff0c;多线程的很多概念听起来就很难理解。比方说&#xff1a; 进程&#xff0c;是对运行时程序的封装&#xff0c;是系统进行资源调度和分配的基本单位&#xff0c;实现了操作系统的并发。线程&#xff0c;是进程的子任务&#xff0c;是 CPU 调度和…

提示缺少Microsoft Visual C++ 2019 Redistributable Package (x64)(下载)

下载地址&#xff1a;这个是官网下载地址&#xff1a;Microsoft Visual C 2019 Redistributable Package (x64) 步骤&#xff1a; 第一步&#xff1a;点开链接&#xff0c;找到下图所示的东西 第二步&#xff1a;点击保存下载 第三步&#xff1a;双击运行安装 第四步&#xf…

让工厂像手机一样更“聪明”

手机&#xff0c;作为我们日常生活中不可或缺的一部分&#xff0c;以其智能、便捷、高效的特点&#xff0c;彻底改变了我们的沟通、娱乐和工作方式。那么&#xff0c;想象一下&#xff0c;如果工厂能像手机一样便捷&#xff0c;那么生产过程中的每一个环节都将变得触手可及。通…

揭秘Redis中的高级数据结构:跳跃表Skiplist

Redis数据结构-跳跃表Skiplist 1. 简介1.1. Redis高性能键值存储数据库1.2. Redis的特点和优势1.3. 跳跃表Skiplist 2. 跳跃表的概念和背景2.1 跳跃表的概念2.2 跳跃表的发展历程和提出背景 3. 跳跃表的基本原理3.1 结构概述3.1.1 跳跃表的结构概述3.1.2 跳跃表的节点结构 3.2 …

C#语言+net技术架构+ VS2019开发的微信公众号预约挂号系统源码 微信就医全流程体验 什么是微信预约挂号系统?

C#语言net技术架构 VS2019开发的微信公众号预约挂号系统源码 微信就医全流程体验 什么是微信预约挂号系统&#xff1f; 微信预约挂号系统是一种基于互联网的预约挂号平台&#xff0c;通过与医院信息系统的对接&#xff0c;实现了患者通过手机微信轻松预约挂号的功能。预约挂号系…

【AI大模型】Transformers大模型库(十一):Trainer训练类

目录 一、引言 二、Trainer训练类 2.1 概述 2.2 使用示例 三、总结 一、引言 这里的Transformers指的是huggingface开发的大模型库&#xff0c;为huggingface上数以万计的预训练大模型提供预测、训练等服务。 &#x1f917; Transformers 提供了数以千计的预训练模型&am…

基于FreeRTOS+STM32CubeMX+LCD1602+MCP4152(SPI接口)的数字电位器Proteus仿真

一、仿真原理图: 二、仿真效果: 三、软件部分: 1)、时钟配置初始化: void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Initializes the CPU, AHB and APB busses clocks */ RCC…