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>
<artifactId>continew-starter-extension-tenant-mp</artifactId>
</dependency>
<!-- Dynamic Datasource基于 Spring Boot 的快速集成多数据源的启动器) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

@@ -84,11 +84,6 @@ public class SysConstants {
*/
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;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
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.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.context.TenantContext;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
@@ -46,39 +38,21 @@ public class DefaultTenantProvider implements TenantProvider {
private final TenantProperties tenantProperties;
private final TenantService tenantService;
private final DatasourceService datasourceService;
@Override
public TenantContext getByTenantId(String tenantIdAsString, boolean verify) {
TenantContext context = new TenantContext();
// 超级租户默认使用行级隔离
context.setIsolationLevel(TenantIsolationLevel.LINE);
// 超级租户
Long superTenantId = tenantProperties.getSuperTenantId();
if (StrUtil.isBlank(tenantIdAsString) || superTenantId.toString().equals(tenantIdAsString)) {
context.setTenantId(superTenantId);
context.setIsolationLevel(TenantIsolationLevel.LINE);
return context;
}
// 获取租户信息
Long tenantId = Long.valueOf(tenantIdAsString);
TenantDO tenant = tenantService.checkStatus(tenantId);
TenantIsolationLevel isolationLevel = tenant.getIsolationLevel().getLevel();
tenantService.checkStatus(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;
}
}

View File

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

View File

@@ -24,11 +24,6 @@ package top.continew.admin.tenant.constant;
*/
public class TenantConstants {
/**
* 租户数据库前缀
*/
public static final String TENANT_DB_PREFIX = "tenant_";
/**
* 编码生成器 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.RestController;
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.config.TenantProperties;
import top.continew.admin.system.model.query.MenuQuery;
import top.continew.admin.system.service.MenuService;
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})
public class PackageController extends BaseController<PackageService, PackageResp, PackageDetailResp, PackageQuery, PackageReq> {
private final TenantProperties tenantProperties;
private final TenantExtensionProperties tenantExtensionProperties;
private final MenuService menuService;
@Operation(summary = "查询租户套餐菜单", description = "查询租户套餐菜单树列表")
@@ -61,7 +61,7 @@ public class PackageController extends BaseController<PackageService, PackageRes
MenuQuery query = new MenuQuery();
query.setStatus(DisEnableStatusEnum.ENABLE);
// 过滤掉租户不能使用的菜单
query.setExcludeMenuIdList(tenantProperties.getIgnoreMenus());
query.setExcludeMenuIdList(tenantExtensionProperties.getIgnoreMenus());
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.SaIgnore;
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.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
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.system.model.entity.user.UserDO;
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.enums.Api;
import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
/**
* 租户管理 API
@@ -71,7 +70,6 @@ public class TenantController extends BaseController<TenantService, TenantResp,
return commonResp;
}
@DSTransactional(rollbackFor = Exception.class)
@Operation(summary = "修改租户管理员密码", description = "修改租户管理员密码")
@SaCheckPermission("tenant:management:updateAdminUserPwd")
@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 top.continew.admin.common.base.model.entity.BaseDO;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum;
import java.io.Serial;
import java.time.LocalDateTime;
@@ -59,11 +58,6 @@ public class TenantDO extends BaseDO {
*/
private LocalDateTime expireTime;
/**
* 隔离级别
*/
private TenantIsolationLevelEnum isolationLevel;
/**
* 描述
*/
@@ -83,9 +77,4 @@ public class TenantDO extends BaseDO {
* 套餐 ID
*/
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)
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 {
/**
* 新密码
* 新密码(加密)
*/
@Schema(description = "新密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
@NotBlank(message = "新密码不能为空")
private String password;
}

View File

@@ -16,8 +16,6 @@
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 jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
@@ -27,7 +25,6 @@ import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.common.constant.RegexConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum;
import top.continew.starter.extension.crud.validation.CrudValidationGroup;
import java.io.Serial;
@@ -42,7 +39,6 @@ import java.time.LocalDateTime;
* @since 2024/11/26 17:20
*/
@Data
@SpelValid
@Schema(description = "租户创建或修改请求参数")
public class TenantReq implements Serializable {
@@ -52,7 +48,7 @@ public class TenantReq implements Serializable {
/**
* 名称
*/
@Schema(description = "名称", example = "T0001租户")
@Schema(description = "名称", example = "Xxx租户")
@NotBlank(message = "名称不能为空")
@Length(max = 30, message = "名称长度不能超过 {max} 个字符")
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} 个字符")
private String domain;
@@ -71,17 +67,10 @@ public class TenantReq implements Serializable {
@Future(message = "过期时间必须是未来时间")
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} 个字符")
private String description;
@@ -98,13 +87,6 @@ public class TenantReq implements Serializable {
@NotNull(message = "套餐不能为空")
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")
@ExcelProperty(value = "租户管理员", order = 13)
@ExcelProperty(value = "租户管理员", order = 11)
private Long adminUser;
}

View File

@@ -25,8 +25,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.common.base.model.resp.BaseDetailResp;
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.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)
private String name;
/**
* 编码
*/
@Schema(description = "编码", example = "T0001")
@Schema(description = "编码", example = "T0sL6RWv0vFh")
@ExcelProperty(value = "编码", order = 3)
private String code;
/**
* 域名
*/
@Schema(description = "域名", example = "https://t0001.continew.top/")
@Schema(description = "域名", example = "https://T0sL6RWv0vFh.continew.top/")
@ExcelProperty(value = "域名", order = 4)
private String domain;
@@ -76,17 +74,10 @@ public class TenantResp extends BaseDetailResp {
@ExcelProperty(value = "过期时间", order = 5)
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)
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))
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 = "初级套餐")
@ExcelProperty(value = "套餐名称", order = 11)
@ExcelProperty(value = "套餐名称", order = 10)
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
* @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.
*/
package top.continew.admin.tenant.service;
package top.continew.admin.tenant.service.impl;
import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor;
@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.tenant.mapper.PackageMenuMapper;
import top.continew.admin.tenant.model.entity.PackageMenuDO;
import top.continew.admin.tenant.service.PackageMenuService;
import java.util.List;

View File

@@ -17,7 +17,7 @@
package top.continew.admin.tenant.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import cn.hutool.extra.spring.SpringUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
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 MenuService menuService;
private final TenantMapper tenantMapper;
private final TenantHandler tenantHandler;
@Override
@DSTransactional(rollbackFor = Exception.class)
public Long create(PackageReq req) {
this.checkNameRepeat(req.getName(), null);
// 新增信息
@@ -67,7 +65,6 @@ public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO
}
@Override
@DSTransactional(rollbackFor = Exception.class)
public void update(PackageReq req, Long id) {
this.checkNameRepeat(req.getName(), id);
// 更新信息
@@ -95,8 +92,8 @@ public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO
deleteMenuIds.removeAll(newMenuIds);
if (CollUtil.isNotEmpty(deleteMenuIds)) {
List<MenuDO> deleteMenus = menuService.listByIds(deleteMenuIds);
tenantIdList.forEach(tenantId -> tenantHandler.execute(tenantId, () -> menuService
.deleteTenantMenus(deleteMenus)));
tenantIdList.forEach(tenantId -> SpringUtil.getBean(TenantHandler.class)
.execute(tenantId, () -> menuService.deleteTenantMenus(deleteMenus)));
}
// 如果有新增的菜单则绑定了套餐的租户对应的菜单也会新增
List<Long> addMenuIds = new ArrayList<>(newMenuIds);
@@ -105,8 +102,8 @@ public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO
List<MenuDO> addMenus = menuService.listByIds(addMenuIds);
for (MenuDO addMenu : addMenus) {
MenuDO parentMenu = addMenu.getParentId() != 0 ? menuService.getById(addMenu.getParentId()) : null;
tenantIdList.forEach(tenantId -> tenantHandler.execute(tenantId, () -> menuService
.addTenantMenu(addMenu, parentMenu)));
tenantIdList.forEach(tenantId -> SpringUtil.getBean(TenantHandler.class)
.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.extra.spring.SpringUtil;
import com.alicp.jetcache.anno.Cached;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import me.ahoo.cosid.provider.IdGeneratorProvider;
import org.springframework.stereotype.Service;
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.tenant.constant.TenantCacheConstants;
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.model.entity.PackageDO;
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.req.TenantReq;
import top.continew.admin.tenant.model.resp.TenantAvailableResp;
import top.continew.admin.tenant.model.resp.TenantDetailResp;
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.PackageService;
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.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -68,11 +65,9 @@ public class TenantServiceImpl extends BaseServiceImpl<TenantMapper, TenantDO, T
private final IdGeneratorProvider idGeneratorProvider;
private final PackageMenuService packageMenuService;
private final PackageService packageService;
private final DatasourceService datasourceService;
private final TenantDataHandler tenantDataHandler;
@Override
@DSTransactional(rollbackFor = Exception.class)
public Long create(TenantReq req) {
this.checkNameRepeat(req.getName(), null);
// 检查租户套餐
@@ -82,10 +77,6 @@ public class TenantServiceImpl extends BaseServiceImpl<TenantMapper, TenantDO, T
req.setCode(this.generateCode());
// 新增信息
Long id = super.create(req);
// 初始化数据库
if (TenantIsolationLevelEnum.DATASOURCE.equals(req.getIsolationLevel())) {
datasourceService.initDb(TenantConstants.TENANT_DB_PREFIX + req.getCode(), req.getDatasourceId());
}
// 初始化租户数据
req.setId(id);
tenantDataHandler.init(req);
@@ -123,25 +114,24 @@ public class TenantServiceImpl extends BaseServiceImpl<TenantMapper, TenantDO, T
}
@Override
public TenantDO checkStatus(Long id) {
public void checkStatus(Long id) {
TenantDO tenant = this.getById(id);
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()
.isBefore(LocalDateTime.now()), "此租户已过期");
// 检查套餐
PackageDO tenantPackage = packageService.getById(tenant.getPackageId());
CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE.getValue(), tenantPackage.getStatus(), "此租户套餐已被禁用");
return tenant;
CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE, tenantPackage.getStatus(), "此租户套餐已被禁用");
}
@Override
public List<TenantAvailableResp> getAvailableList() {
List<TenantDO> tenantList = baseMapper.selectList(Wrappers.lambdaQuery(TenantDO.class)
.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())));
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.resp.file.FileUploadResp;
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.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
@@ -66,8 +62,6 @@ public class CommonController {
private final MenuService menuService;
private final UserService userService;
private final RoleService roleService;
private final PackageService packageService;
private final DatasourceService datasourceService;
private final DictItemService dictItemService;
private final OptionService optionService;
@@ -110,18 +104,6 @@ public class CommonController {
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 = "查询字典列表")
@Parameter(name = "code", description = "字典编码", example = "notice_type", in = ParameterIn.PATH)
@GetMapping("/dict/{code}")

View File

@@ -11,42 +11,29 @@ server:
--- ### 数据源配置
spring.datasource:
type: com.zaxxer.hikari.HikariDataSource
## 动态数据源配置可配多主多从m1、s1...纯粹多库mysql、oracle...混合配置m1、s1、oracle...
dynamic:
# 是否启用 P6SpySQL 性能分析组件,该插件有性能损耗,不建议生产环境使用)
p6spy: true
# 设置默认的数据源或者数据源组默认master
primary: master
# 严格匹配数据源true未匹配到指定数据源时抛异常false使用默认数据源默认 false
strict: false
datasource:
# 主库配置(可配多个,构成多主)
master:
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
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
username: ${DB_USER:root}
password: ${DB_PWD:123456}
driver-class-name: com.mysql.cj.jdbc.Driver
type: ${spring.datasource.type}
# # PostgreSQL 配置
# 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
# username: ${DB_USER:postgres}
# password: ${DB_PWD:123456}
# driver-class-name: org.postgresql.Driver
# Hikari 连接池配置完整配置请参阅https://github.com/brettwooldridge/HikariCP
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
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
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
username: ${DB_USER:root}
password: ${DB_PWD:123456}
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# # PostgreSQL 配置
# 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
# username: ${DB_USER:postgres}
# password: ${DB_PWD:123456}
# driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# Hikari 连接池配置
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 配置
spring.liquibase:
# 是否启用

View File

@@ -13,42 +13,29 @@ server:
--- ### 数据源配置
spring.datasource:
type: com.zaxxer.hikari.HikariDataSource
## 动态数据源配置可配多主多从m1、s1...纯粹多库mysql、oracle...混合配置m1、s1、oracle...
dynamic:
# 是否启用 P6SpySQL 性能分析组件,该插件有性能损耗,不建议生产环境使用)
p6spy: false
# 设置默认的数据源或者数据源组默认master
primary: master
# 严格匹配数据源true未匹配到指定数据源时抛异常false使用默认数据源默认 false
strict: false
datasource:
# 主库配置(可配多个,构成多主)
master:
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
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
username: ${DB_USER:root}
password: ${DB_PWD:123456}
driver-class-name: com.mysql.cj.jdbc.Driver
type: ${spring.datasource.type}
# # PostgreSQL 配置
# 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
# username: ${DB_USER:postgres}
# password: ${DB_PWD:123456}
# driver-class-name: org.postgresql.Driver
# Hikari 连接池配置完整配置请参阅https://github.com/brettwooldridge/HikariCP
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
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
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
username: ${DB_USER:root}
password: ${DB_PWD:123456}
driver-class-name: com.mysql.cj.jdbc.Driver
# # PostgreSQL 配置
# 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
# username: ${DB_USER:postgres}
# password: ${DB_PWD:123456}
# driver-class-name: org.postgresql.Driver
# Hikari 连接池配置
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 配置
spring.liquibase:
# 是否启用

View File

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

View File

@@ -4,26 +4,23 @@
-- comment 初始化租户插件数据表
-- 初始化表结构
CREATE TABLE IF NOT EXISTS `tenant` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(30) NOT NULL COMMENT '名称',
`code` varchar(30) NOT NULL COMMENT '编码',
`domain` varchar(255) DEFAULT NULL COMMENT '域名',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`isolation_level` tinyint(1) UNSIGNED NOT NULL COMMENT '隔离级别',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用2禁用',
`admin_user` bigint(20) DEFAULT NULL COMMENT '租户管理员',
`package_id` bigint(20) NOT NULL COMMENT '套餐ID',
`datasource_id` bigint(20) DEFAULT NULL COMMENT '数据源ID',
`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 '修改时间',
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(30) NOT NULL COMMENT '名称',
`code` varchar(30) NOT NULL COMMENT '编码',
`domain` varchar(255) DEFAULT NULL COMMENT '域名',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用2禁用',
`admin_user` bigint(20) DEFAULT NULL COMMENT '租户管理员',
`package_id` bigint(20) NOT NULL COMMENT '套餐ID',
`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`),
UNIQUE INDEX `uk_code`(`code`),
INDEX `idx_admin_user`(`admin_user`),
INDEX `idx_package_id`(`package_id`),
INDEX `idx_datasource_id`(`datasource_id`),
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户表';
@@ -50,24 +47,6 @@ CREATE TABLE IF NOT EXISTS `tenant_package_menu` (
PRIMARY KEY (`package_id`, `menu_id`)
) 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`
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()),
(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()),
(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());
(3025, '删除', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:delete', 5, 1, 1, NOW());

View File

@@ -4,21 +4,19 @@
-- comment 初始化租户插件数据表
-- 初始化表结构
CREATE TABLE IF NOT EXISTS "tenant" (
"id" int8 NOT NULL,
"name" varchar(30) NOT NULL,
"code" varchar(30) NOT NULL,
"domain" varchar(255) DEFAULT NULL,
"expire_time" timestamp DEFAULT NULL,
"isolation_level" int2 NOT NULL,
"description" varchar(200) DEFAULT NULL,
"status" int2 NOT NULL DEFAULT 1,
"admin_user" int8 DEFAULT NULL,
"package_id" int8 NOT NULL,
"datasource_id" int8 DEFAULT NULL,
"create_user" int8 NOT NULL,
"create_time" timestamp NOT NULL,
"update_user" int8 DEFAULT NULL,
"update_time" timestamp DEFAULT NULL,
"id" int8 NOT NULL,
"name" varchar(30) NOT NULL,
"code" varchar(30) NOT NULL,
"domain" varchar(255) DEFAULT NULL,
"expire_time" timestamp DEFAULT NULL,
"description" varchar(200) DEFAULT NULL,
"status" int2 NOT NULL DEFAULT 1,
"admin_user" int8 DEFAULT NULL,
"package_id" int8 NOT 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 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"."domain" 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"."status" IS '状态1启用2禁用';
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"."create_user" 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 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;
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()),
(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()),
(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());
(3025, '删除', 3020, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package: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.resp.OnlineUserResp;
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.UserContextHolder;
import top.continew.admin.common.context.UserExtraContext;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.model.query.PageQuery;
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 java.time.LocalDateTime;