Skip to content

租户体系

租户体系是saas系统的核心。

实现方式

  • 天源云支持租户模式、非租户模式,其中租户模式有3种常见的数据存储方式
  • 实现不同租户模式的方式
    • 通过注解 + 拦截器的方式实现租户数据的隔离
    • 程序员sql编码完全无需拼接租户相关的sql(一套业务代码,可以通用所有模式)
    • 通过启动配置决定启用何种模式的插件,程序就能按照何种模式来隔离数据。

独立数据源模式

TIP

一个租户独享一个独立的数据库,可以是物理数据库或云数据库,隔离级别最高、最安全、同时成本也更高。

img

  • 优点

    可独立部署数据库,数据隔离性好、扩展性高、故障影响小

  • 缺点

    相对复杂、开发需要注意切换数据源时的事务问题、需要较多的数据库

运行过程

过程

  • 新增租户
  • 初始化租户数据库表结构和初始数据
    • 创建租户数据库
    • 连接数据源
    • 更新状态
    • 自动授权
  • 初始化其他服务的数据源
    • 查询待初始化的服务
    • 手动初始化
    • 修改租户状态
新增租户:向def_tenant表插入一条数据,状态为待初始化结构。
java
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
    // 数据初始化
    DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
    tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
    tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
    tenant.setReadonly(false);
    if (StrUtil.isEmpty(tenant.getCreatedName())) {
        DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
        if (result != null) {
            tenant.setCreatedName(result.getNickName());
        }
    }
    return tenant;
}

@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
  	// 保存租户logo
    appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
    // 数据初始化
    DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
    tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
    tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
    tenant.setReadonly(false);
    if (StrUtil.isEmpty(tenant.getCreatedName())) {
        DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
        if (result != null) {
            tenant.setCreatedName(result.getNickName());
        }
    }
    return tenant;
}

@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
  	// 保存租户logo
    appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
初始化租户数据库表结构和初始数据

主要步骤:

  • 创建租户数据库

仅选择了系统内置数据源时,系统会自动创建租户数据库,自定义数据源需要开发者提前创建好数据库。 默认的租户数据库前缀为 acuity_base,完整的租户数据库名称为 acuity_base_

  • 连接数据源

调用dynamic-datasource-spring-boot-starter组件提供的接口,动态连接新建的租户数据源。 acuity-system-server连接上新数据源后,在新的租户数据库中,执行SQL脚本,创建表结构和插入数据。

  • 更新状态

修改租户状态为待初始化数据源

  • 自动授权

自动给该租户授权基础平台

java
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/defTenant")
@Api(value = "DefTenant", tags = "企业")
public class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> {

    /**
     * 初始化数据
     */
    @ApiOperation(value = "初始化数据", notes = "初始化数据")
    @PostMapping("/initData")
    @WebLog("连接数据源")
    public R<Boolean> initData(@Validated @RequestBody DefTenantInitVO tenantConnect) {
        return success(superService.initData(tenantConnect));
    }

}
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/defTenant")
@Api(value = "DefTenant", tags = "企业")
public class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> {

    /**
     * 初始化数据
     */
    @ApiOperation(value = "初始化数据", notes = "初始化数据")
    @PostMapping("/initData")
    @WebLog("连接数据源")
    public R<Boolean> initData(@Validated @RequestBody DefTenantInitVO tenantConnect) {
        return success(superService.initData(tenantConnect));
    }

}
java
@Slf4j
@Service
@DS(DsConstant.DEFAULTS)
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DefTenantServiceImpl extends SuperCacheServiceImpl<DefTenantManager, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> implements DefTenantService {
  
  	// 构造器注入
  	private final InitSystemContext initSystemContext;
  
		@Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean initData(DefTenantInitVO tenantConnect) {
      	// 初始数据 && 更新状态
        return initSystemContext.initData(tenantConnect) && updateTenantStatus(tenantConnect);
    }

    private Boolean updateTenantStatus(DefTenantInitVO initVO) {
        Boolean flag = superManager.update(Wraps.<DefTenant>lbU().set(DefTenant::getStatus, DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode())
                .set(DefTenant::getConnectType, initVO.getConnectType())
                .eq(DefTenant::getId, initVO.getId()));
        superManager.delCache(initVO.getId());
      	
      	// 自动授权基础平台
        defTenantApplicationRelManager.grantGeneralApplication(initVO.getId());
        return flag;
    }
}
@Slf4j
@Service
@DS(DsConstant.DEFAULTS)
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DefTenantServiceImpl extends SuperCacheServiceImpl<DefTenantManager, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> implements DefTenantService {
  
  	// 构造器注入
  	private final InitSystemContext initSystemContext;
  
		@Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean initData(DefTenantInitVO tenantConnect) {
      	// 初始数据 && 更新状态
        return initSystemContext.initData(tenantConnect) && updateTenantStatus(tenantConnect);
    }

    private Boolean updateTenantStatus(DefTenantInitVO initVO) {
        Boolean flag = superManager.update(Wraps.<DefTenant>lbU().set(DefTenant::getStatus, DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode())
                .set(DefTenant::getConnectType, initVO.getConnectType())
                .eq(DefTenant::getId, initVO.getId()));
        superManager.delCache(initVO.getId());
      	
      	// 自动授权基础平台
        defTenantApplicationRelManager.grantGeneralApplication(initVO.getId());
        return flag;
    }
}
java
@Service("DATASOURCE_COLUMN")
@Slf4j
@RequiredArgsConstructor
public class DatasourceColumnInitSystemStrategy implements InitSystemStrategy {
		private final DefDatasourceConfigManager defDatasourceConfigManager;
    private final DefTenantDatasourceConfigRelManager defTenantDatasourceConfigRelManager;
    private final DataSourceService dataSourceService;

    /**
     *
     * @param tenantInitVO 链接信息
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean initData(DefTenantInitVO tenantInitVO) {
      	// 自定义数据源
        if (TenantConnectTypeEnum.CUSTOM.eq(tenantInitVO.getConnectType())) {
            ArgumentAssert.notNull(tenantInitVO.getBaseDatasourceId(), "请配置基础库数据库链接信息");
            List<Long> ids = Collections.singletonList(tenantInitVO.getBaseDatasourceId());
            if (tenantInitVO.getExtendDatasourceId() != null) {
                ids.add(tenantInitVO.getExtendDatasourceId());
            }
            List<DefDatasourceConfig> dcList = defDatasourceConfigManager.listByIds(ids);
            ArgumentAssert.notEmpty(dcList, "请配置正确的基础库和扩展库数据库链接信息");

          	// 存储租户自定义的数据源信息
            defTenantDatasourceConfigRelManager.remove(Wraps.<DefTenantDatasourceConfigRel>lbQ().eq(DefTenantDatasourceConfigRel::getTenantId, tenantInitVO.getId()));
            List<DefTenantDatasourceConfigRel> list = new ArrayList<>();
            list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_BASE_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
                    .datasourceConfigId(tenantInitVO.getBaseDatasourceId()).build());
            if (tenantInitVO.getExtendDatasourceId() != null) {
              list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_EXTEND_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
                        .datasourceConfigId(tenantInitVO.getExtendDatasourceId()).build());
            }
            defTenantDatasourceConfigRelManager.saveBatch(list);

          	// 动态添加租户服务的数据源链接 & 创建表 & 初始数据
            dataSourceService.addCustomDsAndData(tenantInitVO.getId());
        } 
      	// 系统内置数据源
      	else if (TenantConnectTypeEnum.SYSTEM.eq(tenantInitVO.getConnectType())) {
            // 在程序启动时连接的默认库中,创建租户的database
            dataSourceService.createDatabase(tenantInitVO.getId());
            // 2. 动态添加租户服务的数据源链接 & 创建表 & 初始数据
            dataSourceService.addSystemDsAndData(tenantInitVO.getId());
        }
        return true;
    }
}
@Service("DATASOURCE_COLUMN")
@Slf4j
@RequiredArgsConstructor
public class DatasourceColumnInitSystemStrategy implements InitSystemStrategy {
		private final DefDatasourceConfigManager defDatasourceConfigManager;
    private final DefTenantDatasourceConfigRelManager defTenantDatasourceConfigRelManager;
    private final DataSourceService dataSourceService;

    /**
     *
     * @param tenantInitVO 链接信息
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean initData(DefTenantInitVO tenantInitVO) {
      	// 自定义数据源
        if (TenantConnectTypeEnum.CUSTOM.eq(tenantInitVO.getConnectType())) {
            ArgumentAssert.notNull(tenantInitVO.getBaseDatasourceId(), "请配置基础库数据库链接信息");
            List<Long> ids = Collections.singletonList(tenantInitVO.getBaseDatasourceId());
            if (tenantInitVO.getExtendDatasourceId() != null) {
                ids.add(tenantInitVO.getExtendDatasourceId());
            }
            List<DefDatasourceConfig> dcList = defDatasourceConfigManager.listByIds(ids);
            ArgumentAssert.notEmpty(dcList, "请配置正确的基础库和扩展库数据库链接信息");

          	// 存储租户自定义的数据源信息
            defTenantDatasourceConfigRelManager.remove(Wraps.<DefTenantDatasourceConfigRel>lbQ().eq(DefTenantDatasourceConfigRel::getTenantId, tenantInitVO.getId()));
            List<DefTenantDatasourceConfigRel> list = new ArrayList<>();
            list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_BASE_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
                    .datasourceConfigId(tenantInitVO.getBaseDatasourceId()).build());
            if (tenantInitVO.getExtendDatasourceId() != null) {
              list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_EXTEND_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
                        .datasourceConfigId(tenantInitVO.getExtendDatasourceId()).build());
            }
            defTenantDatasourceConfigRelManager.saveBatch(list);

          	// 动态添加租户服务的数据源链接 & 创建表 & 初始数据
            dataSourceService.addCustomDsAndData(tenantInitVO.getId());
        } 
      	// 系统内置数据源
      	else if (TenantConnectTypeEnum.SYSTEM.eq(tenantInitVO.getConnectType())) {
            // 在程序启动时连接的默认库中,创建租户的database
            dataSourceService.createDatabase(tenantInitVO.getId());
            // 2. 动态添加租户服务的数据源链接 & 创建表 & 初始数据
            dataSourceService.addSystemDsAndData(tenantInitVO.getId());
        }
        return true;
    }
}
java
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
  	
		private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
        DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
        ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);

        // 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
        databaseProperties.getInitDatabasePrefix().forEach(database -> {
            // 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
            DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
            newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
            if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
                newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
                newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
            } else {
                String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
                String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
                newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
            }
            if (isInitSchema) {
                DatasourceInitProperties init = newDataSourceProperty.getInit();
                if (init == null) {
                    init = new DatasourceInitProperties();
                }
                // 待创建的表结构                
                init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
                // 待初始化的数据
                init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
                newDataSourceProperty.setInit(init);
            }
            newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
            newDataSourceProperty.setDruid(properties.getDruid());
            if (isNotErrorRetry) {
                // 链接错误重试次数
                newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
                // 获取失败后中断
                newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
            }
            
            // 动态新增数据源
            putDs(newDataSourceProperty);
        });
        return true;
    }

    private Set<String> putDs(DataSourceProperty dsp) {
        try {
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
            DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
            ds.addDataSource(dsp.getPoolName(), newDataSource);
            return ds.getDataSources().keySet();
        } catch (ErrorCreateDataSourceException e) {
            log.error("数据源初始化期间出现异常", e);
            throw new BizException("数据源初始化期间出现异常", e);
        }
    }
	
}
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
  	
		private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
        DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
        ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);

        // 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
        databaseProperties.getInitDatabasePrefix().forEach(database -> {
            // 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
            DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
            newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
            if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
                newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
                newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
            } else {
                String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
                String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
                newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
            }
            if (isInitSchema) {
                DatasourceInitProperties init = newDataSourceProperty.getInit();
                if (init == null) {
                    init = new DatasourceInitProperties();
                }
                // 待创建的表结构                
                init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
                // 待初始化的数据
                init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
                newDataSourceProperty.setInit(init);
            }
            newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
            newDataSourceProperty.setDruid(properties.getDruid());
            if (isNotErrorRetry) {
                // 链接错误重试次数
                newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
                // 获取失败后中断
                newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
            }
            
            // 动态新增数据源
            putDs(newDataSourceProperty);
        });
        return true;
    }

    private Set<String> putDs(DataSourceProperty dsp) {
        try {
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
            DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
            ds.addDataSource(dsp.getPoolName(), newDataSource);
            return ds.getDataSources().keySet();
        } catch (ErrorCreateDataSourceException e) {
            log.error("数据源初始化期间出现异常", e);
            throw new BizException("数据源初始化期间出现异常", e);
        }
    }
	
}
初始化其他服务的数据源
  • 查询待初始化的服务

    acuity-cloud 项目,此代码位于acuity-gateway-server;acuity-boot项目,此代码位于acuity-boot-server

    java
    @Controller
    public class GeneratorController {
      @Autowired
      private DiscoveryClient discoveryClient;
      @Autowired
      private GatewayProperties gatewayProperties;
    
    	@ResponseBody
      @ApiOperation(value = "查询在线服务的前缀")
      @GetMapping("/gateway/findOnlineServicePrefix")
      public R<Map<String, String>> findOnlineServicePrefix() {
          List<String> services = discoveryClient.getServices();
      			// 遍历出acuity-gateway-server.yml中正确配置了,且正常注册到nacos中的所有服务
          Map<String, String> map = MapUtil.newHashMap();
          services.forEach(service ->
                  gatewayProperties.getRoutes().forEach(route -> {
                      if (StrUtil.equalsIgnoreCase(service, route.getUri().getHost())) {
                          if (CollUtil.isEmpty(route.getPredicates())) {
                              return;
                          }
                          PredicateDefinition predicateDefinition = route.getPredicates().get(0);
                          predicateDefinition.getArgs().forEach((k, v) -> {
                              map.put(service, StrUtil.subBetween(v, "/", "/**"));
                          });
                      }
                  })
          );
          return R.success(map);
      }
    }
    @Controller
    public class GeneratorController {
      @Autowired
      private DiscoveryClient discoveryClient;
      @Autowired
      private GatewayProperties gatewayProperties;
    
    	@ResponseBody
      @ApiOperation(value = "查询在线服务的前缀")
      @GetMapping("/gateway/findOnlineServicePrefix")
      public R<Map<String, String>> findOnlineServicePrefix() {
          List<String> services = discoveryClient.getServices();
      			// 遍历出acuity-gateway-server.yml中正确配置了,且正常注册到nacos中的所有服务
          Map<String, String> map = MapUtil.newHashMap();
          services.forEach(service ->
                  gatewayProperties.getRoutes().forEach(route -> {
                      if (StrUtil.equalsIgnoreCase(service, route.getUri().getHost())) {
                          if (CollUtil.isEmpty(route.getPredicates())) {
                              return;
                          }
                          PredicateDefinition predicateDefinition = route.getPredicates().get(0);
                          predicateDefinition.getArgs().forEach((k, v) -> {
                              map.put(service, StrUtil.subBetween(v, "/", "/**"));
                          });
                      }
                  })
          );
          return R.success(map);
      }
    }
  • 手动初始化

    调用各个服务的初始化数据源接口,让其他服务加载数据源。 此代码位于acuity-tenant-datasource-init模块,后台每个业务服务都依赖他。

    java
    @Slf4j
    @RestController
    @RequestMapping("/ds")
    @Api(value = "TenantDs", tags = "数据源")
    @RequiredArgsConstructor
    public class TenantDsController {
    
      private final DataSourceService dataSourceService;
    
      @ApiOperation(value = "初始化数据源", notes = "初始化数据源")
      @PostMapping(value = "/initDataSource")
      public R<Boolean> initDataSource(@RequestParam Long tenantId) {
          return R.success(dataSourceService.initDataSource(tenantId));
      }
    
      @GetMapping("/check")
      @ApiOperation("检测是否存在指定数据源")
      public R<Boolean> check(@RequestParam(value = "tenantId") Long tenantId) {
          return R.success(dataSourceService.check(tenantId));
      }
    
    }
    @Slf4j
    @RestController
    @RequestMapping("/ds")
    @Api(value = "TenantDs", tags = "数据源")
    @RequiredArgsConstructor
    public class TenantDsController {
    
      private final DataSourceService dataSourceService;
    
      @ApiOperation(value = "初始化数据源", notes = "初始化数据源")
      @PostMapping(value = "/initDataSource")
      public R<Boolean> initDataSource(@RequestParam Long tenantId) {
          return R.success(dataSourceService.initDataSource(tenantId));
      }
    
      @GetMapping("/check")
      @ApiOperation("检测是否存在指定数据源")
      public R<Boolean> check(@RequestParam(value = "tenantId") Long tenantId) {
          return R.success(dataSourceService.check(tenantId));
      }
    
    }
  • 修改租户状态

    此代码位于acuity-system-server。

    java
     public class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant,  DefTenantSaveVO,DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> {
      
      @ApiOperation(value = "修改租户审核状态", notes = "修改租户审核状态")
      @PostMapping("/updateStatus")
      @WebLog("修改租户审核状态")
      public R<Boolean> updateStatus(@NotNull(message = "请修改正确的企业") @RequestParam Long id,
                                     @RequestParam @NotNull(message = "请传递状态值") String status,
                                     @RequestParam(required = false) String reviewComments) {
          return success(superService.updateStatus(id, status, reviewComments));
      }
    
    }
     public class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant,  DefTenantSaveVO,DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> {
      
      @ApiOperation(value = "修改租户审核状态", notes = "修改租户审核状态")
      @PostMapping("/updateStatus")
      @WebLog("修改租户审核状态")
      public R<Boolean> updateStatus(@NotNull(message = "请修改正确的企业") @RequestParam Long id,
                                     @RequestParam @NotNull(message = "请传递状态值") String status,
                                     @RequestParam(required = false) String reviewComments) {
          return success(superService.updateStatus(id, status, reviewComments));
      }
    
    }

启动过程

除了系统正常运行时,实时新增租户需要动态创建新租户的数据源,当服务重启时,程序也需要将已经成功创建的租户数据源加载到服务内存中。但database.yml配置文件中,却只配置了默认库的数据库信息。所以,在系统启动时,需要查询数据库读取所有租户的信息,动态创建租户的数据源。 大致流程为:监听系统启动成功事件->查询租户表->初始化租户的数据源

yaml
spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  datasource:
    dynamic:
      enabled: true
      # 从这里开始(dynamic),中间的这段配置用于 acuity.database.multiTenantType == DATASOURCE 时
      p6spy: ${acuity.database.p6spy:false}
      seata: ${acuity.database.isSeata:false}
      primary: "0"
      datasource:
        "0":   # 只配置了默认数据库  acuity_defaults
          <<: *db-mysql
      druid:
        <<: *druid-mysql  
        initialSize: 10
        minIdle: 10
        maxActive: 200
        max-wait: 60000
        pool-prepared-statements: true
        max-pool-prepared-statement-per-connection-size: 20
        test-on-borrow: false
        test-on-return: false
        test-while-idle: true
        time-between-eviction-runs-millis: 60000  #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        min-evictable-idle-time-millis: 300000    #配置一个连接在池中最小生存的时间,单位是毫秒
        filters: stat,wall
        wall:
          enabled: true
          strictSyntaxCheck: false
          comment-allow: true
          multiStatementAllow: true
          noneBaseStatementAllow: true
spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  datasource:
    dynamic:
      enabled: true
      # 从这里开始(dynamic),中间的这段配置用于 acuity.database.multiTenantType == DATASOURCE 时
      p6spy: ${acuity.database.p6spy:false}
      seata: ${acuity.database.isSeata:false}
      primary: "0"
      datasource:
        "0":   # 只配置了默认数据库  acuity_defaults
          <<: *db-mysql
      druid:
        <<: *druid-mysql  
        initialSize: 10
        minIdle: 10
        maxActive: 200
        max-wait: 60000
        pool-prepared-statements: true
        max-pool-prepared-statement-per-connection-size: 20
        test-on-borrow: false
        test-on-return: false
        test-while-idle: true
        time-between-eviction-runs-millis: 60000  #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        min-evictable-idle-time-millis: 300000    #配置一个连接在池中最小生存的时间,单位是毫秒
        filters: stat,wall
        wall:
          enabled: true
          strictSyntaxCheck: false
          comment-allow: true
          multiStatementAllow: true
          noneBaseStatementAllow: true
监听系统启动成功事件
java
@Configuration
public class DatasourceColumnAutoConfiguration {

    /**
     * 项目启动时,初始化数据源
     */
    @Bean
    public InitDatabaseOnStarted getInitDatabaseOnStarted(DatasourceInitDataSourceService initSystemContext) {
        return new InitDatabaseOnStarted(initSystemContext);
    }
}
@Configuration
public class DatasourceColumnAutoConfiguration {

    /**
     * 项目启动时,初始化数据源
     */
    @Bean
    public InitDatabaseOnStarted getInitDatabaseOnStarted(DatasourceInitDataSourceService initSystemContext) {
        return new InitDatabaseOnStarted(initSystemContext);
    }
}
java
@AllArgsConstructor
public class InitDatabaseOnStarted implements ApplicationListener<ApplicationStartedEvent> {

    private final DatasourceInitDataSourceService datasourceInitDataSourceService;

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        datasourceInitDataSourceService.initDataSource();
    }

}
@AllArgsConstructor
public class InitDatabaseOnStarted implements ApplicationListener<ApplicationStartedEvent> {

    private final DatasourceInitDataSourceService datasourceInitDataSourceService;

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        datasourceInitDataSourceService.initDataSource();
    }

}
查询租户表

在acuity_defautls库中查询def_tenant表的数据。 在SpringBoot触发ApplicationStartedEvent事件时,mybatis已经读取了database.yml中配置的默认数据源,并成功加载,所以这个时候已经可以查询默认数据库了。

java
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
		// 加载系统内置租户数据源
  	@Override
    public boolean loadSystemDataSource() {
      	// 查询所有可用的租户
        List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
        List<Long> list = initDbMapper.selectTenantCodeList(status, TenantConnectTypeEnum.SYSTEM.name());
	
      	// 循环初始化系统内置数据源
      	list.forEach(tenantId -> addSystem(tenantId, false, false));
        return true;
    }
  
  	// 加载自定义租户数据源
    @Override
    public boolean loadCustomDataSource() {
        // 查询所有可用的租户
        List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
        List<DefDatasourceConfigBO> dcList = initDbMapper.selectDataSourceConfig(status, TenantConnectTypeEnum.CUSTOM.name());
      	
        // 循环初始化自定义数据源
        return addCustom(dcList, false, false);
    }
}
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
		// 加载系统内置租户数据源
  	@Override
    public boolean loadSystemDataSource() {
      	// 查询所有可用的租户
        List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
        List<Long> list = initDbMapper.selectTenantCodeList(status, TenantConnectTypeEnum.SYSTEM.name());
	
      	// 循环初始化系统内置数据源
      	list.forEach(tenantId -> addSystem(tenantId, false, false));
        return true;
    }
  
  	// 加载自定义租户数据源
    @Override
    public boolean loadCustomDataSource() {
        // 查询所有可用的租户
        List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
        List<DefDatasourceConfigBO> dcList = initDbMapper.selectDataSourceConfig(status, TenantConnectTypeEnum.CUSTOM.name());
      	
        // 循环初始化自定义数据源
        return addCustom(dcList, false, false);
    }
}
初始化租户的数据源

执行这个方法时,由于isInitSchema=false,所以并不会重复在租户数据库中执行创建表结构和初始化数据。

java
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
  	
		private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
        DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
        ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);

        // 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
        databaseProperties.getInitDatabasePrefix().forEach(database -> {
            // 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
            DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
            newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
            if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
                newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
                newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
            } else {
                String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
                String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
                newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
            }
            if (isInitSchema) {
                DatasourceInitProperties init = newDataSourceProperty.getInit();
                if (init == null) {
                    init = new DatasourceInitProperties();
                }
                // 待创建的表结构                
                init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
                // 待初始化的数据
                init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
                newDataSourceProperty.setInit(init);
            }
            newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
            newDataSourceProperty.setDruid(properties.getDruid());
            if (isNotErrorRetry) {
                // 链接错误重试次数
                newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
                // 获取失败后中断
                newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
            }
            
            // 动态新增数据源
            putDs(newDataSourceProperty);
        });
        return true;
    }

    private Set<String> putDs(DataSourceProperty dsp) {
        try {
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
            DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
            ds.addDataSource(dsp.getPoolName(), newDataSource);
            return ds.getDataSources().keySet();
        } catch (ErrorCreateDataSourceException e) {
            log.error("数据源初始化期间出现异常", e);
            throw new BizException("数据源初始化期间出现异常", e);
        }
    }
}
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
  	
		private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
        DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
        ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);

        // 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
        databaseProperties.getInitDatabasePrefix().forEach(database -> {
            // 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
            DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
            newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
            if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
                newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
                newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
            } else {
                String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
                String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
                newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
            }
            if (isInitSchema) {
                DatasourceInitProperties init = newDataSourceProperty.getInit();
                if (init == null) {
                    init = new DatasourceInitProperties();
                }
                // 待创建的表结构                
                init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
                // 待初始化的数据
                init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
                newDataSourceProperty.setInit(init);
            }
            newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
            newDataSourceProperty.setDruid(properties.getDruid());
            if (isNotErrorRetry) {
                // 链接错误重试次数
                newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
                // 获取失败后中断
                newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
            }
            
            // 动态新增数据源
            putDs(newDataSourceProperty);
        });
        return true;
    }

    private Set<String> putDs(DataSourceProperty dsp) {
        try {
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
            DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
            ds.addDataSource(dsp.getPoolName(), newDataSource);
            return ds.getDataSources().keySet();
        } catch (ErrorCreateDataSourceException e) {
            log.error("数据源初始化期间出现异常", e);
            throw new BizException("数据源初始化期间出现异常", e);
        }
    }
}

警告

为了数据的安全考虑,请勿在acuity_base.sql中加入 DROP TABLE 语句,以防误执行

独立数据源模式扩展

扩展

  • 独立数据源模式因为是每个企业是独立的数据库,因此可以在企业内部启用column模式,实现隔离分公司或部门的功能。
  • 先独立数据库隔离租户,在按字段隔离下属单位或门店等:每个租户独立一个 数据库(数据源),执行代码时,动态切换数据源,在动态拼接 子租户id 二次隔离。

DATASOURCE_COLUMN模式和DATASOURCE模式的区别在于前者启动时,需要除了加载多数据源,还需要加载一个COLUMN模式的插件: acuityTenantLineInnerInterceptor。 该插件将会拦截Mapper方法,在Mapper类或方法上标记了@TenantLine 注解后,SQL语句将会自动拼接上租户ID的字段或条件。

java
@Slf4j
@Configuration
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {BUSINESS_PACKAGE, UTIL_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {
    public MybatisAutoConfiguration(DatabaseProperties databaseProperties) {
        super(databaseProperties);
    }

    @Override
    protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
        List<InnerInterceptor> list = new ArrayList<>();
        if (MultiTenantType.DATASOURCE_COLUMN.eq(databaseProperties.getMultiTenantType())) {
            log.info("检查到配置了:{}模式,已加载 column 部分插件", databaseProperties.getMultiTenantType());
            // COLUMN 模式 多租户插件
            acuityTenantLineInnerInterceptor tli = new acuityTenantLineInnerInterceptor();
            tli.setTenantLineHandler(new TenantLineHandler() {
                @Override
                public String getTenantIdColumn() {
                    return databaseProperties.getTenantIdColumn();
                }

                @Override
                public boolean ignoreTable(String tableName) {
                    boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);

                    boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
                            databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
                    return ignoreTable || ignoreTablePrefix;
                }

                @Override
                public Expression getTenantId() {
                  	// 当前单位ID
	                // 注意:若用户A不属于任何单位或用户A仅仅属于一个没有单位的部门,自动拼接租户ID时,ContextUtil.getCurrentCompanyId() = null, 将会导致这里报错。 你可以根据业务,将此方法返回null或者throws一个异常。
                    return new LongValue(ContextUtil.getCurrentCompanyId());
                }
            });
            list.add(tli);
        }

        return list;
    }
}
@Slf4j
@Configuration
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {BUSINESS_PACKAGE, UTIL_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {
    public MybatisAutoConfiguration(DatabaseProperties databaseProperties) {
        super(databaseProperties);
    }

    @Override
    protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
        List<InnerInterceptor> list = new ArrayList<>();
        if (MultiTenantType.DATASOURCE_COLUMN.eq(databaseProperties.getMultiTenantType())) {
            log.info("检查到配置了:{}模式,已加载 column 部分插件", databaseProperties.getMultiTenantType());
            // COLUMN 模式 多租户插件
            acuityTenantLineInnerInterceptor tli = new acuityTenantLineInnerInterceptor();
            tli.setTenantLineHandler(new TenantLineHandler() {
                @Override
                public String getTenantIdColumn() {
                    return databaseProperties.getTenantIdColumn();
                }

                @Override
                public boolean ignoreTable(String tableName) {
                    boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);

                    boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
                            databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
                    return ignoreTable || ignoreTablePrefix;
                }

                @Override
                public Expression getTenantId() {
                  	// 当前单位ID
	                // 注意:若用户A不属于任何单位或用户A仅仅属于一个没有单位的部门,自动拼接租户ID时,ContextUtil.getCurrentCompanyId() = null, 将会导致这里报错。 你可以根据业务,将此方法返回null或者throws一个异常。
                    return new LongValue(ContextUtil.getCurrentCompanyId());
                }
            });
            list.add(tli);
        }

        return list;
    }
}

字段模式

TIP

共享数据库,共享数据架构:租户共用一个数据库,在业务表中增加字段来区分

img

  • 优点

    简单、不复杂、开发无感知

  • 缺点

    数据隔离性差、安全性差、数据备份和恢复困难.

运行过程

创建租户的大致流程为:新增租户->初始化租户数据库表结构和初始数据->初始化其他服务的数据源。但对于字段模式,创建流程应该为:新增租户->初始化租户初始数据。接下来我们介绍每个步骤的代码实现

过程

  • 新增租户
  • 初始化租户初始数据
    • 初始化租户数据
    • 更新状态
    • 自动授权
新增租户:向def_tenant表插入一条数据,状态为待初始化结构。
java
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
    // 数据初始化
    DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
    tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
    tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
    tenant.setReadonly(false);
    if (StrUtil.isEmpty(tenant.getCreatedName())) {
        DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
        if (result != null) {
            tenant.setCreatedName(result.getNickName());
        }
    }
    return tenant;
}

@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
  	// 保存租户logo
    appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
    // 数据初始化
    DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
    tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
    tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
    tenant.setReadonly(false);
    if (StrUtil.isEmpty(tenant.getCreatedName())) {
        DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
        if (result != null) {
            tenant.setCreatedName(result.getNickName());
        }
    }
    return tenant;
}

@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
  	// 保存租户logo
    appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
初始化租户初始数据
  • 初始化租户数据:创建角色信息等需要初始化的数据。
    java
    @Service("COLUMN")
    @Slf4j
    @RequiredArgsConstructor
    public class ColumnInitSystemStrategy implements InitSystemStrategy {
      private final RoleMapper roleMapper;
    
      @Override
      @Transactional(rollbackFor = Exception.class)
      public boolean initData(DefTenantInitVO tenantInitVO) {
          ContextUtil.setTenantBasePoolName(tenantInitVO.getId());
          List<SysRole> roles = new ArrayList<>();
          roles.add(SysRole.builder().code(RoleConstant.TENANT_ADMIN).name("租户管理员").readonly(true).remarks("内置管理员").state(true)
                  .type(DataTypeEnum.SYSTEM.getCode()).category(RoleCategoryEnum.FUNCTION.getCode()).build());
          // 初始化数据
          roleMapper.insertBatchSomeColumn(roles);
          return true;
      }
    }
    @Service("COLUMN")
    @Slf4j
    @RequiredArgsConstructor
    public class ColumnInitSystemStrategy implements InitSystemStrategy {
      private final RoleMapper roleMapper;
    
      @Override
      @Transactional(rollbackFor = Exception.class)
      public boolean initData(DefTenantInitVO tenantInitVO) {
          ContextUtil.setTenantBasePoolName(tenantInitVO.getId());
          List<SysRole> roles = new ArrayList<>();
          roles.add(SysRole.builder().code(RoleConstant.TENANT_ADMIN).name("租户管理员").readonly(true).remarks("内置管理员").state(true)
                  .type(DataTypeEnum.SYSTEM.getCode()).category(RoleCategoryEnum.FUNCTION.getCode()).build());
          // 初始化数据
          roleMapper.insertBatchSomeColumn(roles);
          return true;
      }
    }
  • 更新状态-修改租户状态为待初始化数据源
  • 自动授权-自动给该租户授权基础平台

启动过程

COLUMN模式启动过程跟DATASOURCE完全不同,COLUMN模式启动时没有依赖dynamic-datasource-spring-boot-starter组件,仅依靠druid加载数据源。在启动时,会注册一个租户SQL拦截器,实现租户SQL的重写。

java
@Configuration
@Slf4j
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {UTIL_PACKAGE, BUSINESS_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {

    public MybatisAutoConfiguration(final DatabaseProperties databaseProperties) {
        super(databaseProperties);
    }

    /**
     * COLUMN 模式 SQL动态拼接拦截器
     *
     * @return 插件
     */
    @Override
    protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
        List<InnerInterceptor> list = new ArrayList<>();
        // COLUMN 模式 多租户插件
        TenantLineInnerInterceptor tli = new TenantLineInnerInterceptor();
        tli.setTenantLineHandler(new MultiTenantLineHandler() {
          	//  租户字段的数据库字段名
            @Override
            public String getTenantIdColumn() {
                return databaseProperties.getTenantIdColumn();
            }

            @Override
            public boolean ignoreTable(String tableName) {
                if (ContextUtil.isEmptyBasePoolNameHeader()) {
                    return true;
                }

                boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);

                boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
                        databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
                return ignoreTable || ignoreTablePrefix;
            }
						
            @Override
            public ValueListExpression getTenantIdList() {
                return null;
            }
						// 自动拼接的租户值
            @Override
            public Expression getTenantId() {
                return new LongValue(ContextUtil.getBasePoolNameHeader());
            }
        });
        list.add(tli);
        return list;
    }
}
@Configuration
@Slf4j
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {UTIL_PACKAGE, BUSINESS_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {

    public MybatisAutoConfiguration(final DatabaseProperties databaseProperties) {
        super(databaseProperties);
    }

    /**
     * COLUMN 模式 SQL动态拼接拦截器
     *
     * @return 插件
     */
    @Override
    protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
        List<InnerInterceptor> list = new ArrayList<>();
        // COLUMN 模式 多租户插件
        TenantLineInnerInterceptor tli = new TenantLineInnerInterceptor();
        tli.setTenantLineHandler(new MultiTenantLineHandler() {
          	//  租户字段的数据库字段名
            @Override
            public String getTenantIdColumn() {
                return databaseProperties.getTenantIdColumn();
            }

            @Override
            public boolean ignoreTable(String tableName) {
                if (ContextUtil.isEmptyBasePoolNameHeader()) {
                    return true;
                }

                boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);

                boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
                        databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
                return ignoreTable || ignoreTablePrefix;
            }
						
            @Override
            public ValueListExpression getTenantIdList() {
                return null;
            }
						// 自动拼接的租户值
            @Override
            public Expression getTenantId() {
                return new LongValue(ContextUtil.getBasePoolNameHeader());
            }
        });
        list.add(tli);
        return list;
    }
}

无租户模式

独立系统

欢迎使用天源云Saas快速开发系统