refactor(tenant): 移除租户数据源及数据源级隔离适配代码

This commit is contained in:
2025-07-15 21:54:22 +08:00
parent af1079da6d
commit 6e7d371565
39 changed files with 172 additions and 1292 deletions

View File

@@ -173,10 +173,5 @@
<groupId>top.continew.starter</groupId> <groupId>top.continew.starter</groupId>
<artifactId>continew-starter-extension-tenant-mp</artifactId> <artifactId>continew-starter-extension-tenant-mp</artifactId>
</dependency> </dependency>
<!-- Dynamic Datasource基于 Spring Boot 的快速集成多数据源的启动器) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -17,12 +17,14 @@
package top.continew.admin.common.config; package top.continew.admin.common.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import top.continew.starter.core.constant.PropertiesConstants;
import java.util.List; import java.util.List;
/** /**
* 租户配置属性 * 租户扩展配置属性
* *
* @author 小熊 * @author 小熊
* @author Charles7c * @author Charles7c
@@ -30,7 +32,8 @@ import java.util.List;
*/ */
@Data @Data
@Component @Component
public class TenantProperties extends top.continew.starter.extension.tenant.autoconfigure.TenantProperties { @ConfigurationProperties(prefix = PropertiesConstants.TENANT)
public class TenantExtensionProperties {
/** /**
* 忽略菜单 ID租户不能使用的菜单 * 忽略菜单 ID租户不能使用的菜单

View File

@@ -84,11 +84,6 @@ public class SysConstants {
*/ */
public static final String LOGOUT_URI = "/auth/logout"; public static final String LOGOUT_URI = "/auth/logout";
/**
* 租户默认数据源
*/
public static final String DEFAULT_TENANT_DATASOURCE = "master";
/** /**
* 租户管理员角色编码 * 租户管理员角色编码
*/ */

View File

@@ -1,35 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.annotation;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import top.continew.starter.core.constant.PropertiesConstants;
import java.lang.annotation.*;
/**
* 是否启用 Tenant 判断注解
*
* @author Charles7c
* @since 2025/5/18 12:03
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ConditionalOnProperty(prefix = PropertiesConstants.TENANT, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public @interface ConditionalOnEnabledTenant {
}

View File

@@ -16,19 +16,11 @@
package top.continew.admin.tenant.config; package top.continew.admin.tenant.config;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import top.continew.admin.common.config.TenantProperties;
import top.continew.admin.tenant.constant.TenantConstants;
import top.continew.admin.tenant.model.entity.DatasourceDO;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.enums.DatasourceDatabaseTypeEnum;
import top.continew.admin.tenant.model.req.DatasourceReq;
import top.continew.admin.tenant.service.DatasourceService;
import top.continew.admin.tenant.service.TenantService; import top.continew.admin.tenant.service.TenantService;
import top.continew.starter.extension.tenant.config.TenantDataSource; import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
import top.continew.starter.extension.tenant.config.TenantProvider; import top.continew.starter.extension.tenant.config.TenantProvider;
import top.continew.starter.extension.tenant.context.TenantContext; import top.continew.starter.extension.tenant.context.TenantContext;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel; import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
@@ -46,39 +38,21 @@ public class DefaultTenantProvider implements TenantProvider {
private final TenantProperties tenantProperties; private final TenantProperties tenantProperties;
private final TenantService tenantService; private final TenantService tenantService;
private final DatasourceService datasourceService;
@Override @Override
public TenantContext getByTenantId(String tenantIdAsString, boolean verify) { public TenantContext getByTenantId(String tenantIdAsString, boolean verify) {
TenantContext context = new TenantContext(); TenantContext context = new TenantContext();
// 超级租户默认使用行级隔离 context.setIsolationLevel(TenantIsolationLevel.LINE);
// 超级租户
Long superTenantId = tenantProperties.getSuperTenantId(); Long superTenantId = tenantProperties.getSuperTenantId();
if (StrUtil.isBlank(tenantIdAsString) || superTenantId.toString().equals(tenantIdAsString)) { if (StrUtil.isBlank(tenantIdAsString) || superTenantId.toString().equals(tenantIdAsString)) {
context.setTenantId(superTenantId); context.setTenantId(superTenantId);
context.setIsolationLevel(TenantIsolationLevel.LINE);
return context; return context;
} }
// 获取租户信息 // 获取租户信息
Long tenantId = Long.valueOf(tenantIdAsString); Long tenantId = Long.valueOf(tenantIdAsString);
TenantDO tenant = tenantService.checkStatus(tenantId); tenantService.checkStatus(tenantId);
TenantIsolationLevel isolationLevel = tenant.getIsolationLevel().getLevel();
context.setTenantId(tenantId); context.setTenantId(tenantId);
context.setIsolationLevel(isolationLevel);
// 数据源级隔离级别需要提供数据源信息
if (TenantIsolationLevel.DATASOURCE == isolationLevel) {
// 获取数据源配置
DatasourceDO datasource = datasourceService.getById(tenant.getDatasourceId());
DatasourceDatabaseTypeEnum databaseType = datasource.getDatabaseType();
// 填充数据源信息
TenantDataSource tenantDataSource = new TenantDataSource();
tenantDataSource.setPoolName(tenantIdAsString);
tenantDataSource.setDriverClassName(databaseType.getDriverClassName());
tenantDataSource.setUrl(databaseType.getJdbcUrl(BeanUtil
.toBean(datasource, DatasourceReq.class), TenantConstants.TENANT_DB_PREFIX + tenant.getCode()));
tenantDataSource.setUsername(datasource.getUsername());
tenantDataSource.setPassword(datasource.getPassword());
context.setDataSource(tenantDataSource);
}
return context; return context;
} }
} }

View File

@@ -36,11 +36,6 @@ public class TenantCacheConstants {
*/ */
public static final String TENANT_KEY_PREFIX = "TENANT" + DELIMITER; public static final String TENANT_KEY_PREFIX = "TENANT" + DELIMITER;
/**
* 租户数据源前缀
*/
public static final String TENANT_DATASOURCE_KEY_PREFIX = TENANT_KEY_PREFIX + "DATASOURCE" + DELIMITER;
private TenantCacheConstants() { private TenantCacheConstants() {
} }
} }

View File

@@ -24,11 +24,6 @@ package top.continew.admin.tenant.constant;
*/ */
public class TenantConstants { public class TenantConstants {
/**
* 租户数据库前缀
*/
public static final String TENANT_DB_PREFIX = "tenant_";
/** /**
* 编码生成器 KEY * 编码生成器 KEY
*/ */

View File

@@ -1,55 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import top.continew.admin.common.base.controller.BaseController;
import top.continew.admin.tenant.model.query.DatasourceQuery;
import top.continew.admin.tenant.model.req.DatasourceReq;
import top.continew.admin.tenant.model.resp.DatasourceDetailResp;
import top.continew.admin.tenant.model.resp.DatasourceResp;
import top.continew.admin.tenant.service.DatasourceService;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
/**
* 数据源管理 API
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 19:13
*/
@Tag(name = "数据源管理 API")
@RestController
@CrudRequestMapping(value = "/tenant/datasource", api = {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE})
public class DatasourceController extends BaseController<DatasourceService, DatasourceResp, DatasourceDetailResp, DatasourceQuery, DatasourceReq> {
@Operation(summary = "测试连接", description = "测试数据源连接可用性")
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@SaCheckPermission("tenant:datasource:testConnection")
@PostMapping("/{id}/test/connection")
public void testConnection(@PathVariable Long id) {
baseService.testConnection(id);
}
}

View File

@@ -24,8 +24,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import top.continew.admin.common.base.controller.BaseController; import top.continew.admin.common.base.controller.BaseController;
import top.continew.admin.common.config.TenantExtensionProperties;
import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.common.config.TenantProperties;
import top.continew.admin.system.model.query.MenuQuery; import top.continew.admin.system.model.query.MenuQuery;
import top.continew.admin.system.service.MenuService; import top.continew.admin.system.service.MenuService;
import top.continew.admin.tenant.model.query.PackageQuery; import top.continew.admin.tenant.model.query.PackageQuery;
@@ -51,7 +51,7 @@ import java.util.List;
@CrudRequestMapping(value = "/tenant/package", api = {Api.LIST, Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE}) @CrudRequestMapping(value = "/tenant/package", api = {Api.LIST, Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE})
public class PackageController extends BaseController<PackageService, PackageResp, PackageDetailResp, PackageQuery, PackageReq> { public class PackageController extends BaseController<PackageService, PackageResp, PackageDetailResp, PackageQuery, PackageReq> {
private final TenantProperties tenantProperties; private final TenantExtensionProperties tenantExtensionProperties;
private final MenuService menuService; private final MenuService menuService;
@Operation(summary = "查询租户套餐菜单", description = "查询租户套餐菜单树列表") @Operation(summary = "查询租户套餐菜单", description = "查询租户套餐菜单树列表")
@@ -61,7 +61,7 @@ public class PackageController extends BaseController<PackageService, PackageRes
MenuQuery query = new MenuQuery(); MenuQuery query = new MenuQuery();
query.setStatus(DisEnableStatusEnum.ENABLE); query.setStatus(DisEnableStatusEnum.ENABLE);
// 过滤掉租户不能使用的菜单 // 过滤掉租户不能使用的菜单
query.setExcludeMenuIdList(tenantProperties.getIgnoreMenus()); query.setExcludeMenuIdList(tenantExtensionProperties.getIgnoreMenus());
return menuService.tree(query, null, true); return menuService.tree(query, null, true);
} }
} }

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.continew.admin.tenant.model.query.PackageQuery;
import top.continew.admin.tenant.service.PackageService;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.log.annotation.Log;
import java.util.List;
/**
* 公共 API
*
* @author Charles7c
* @since 2025/7/15 20:32
*/
@Tag(name = "公共 API")
@Log(ignore = true)
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/tenant/common")
public class TenantCommonController {
private final PackageService packageService;
@Operation(summary = "查询套餐字典", description = "查询套餐字典列表")
@GetMapping("/dict/package")
public List<LabelValueResp> listPackageDict(PackageQuery query, SortQuery sortQuery) {
return packageService.listDict(query, sortQuery);
}
}

View File

@@ -19,14 +19,12 @@ package top.continew.admin.tenant.controller;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import top.continew.admin.common.base.controller.BaseController; import top.continew.admin.common.base.controller.BaseController;
import top.continew.admin.common.config.TenantProperties;
import top.continew.admin.common.util.SecureUtils; import top.continew.admin.common.util.SecureUtils;
import top.continew.admin.system.model.entity.user.UserDO; import top.continew.admin.system.model.entity.user.UserDO;
import top.continew.admin.system.model.req.user.UserPasswordResetReq; import top.continew.admin.system.model.req.user.UserPasswordResetReq;
@@ -44,6 +42,7 @@ import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping; import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api; import top.continew.starter.extension.crud.enums.Api;
import top.continew.starter.extension.tenant.TenantHandler; import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
/** /**
* 租户管理 API * 租户管理 API
@@ -71,7 +70,6 @@ public class TenantController extends BaseController<TenantService, TenantResp,
return commonResp; return commonResp;
} }
@DSTransactional(rollbackFor = Exception.class)
@Operation(summary = "修改租户管理员密码", description = "修改租户管理员密码") @Operation(summary = "修改租户管理员密码", description = "修改租户管理员密码")
@SaCheckPermission("tenant:management:updateAdminUserPwd") @SaCheckPermission("tenant:management:updateAdminUserPwd")
@PutMapping("/{id}/admin/pwd") @PutMapping("/{id}/admin/pwd")

View File

@@ -1,30 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.mapper;
import org.apache.ibatis.annotations.Mapper;
import top.continew.admin.tenant.model.entity.DatasourceDO;
import top.continew.starter.data.mapper.BaseMapper;
/**
* 数据源 Mapper
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Mapper
public interface DatasourceMapper extends BaseMapper<DatasourceDO> {}

View File

@@ -1,78 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import top.continew.admin.common.base.model.entity.BaseDO;
import top.continew.admin.tenant.model.enums.DatasourceDatabaseTypeEnum;
import top.continew.starter.extension.crud.annotation.DictModel;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import java.io.Serial;
/**
* 数据源实体
*
* @author 小熊
* @since Charles7c
* @since 2024/12/12 19:13
*/
@Data
@DictModel
@TableName("tenant_datasource")
public class DatasourceDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 名称
*/
private String name;
/**
* 数据库类型
*/
private DatasourceDatabaseTypeEnum databaseType;
/**
* 主机
*/
private String host;
/**
* 端口
*/
private Integer port;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
@FieldEncrypt
private String password;
/**
* 描述
*/
private String description;
}

View File

@@ -20,7 +20,6 @@ import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import top.continew.admin.common.base.model.entity.BaseDO; import top.continew.admin.common.base.model.entity.BaseDO;
import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum;
import java.io.Serial; import java.io.Serial;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -59,11 +58,6 @@ public class TenantDO extends BaseDO {
*/ */
private LocalDateTime expireTime; private LocalDateTime expireTime;
/**
* 隔离级别
*/
private TenantIsolationLevelEnum isolationLevel;
/** /**
* 描述 * 描述
*/ */
@@ -83,9 +77,4 @@ public class TenantDO extends BaseDO {
* 套餐 ID * 套餐 ID
*/ */
private Long packageId; private Long packageId;
/**
* 数据源 ID
*/
private Long datasourceId;
} }

View File

@@ -1,133 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.enums;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.jdbc.DataSourceBuilder;
import top.continew.admin.tenant.model.req.DatasourceReq;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.enums.BaseEnum;
import top.continew.starter.core.exception.BusinessException;
import javax.sql.DataSource;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.util.Map;
/**
* 数据源数据库类型枚举
*
* @author Charles7c
* @author 小熊
* @since 2024/11/26 17:20
*/
@Slf4j
@Getter
@RequiredArgsConstructor
public enum DatasourceDatabaseTypeEnum implements BaseEnum<Integer> {
/**
* MySQL
*/
MYSQL(1, "MySQL", "com.mysql.cj.jdbc.Driver") {
@Override
public DataSource buildDataSource(DatasourceReq datasource) {
return DataSourceBuilder.create()
.url(this.getJdbcUrl(datasource, null))
.driverClassName(this.getDriverClassName())
.username(datasource.getUsername())
.password(datasource.getPassword())
.build();
}
@Override
public void testConnection(DatasourceReq datasource) {
DataSource dataSource = this.buildDataSource(datasource);
try (Connection ignored = dataSource.getConnection()) {
log.info("数据源 [{}] 测试连接成功", datasource.getName());
} catch (Exception e) {
throw new BusinessException("数据源 [%s] 测试连接失败".formatted(datasource.getName()));
}
}
@Override
public String getJdbcUrl(DatasourceReq datasource, String databaseName) {
StringBuilder urlBuilder = new StringBuilder("jdbc:mysql://%s:%s".formatted(datasource.getHost(), datasource
.getPort()));
if (StrUtil.isNotBlank(databaseName)) {
urlBuilder.append(StringConstants.SLASH).append(databaseName);
urlBuilder.append(StringConstants.QUESTION_MARK);
urlBuilder.append(URLUtil.buildQuery(this.getDefaultParameters(), StandardCharsets.UTF_8));
}
return urlBuilder.toString();
}
@Override
public Map<String, String> getDefaultParameters() {
Map<String, String> parameter = MapUtil.newHashMap(8);
parameter.put("serverTimezone", "Asia/Shanghai");
parameter.put("useSSL", "true");
parameter.put("useUnicode", "true");
parameter.put("characterEncoding", "utf8");
parameter.put("rewriteBatchedStatements", "true");
parameter.put("autoReconnect", "true");
parameter.put("allowPublicKeyRetrieval", "true");
parameter.put("nullCatalogMeansCurrent", "true");
return parameter;
}
};
private final Integer value;
private final String description;
private final String driverClassName;
/**
* 构建数据源
*
* @param datasource 数据源配置
* @return 数据源
*/
public abstract DataSource buildDataSource(DatasourceReq datasource);
/**
* 测试连接
*
* @param datasource 数据源配置
*/
public abstract void testConnection(DatasourceReq datasource);
/**
* 获取 JDBC URL
*
* @param datasource 数据源配置
* @param databaseName 数据库名称
* @return JDBC URL
*/
public abstract String getJdbcUrl(DatasourceReq datasource, String databaseName);
/**
* 获取默认数据库连接参数
*
* @return 默认数据库连接参数
*/
public abstract Map<String, String> getDefaultParameters();
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import top.continew.starter.core.enums.BaseEnum;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
/**
* 租户隔离级别枚举
*
* @author Charles7c
* @since 2025/7/14 20:31
*/
@Slf4j
@Getter
@RequiredArgsConstructor
public enum TenantIsolationLevelEnum implements BaseEnum<Integer> {
/**
* 行级
*/
LINE(1, "行级", TenantIsolationLevel.LINE),
/**
* 数据源级
*/
DATASOURCE(2, "数据源级", TenantIsolationLevel.DATASOURCE);
private final Integer value;
private final String description;
private final TenantIsolationLevel level;
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.starter.data.annotation.Query;
import top.continew.starter.data.enums.QueryType;
import java.io.Serial;
import java.io.Serializable;
/**
* 数据源查询条件
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 19:13
*/
@Data
@Schema(description = "数据源查询条件")
public class DatasourceQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 关键词
*/
@Schema(description = "关键词", example = "数据源")
@Query(columns = {"name", "description"}, type = QueryType.LIKE)
private String description;
}

View File

@@ -41,7 +41,7 @@ public class TenantQuery implements Serializable {
/** /**
* 关键词 * 关键词
*/ */
@Schema(description = "关键词", example = "T0001") @Schema(description = "关键词", example = "Xxx租户")
@Query(columns = {"name", "description"}, type = QueryType.LIKE) @Query(columns = {"name", "description"}, type = QueryType.LIKE)
private String description; private String description;

View File

@@ -1,95 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.tenant.model.enums.DatasourceDatabaseTypeEnum;
import top.continew.starter.extension.crud.validation.CrudValidationGroup;
import java.io.Serial;
import java.io.Serializable;
/**
* 数据源创建或修改请求参数
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 19:13
*/
@Data
@Schema(description = "数据源创建或修改请求参数")
public class DatasourceReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 名称
*/
@Schema(description = "名称", example = "T0001数据源")
@NotBlank(message = "名称不能为空")
@Length(max = 30, message = "名称长度不能超过 {max} 个字符")
private String name;
/**
* 数据库类型
*/
@Schema(description = "数据库类型", example = "1")
@NotNull(message = "数据库类型无效")
private DatasourceDatabaseTypeEnum databaseType;
/**
* 主机
*/
@Schema(description = "主机", example = "123.56.195.68")
@NotBlank(message = "主机不能为空")
@Length(max = 128, message = "主机长度不能超过 {max} 个字符")
private String host;
/**
* 端口
*/
@Schema(description = "端口", example = "3306")
@NotNull(message = "端口不能为空")
private Integer port;
/**
* 用户名
*/
@Schema(description = "用户名", example = "root")
@NotBlank(message = "用户名不能为空")
@Length(max = 128, message = "用户名长度不能超过 {max} 个字符")
private String username;
/**
* 密码
*/
@Schema(description = "密码", example = "jXo1Mwsuoz+XhLy6tOhdzbTJ3gIDxciTAnCjcOO8akglghVDO3jR5pqOp95LkSBp1Yd9bltYzWDNjNvL6yD3TQ==")
@NotBlank(message = "密码不能为空", groups = CrudValidationGroup.Create.class)
private String password;
/**
* 描述
*/
@Schema(description = "描述", example = "T0001数据源描述")
@Length(max = 200, message = "描述长度不能超过 {max} 个字符")
private String description;
}

View File

@@ -33,8 +33,9 @@ import java.io.Serializable;
public class TenantAdminUserPwdUpdateReq implements Serializable { public class TenantAdminUserPwdUpdateReq implements Serializable {
/** /**
* 新密码 * 新密码(加密)
*/ */
@Schema(description = "新密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
@NotBlank(message = "新密码不能为空") @NotBlank(message = "新密码不能为空")
private String password; private String password;
} }

View File

@@ -16,8 +16,6 @@
package top.continew.admin.tenant.model.req; package top.continew.admin.tenant.model.req;
import cn.sticki.spel.validator.constrain.SpelNotNull;
import cn.sticki.spel.validator.jakarta.SpelValid;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Future; import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
@@ -27,7 +25,6 @@ import lombok.Data;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
import top.continew.admin.common.constant.RegexConstants; import top.continew.admin.common.constant.RegexConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum;
import top.continew.starter.extension.crud.validation.CrudValidationGroup; import top.continew.starter.extension.crud.validation.CrudValidationGroup;
import java.io.Serial; import java.io.Serial;
@@ -42,7 +39,6 @@ import java.time.LocalDateTime;
* @since 2024/11/26 17:20 * @since 2024/11/26 17:20
*/ */
@Data @Data
@SpelValid
@Schema(description = "租户创建或修改请求参数") @Schema(description = "租户创建或修改请求参数")
public class TenantReq implements Serializable { public class TenantReq implements Serializable {
@@ -52,7 +48,7 @@ public class TenantReq implements Serializable {
/** /**
* 名称 * 名称
*/ */
@Schema(description = "名称", example = "T0001租户") @Schema(description = "名称", example = "Xxx租户")
@NotBlank(message = "名称不能为空") @NotBlank(message = "名称不能为空")
@Length(max = 30, message = "名称长度不能超过 {max} 个字符") @Length(max = 30, message = "名称长度不能超过 {max} 个字符")
private String name; private String name;
@@ -60,7 +56,7 @@ public class TenantReq implements Serializable {
/** /**
* 域名 * 域名
*/ */
@Schema(description = "域名", example = "https://t0001.continew.top/") @Schema(description = "域名", example = "https://T0sL6RWv0vFh.continew.top/")
@Length(max = 255, message = "域名长度不能超过 {max} 个字符") @Length(max = 255, message = "域名长度不能超过 {max} 个字符")
private String domain; private String domain;
@@ -71,17 +67,10 @@ public class TenantReq implements Serializable {
@Future(message = "过期时间必须是未来时间") @Future(message = "过期时间必须是未来时间")
private LocalDateTime expireTime; private LocalDateTime expireTime;
/**
* 隔离级别
*/
@Schema(description = "隔离级别", example = "2")
@NotNull(message = "隔离级别不能为空", groups = CrudValidationGroup.Create.class)
private TenantIsolationLevelEnum isolationLevel;
/** /**
* 描述 * 描述
*/ */
@Schema(description = "描述", example = "T0001租户描述") @Schema(description = "描述", example = "租户描述")
@Length(max = 200, message = "描述长度不能超过 {max} 个字符") @Length(max = 200, message = "描述长度不能超过 {max} 个字符")
private String description; private String description;
@@ -98,13 +87,6 @@ public class TenantReq implements Serializable {
@NotNull(message = "套餐不能为空") @NotNull(message = "套餐不能为空")
private Long packageId; private Long packageId;
/**
* 数据源 ID
*/
@Schema(description = "数据源 ID")
@SpelNotNull(condition = "#this.isolationLevel == T(top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum).DATASOURCE", message = "数据源不能为空")
private Long datasourceId;
/** /**
* 用户名 * 用户名
*/ */

View File

@@ -1,37 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
/**
* 数据源详情响应参数
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 19:13
*/
@Data
@Schema(description = "数据源详情响应参数")
public class DatasourceDetailResp extends DatasourceResp {
@Serial
private static final long serialVersionUID = 1L;
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.resp;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.common.base.model.resp.BaseDetailResp;
import top.continew.admin.tenant.model.enums.DatasourceDatabaseTypeEnum;
import top.continew.starter.excel.converter.ExcelBaseEnumConverter;
import java.io.Serial;
/**
* 数据源响应参数
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 19:13
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "数据源响应参数")
public class DatasourceResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 名称
*/
@Schema(description = "名称", example = "T0001数据源")
@ExcelProperty(value = "名称", order = 2)
private String name;
/**
* 数据库类型
*/
@Schema(description = "数据库类型", example = "1")
@ExcelProperty(value = "数据库类型", converter = ExcelBaseEnumConverter.class, order = 3)
private DatasourceDatabaseTypeEnum databaseType;
/**
* 主机
*/
@Schema(description = "主机", example = "123.56.195.68")
@ExcelProperty(value = "主机", order = 4)
private String host;
/**
* 端口
*/
@Schema(description = "端口", example = "3306")
@ExcelProperty(value = "端口", order = 5)
private Integer port;
/**
* 用户名
*/
@Schema(description = "用户名", example = "root")
@ExcelProperty(value = "用户名", order = 6)
private String username;
/**
* 描述
*/
@Schema(description = "描述", example = "T0001数据源描述")
@ExcelProperty(value = "描述", order = 7)
private String description;
}

View File

@@ -40,6 +40,6 @@ public class TenantDetailResp extends TenantResp {
* 租户管理员 * 租户管理员
*/ */
@Schema(description = "租户管理员", example = "666") @Schema(description = "租户管理员", example = "666")
@ExcelProperty(value = "租户管理员", order = 13) @ExcelProperty(value = "租户管理员", order = 11)
private Long adminUser; private Long adminUser;
} }

View File

@@ -25,8 +25,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import top.continew.admin.common.base.model.resp.BaseDetailResp; import top.continew.admin.common.base.model.resp.BaseDetailResp;
import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum;
import top.continew.admin.tenant.service.DatasourceService;
import top.continew.admin.tenant.service.PackageService; import top.continew.admin.tenant.service.PackageService;
import top.continew.starter.excel.converter.ExcelBaseEnumConverter; import top.continew.starter.excel.converter.ExcelBaseEnumConverter;
@@ -51,21 +49,21 @@ public class TenantResp extends BaseDetailResp {
/** /**
* 名称 * 名称
*/ */
@Schema(description = "名称", example = "T0001租户") @Schema(description = "名称", example = "Xxx租户")
@ExcelProperty(value = "名称", order = 2) @ExcelProperty(value = "名称", order = 2)
private String name; private String name;
/** /**
* 编码 * 编码
*/ */
@Schema(description = "编码", example = "T0001") @Schema(description = "编码", example = "T0sL6RWv0vFh")
@ExcelProperty(value = "编码", order = 3) @ExcelProperty(value = "编码", order = 3)
private String code; private String code;
/** /**
* 域名 * 域名
*/ */
@Schema(description = "域名", example = "https://t0001.continew.top/") @Schema(description = "域名", example = "https://T0sL6RWv0vFh.continew.top/")
@ExcelProperty(value = "域名", order = 4) @ExcelProperty(value = "域名", order = 4)
private String domain; private String domain;
@@ -76,17 +74,10 @@ public class TenantResp extends BaseDetailResp {
@ExcelProperty(value = "过期时间", order = 5) @ExcelProperty(value = "过期时间", order = 5)
private LocalDateTime expireTime; private LocalDateTime expireTime;
/**
* 隔离级别
*/
@Schema(description = "隔离级别", example = "2")
@ExcelProperty(value = "隔离级别", converter = ExcelBaseEnumConverter.class, order = 6)
private TenantIsolationLevelEnum isolationLevel;
/** /**
* 描述 * 描述
*/ */
@Schema(description = "描述", example = "T0001租户描述") @Schema(description = "描述", example = "租户描述")
@ExcelProperty(value = "描述", order = 7) @ExcelProperty(value = "描述", order = 7)
private String description; private String description;
@@ -105,25 +96,10 @@ public class TenantResp extends BaseDetailResp {
@AssembleMethod(props = @Mapping(src = "name", ref = "packageName"), targetType = PackageService.class, method = @ContainerMethod(bindMethod = "get", resultType = PackageResp.class)) @AssembleMethod(props = @Mapping(src = "name", ref = "packageName"), targetType = PackageService.class, method = @ContainerMethod(bindMethod = "get", resultType = PackageResp.class))
private Long packageId; private Long packageId;
/**
* 数据源 ID
*/
@Schema(description = "数据源 ID", example = "1")
@ExcelProperty(value = "数据源 ID", order = 10)
@AssembleMethod(props = @Mapping(src = "name", ref = "datasourceName"), targetType = DatasourceService.class, method = @ContainerMethod(bindMethod = "get", resultType = DatasourceResp.class))
private Long datasourceId;
/** /**
* 套餐名称 * 套餐名称
*/ */
@Schema(description = "套餐名称", example = "初级套餐") @Schema(description = "套餐名称", example = "初级套餐")
@ExcelProperty(value = "套餐名称", order = 11) @ExcelProperty(value = "套餐名称", order = 10)
private String packageName; private String packageName;
/**
* 数据源名称
*/
@Schema(description = "数据源名称", example = "T0001数据源")
@ExcelProperty(value = "数据源名称", order = 12)
private String datasourceName;
} }

View File

@@ -1,50 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.service;
import top.continew.admin.common.base.service.BaseService;
import top.continew.admin.tenant.model.entity.DatasourceDO;
import top.continew.admin.tenant.model.query.DatasourceQuery;
import top.continew.admin.tenant.model.req.DatasourceReq;
import top.continew.admin.tenant.model.resp.DatasourceDetailResp;
import top.continew.admin.tenant.model.resp.DatasourceResp;
import top.continew.starter.data.service.IService;
/**
* 数据源业务接口
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 19:13
*/
public interface DatasourceService extends BaseService<DatasourceResp, DatasourceDetailResp, DatasourceQuery, DatasourceReq>, IService<DatasourceDO> {
/**
* 测试连接
*
* @param id ID
*/
void testConnection(Long id);
/**
* 初始化数据库
*
* @param databaseName 数据库名称
* @param id ID
*/
void initDb(String databaseName, Long id);
}

View File

@@ -40,9 +40,8 @@ public interface TenantService extends BaseService<TenantResp, TenantDetailResp,
* 检查租户状态 * 检查租户状态
* *
* @param id ID * @param id ID
* @return 租户信息
*/ */
TenantDO checkStatus(Long id); void checkStatus(Long id);
/** /**
* 获取所有可用的租户列表 * 获取所有可用的租户列表

View File

@@ -1,179 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.util.StrUtil;
import com.alicp.jetcache.anno.Cached;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import top.continew.admin.common.base.service.BaseServiceImpl;
import top.continew.admin.common.util.SecureUtils;
import top.continew.admin.tenant.constant.TenantCacheConstants;
import top.continew.admin.tenant.mapper.DatasourceMapper;
import top.continew.admin.tenant.mapper.TenantMapper;
import top.continew.admin.tenant.model.entity.DatasourceDO;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.query.DatasourceQuery;
import top.continew.admin.tenant.model.req.DatasourceReq;
import top.continew.admin.tenant.model.resp.DatasourceDetailResp;
import top.continew.admin.tenant.model.resp.DatasourceResp;
import top.continew.admin.tenant.service.DatasourceService;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
/**
* 数据源业务实现
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 19:13
*/
@Service
@RequiredArgsConstructor
public class DatasourceServiceImpl extends BaseServiceImpl<DatasourceMapper, DatasourceDO, DatasourceResp, DatasourceDetailResp, DatasourceQuery, DatasourceReq> implements DatasourceService {
private final TenantMapper tenantMapper;
@Override
@Cached(name = TenantCacheConstants.TENANT_DATASOURCE_KEY_PREFIX, key = "#id")
public DatasourceDetailResp get(Long id) {
return super.get(id);
}
@Override
public void beforeCreate(DatasourceReq req) {
// 解密密码
req.setPassword(this.decryptPassword(req.getPassword(), null));
// 检查是否重复
this.checkRepeat(req, null);
// 测试连接
req.getDatabaseType().testConnection(req);
}
@Override
public void beforeUpdate(DatasourceReq req, Long id) {
DatasourceDO oldDatasource = super.getById(id);
// 解密密码
req.setPassword(this.decryptPassword(req.getPassword(), oldDatasource));
// 检查是否重复
this.checkRepeat(req, id);
CheckUtils.throwIf(oldDatasource.getDatabaseType() != req.getDatabaseType(), "数据库类型不能修改");
// 测试连接
boolean isUpdated = this.isUpdated(req, oldDatasource);
if (isUpdated) {
req.getDatabaseType().testConnection(req);
}
}
@Override
public void afterUpdate(DatasourceReq req, DatasourceDO entity) {
RedisUtils.delete(TenantCacheConstants.TENANT_DATASOURCE_KEY_PREFIX + entity.getId());
}
@Override
public void beforeDelete(List<Long> ids) {
CheckUtils.throwIf(tenantMapper.lambdaQuery().in(TenantDO::getDatasourceId, ids).exists(), "所选数据源存在关联租户,不允许删除");
}
@Override
public void afterDelete(List<Long> ids) {
ids.forEach(id -> RedisUtils.delete(TenantCacheConstants.TENANT_DATASOURCE_KEY_PREFIX + id));
}
@Override
public void testConnection(Long id) {
DatasourceDO datasource = super.getById(id);
datasource.getDatabaseType().testConnection(BeanUtil.copyProperties(datasource, DatasourceReq.class));
}
@Override
public void initDb(String databaseName, Long id) {
DatasourceDO datasource = super.getById(id);
DataSource ds = datasource.getDatabaseType()
.buildDataSource(BeanUtil.copyProperties(datasource, DatasourceReq.class));
JdbcTemplate jdbcTemplate = new JdbcTemplate(ds);
// 建库
jdbcTemplate.execute("CREATE DATABASE %s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
.formatted(databaseName));
jdbcTemplate.execute("USE %s;".formatted(databaseName));
// TODO 初始化数据
Resource resource = new ClassPathResource("db/changelog/mysql/tenant_table.sql");
Arrays.stream(resource.readUtf8Str().split(StringConstants.SEMICOLON))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.forEach(jdbcTemplate::execute);
}
/**
* 解密密码
*
* @param encryptPassword 加密的密码
* @param oldDatasource 旧数据源
* @return 解密后的密码
*/
private String decryptPassword(String encryptPassword, DatasourceDO oldDatasource) {
// 修改时,密码为空将不更改密码
if (oldDatasource != null && StrUtil.isBlank(encryptPassword)) {
return oldDatasource.getPassword();
}
// 解密
String decryptPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(encryptPassword));
ValidationUtils.throwIfNull(decryptPassword, "密码解密失败");
ValidationUtils.throwIf(decryptPassword.length() > 128, "密码长度不能超过 128 个字符");
return decryptPassword;
}
/**
* 检查数据源是否存在
*
* @param req 数据源信息
* @param id ID
*/
private void checkRepeat(DatasourceReq req, Long id) {
CheckUtils.throwIf(baseMapper.lambdaQuery()
.eq(DatasourceDO::getHost, req.getHost())
.eq(DatasourceDO::getPort, req.getPort())
.eq(DatasourceDO::getUsername, req.getUsername())
.ne(id != null, DatasourceDO::getId, id)
.exists(), "相同配置数据源已存在");
}
/**
* 是否更新了配置
*
* @param req 数据源请求参数
* @param oldDatasource 旧数据源
* @return 是否更新了配置
*/
private boolean isUpdated(DatasourceReq req, DatasourceDO oldDatasource) {
return !(oldDatasource.getHost().equals(req.getHost()) && oldDatasource.getPort()
.equals(req.getPort()) && oldDatasource.getUsername().equals(req.getUsername()) && oldDatasource
.getPassword()
.equals(req.getPassword()));
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package top.continew.admin.tenant.service; package top.continew.admin.tenant.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.tenant.mapper.PackageMenuMapper; import top.continew.admin.tenant.mapper.PackageMenuMapper;
import top.continew.admin.tenant.model.entity.PackageMenuDO; import top.continew.admin.tenant.model.entity.PackageMenuDO;
import top.continew.admin.tenant.service.PackageMenuService;
import java.util.List; import java.util.List;

View File

@@ -17,7 +17,7 @@
package top.continew.admin.tenant.service.impl; package top.continew.admin.tenant.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import com.baomidou.dynamic.datasource.annotation.DSTransactional; import cn.hutool.extra.spring.SpringUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import top.continew.admin.common.base.service.BaseServiceImpl; import top.continew.admin.common.base.service.BaseServiceImpl;
@@ -53,10 +53,8 @@ public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO
private final PackageMenuService packageMenuService; private final PackageMenuService packageMenuService;
private final MenuService menuService; private final MenuService menuService;
private final TenantMapper tenantMapper; private final TenantMapper tenantMapper;
private final TenantHandler tenantHandler;
@Override @Override
@DSTransactional(rollbackFor = Exception.class)
public Long create(PackageReq req) { public Long create(PackageReq req) {
this.checkNameRepeat(req.getName(), null); this.checkNameRepeat(req.getName(), null);
// 新增信息 // 新增信息
@@ -67,7 +65,6 @@ public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO
} }
@Override @Override
@DSTransactional(rollbackFor = Exception.class)
public void update(PackageReq req, Long id) { public void update(PackageReq req, Long id) {
this.checkNameRepeat(req.getName(), id); this.checkNameRepeat(req.getName(), id);
// 更新信息 // 更新信息
@@ -95,8 +92,8 @@ public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO
deleteMenuIds.removeAll(newMenuIds); deleteMenuIds.removeAll(newMenuIds);
if (CollUtil.isNotEmpty(deleteMenuIds)) { if (CollUtil.isNotEmpty(deleteMenuIds)) {
List<MenuDO> deleteMenus = menuService.listByIds(deleteMenuIds); List<MenuDO> deleteMenus = menuService.listByIds(deleteMenuIds);
tenantIdList.forEach(tenantId -> tenantHandler.execute(tenantId, () -> menuService tenantIdList.forEach(tenantId -> SpringUtil.getBean(TenantHandler.class)
.deleteTenantMenus(deleteMenus))); .execute(tenantId, () -> menuService.deleteTenantMenus(deleteMenus)));
} }
// 如果有新增的菜单则绑定了套餐的租户对应的菜单也会新增 // 如果有新增的菜单则绑定了套餐的租户对应的菜单也会新增
List<Long> addMenuIds = new ArrayList<>(newMenuIds); List<Long> addMenuIds = new ArrayList<>(newMenuIds);
@@ -105,8 +102,8 @@ public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO
List<MenuDO> addMenus = menuService.listByIds(addMenuIds); List<MenuDO> addMenus = menuService.listByIds(addMenuIds);
for (MenuDO addMenu : addMenus) { for (MenuDO addMenu : addMenus) {
MenuDO parentMenu = addMenu.getParentId() != 0 ? menuService.getById(addMenu.getParentId()) : null; MenuDO parentMenu = addMenu.getParentId() != 0 ? menuService.getById(addMenu.getParentId()) : null;
tenantIdList.forEach(tenantId -> tenantHandler.execute(tenantId, () -> menuService tenantIdList.forEach(tenantId -> SpringUtil.getBean(TenantHandler.class)
.addTenantMenu(addMenu, parentMenu))); .execute(tenantId, () -> menuService.addTenantMenu(addMenu, parentMenu)));
} }
} }
} }

View File

@@ -20,13 +20,11 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import com.alicp.jetcache.anno.Cached; import com.alicp.jetcache.anno.Cached;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import me.ahoo.cosid.provider.IdGeneratorProvider; import me.ahoo.cosid.provider.IdGeneratorProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import top.continew.admin.common.base.service.BaseServiceImpl; import top.continew.admin.common.base.service.BaseServiceImpl;
import top.continew.admin.common.config.TenantProperties;
import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.constant.TenantCacheConstants; import top.continew.admin.tenant.constant.TenantCacheConstants;
import top.continew.admin.tenant.constant.TenantConstants; import top.continew.admin.tenant.constant.TenantConstants;
@@ -34,13 +32,11 @@ import top.continew.admin.tenant.handler.TenantDataHandler;
import top.continew.admin.tenant.mapper.TenantMapper; import top.continew.admin.tenant.mapper.TenantMapper;
import top.continew.admin.tenant.model.entity.PackageDO; import top.continew.admin.tenant.model.entity.PackageDO;
import top.continew.admin.tenant.model.entity.TenantDO; import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum;
import top.continew.admin.tenant.model.query.TenantQuery; import top.continew.admin.tenant.model.query.TenantQuery;
import top.continew.admin.tenant.model.req.TenantReq; import top.continew.admin.tenant.model.req.TenantReq;
import top.continew.admin.tenant.model.resp.TenantAvailableResp; import top.continew.admin.tenant.model.resp.TenantAvailableResp;
import top.continew.admin.tenant.model.resp.TenantDetailResp; import top.continew.admin.tenant.model.resp.TenantDetailResp;
import top.continew.admin.tenant.model.resp.TenantResp; import top.continew.admin.tenant.model.resp.TenantResp;
import top.continew.admin.tenant.service.DatasourceService;
import top.continew.admin.tenant.service.PackageMenuService; import top.continew.admin.tenant.service.PackageMenuService;
import top.continew.admin.tenant.service.PackageService; import top.continew.admin.tenant.service.PackageService;
import top.continew.admin.tenant.service.TenantService; import top.continew.admin.tenant.service.TenantService;
@@ -48,6 +44,7 @@ import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.util.validation.CheckUtils; import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.extension.crud.model.entity.BaseIdDO; import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.tenant.TenantHandler; import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -68,11 +65,9 @@ public class TenantServiceImpl extends BaseServiceImpl<TenantMapper, TenantDO, T
private final IdGeneratorProvider idGeneratorProvider; private final IdGeneratorProvider idGeneratorProvider;
private final PackageMenuService packageMenuService; private final PackageMenuService packageMenuService;
private final PackageService packageService; private final PackageService packageService;
private final DatasourceService datasourceService;
private final TenantDataHandler tenantDataHandler; private final TenantDataHandler tenantDataHandler;
@Override @Override
@DSTransactional(rollbackFor = Exception.class)
public Long create(TenantReq req) { public Long create(TenantReq req) {
this.checkNameRepeat(req.getName(), null); this.checkNameRepeat(req.getName(), null);
// 检查租户套餐 // 检查租户套餐
@@ -82,10 +77,6 @@ public class TenantServiceImpl extends BaseServiceImpl<TenantMapper, TenantDO, T
req.setCode(this.generateCode()); req.setCode(this.generateCode());
// 新增信息 // 新增信息
Long id = super.create(req); Long id = super.create(req);
// 初始化数据库
if (TenantIsolationLevelEnum.DATASOURCE.equals(req.getIsolationLevel())) {
datasourceService.initDb(TenantConstants.TENANT_DB_PREFIX + req.getCode(), req.getDatasourceId());
}
// 初始化租户数据 // 初始化租户数据
req.setId(id); req.setId(id);
tenantDataHandler.init(req); tenantDataHandler.init(req);
@@ -123,25 +114,24 @@ public class TenantServiceImpl extends BaseServiceImpl<TenantMapper, TenantDO, T
} }
@Override @Override
public TenantDO checkStatus(Long id) { public void checkStatus(Long id) {
TenantDO tenant = this.getById(id); TenantDO tenant = this.getById(id);
if (!tenantProperties.isEnabled() || id.equals(tenantProperties.getSuperTenantId())) { if (!tenantProperties.isEnabled() || id.equals(tenantProperties.getSuperTenantId())) {
return tenant; return;
} }
CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE.getValue(), tenant.getStatus(), "此租户已被禁用"); CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE, tenant.getStatus(), "此租户已被禁用");
CheckUtils.throwIf(tenant.getExpireTime() != null && tenant.getExpireTime() CheckUtils.throwIf(tenant.getExpireTime() != null && tenant.getExpireTime()
.isBefore(LocalDateTime.now()), "此租户已过期"); .isBefore(LocalDateTime.now()), "此租户已过期");
// 检查套餐 // 检查套餐
PackageDO tenantPackage = packageService.getById(tenant.getPackageId()); PackageDO tenantPackage = packageService.getById(tenant.getPackageId());
CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE.getValue(), tenantPackage.getStatus(), "此租户套餐已被禁用"); CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE, tenantPackage.getStatus(), "此租户套餐已被禁用");
return tenant;
} }
@Override @Override
public List<TenantAvailableResp> getAvailableList() { public List<TenantAvailableResp> getAvailableList() {
List<TenantDO> tenantList = baseMapper.selectList(Wrappers.lambdaQuery(TenantDO.class) List<TenantDO> tenantList = baseMapper.selectList(Wrappers.lambdaQuery(TenantDO.class)
.select(TenantDO::getName, BaseIdDO::getId, TenantDO::getDomain) .select(TenantDO::getName, BaseIdDO::getId, TenantDO::getDomain)
.eq(TenantDO::getStatus, DisEnableStatusEnum.ENABLE.getValue()) .eq(TenantDO::getStatus, DisEnableStatusEnum.ENABLE)
.and(t -> t.isNull(TenantDO::getExpireTime).or().ge(TenantDO::getExpireTime, DateUtil.date()))); .and(t -> t.isNull(TenantDO::getExpireTime).or().ge(TenantDO::getExpireTime, DateUtil.date())));
return BeanUtil.copyToList(tenantList, TenantAvailableResp.class); return BeanUtil.copyToList(tenantList, TenantAvailableResp.class);
} }

View File

@@ -1,80 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.config.tenant;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.tenant.annotation.ConditionalOnEnabledTenant;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
/**
* 租户主数据源切面
*
* @author 小熊
* @author Charles7c
* @since 2025/1/15 16:02
*/
@Aspect
@Component
@ConditionalOnEnabledTenant
@RequiredArgsConstructor
public class TenantDataSourceSwitchAspect {
@Pointcut("""
execution(* top.continew.admin.tenant.mapper..*(..))
|| execution(* top.continew.admin.tenant.service..*(..))
|| execution(* top.continew.admin.system.mapper.ClientMapper.*(..))
|| execution(* top.continew.admin.system.service.ClientService.*(..))
|| execution(* top.continew.admin.system.mapper.DictMapper.*(..))
|| execution(* top.continew.admin.system.service.DictService.*(..))
|| execution(* top.continew.admin.system.mapper.DictItemMapper.*(..))
|| execution(* top.continew.admin.system.service.DictItemService.*(..))
|| execution(* top.continew.admin.system.mapper.OptionMapper.*(..))
|| execution(* top.continew.admin.system.service.OptionService.*(..))
|| execution(* top.continew.admin.system.mapper.StorageMapper.*(..))
|| execution(* top.continew.admin.system.service.StorageService.*(..))
""")
public void masterDataSourceMethods() {
}
/**
* 切换到主数据源
*/
@Before("masterDataSourceMethods()")
public void switchToMasterDataSource() {
if (TenantContextHolder.getIsolationLevel() == TenantIsolationLevel.DATASOURCE) {
DynamicDataSourceContextHolder.push(SysConstants.DEFAULT_TENANT_DATASOURCE);
}
}
/**
* 清空数据源
*/
@After("masterDataSourceMethods()")
public void clearDataSourceContext() {
if (TenantContextHolder.getIsolationLevel() == TenantIsolationLevel.DATASOURCE) {
DynamicDataSourceContextHolder.poll();
}
}
}

View File

@@ -35,10 +35,6 @@ import top.continew.admin.system.enums.OptionCategoryEnum;
import top.continew.admin.system.model.query.*; import top.continew.admin.system.model.query.*;
import top.continew.admin.system.model.resp.file.FileUploadResp; import top.continew.admin.system.model.resp.file.FileUploadResp;
import top.continew.admin.system.service.*; import top.continew.admin.system.service.*;
import top.continew.admin.tenant.model.query.DatasourceQuery;
import top.continew.admin.tenant.model.query.PackageQuery;
import top.continew.admin.tenant.service.DatasourceService;
import top.continew.admin.tenant.service.PackageService;
import top.continew.starter.core.util.validation.ValidationUtils; import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.extension.crud.model.query.SortQuery; import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp; import top.continew.starter.extension.crud.model.resp.LabelValueResp;
@@ -66,8 +62,6 @@ public class CommonController {
private final MenuService menuService; private final MenuService menuService;
private final UserService userService; private final UserService userService;
private final RoleService roleService; private final RoleService roleService;
private final PackageService packageService;
private final DatasourceService datasourceService;
private final DictItemService dictItemService; private final DictItemService dictItemService;
private final OptionService optionService; private final OptionService optionService;
@@ -110,18 +104,6 @@ public class CommonController {
return roleService.listDict(query, sortQuery); return roleService.listDict(query, sortQuery);
} }
@Operation(summary = "查询套餐字典", description = "查询套餐字典列表")
@GetMapping("/dict/package")
public List<LabelValueResp> listPackageDict(PackageQuery query, SortQuery sortQuery) {
return packageService.listDict(query, sortQuery);
}
@Operation(summary = "查询数据源字典", description = "查询数据源字典列表")
@GetMapping("/dict/datasource")
public List<LabelValueResp> listDatasourceDict(DatasourceQuery query, SortQuery sortQuery) {
return datasourceService.listDict(query, sortQuery);
}
@Operation(summary = "查询字典", description = "查询字典列表") @Operation(summary = "查询字典", description = "查询字典列表")
@Parameter(name = "code", description = "字典编码", example = "notice_type", in = ParameterIn.PATH) @Parameter(name = "code", description = "字典编码", example = "notice_type", in = ParameterIn.PATH)
@GetMapping("/dict/{code}") @GetMapping("/dict/{code}")

View File

@@ -11,42 +11,29 @@ server:
--- ### 数据源配置 --- ### 数据源配置
spring.datasource: spring.datasource:
type: com.zaxxer.hikari.HikariDataSource type: com.zaxxer.hikari.HikariDataSource
## 动态数据源配置可配多主多从m1、s1...纯粹多库mysql、oracle...混合配置m1、s1、oracle... # 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
dynamic: url: jdbc:p6spy:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
# 是否启用 P6SpySQL 性能分析组件,该插件有性能损耗,不建议生产环境使用) username: ${DB_USER:root}
p6spy: true password: ${DB_PWD:123456}
# 设置默认的数据源或者数据源组默认master driver-class-name: com.p6spy.engine.spy.P6SpyDriver
primary: master # # PostgreSQL 配置
# 严格匹配数据源true未匹配到指定数据源时抛异常false使用默认数据源默认 false # url: jdbc:p6spy:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:5432}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&stringtype=unspecified
strict: false # username: ${DB_USER:postgres}
datasource: # password: ${DB_PWD:123456}
# 主库配置(可配多个,构成多主) # driver-class-name: com.p6spy.engine.spy.P6SpyDriver
master: # Hikari 连接池配置
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置 hikari:
url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # 最大连接数量(默认 10根据实际环境调整
username: ${DB_USER:root} # 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒
password: ${DB_PWD:123456} maximum-pool-size: 20
driver-class-name: com.mysql.cj.jdbc.Driver # 获取连接超时时间(默认 30000 毫秒30 秒)
type: ${spring.datasource.type} connection-timeout: 30000
# # PostgreSQL 配置 # 空闲连接最大存活时间(默认 600000 毫秒10 分钟)
# url: jdbc:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:5432}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&stringtype=unspecified idle-timeout: 600000
# username: ${DB_USER:postgres} # 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime默认 0禁用
# password: ${DB_PWD:123456} keepaliveTime: 30000
# driver-class-name: org.postgresql.Driver # 连接最大生存时间(默认 1800000 毫秒30 分钟)
# Hikari 连接池配置完整配置请参阅https://github.com/brettwooldridge/HikariCP max-lifetime: 1800000
hikari:
# 最大连接数量(默认 10根据实际环境调整
# 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒
maximum-pool-size: 20
# 获取连接超时时间(默认 30000 毫秒30 秒)
connection-timeout: 30000
# 空闲连接最大存活时间(默认 600000 毫秒10 分钟)
idle-timeout: 600000
# 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime默认 0禁用
keepaliveTime: 30000
# 连接最大生存时间(默认 1800000 毫秒30 分钟)
max-lifetime: 1800000
## Liquibase 配置 ## Liquibase 配置
spring.liquibase: spring.liquibase:
# 是否启用 # 是否启用

View File

@@ -13,42 +13,29 @@ server:
--- ### 数据源配置 --- ### 数据源配置
spring.datasource: spring.datasource:
type: com.zaxxer.hikari.HikariDataSource type: com.zaxxer.hikari.HikariDataSource
## 动态数据源配置可配多主多从m1、s1...纯粹多库mysql、oracle...混合配置m1、s1、oracle... # 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
dynamic: url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
# 是否启用 P6SpySQL 性能分析组件,该插件有性能损耗,不建议生产环境使用) username: ${DB_USER:root}
p6spy: false password: ${DB_PWD:123456}
# 设置默认的数据源或者数据源组默认master driver-class-name: com.mysql.cj.jdbc.Driver
primary: master # # PostgreSQL 配置
# 严格匹配数据源true未匹配到指定数据源时抛异常false使用默认数据源默认 false # url: jdbc:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:5432}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&stringtype=unspecified
strict: false # username: ${DB_USER:postgres}
datasource: # password: ${DB_PWD:123456}
# 主库配置(可配多个,构成多主) # driver-class-name: org.postgresql.Driver
master: # Hikari 连接池配置
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置 hikari:
url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # 最大连接数量(默认 10根据实际环境调整
username: ${DB_USER:root} # 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒
password: ${DB_PWD:123456} maximum-pool-size: 20
driver-class-name: com.mysql.cj.jdbc.Driver # 获取连接超时时间(默认 30000 毫秒30 秒)
type: ${spring.datasource.type} connection-timeout: 30000
# # PostgreSQL 配置 # 空闲连接最大存活时间(默认 600000 毫秒10 分钟)
# url: jdbc:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:5432}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&stringtype=unspecified idle-timeout: 600000
# username: ${DB_USER:postgres} # 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime默认 0禁用
# password: ${DB_PWD:123456} keepaliveTime: 30000
# driver-class-name: org.postgresql.Driver # 连接最大生存时间(默认 1800000 毫秒30 分钟)
# Hikari 连接池配置完整配置请参阅https://github.com/brettwooldridge/HikariCP max-lifetime: 1800000
hikari:
# 最大连接数量(默认 10根据实际环境调整
# 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒
maximum-pool-size: 20
# 获取连接超时时间(默认 30000 毫秒30 秒)
connection-timeout: 30000
# 空闲连接最大存活时间(默认 600000 毫秒10 分钟)
idle-timeout: 600000
# 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime默认 0禁用
keepaliveTime: 30000
# 连接最大生存时间(默认 1800000 毫秒30 分钟)
max-lifetime: 1800000
## Liquibase 配置 ## Liquibase 配置
spring.liquibase: spring.liquibase:
# 是否启用 # 是否启用

View File

@@ -200,6 +200,8 @@ continew-starter.crud:
--- ### 租户配置 --- ### 租户配置
continew-starter.tenant: continew-starter.tenant:
enabled: true enabled: true
# 隔离级别默认LINE行级
isolation-level: LINE
# 超级/默认租户 ID # 超级/默认租户 ID
super-tenant-id: 0 super-tenant-id: 0
# 忽略表(忽略拼接租户条件) # 忽略表(忽略拼接租户条件)
@@ -294,6 +296,7 @@ mybatis-plus:
# 分页插件配置 # 分页插件配置
pagination: pagination:
enabled: true enabled: true
db-type: MYSQL
--- ### CosId 配置 --- ### CosId 配置
cosid: cosid:

View File

@@ -4,26 +4,23 @@
-- comment 初始化租户插件数据表 -- comment 初始化租户插件数据表
-- 初始化表结构 -- 初始化表结构
CREATE TABLE IF NOT EXISTS `tenant` ( CREATE TABLE IF NOT EXISTS `tenant` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(30) NOT NULL COMMENT '名称', `name` varchar(30) NOT NULL COMMENT '名称',
`code` varchar(30) NOT NULL COMMENT '编码', `code` varchar(30) NOT NULL COMMENT '编码',
`domain` varchar(255) DEFAULT NULL COMMENT '域名', `domain` varchar(255) DEFAULT NULL COMMENT '域名',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间', `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`isolation_level` tinyint(1) UNSIGNED NOT NULL COMMENT '隔离级别', `description` varchar(200) DEFAULT NULL COMMENT '描述',
`description` varchar(200) DEFAULT NULL COMMENT '描述', `status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用2禁用',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用2禁用', `admin_user` bigint(20) DEFAULT NULL COMMENT '租户管理员',
`admin_user` bigint(20) DEFAULT NULL COMMENT '租户管理员', `package_id` bigint(20) NOT NULL COMMENT '套餐ID',
`package_id` bigint(20) NOT NULL COMMENT '套餐ID', `create_user` bigint(20) NOT NULL COMMENT '创建人',
`datasource_id` bigint(20) DEFAULT NULL COMMENT '数据源ID', `create_time` datetime NOT NULL COMMENT '创建时间',
`create_user` bigint(20) NOT NULL COMMENT '创建', `update_user` bigint(20) DEFAULT NULL COMMENT '修改',
`create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '修改时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE INDEX `uk_code`(`code`), UNIQUE INDEX `uk_code`(`code`),
INDEX `idx_admin_user`(`admin_user`), INDEX `idx_admin_user`(`admin_user`),
INDEX `idx_package_id`(`package_id`), INDEX `idx_package_id`(`package_id`),
INDEX `idx_datasource_id`(`datasource_id`),
INDEX `idx_create_user`(`create_user`), INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`) INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户表';
@@ -50,24 +47,6 @@ CREATE TABLE IF NOT EXISTS `tenant_package_menu` (
PRIMARY KEY (`package_id`, `menu_id`) PRIMARY KEY (`package_id`, `menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户套餐和菜单关联表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户套餐和菜单关联表';
CREATE TABLE IF NOT EXISTS `tenant_datasource` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(30) NOT NULL COMMENT '名称',
`database_type` tinyint(1) NOT NULL COMMENT '数据库类型1MySQL',
`host` varchar(128) NOT NULL COMMENT '主机',
`port` int NOT NULL COMMENT '端口',
`username` varchar(128) NOT NULL COMMENT '用户名',
`password` varchar(128) NOT NULL COMMENT '密码',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户数据源表';
-- 为已有表增加租户字段 -- 为已有表增加租户字段
ALTER TABLE `sys_menu` ALTER TABLE `sys_menu`
ADD COLUMN `tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID', ADD COLUMN `tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
@@ -160,12 +139,4 @@ VALUES
(3022, '详情', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:get', 2, 1, 1, NOW()), (3022, '详情', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:get', 2, 1, 1, NOW()),
(3023, '新增', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:create', 3, 1, 1, NOW()), (3023, '新增', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:create', 3, 1, 1, NOW()),
(3024, '修改', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:update', 4, 1, 1, NOW()), (3024, '修改', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:update', 4, 1, 1, NOW()),
(3025, '删除', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:delete', 5, 1, 1, NOW()), (3025, '删除', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:delete', 5, 1, 1, NOW());
(3030, '数据源管理', 3000, 2, '/tenant/datasource', 'TenantDatasource', 'tenant/datasource/index', NULL, 'storage', b'0', b'0', b'0', NULL, 3, 1, 1, NOW()),
(3031, '列表', 3030, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:datasource:list', 1, 1, 1, NOW()),
(3032, '详情', 3030, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:datasource:get', 2, 1, 1, NOW()),
(3033, '新增', 3030, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:datasource:create', 3, 1, 1, NOW()),
(3034, '修改', 3030, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:datasource:update', 4, 1, 1, NOW()),
(3035, '删除', 3030, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:datasource:delete', 5, 1, 1, NOW()),
(3036, '测试连接', 3030, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:datasource:testConnection', 6, 1, 1, NOW());

View File

@@ -4,21 +4,19 @@
-- comment 初始化租户插件数据表 -- comment 初始化租户插件数据表
-- 初始化表结构 -- 初始化表结构
CREATE TABLE IF NOT EXISTS "tenant" ( CREATE TABLE IF NOT EXISTS "tenant" (
"id" int8 NOT NULL, "id" int8 NOT NULL,
"name" varchar(30) NOT NULL, "name" varchar(30) NOT NULL,
"code" varchar(30) NOT NULL, "code" varchar(30) NOT NULL,
"domain" varchar(255) DEFAULT NULL, "domain" varchar(255) DEFAULT NULL,
"expire_time" timestamp DEFAULT NULL, "expire_time" timestamp DEFAULT NULL,
"isolation_level" int2 NOT NULL, "description" varchar(200) DEFAULT NULL,
"description" varchar(200) DEFAULT NULL, "status" int2 NOT NULL DEFAULT 1,
"status" int2 NOT NULL DEFAULT 1, "admin_user" int8 DEFAULT NULL,
"admin_user" int8 DEFAULT NULL, "package_id" int8 NOT NULL,
"package_id" int8 NOT NULL, "create_user" int8 NOT NULL,
"datasource_id" int8 DEFAULT NULL, "create_time" timestamp NOT NULL,
"create_user" int8 NOT NULL, "update_user" int8 DEFAULT NULL,
"create_time" timestamp NOT NULL, "update_time" timestamp DEFAULT NULL,
"update_user" int8 DEFAULT NULL,
"update_time" timestamp DEFAULT NULL,
PRIMARY KEY ("id") PRIMARY KEY ("id")
); );
CREATE UNIQUE INDEX "uk_tenant_code" ON "tenant" ("code"); CREATE UNIQUE INDEX "uk_tenant_code" ON "tenant" ("code");
@@ -32,11 +30,9 @@ COMMENT ON COLUMN "tenant"."name" IS '名称';
COMMENT ON COLUMN "tenant"."code" IS '编码'; COMMENT ON COLUMN "tenant"."code" IS '编码';
COMMENT ON COLUMN "tenant"."domain" IS '域名'; COMMENT ON COLUMN "tenant"."domain" IS '域名';
COMMENT ON COLUMN "tenant"."expire_time" IS '过期时间'; COMMENT ON COLUMN "tenant"."expire_time" IS '过期时间';
COMMENT ON COLUMN "tenant"."isolation_level" IS '隔离级别';
COMMENT ON COLUMN "tenant"."description" IS '描述'; COMMENT ON COLUMN "tenant"."description" IS '描述';
COMMENT ON COLUMN "tenant"."status" IS '状态1启用2禁用'; COMMENT ON COLUMN "tenant"."status" IS '状态1启用2禁用';
COMMENT ON COLUMN "tenant"."package_id" IS '套餐ID'; COMMENT ON COLUMN "tenant"."package_id" IS '套餐ID';
COMMENT ON COLUMN "tenant"."datasource_id" IS '数据源ID';
COMMENT ON COLUMN "tenant"."admin_user" IS '租户管理员'; COMMENT ON COLUMN "tenant"."admin_user" IS '租户管理员';
COMMENT ON COLUMN "tenant"."create_user" IS '创建人'; COMMENT ON COLUMN "tenant"."create_user" IS '创建人';
COMMENT ON COLUMN "tenant"."create_time" IS '创建时间'; COMMENT ON COLUMN "tenant"."create_time" IS '创建时间';
@@ -80,37 +76,6 @@ COMMENT ON COLUMN "tenant_package_menu"."package_id" IS '套餐ID';
COMMENT ON COLUMN "tenant_package_menu"."menu_id" IS '菜单ID'; COMMENT ON COLUMN "tenant_package_menu"."menu_id" IS '菜单ID';
COMMENT ON TABLE "tenant_package_menu" IS '租户套餐和菜单关联表'; COMMENT ON TABLE "tenant_package_menu" IS '租户套餐和菜单关联表';
CREATE TABLE IF NOT EXISTS "tenant_datasource" (
"id" int8 NOT NULL,
"name" varchar(30) NOT NULL,
"database_type" int2 NOT NULL,
"host" varchar(128) NOT NULL,
"port" int4 NOT NULL,
"username" varchar(128) NOT NULL,
"password" varchar(128) NOT NULL,
"description" varchar(200) DEFAULT NULL,
"create_user" int8 NOT NULL,
"create_time" timestamp NOT NULL,
"update_user" int8 DEFAULT NULL,
"update_time" timestamp DEFAULT NULL,
PRIMARY KEY ("id")
);
CREATE INDEX "idx_tenant_datasource_create_user" ON "tenant_datasource" ("create_user");
CREATE INDEX "idx_tenant_datasource_update_user" ON "tenant_datasource" ("update_user");
COMMENT ON COLUMN "tenant_datasource"."id" IS 'ID';
COMMENT ON COLUMN "tenant_datasource"."name" IS '名称';
COMMENT ON COLUMN "tenant_datasource"."database_type" IS '数据库类型1MySQL';
COMMENT ON COLUMN "tenant_datasource"."host" IS '主机';
COMMENT ON COLUMN "tenant_datasource"."port" IS '端口';
COMMENT ON COLUMN "tenant_datasource"."username" IS '用户名';
COMMENT ON COLUMN "tenant_datasource"."password" IS '密码';
COMMENT ON COLUMN "tenant_datasource"."description" IS '描述';
COMMENT ON COLUMN "tenant_datasource"."create_user" IS '创建人';
COMMENT ON COLUMN "tenant_datasource"."create_time" IS '创建时间';
COMMENT ON COLUMN "tenant_datasource"."update_user" IS '修改人';
COMMENT ON COLUMN "tenant_datasource"."update_time" IS '修改时间';
COMMENT ON TABLE "tenant_datasource" IS '租户数据源表';
-- 为已有表增加租户字段 -- 为已有表增加租户字段
ALTER TABLE "sys_menu" ADD COLUMN "tenant_id" int8 NOT NULL DEFAULT 0; ALTER TABLE "sys_menu" ADD COLUMN "tenant_id" int8 NOT NULL DEFAULT 0;
COMMENT ON COLUMN "sys_menu"."tenant_id" IS '租户ID'; COMMENT ON COLUMN "sys_menu"."tenant_id" IS '租户ID';
@@ -216,11 +181,4 @@ VALUES
(3022, '详情', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:get', 2, 1, 1, NOW()), (3022, '详情', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:get', 2, 1, 1, NOW()),
(3023, '新增', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:create', 3, 1, 1, NOW()), (3023, '新增', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:create', 3, 1, 1, NOW()),
(3024, '修改', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:update', 4, 1, 1, NOW()), (3024, '修改', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:update', 4, 1, 1, NOW()),
(3025, '删除', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:delete', 5, 1, 1, NOW()), (3025, '删除', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:delete', 5, 1, 1, NOW());
(3030, '数据源管理', 3000, 2, '/tenant/datasource', 'TenantDatasource', 'tenant/datasource/index', NULL, 'storage', false, false, false, NULL, 3, 1, 1, NOW()),
(3031, '列表', 3030, 3, NULL, NULL, NULL, NULL, NULL, false, false, false, 'tenant:datasource:list', 1, 1, 1, NOW()),
(3032, '详情', 3030, 3, NULL, NULL, NULL, NULL, NULL, false, false, false, 'tenant:datasource:get', 2, 1, 1, NOW()),
(3033, '新增', 3030, 3, NULL, NULL, NULL, NULL, NULL, false, false, false, 'tenant:datasource:create', 3, 1, 1, NOW()),
(3034, '修改', 3030, 3, NULL, NULL, NULL, NULL, NULL, false, false, false, 'tenant:datasource:update', 4, 1, 1, NOW()),
(3035, '删除', 3030, 3, NULL, NULL, NULL, NULL, NULL, false, false, false, 'tenant:datasource:delete', 5, 1, 1, NOW());

View File

@@ -29,13 +29,13 @@ import org.springframework.stereotype.Service;
import top.continew.admin.auth.model.query.OnlineUserQuery; import top.continew.admin.auth.model.query.OnlineUserQuery;
import top.continew.admin.auth.model.resp.OnlineUserResp; import top.continew.admin.auth.model.resp.OnlineUserResp;
import top.continew.admin.auth.service.OnlineUserService; import top.continew.admin.auth.service.OnlineUserService;
import top.continew.admin.common.config.TenantProperties;
import top.continew.admin.common.context.UserContext; import top.continew.admin.common.context.UserContext;
import top.continew.admin.common.context.UserContextHolder; import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.common.context.UserExtraContext; import top.continew.admin.common.context.UserExtraContext;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.model.query.PageQuery; import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp; import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
import top.continew.starter.extension.tenant.context.TenantContextHolder; import top.continew.starter.extension.tenant.context.TenantContextHolder;
import java.time.LocalDateTime; import java.time.LocalDateTime;