refactor(tenant): 优化租户相关代码

This commit is contained in:
2025-07-15 20:09:54 +08:00
parent ed6dd65a51
commit af1079da6d
111 changed files with 2741 additions and 2319 deletions

View File

@@ -0,0 +1,35 @@
/*
* 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

@@ -0,0 +1,84 @@
/*
* 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.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.config.TenantProvider;
import top.continew.starter.extension.tenant.context.TenantContext;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
/**
* 默认租户提供者
*
* @author 小熊
* @author Charles7c
* @since 2024/12/12 15:35
*/
@Service
@RequiredArgsConstructor
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();
// 超级租户默认使用行级隔离
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();
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

@@ -0,0 +1,39 @@
/*
* 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.config;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 租户配置
*
* @author Charles7c
* @since 2025/7/12 13:30
*/
@Configuration
public class TenantConfiguration {
/**
* API 文档分组配置
*/
@Bean
public GroupedOpenApi tenantApi() {
return GroupedOpenApi.builder().group("tenant").displayName("租户").pathsToMatch("/tenant/**").build();
}
}

View File

@@ -14,25 +14,33 @@
* limitations under the License.
*/
package top.continew.admin.tenant.model.enums;
package top.continew.admin.tenant.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
import top.continew.starter.core.exception.BusinessException;
import top.continew.admin.common.constant.CacheConstants;
@Getter
@AllArgsConstructor
public enum TenantConnectTypeEnum {
/**
* 租户缓存相关常量
*
* @author Charles7c
* @since 2025/7/14 20:35
*/
public class TenantCacheConstants {
MYSQL;
/**
* 分隔符
*/
public static final String DELIMITER = CacheConstants.DELIMITER;
public static TenantConnectTypeEnum getByOrdinal(Integer ordinal) {
for (TenantConnectTypeEnum item : TenantConnectTypeEnum.values()) {
if (item.ordinal() == ordinal) {
return item;
}
}
throw new BusinessException("未知的连接类型");
/**
* 租户前缀
*/
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

@@ -0,0 +1,39 @@
/*
* 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.constant;
/**
* 租户相关常量
*
* @author Charles7c
* @since 2025/7/14 20:27
*/
public class TenantConstants {
/**
* 租户数据库前缀
*/
public static final String TENANT_DB_PREFIX = "tenant_";
/**
* 编码生成器 KEY
*/
public static final String CODE_GENERATOR_KEY = "tenant-code";
private TenantConstants() {
}
}

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 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

@@ -0,0 +1,67 @@
/*
* 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 cn.hutool.core.lang.tree.Tree;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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.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;
import top.continew.admin.tenant.model.req.PackageReq;
import top.continew.admin.tenant.model.resp.PackageDetailResp;
import top.continew.admin.tenant.model.resp.PackageResp;
import top.continew.admin.tenant.service.PackageService;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import java.util.List;
/**
* 套餐管理 API
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 11:25
*/
@Tag(name = "套餐管理 API")
@RestController
@RequiredArgsConstructor
@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 MenuService menuService;
@Operation(summary = "查询租户套餐菜单", description = "查询租户套餐菜单树列表")
@SaCheckPermission("tenant:package:list")
@GetMapping("/menu/tree")
public List<Tree<Long>> listMenuTree() {
MenuQuery query = new MenuQuery();
query.setStatus(DisEnableStatusEnum.ENABLE);
// 过滤掉租户不能使用的菜单
query.setExcludeMenuIdList(tenantProperties.getIgnoreMenus());
return menuService.tree(query, null, true);
}
}

View File

@@ -18,71 +18,52 @@ package top.continew.admin.tenant.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.validation.annotation.Validated;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import top.continew.admin.common.base.controller.BaseController;
import top.continew.admin.common.config.properties.TenantProperties;
import top.continew.admin.common.config.TenantProperties;
import top.continew.admin.common.util.SecureUtils;
import top.continew.admin.system.model.entity.MenuDO;
import top.continew.admin.system.model.entity.user.UserDO;
import top.continew.admin.system.model.req.user.UserPasswordResetReq;
import top.continew.admin.system.service.*;
import top.continew.admin.system.service.UserService;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.query.TenantQuery;
import top.continew.admin.tenant.model.req.TenantLoginUserInfoReq;
import top.continew.admin.tenant.model.req.TenantAdminUserPwdUpdateReq;
import top.continew.admin.tenant.model.req.TenantReq;
import top.continew.admin.tenant.model.resp.*;
import top.continew.admin.tenant.service.TenantDbConnectService;
import top.continew.admin.tenant.service.TenantPackageService;
import top.continew.admin.tenant.model.resp.TenantCommonResp;
import top.continew.admin.tenant.model.resp.TenantDetailResp;
import top.continew.admin.tenant.model.resp.TenantResp;
import top.continew.admin.tenant.service.TenantService;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.validation.CheckUtils;
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.crud.model.entity.BaseIdDO;
import top.continew.admin.common.base.model.resp.BaseResp;
import top.continew.starter.extension.crud.model.req.IdsReq;
import top.continew.starter.extension.crud.model.resp.IdResp;
import top.continew.starter.extension.tenant.TenantHandler;
import java.util.List;
/**
* 租户管理 API
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
@Tag(name = "租户管理 API")
@RestController
@AllArgsConstructor
@CrudRequestMapping(value = "/tenant/user", api = {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE})
@RequiredArgsConstructor
@CrudRequestMapping(value = "/tenant/management", api = {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE})
public class TenantController extends BaseController<TenantService, TenantResp, TenantDetailResp, TenantQuery, TenantReq> {
private final TenantProperties tenantProperties;
private final DeptService deptService;
private final MenuService menuService;
private final TenantPackageService packageService;
private final RoleService roleService;
private final UserService userService;
private final TenantSysDataService tenantSysDataService;
private final RoleMenuService roleMenuService;
private final TenantDbConnectService dbConnectService;
@GetMapping("/common")
@SaIgnore
@Operation(summary = "多租户通用信息查询", description = "多租户通用信息查询")
@GetMapping("/common")
@Operation(summary = "租户通用信息查询", description = "租户通用信息查询")
public TenantCommonResp common() {
TenantCommonResp commonResp = new TenantCommonResp();
commonResp.setIsEnabled(tenantProperties.isEnabled());
@@ -90,122 +71,20 @@ public class TenantController extends BaseController<TenantService, TenantResp,
return commonResp;
}
@Override
@DSTransactional
public IdResp<Long> create(TenantReq req) {
//套餐菜单
TenantPackageDetailResp detailResp = packageService.get(req.getPackageId());
CheckUtils.throwIf(detailResp.getMenuIds().isEmpty(), "该套餐无可用菜单");
List<MenuDO> menuRespList = menuService.listByIds(detailResp.getMenuIds());
//租户添加
IdResp<Long> baseIdResp = super.create(req);
//在租户中执行数据插入
SpringUtil.getBean(TenantHandler.class).execute(baseIdResp.getId(), () -> {
//租户部门初始化
Long deptId = deptService.initTenantDept(req.getName());
//租户菜单初始化
menuService.menuInit(menuRespList, 0L, 0L);
//租户角色初始化
Long roleId = roleService.initTenantRole();
//角色绑定菜单
roleMenuService.add(menuService.listAll(baseIdResp.getId()).stream().map(BaseResp::getId).toList(), roleId);
//管理用户初始化
Long userId = userService.initTenantUser(req.getUsername(), req.getPassword(), deptId);
//用户绑定角色
roleService.assignToUsers(roleId, ListUtil.of(userId));
//租户绑定用户
baseService.bindUser(baseIdResp.getId(), userId);
});
return baseIdResp;
}
@Override
public void delete(Long id) {
SpringUtil.getBean(TenantHandler.class).execute(id, () -> {
//系统数据清除
tenantSysDataService.clear();
});
super.delete(id);
}
@Override
public void batchDelete(@Valid IdsReq ids) {
for (Long id : ids.getIds()) {
//在租户中执行数据清除
SpringUtil.getBean(TenantHandler.class).execute(id, () -> {
//系统数据清除
tenantSysDataService.clear();
});
}
super.batchDelete(ids);
}
/**
* 获取租户管理账号用户名
*/
@GetMapping("/loginUser/{tenantId}")
@Operation(summary = "获取租户管理账号信息", description = "获取租户管理账号信息")
@SaCheckPermission("tenant:user:editLoginUserInfo")
public String loginUserInfo(@PathVariable Long tenantId) {
TenantDO tenantDO = baseService.getTenantById(tenantId);
CheckUtils.throwIfNull(tenantDO, "租户不存在");
StringBuilder username = new StringBuilder();
SpringUtil.getBean(TenantHandler.class).execute(tenantDO.getId(), () -> {
UserDO userDO = userService.getById(tenantDO.getUserId());
CheckUtils.throwIfNull(userDO, "租户管理用户不存在");
username.append(userDO.getUsername());
});
return username.toString();
}
/**
* 租户管理账号信息更新
*/
@PutMapping("/loginUser")
@Operation(summary = "租户管理账号信息更新", description = "租户管理账号信息更新")
@SaCheckPermission("tenant:user:editLoginUserInfo")
@DSTransactional
public void editLoginUserInfo(@Validated @RequestBody TenantLoginUserInfoReq req) {
TenantDO tenantDO = baseService.getTenantById(req.getTenantId());
CheckUtils.throwIfNull(tenantDO, "租户不存在");
SpringUtil.getBean(TenantHandler.class).execute(tenantDO.getId(), () -> {
UserDO userDO = userService.getById(tenantDO.getUserId());
CheckUtils.throwIfNull(userDO, "用户不存在");
//修改用户名
if (!req.getUsername().equals(userDO.getUsername())) {
userService.update(Wrappers.lambdaUpdate(UserDO.class)
.set(UserDO::getUsername, req.getUsername())
.eq(BaseIdDO::getId, userDO.getId()));
}
//修改密码
if (StrUtil.isNotEmpty(req.getPassword())) {
String password = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword()));
ValidationUtils.throwIfNull(password, "新密码解密失败");
UserPasswordResetReq passwordResetReq = new UserPasswordResetReq();
passwordResetReq.setNewPassword(password);
userService.resetPassword(passwordResetReq, userDO.getId());
}
@DSTransactional(rollbackFor = Exception.class)
@Operation(summary = "修改租户管理员密码", description = "修改租户管理员密码")
@SaCheckPermission("tenant:management:updateAdminUserPwd")
@PutMapping("/{id}/admin/pwd")
public void updateAdminUserPwd(@Valid @RequestBody TenantAdminUserPwdUpdateReq req, @PathVariable Long id) {
TenantDO tenant = baseService.getById(id);
String encryptPassword = req.getPassword();
SpringUtil.getBean(TenantHandler.class).execute(tenant.getId(), () -> {
UserDO user = userService.getById(tenant.getAdminUser());
String password = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(encryptPassword));
ValidationUtils.throwIfNull(password, "新密码解密失败");
UserPasswordResetReq passwordResetReq = new UserPasswordResetReq();
passwordResetReq.setNewPassword(password);
userService.resetPassword(passwordResetReq, user.getId());
});
}
/**
* 查询所有租户套餐
*/
@GetMapping("/all/package")
@Operation(summary = "查询所有租户套餐", description = "查询所有租户套餐")
@SaCheckPermission(value = {"tenant:user:add", "tenant:user:update"}, mode = SaMode.OR)
public List<TenantPackageResp> packageList() {
return packageService.list(null, null);
}
/**
* 查询所有数据库连接
*/
@GetMapping("/all/dbConnect")
@Operation(summary = "获取租户数据连接列表", description = "获取租户数据连接列表")
@SaCheckPermission(value = {"tenant:user:add", "tenant:user:update"}, mode = SaMode.OR)
public List<TenantDbConnectResp> dbConnectList() {
return dbConnectService.list(null, null);
}
}

View File

@@ -1,39 +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 io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.RestController;
import top.continew.admin.common.base.controller.BaseController;
import top.continew.admin.tenant.model.query.TenantDbConnectQuery;
import top.continew.admin.tenant.model.req.TenantDbConnectReq;
import top.continew.admin.tenant.model.resp.TenantDbConnectDetailResp;
import top.continew.admin.tenant.model.resp.TenantDbConnectResp;
import top.continew.admin.tenant.service.TenantDbConnectService;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
/**
* 租户数据连接管理 API
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Tag(name = "租户数据连接管理 API")
@RestController
@CrudRequestMapping(value = "/tenant/dbConnect", api = {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE})
public class TenantDbConnectController extends BaseController<TenantDbConnectService, TenantDbConnectResp, TenantDbConnectDetailResp, TenantDbConnectQuery, TenantDbConnectReq> {}

View File

@@ -1,116 +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 cn.hutool.core.lang.tree.Tree;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
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.properties.TenantProperties;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.system.model.entity.MenuDO;
import top.continew.admin.system.model.query.MenuQuery;
import top.continew.admin.system.service.MenuService;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.query.TenantPackageQuery;
import top.continew.admin.tenant.model.req.TenantPackageReq;
import top.continew.admin.tenant.model.resp.TenantPackageDetailResp;
import top.continew.admin.tenant.model.resp.TenantPackageResp;
import top.continew.admin.tenant.service.TenantPackageService;
import top.continew.admin.tenant.service.TenantService;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import top.continew.starter.extension.tenant.TenantHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 租户套餐管理 API
*
* @author 小熊
* @since 2024/11/26 11:25
*/
@Tag(name = "租户套餐管理 API")
@RestController
@AllArgsConstructor
@CrudRequestMapping(value = "/tenant/package", api = {Api.LIST, Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE})
public class TenantPackageController extends BaseController<TenantPackageService, TenantPackageResp, TenantPackageDetailResp, TenantPackageQuery, TenantPackageReq> {
private final MenuService menuService;
private final TenantProperties tenantProperties;
private final TenantService tenantService;
@GetMapping("/menuTree")
@SaCheckPermission("tenant:package:get")
@Operation(summary = "获取租户套餐菜单", description = "获取租户套餐菜单")
public List<Tree<Long>> menuTree() {
MenuQuery query = new MenuQuery();
//必须是启用状态的菜单
query.setStatus(DisEnableStatusEnum.ENABLE);
//过滤掉租户不能使用的菜单
query.setExcludeMenuIdList(tenantProperties.getIgnoreMenus());
return menuService.tree(query, null, true);
}
@Override
@DSTransactional
public void update(TenantPackageReq req, Long id) {
//查询套餐对应的租户
List<TenantDO> tenantDOList = tenantService.list(Wrappers.lambdaQuery(TenantDO.class)
.eq(TenantDO::getPackageId, id));
if (!tenantDOList.isEmpty()) {
TenantPackageDetailResp detail = baseService.get(id);
List<Long> oldMenuIds = detail.getMenuIds();
List<Long> newMenuIds = Arrays.stream(req.getMenuIds()).toList();
//删除的菜单
List<Long> deleteMenuIds = new ArrayList<>(oldMenuIds);
deleteMenuIds.removeAll(newMenuIds);
//如果有删除的菜单则绑定了套餐的租户对应的菜单也会删除
if (!deleteMenuIds.isEmpty()) {
List<MenuDO> deleteMenus = menuService.listByIds(deleteMenuIds);
tenantDOList.forEach(tenantDO -> SpringUtil.getBean(TenantHandler.class)
.execute(tenantDO.getId(), () -> menuService.deleteTenantMenus(deleteMenus)));
}
//新增的菜单
List<Long> addMenuIds = new ArrayList<>(newMenuIds);
addMenuIds.removeAll(oldMenuIds);
//如果有新增的菜单则绑定了套餐的租户对应的菜单也会新增
if (!addMenuIds.isEmpty()) {
List<MenuDO> addMenus = menuService.listByIds(addMenuIds);
for (MenuDO addMenu : addMenus) {
MenuDO pMenu = addMenu.getParentId() != 0 ? menuService.getById(addMenu.getParentId()) : null;
tenantDOList.forEach(tenantDO -> SpringUtil.getBean(TenantHandler.class)
.execute(tenantDO.getId(), () -> menuService.addTenantMenu(addMenu, pMenu)));
}
RedisUtils.deleteByPattern(CacheConstants.ROLE_MENU_KEY_PREFIX + StringConstants.ASTERISK);
}
}
super.update(req, id);
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.handler;
import top.continew.admin.tenant.model.req.TenantReq;
/**
* 租户数据处理器
*
* @author 小熊
* @author Charles7c
* @since 2024/12/2 20:08
*/
public interface TenantDataHandler {
/**
* 初始化数据
*
* @param tenant 租户信息
*/
void init(TenantReq tenant);
/**
* 清除数据
*/
void clear();
}

View File

@@ -0,0 +1,245 @@
/*
* 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.handler;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.common.constant.RegexConstants;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.enums.DataScopeEnum;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.common.enums.GenderEnum;
import top.continew.admin.common.util.SecureUtils;
import top.continew.admin.system.mapper.*;
import top.continew.admin.system.mapper.user.UserMapper;
import top.continew.admin.system.mapper.user.UserPasswordHistoryMapper;
import top.continew.admin.system.mapper.user.UserSocialMapper;
import top.continew.admin.system.model.entity.DeptDO;
import top.continew.admin.system.model.entity.MenuDO;
import top.continew.admin.system.model.entity.RoleDO;
import top.continew.admin.system.model.entity.user.UserDO;
import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.RoleMenuService;
import top.continew.admin.system.service.RoleService;
import top.continew.admin.tenant.constant.TenantCacheConstants;
import top.continew.admin.tenant.mapper.TenantMapper;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.req.TenantReq;
import top.continew.admin.tenant.service.PackageMenuService;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.tenant.TenantHandler;
import java.time.LocalDateTime;
import java.util.List;
/**
* 租户数据处理器
*
* @author 小熊
* @author Charles7c
* @since 2024/12/2 20:12
*/
@Service
@RequiredArgsConstructor
public class TenantDataHandlerForSystem implements TenantDataHandler {
private final PackageMenuService packageMenuService;
private final DeptMapper deptMapper;
private final MenuMapper menuMapper;
private final RoleMapper roleMapper;
private final RoleMenuService roleMenuService;
private final RoleMenuMapper roleMenuMapper;
private final RoleService roleService;
private final TenantMapper tenantMapper;
private final FileService fileService;
private final LogMapper logMapper;
private final MessageMapper messageMapper;
private final MessageMapper messageUserMapper;
private final NoticeMapper noticeMapper;
private final RoleDeptMapper roleDeptMapper;
private final UserMapper userMapper;
private final UserPasswordHistoryMapper userPasswordHistoryMapper;
private final UserRoleMapper userRoleMapper;
private final UserSocialMapper userSocialMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void init(TenantReq tenant) {
Long tenantId = tenant.getId();
SpringUtil.getBean(TenantHandler.class).execute(tenantId, () -> {
// 初始化部门
Long deptId = this.initDeptData(tenant);
// 初始化菜单
List<Long> menuIds = packageMenuService.listMenuIdsByPackageId(tenant.getPackageId());
List<MenuDO> menuList = menuMapper.lambdaQuery().in(MenuDO::getId, menuIds).list();
this.initMenuData(menuList, 0L, 0L);
// 初始化角色
Long roleId = this.initRoleData(tenant);
// 角色绑定菜单
roleMenuService.add(menuIds, roleId);
// 初始化管理用户
Long userId = this.initUserData(tenant, deptId);
// 用户绑定角色
roleService.assignToUsers(roleId, ListUtil.of(userId));
// 租户绑定用户
this.bindTenantUser(tenantId, userId);
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void clear() {
// 退出所有用户
List<UserDO> userList = userMapper.selectList(null);
for (UserDO user : userList) {
StpUtil.logout(user.getId());
}
Wrapper dw = Wrappers.query().eq("1", 1);
// 部门清除
deptMapper.delete(dw);
// 文件清除
List<Long> fileIds = fileService.list().stream().map(BaseIdDO::getId).toList();
if (!fileIds.isEmpty()) {
fileService.delete(fileIds);
}
// 日志清除
logMapper.delete(dw);
// 菜单清除
menuMapper.delete(dw);
// 消息清除
messageMapper.delete(dw);
messageUserMapper.delete(dw);
// 通知清除
noticeMapper.delete(dw);
// 角色相关数据清除
roleMapper.delete(dw);
roleDeptMapper.delete(dw);
roleMenuMapper.delete(dw);
// 用户数据清除
userMapper.delete(dw);
userPasswordHistoryMapper.delete(dw);
userRoleMapper.delete(dw);
userSocialMapper.delete(dw);
}
/**
* 初始化部门数据
*
* @param tenant 租户信息
* @return 部门 ID
*/
private Long initDeptData(TenantReq tenant) {
DeptDO dept = new DeptDO();
dept.setName(tenant.getName());
dept.setParentId(SysConstants.SUPER_PARENT_ID);
dept.setAncestors("0");
dept.setDescription("系统初始部门");
dept.setSort(1);
dept.setStatus(DisEnableStatusEnum.ENABLE);
deptMapper.insert(dept);
return dept.getId();
}
/**
* 递归初始化菜单数据
*
* @param menuList 菜单列表
* @param oldParentId 旧父级 ID
* @param newParentId 新父级 ID
*/
private void initMenuData(List<MenuDO> menuList, Long oldParentId, Long newParentId) {
List<MenuDO> children = menuList.stream().filter(menuDO -> menuDO.getParentId().equals(oldParentId)).toList();
for (MenuDO menu : children) {
Long oldId = menu.getId();
menu.setId(null);
menu.setParentId(newParentId);
menuMapper.insert(menu);
initMenuData(menuList, oldId, menu.getId());
}
}
/**
* 初始化角色数据
*
* @param tenant 租户信息
* @return 角色 ID
*/
private Long initRoleData(TenantReq tenant) {
RoleDO role = new RoleDO();
role.setName("系统管理员");
role.setCode(SysConstants.TENANT_ADMIN_ROLE_CODE);
role.setDataScope(DataScopeEnum.ALL);
role.setDescription("系统初始角色");
role.setSort(1);
role.setIsSystem(true);
role.setMenuCheckStrictly(true);
role.setDeptCheckStrictly(true);
roleMapper.insert(role);
return role.getId();
}
/**
* 初始化用户数据
*
* @param tenant 租户信息
* @param deptId 部门 ID
* @return 用户 ID
*/
private Long initUserData(TenantReq tenant, Long deptId) {
// 解密密码
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(tenant.getPassword()));
ValidationUtils.throwIfNull(rawPassword, "密码解密失败");
ValidationUtils.throwIf(!ReUtil
.isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
// 初始化用户
UserDO user = new UserDO();
user.setUsername(tenant.getUsername());
user.setNickname("系统管理员");
user.setPassword(rawPassword);
user.setGender(GenderEnum.UNKNOWN);
user.setDescription("系统初始用户");
user.setStatus(DisEnableStatusEnum.ENABLE);
user.setIsSystem(true);
user.setPwdResetTime(LocalDateTime.now());
user.setDeptId(deptId);
userMapper.insert(user);
return user.getId();
}
/**
* 绑定租户管理员用户
*
* @param tenantId 租户 ID
* @param userId 用户 ID
*/
public void bindTenantUser(Long tenantId, Long userId) {
tenantMapper.lambdaUpdate().set(TenantDO::getAdminUser, userId).eq(BaseIdDO::getId, tenantId).update();
// 更新租户缓存
TenantDO entity = tenantMapper.selectById(tenantId);
RedisUtils.set(TenantCacheConstants.TENANT_KEY_PREFIX + tenantId, entity);
}
}

View File

@@ -16,13 +16,15 @@
package top.continew.admin.tenant.mapper;
import top.continew.admin.tenant.model.entity.TenantDbConnectDO;
import org.apache.ibatis.annotations.Mapper;
import top.continew.admin.tenant.model.entity.DatasourceDO;
import top.continew.starter.data.mapper.BaseMapper;
/**
* 租户数据连接 Mapper
* 数据源 Mapper
*
* @author 小熊
* @since 2024/12/12 19:13
*/
public interface TenantDbConnectMapper extends BaseMapper<TenantDbConnectDO> {}
@Mapper
public interface DatasourceMapper extends BaseMapper<DatasourceDO> {}

View File

@@ -16,14 +16,16 @@
package top.continew.admin.tenant.mapper;
import org.apache.ibatis.annotations.Mapper;
import top.continew.starter.data.mapper.BaseMapper;
import top.continew.admin.tenant.model.entity.TenantPackageDO;
import top.continew.admin.tenant.model.entity.PackageDO;
/**
* 租户套餐 Mapper
* 套餐 Mapper
*
* @author 小熊
* @since 2024/11/26 11:25
*/
public interface TenantPackageMapper extends BaseMapper<TenantPackageDO> {
@Mapper
public interface PackageMapper extends BaseMapper<PackageDO> {
}

View File

@@ -0,0 +1,31 @@
/*
* 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.PackageMenuDO;
import top.continew.starter.data.mapper.BaseMapper;
/**
* 套餐和菜单关联 Mapper
*
* @author Charles7c
* @since 2025/7/13 20:24
*/
@Mapper
public interface PackageMenuMapper extends BaseMapper<PackageMenuDO> {
}

View File

@@ -16,16 +16,8 @@
package top.continew.admin.tenant.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.resp.TenantResp;
import top.continew.starter.data.mapper.BaseMapper;
/**
@@ -34,11 +26,6 @@ import top.continew.starter.data.mapper.BaseMapper;
* @author 小熊
* @since 2024/11/26 17:20
*/
@DS(SysConstants.DEFAULT_DATASOURCE)
@Mapper
public interface TenantMapper extends BaseMapper<TenantDO> {
@Select("SELECT sys_tenant.*,sys_tenant_package.`name` as package_name FROM sys_tenant\n" + "LEFT JOIN sys_tenant_package ON sys_tenant.package_id = sys_tenant_package.id\n" + "${ew.getCustomSqlSegment}")
IPage<TenantResp> listTenant(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper);
}

View File

@@ -19,49 +19,60 @@ 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
@TableName("sys_tenant_db_connect")
public class TenantDbConnectDO extends BaseDO {
@DictModel
@TableName("tenant_datasource")
public class DatasourceDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
* 名称
*/
private String connectName;
private String name;
/**
* 连接类型
* 数据库类型
*/
private Integer type;
private DatasourceDatabaseTypeEnum databaseType;
/**
* 连接主机地址
* 主机
*/
private String host;
/**
* 连接端口
* 端口
*/
private Integer port;
/**
* 连接用户名
* 用户名
*/
private String username;
/**
* 连接密码
* 密码
*/
@FieldEncrypt
private String password;
/**
* 描述
*/
private String description;
}

View File

@@ -16,35 +16,38 @@
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.common.enums.DisEnableStatusEnum;
import top.continew.starter.extension.crud.annotation.DictModel;
import java.io.Serial;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import top.continew.admin.common.base.model.entity.BaseDO;
/**
* 租户套餐实体
* 套餐实体
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 11:25
*/
@Data
@TableName("sys_tenant_package")
public class TenantPackageDO extends BaseDO {
@DictModel
@TableName("tenant_package")
public class PackageDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
* 名称
*/
private String name;
/**
* 关联的菜单ids
* 排序
*/
private String menuIds;
private Integer sort;
/**
* 菜单选择是否父子节点关联
@@ -52,7 +55,12 @@ public class TenantPackageDO extends BaseDO {
private Boolean menuCheckStrictly;
/**
* 状态1启用2禁用
* 描述
*/
private Integer status;
private String description;
/**
* 状态
*/
private DisEnableStatusEnum status;
}

View File

@@ -0,0 +1,54 @@
/*
* 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 lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 套餐和菜单关联实体
*
* @author Charles7c
* @since 2025/7/11 22:01
*/
@Data
@NoArgsConstructor
@TableName("tenant_package_menu")
public class PackageMenuDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐 ID
*/
private Long packageId;
/**
* 菜单 ID
*/
private Long menuId;
public PackageMenuDO(Long packageId, Long menuId) {
this.packageId = packageId;
this.menuId = menuId;
}
}

View File

@@ -19,6 +19,8 @@ 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.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum;
import java.io.Serial;
import java.time.LocalDateTime;
@@ -27,57 +29,63 @@ import java.time.LocalDateTime;
* 租户实体
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
@Data
@TableName("sys_tenant")
@TableName("tenant")
public class TenantDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
* 名称
*/
private String name;
/**
* 绑定的域名
* 编码
*/
private String code;
/**
* 域名
*/
private String domain;
/**
* 租户套餐编号
*/
private Long packageId;
/**
* 状态1启用2禁用
*/
private Integer status;
/**
* 租户过期时间
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 用户ID
*/
private Long userId;
/**
* 租户编号
*/
private String tenantSn;
/**
* 隔离级别
*/
private Integer isolationLevel;
private TenantIsolationLevelEnum isolationLevel;
/**
* 数据连接ID
* 描述
*/
private Long dbConnectId;
private String description;
/**
* 状态
*/
private DisEnableStatusEnum status;
/**
* 租户管理员
*/
private Long adminUser;
/**
* 套餐 ID
*/
private Long packageId;
/**
* 数据源 ID
*/
private Long datasourceId;
}

View File

@@ -0,0 +1,133 @@
/*
* 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

@@ -0,0 +1,49 @@
/*
* 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

@@ -16,34 +16,32 @@
package top.continew.admin.tenant.model.query;
import java.io.Serial;
import java.io.Serializable;
import java.time.*;
import lombok.Data;
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 TenantDbConnectQuery implements Serializable {
@Schema(description = "数据源查询条件")
public class DatasourceQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
* 关键词
*/
@Schema(description = "连接名称")
@Query(type = QueryType.EQ)
private String connectName;
@Schema(description = "关键词", example = "数据源")
@Query(columns = {"name", "description"}, type = QueryType.LIKE)
private String description;
}

View File

@@ -16,40 +16,40 @@
package top.continew.admin.tenant.model.query;
import java.io.Serial;
import java.io.Serializable;
import java.time.*;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.common.enums.DisEnableStatusEnum;
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/11/26 11:25
*/
@Data
@Schema(description = "租户套餐查询条件")
public class TenantPackageQuery implements Serializable {
@Schema(description = "套餐查询条件")
public class PackageQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
* 关键词
*/
@Schema(description = "套餐名称")
@Query(type = QueryType.EQ)
private String name;
@Schema(description = "关键词", example = "初级套餐")
@Query(columns = {"name", "description"}, type = QueryType.LIKE)
private String description;
/**
* 状态1启用2禁用
* 状态
*/
@Schema(description = "状态1启用2禁用")
@Schema(description = "状态", example = "1")
@Query(type = QueryType.EQ)
private Integer status;
private DisEnableStatusEnum status;
}

View File

@@ -28,6 +28,7 @@ import java.io.Serializable;
* 租户查询条件
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
@Data
@@ -38,17 +39,16 @@ public class TenantQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 租户名称
* 关键词
*/
@Schema(description = "租户名称")
@Query(type = QueryType.LIKE)
private String name;
@Schema(description = "关键词", example = "T0001")
@Query(columns = {"name", "description"}, type = QueryType.LIKE)
private String description;
/**
* 租户套餐编号
* 套餐 ID
*/
@Schema(description = "租户套餐编号")
@Schema(description = "套餐 ID", example = "1")
@Query(type = QueryType.EQ)
private Long packageId;
}

View File

@@ -0,0 +1,95 @@
/*
* 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

@@ -16,61 +16,67 @@
package top.continew.admin.tenant.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import java.io.Serial;
import java.io.Serializable;
import java.time.*;
import jakarta.validation.constraints.*;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import org.hibernate.validator.constraints.Length;
import java.util.ArrayList;
import java.util.List;
/**
* 创建或修改租户套餐参数
* 套餐创建或修改请求参数
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 11:25
*/
@Data
@Schema(description = "创建或修改租户套餐参数")
public class TenantPackageReq implements Serializable {
@Schema(description = "套餐创建或修改请求参数")
public class PackageReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
* 名称
*/
@Schema(description = "套餐名称")
@NotBlank(message = "套餐名称不能为空")
@Length(max = 64, message = "套餐名称长度不能超过 {max} 个字符")
@Schema(description = "名称", example = "初级套餐")
@NotBlank(message = "名称不能为空")
@Length(max = 30, message = "名称长度不能超过 {max} 个字符")
private String name;
/**
* 关联的菜单ids
* 排序
*/
@Schema(description = "关联的菜单ids")
private Long[] menuIds;
@Schema(description = "排序", example = "1")
private Integer sort;
/**
* 菜单选择是否父子节点关联
*/
@Schema(description = "菜单选择是否父子节点关联")
@Schema(description = "菜单选择是否父子节点关联", example = "true")
private Boolean menuCheckStrictly;
/**
* 描述
*/
@Schema(description = "描述", example = "初级套餐描述")
@Length(max = 200, message = "描述长度不能超过 {max} 个字符")
private String description;
/**
* 状态
*/
@Schema(description = "状态")
@NotNull(message = "状态不能为空")
private Integer status;
@Schema(description = "状态", example = "1")
private DisEnableStatusEnum status;
/**
* ID
* 关联的菜单 ID 列表
*/
@Schema(hidden = true)
private Long id;
@Schema(description = "关联的菜单 ID 列表", example = "[1000, 1010, 1011]")
private List<Long> menuIds = new ArrayList<>();
}

View File

@@ -17,40 +17,24 @@
package top.continew.admin.tenant.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* @description: 租户登录用户信息
* @author: 小熊
* @create: 2024-12-02 20:41
* 租户管理员密码修改请求参数
*
* @author 小熊
* @since 2024/12/2 20:41
*/
@Data
public class TenantLoginUserInfoReq implements Serializable {
@Schema(description = "租户管理员密码修改请求参数")
public class TenantAdminUserPwdUpdateReq implements Serializable {
/**
* 租户id
*/
@NotNull(message = "租户ID不能为空")
private Long tenantId;
/**
* 登录用户名
*/
@NotEmpty(message = "登录用户名不能为空")
private String username;
/**
* 登录密码
* 新密码
*/
@NotBlank(message = "新密码不能为空")
private String password;
/**
* ID
*/
@Schema(hidden = true)
private Long id;
}

View File

@@ -1,92 +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 java.io.Serial;
import java.io.Serializable;
/**
* 创建或修改租户数据连接参数
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Data
@Schema(description = "创建或修改租户数据连接参数")
public class TenantDbConnectReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
*/
@Schema(description = "连接名称")
@NotBlank(message = "连接名称不能为空")
@Length(max = 128, message = "连接名称长度不能超过 {max} 个字符")
private String connectName;
/**
* 连接类型
*/
@Schema(description = "连接类型")
@NotNull(message = "连接类型不能为空")
private Integer type;
/**
* 连接主机地址
*/
@Schema(description = "连接主机地址")
@NotBlank(message = "连接主机地址不能为空")
@Length(max = 128, message = "连接主机地址长度不能超过 {max} 个字符")
private String host;
/**
* 连接端口
*/
@Schema(description = "连接端口")
@NotNull(message = "连接端口不能为空")
private Integer port;
/**
* 连接用户名
*/
@Schema(description = "连接用户名")
@NotBlank(message = "连接用户名不能为空")
@Length(max = 128, message = "连接用户名长度不能超过 {max} 个字符")
private String username;
/**
* 连接密码
*/
@Schema(description = "连接密码")
@NotBlank(message = "连接密码不能为空")
@Length(max = 128, message = "连接密码长度不能超过 {max} 个字符")
private String password;
/**
* ID
*/
@Schema(hidden = true)
private Long id;
}

View File

@@ -16,6 +16,8 @@
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;
@@ -24,6 +26,8 @@ import jakarta.validation.constraints.Pattern;
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;
@@ -31,53 +35,75 @@ import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 创建或修改租户参数
* 租户创建或修改请求参数
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
@Data
@Schema(description = "创建或修改租户参数")
@SpelValid
@Schema(description = "租户创建或修改请求参数")
public class TenantReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
* 名称
*/
@Schema(description = "租户名称")
@NotBlank(message = "租户名称不能为空")
@Length(max = 64, message = "租户名称长度不能超过 {max} 个字符")
@Schema(description = "名称", example = "T0001租户")
@NotBlank(message = "名称不能为空")
@Length(max = 30, message = "名称长度不能超过 {max} 个字符")
private String name;
/**
* 绑定的域名
* 域名
*/
@Schema(description = "绑定的域名")
@Length(max = 128, message = "绑定的域名长度不能超过 {max} 个字符")
@Schema(description = "域名", example = "https://t0001.continew.top/")
@Length(max = 255, message = "域名长度不能超过 {max} 个字符")
private String domain;
/**
* 租户套餐编号
* 过期时间
*/
@Schema(description = "租户套餐编号")
@NotNull(message = "租户套餐编号不能为空")
@Schema(description = "过期时间", example = "2023-08-08 08:08:08", type = "string")
@Future(message = "过期时间必须是未来时间")
private LocalDateTime expireTime;
/**
* 隔离级别
*/
@Schema(description = "隔离级别", example = "2")
@NotNull(message = "隔离级别不能为空", groups = CrudValidationGroup.Create.class)
private TenantIsolationLevelEnum isolationLevel;
/**
* 描述
*/
@Schema(description = "描述", example = "T0001租户描述")
@Length(max = 200, message = "描述长度不能超过 {max} 个字符")
private String description;
/**
* 状态
*/
@Schema(description = "状态", example = "1")
private DisEnableStatusEnum status;
/**
* 套餐 ID
*/
@Schema(description = "套餐 ID", example = "1")
@NotNull(message = "套餐不能为空")
private Long packageId;
/**
* 状态1启用2禁用
* 数据源 ID
*/
@Schema(description = "状态")
@NotNull(message = "状态不能为空")
private Integer status;
/**
* 租户过期时间
*/
@Schema(description = "租户过期时间")
@Future(message = "过期时间必须是未来时间")
private LocalDateTime expireTime;
@Schema(description = "数据源 ID")
@SpelNotNull(condition = "#this.isolationLevel == T(top.continew.admin.tenant.model.enums.TenantIsolationLevelEnum).DATASOURCE", message = "数据源不能为空")
private Long datasourceId;
/**
* 用户名
@@ -95,22 +121,10 @@ public class TenantReq implements Serializable {
private String password;
/**
* 租户编号
* 编码
*/
private String tenantSn;
/**
* 隔离级别
*/
@Schema(description = "隔离级别")
@NotNull(message = "隔离级别不能为空", groups = CrudValidationGroup.Create.class)
private Integer isolationLevel;
/**
* 数据连接ID
*/
@Schema(description = "数据连接ID")
private Long dbConnectId;
@Schema(hidden = true)
private String code;
/**
* ID

View File

@@ -0,0 +1,37 @@
/*
* 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

@@ -21,62 +21,65 @@ 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 TenantDbConnectDetailResp extends BaseDetailResp {
@Schema(description = "数据源响应参数")
public class DatasourceResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
* 名称
*/
@Schema(description = "连接名称")
@ExcelProperty(value = "连接名称")
private String connectName;
@Schema(description = "名称", example = "T0001数据源")
@ExcelProperty(value = "名称", order = 2)
private String name;
/**
* 连接类型
* 数据库类型
*/
@Schema(description = "连接类型")
@ExcelProperty(value = "连接类型")
private Integer type;
@Schema(description = "数据库类型", example = "1")
@ExcelProperty(value = "数据库类型", converter = ExcelBaseEnumConverter.class, order = 3)
private DatasourceDatabaseTypeEnum databaseType;
/**
* 连接主机地址
* 主机
*/
@Schema(description = "连接主机地址")
@ExcelProperty(value = "连接主机地址")
@Schema(description = "主机", example = "123.56.195.68")
@ExcelProperty(value = "主机", order = 4)
private String host;
/**
* 连接端口
* 端口
*/
@Schema(description = "连接端口")
@ExcelProperty(value = "连接端口")
@Schema(description = "端口", example = "3306")
@ExcelProperty(value = "端口", order = 5)
private Integer port;
/**
* 连接用户名
* 用户名
*/
@Schema(description = "连接用户名")
@ExcelProperty(value = "连接用户名")
@Schema(description = "用户名", example = "root")
@ExcelProperty(value = "用户名", order = 6)
private String username;
/**
* 连接密码
* 描述
*/
@Schema(description = "连接密码")
@ExcelProperty(value = "连接密码")
private String password;
@Schema(description = "描述", example = "T0001数据源描述")
@ExcelProperty(value = "描述", order = 7)
private String description;
}

View File

@@ -16,50 +16,34 @@
package top.continew.admin.tenant.model.resp;
import cn.crane4j.annotation.AssembleMethod;
import cn.crane4j.annotation.ContainerMethod;
import cn.crane4j.annotation.MappingType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.tenant.service.PackageMenuService;
import java.io.Serial;
import java.time.*;
import java.util.List;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import top.continew.admin.common.base.model.resp.BaseResp;
/**
* 租户套餐信息
* 套餐详情响应参数
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 11:25
*/
@Data
@Schema(description = "租户套餐信息")
public class TenantPackageResp extends BaseResp {
@Schema(description = "套餐详情响应参数")
@AssembleMethod(key = "id", prop = ":menuIds", targetType = PackageMenuService.class, method = @ContainerMethod(bindMethod = "listMenuIdsByPackageId", type = MappingType.ORDER_OF_KEYS))
public class PackageDetailResp extends PackageResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
* 关联的菜单 ID 列表
*/
@Schema(description = "套餐名称")
private String name;
/**
* 关联的菜单ids
*/
@Schema(description = "关联的菜单ids")
@Schema(description = "关联的菜单 ID 列表", example = "[1000, 1010, 1011]")
private List<Long> menuIds;
/**
* 菜单选择是否父子节点关联
*/
@Schema(description = "菜单选择是否父子节点关联")
private Boolean menuCheckStrictly;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
private Integer status;
}

View File

@@ -16,57 +16,63 @@
package top.continew.admin.tenant.model.resp;
import java.io.Serial;
import java.time.*;
import java.util.List;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import top.continew.admin.common.base.model.resp.BaseDetailResp;
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.common.enums.DisEnableStatusEnum;
import top.continew.starter.excel.converter.ExcelBaseEnumConverter;
import java.io.Serial;
/**
* 租户套餐详情信息
* 套餐响应参数
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 11:25
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "租户套餐详情信息")
public class TenantPackageDetailResp extends BaseDetailResp {
@Schema(description = "套餐响应参数")
public class PackageResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
* 名称
*/
@Schema(description = "套餐名称")
@ExcelProperty(value = "套餐名称")
@Schema(description = "名称", example = "初级套餐")
@ExcelProperty(value = "名称", order = 2)
private String name;
/**
* 关联的菜单ids
* 排序
*/
@Schema(description = "关联的菜单ids")
@ExcelProperty(value = "关联的菜单ids")
private List<Long> menuIds;
@Schema(description = "排序", example = "1")
@ExcelProperty(value = "排序", order = 3)
private Integer sort;
/**
* 菜单选择是否父子节点关联
*/
@Schema(description = "菜单选择是否父子节点关联")
@ExcelProperty(value = "菜单选择是否父子节点关联")
@Schema(description = "菜单选择是否父子节点关联", example = "true")
@ExcelProperty(value = "菜单选择是否父子节点关联", order = 4)
private Boolean menuCheckStrictly;
/**
* 状态1启用2禁用
* 描述
*/
@Schema(description = "状态1启用2禁用")
@ExcelProperty(value = "状态1启用2禁用")
private Integer status;
@Schema(description = "描述", example = "初级套餐")
@ExcelProperty(value = "描述", order = 5)
private String description;
/**
* 状态
*/
@Schema(description = "状态", example = "1")
@ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 6)
private DisEnableStatusEnum status;
}

View File

@@ -21,15 +21,16 @@ import lombok.Data;
import java.util.List;
/**
* @description: 租户通用信息返回
* @author: 小熊
* @create: 2024-11-28 09:53
* 租户通用信息返回
*
* @author 小熊
* @since 2024/11/28 09:53
*/
@Data
public class TenantCommonResp {
/**
* 是否开启了租户
* 是否开启了租户
*/
private Boolean isEnabled;

View File

@@ -1,68 +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 top.continew.admin.common.base.model.resp.BaseResp;
import java.io.Serial;
/**
* 租户数据连接信息
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Data
@Schema(description = "租户数据连接信息")
public class TenantDbConnectResp extends BaseResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
*/
@Schema(description = "连接名称")
private String connectName;
/**
* 连接类型
*/
@Schema(description = "连接类型")
private Integer type;
/**
* 连接主机地址
*/
@Schema(description = "连接主机地址")
private String host;
/**
* 连接端口
*/
@Schema(description = "连接端口")
private Integer port;
/**
* 连接用户名
*/
@Schema(description = "连接用户名")
private String username;
}

View File

@@ -16,84 +16,30 @@
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 java.io.Serial;
import java.time.LocalDateTime;
import java.util.List;
/**
* 租户详情信息
* 租户详情响应参数
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "租户详情信息")
public class TenantDetailResp extends BaseDetailResp {
@Schema(description = "租户详情响应参数")
public class TenantDetailResp extends TenantResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
* 租户管理员
*/
@Schema(description = "租户名称")
@ExcelProperty(value = "租户名称")
private String name;
/**
* 绑定的域名
*/
@Schema(description = "绑定的域名")
@ExcelProperty(value = "绑定的域名")
private String domain;
/**
* 租户套餐编号
*/
@Schema(description = "租户套餐编号")
@ExcelProperty(value = "租户套餐编号")
private Long packageId;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
@ExcelProperty(value = "状态1启用2禁用")
private Integer status;
/**
* 租户过期时间
*/
@Schema(description = "租户过期时间")
@ExcelProperty(value = "租户过期时间")
private LocalDateTime expireTime;
/**
* 绑定的套餐名称
*/
@Schema(description = "绑定的套餐名称")
private String packageName;
/**
* 套餐关联的菜单
*/
@Schema(description = "关联的菜单ids")
private List<Long> menuIds;
/**
* 租户编号
*/
private String tenantSn;
/**
* 租户绑定的管理用户id
*/
private Long userId;
@Schema(description = "租户管理员", example = "666")
@ExcelProperty(value = "租户管理员", order = 13)
private Long adminUser;
}

View File

@@ -16,76 +16,114 @@
package top.continew.admin.tenant.model.resp;
import cn.crane4j.annotation.AssembleMethod;
import cn.crane4j.annotation.ContainerMethod;
import cn.crane4j.annotation.Mapping;
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.BaseResp;
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;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 租户信息
* 租户响应参数
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
@Data
@Schema(description = "租户信息")
public class TenantResp extends BaseResp {
@ExcelIgnoreUnannotated
@Schema(description = "租户响应参数")
public class TenantResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
* 名称
*/
@Schema(description = "租户名称")
@Schema(description = "名称", example = "T0001租户")
@ExcelProperty(value = "名称", order = 2)
private String name;
/**
* 绑定的域名
* 编码
*/
@Schema(description = "绑定的域名")
@Schema(description = "编码", example = "T0001")
@ExcelProperty(value = "编码", order = 3)
private String code;
/**
* 域名
*/
@Schema(description = "域名", example = "https://t0001.continew.top/")
@ExcelProperty(value = "域名", order = 4)
private String domain;
/**
* 租户套餐编号
* 过期时间
*/
@Schema(description = "租户套餐编号")
private Long packageId;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
private Integer status;
/**
* 租户过期时间
*/
@Schema(description = "租户过期时间")
@Schema(description = "过期时间", example = "2023-08-08 08:08:08")
@ExcelProperty(value = "过期时间", order = 5)
private LocalDateTime expireTime;
/**
* 绑定的套餐名称
*/
@Schema(description = "绑定的套餐名称")
private String packageName;
/**
* 租户编号
*/
private String tenantSn;
/**
* 隔离级别
*/
@Schema(description = "隔离级别")
private Integer isolationLevel;
@Schema(description = "隔离级别", example = "2")
@ExcelProperty(value = "隔离级别", converter = ExcelBaseEnumConverter.class, order = 6)
private TenantIsolationLevelEnum isolationLevel;
/**
* 数据连接ID
* 描述
*/
@Schema(description = "数据连接ID")
private Long dbConnectId;
@Schema(description = "描述", example = "T0001租户描述")
@ExcelProperty(value = "描述", order = 7)
private String description;
/**
* 状态
*/
@Schema(description = "状态", example = "1")
@ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 8)
private DisEnableStatusEnum status;
/**
* 套餐 ID
*/
@Schema(description = "套餐 ID", example = "1")
@ExcelProperty(value = "套餐 ID", order = 9)
@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)
private String packageName;
/**
* 数据源名称
*/
@Schema(description = "数据源名称", example = "T0001数据源")
@ExcelProperty(value = "数据源名称", order = 12)
private String datasourceName;
}

View File

@@ -0,0 +1,50 @@
/*
* 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

@@ -0,0 +1,45 @@
/*
* 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 java.util.List;
/**
* 套餐和菜单关联业务接口
*
* @author Charles7c
* @since 2025/7/13 20:44
*/
public interface PackageMenuService {
/**
* 新增
*
* @param menuIds 菜单 ID 列表
* @param packageId 套餐 ID
* @return 是否成功true成功false无变更/失败)
*/
boolean add(List<Long> menuIds, Long packageId);
/**
* 根据套餐 ID 查询
*
* @param packageId 套餐 ID
* @return 菜单 ID 列表
*/
List<Long> listMenuIdsByPackageId(Long packageId);
}

View File

@@ -0,0 +1,71 @@
/*
* 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 cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor;
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 java.util.List;
/**
* 套餐和菜单关联业务实现
*
* @author Charles7c
* @since 2025/7/13 20:45
*/
@Service
@RequiredArgsConstructor
public class PackageMenuServiceImpl implements PackageMenuService {
private final PackageMenuMapper baseMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean add(List<Long> menuIds, Long packageId) {
// 检查是否有变更
List<Long> oldMenuIdList = baseMapper.lambdaQuery()
.select(PackageMenuDO::getMenuId)
.eq(PackageMenuDO::getPackageId, packageId)
.list()
.stream()
.map(PackageMenuDO::getMenuId)
.toList();
if (CollUtil.isEmpty(CollUtil.disjunction(menuIds, oldMenuIdList))) {
return false;
}
// 删除原有关联
baseMapper.lambdaUpdate().eq(PackageMenuDO::getPackageId, packageId).remove();
// 保存最新关联
List<PackageMenuDO> newList = menuIds.stream().map(menuId -> new PackageMenuDO(packageId, menuId)).toList();
return baseMapper.insertBatch(newList);
}
@Override
public List<Long> listMenuIdsByPackageId(Long packageId) {
return baseMapper.lambdaQuery()
.select(PackageMenuDO::getMenuId)
.eq(PackageMenuDO::getPackageId, packageId)
.list()
.stream()
.map(PackageMenuDO::getMenuId)
.toList();
}
}

View File

@@ -17,15 +17,17 @@
package top.continew.admin.tenant.service;
import top.continew.admin.common.base.service.BaseService;
import top.continew.admin.tenant.model.query.TenantPackageQuery;
import top.continew.admin.tenant.model.req.TenantPackageReq;
import top.continew.admin.tenant.model.resp.TenantPackageDetailResp;
import top.continew.admin.tenant.model.resp.TenantPackageResp;
import top.continew.admin.tenant.model.entity.PackageDO;
import top.continew.admin.tenant.model.query.PackageQuery;
import top.continew.admin.tenant.model.req.PackageReq;
import top.continew.admin.tenant.model.resp.PackageDetailResp;
import top.continew.admin.tenant.model.resp.PackageResp;
import top.continew.starter.data.service.IService;
/**
* 租户套餐业务接口
* 套餐业务接口
*
* @author 小熊
* @since 2024/11/26 11:25
*/
public interface TenantPackageService extends BaseService<TenantPackageResp, TenantPackageDetailResp, TenantPackageQuery, TenantPackageReq> {}
public interface PackageService extends BaseService<PackageResp, PackageDetailResp, PackageQuery, PackageReq>, IService<PackageDO> {}

View File

@@ -1,36 +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 org.springframework.jdbc.core.JdbcTemplate;
import top.continew.admin.common.base.service.BaseService;
import top.continew.admin.tenant.model.query.TenantDbConnectQuery;
import top.continew.admin.tenant.model.req.TenantDbConnectReq;
import top.continew.admin.tenant.model.resp.TenantDbConnectDetailResp;
import top.continew.admin.tenant.model.resp.TenantDbConnectResp;
/**
* 租户数据连接业务接口
*
* @author 小熊
* @since 2024/12/12 19:13
*/
public interface TenantDbConnectService extends BaseService<TenantDbConnectResp, TenantDbConnectDetailResp, TenantDbConnectQuery, TenantDbConnectReq> {
JdbcTemplate getConnectJdbcTemplateById(Long id);
}

View File

@@ -31,33 +31,21 @@ import java.util.List;
* 租户业务接口
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
public interface TenantService extends BaseService<TenantResp, TenantDetailResp, TenantQuery, TenantReq>, IService<TenantDO> {
/**
* 检查租户状态
*
* @param id ID
* @return 租户信息
*/
TenantDO checkStatus(Long id);
/**
* 获取所有可用的租户列表
*/
List<TenantAvailableResp> getAvailableList();
/**
* 租户绑定用户
*/
void bindUser(Long tenantId, Long userId);
/**
* 检查租户状态
*/
void checkStatus();
/**
* 根据id获取租户DO
*/
TenantDO getTenantById(Long id);
/**
* 根据用户id获取租户信息
*/
TenantDO getTenantByUserId(Long userId);
}

View File

@@ -0,0 +1,179 @@
/*
* 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

@@ -0,0 +1,131 @@
/*
* 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.collection.CollUtil;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.admin.common.base.service.BaseServiceImpl;
import top.continew.admin.system.model.entity.MenuDO;
import top.continew.admin.system.service.MenuService;
import top.continew.admin.tenant.mapper.PackageMapper;
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.query.PackageQuery;
import top.continew.admin.tenant.model.req.PackageReq;
import top.continew.admin.tenant.model.resp.PackageDetailResp;
import top.continew.admin.tenant.model.resp.PackageResp;
import top.continew.admin.tenant.service.PackageMenuService;
import top.continew.admin.tenant.service.PackageService;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.extension.tenant.TenantHandler;
import java.util.ArrayList;
import java.util.List;
/**
* 套餐业务实现
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 11:25
*/
@Service
@RequiredArgsConstructor
public class PackageServiceImpl extends BaseServiceImpl<PackageMapper, PackageDO, PackageResp, PackageDetailResp, PackageQuery, PackageReq> implements PackageService {
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);
// 新增信息
Long id = super.create(req);
// 保存套餐和菜单关联
packageMenuService.add(req.getMenuIds(), id);
return id;
}
@Override
@DSTransactional(rollbackFor = Exception.class)
public void update(PackageReq req, Long id) {
this.checkNameRepeat(req.getName(), id);
// 更新信息
super.update(req, id);
// 保存套餐和菜单关联
boolean isSaveMenuSuccess = packageMenuService.add(req.getMenuIds(), id);
if (!isSaveMenuSuccess) {
return;
}
// 更新租户菜单
List<Long> tenantIdList = tenantMapper.lambdaQuery()
.select(TenantDO::getId)
.eq(TenantDO::getPackageId, id)
.list()
.stream()
.map(TenantDO::getId)
.toList();
if (CollUtil.isEmpty(tenantIdList)) {
return;
}
List<Long> oldMenuIds = packageMenuService.listMenuIdsByPackageId(id);
List<Long> newMenuIds = req.getMenuIds();
// 如果有删除的菜单则绑定了套餐的租户对应的菜单也会删除
List<Long> deleteMenuIds = new ArrayList<>(oldMenuIds);
deleteMenuIds.removeAll(newMenuIds);
if (CollUtil.isNotEmpty(deleteMenuIds)) {
List<MenuDO> deleteMenus = menuService.listByIds(deleteMenuIds);
tenantIdList.forEach(tenantId -> tenantHandler.execute(tenantId, () -> menuService
.deleteTenantMenus(deleteMenus)));
}
// 如果有新增的菜单则绑定了套餐的租户对应的菜单也会新增
List<Long> addMenuIds = new ArrayList<>(newMenuIds);
addMenuIds.removeAll(oldMenuIds);
if (CollUtil.isNotEmpty(addMenuIds)) {
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)));
}
}
}
@Override
public void beforeDelete(List<Long> ids) {
CheckUtils.throwIf(tenantMapper.lambdaQuery().in(TenantDO::getPackageId, ids).exists(), "所选套餐存在关联租户,不允许删除");
}
/**
* 名称是否存在
*
* @param name 名称
* @param id ID
*/
private void checkNameRepeat(String name, Long id) {
CheckUtils.throwIf(baseMapper.lambdaQuery()
.eq(PackageDO::getName, name)
.ne(id != null, PackageDO::getId, id)
.exists(), "名称为 [{}] 的套餐已存在", name);
}
}

View File

@@ -1,120 +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 com.alicp.jetcache.anno.Cached;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.constant.CacheConstants;
import top.continew.admin.tenant.mapper.TenantDbConnectMapper;
import top.continew.admin.tenant.mapper.TenantMapper;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.entity.TenantDbConnectDO;
import top.continew.admin.tenant.model.enums.TenantConnectTypeEnum;
import top.continew.admin.tenant.model.query.TenantDbConnectQuery;
import top.continew.admin.tenant.model.req.TenantDbConnectReq;
import top.continew.admin.tenant.model.resp.TenantDbConnectDetailResp;
import top.continew.admin.tenant.model.resp.TenantDbConnectResp;
import top.continew.admin.tenant.service.TenantDbConnectService;
import top.continew.admin.tenant.util.DbConnectUtil;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import javax.sql.DataSource;
import java.util.List;
/**
* 租户数据连接业务实现
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Service
@RequiredArgsConstructor
public class TenantDbConnectServiceImpl extends BaseServiceImpl<TenantDbConnectMapper, TenantDbConnectDO, TenantDbConnectResp, TenantDbConnectDetailResp, TenantDbConnectQuery, TenantDbConnectReq> implements TenantDbConnectService {
private final TenantMapper tenantMapper;
@Override
@Cached(name = CacheConstants.DB_CONNECT_KEY_PREFIX, key = "#id")
public TenantDbConnectDetailResp get(Long id) {
return super.get(id);
}
@Override
protected void beforeCreate(TenantDbConnectReq req) {
TenantConnectTypeEnum connectTypeEnum = TenantConnectTypeEnum.getByOrdinal(req.getType());
if (TenantConnectTypeEnum.MYSQL.equals(connectTypeEnum)) {
DbConnectUtil.getMysqlDataSource(req.getHost(), req.getPort(), req.getUsername(), req
.getPassword(), null, null);
checkRepeat(req, null);
}
}
/**
* 验证重复数据
*/
private void checkRepeat(TenantDbConnectReq req, Long id) {
CheckUtils.throwIf(baseMapper.exists(Wrappers.lambdaQuery(TenantDbConnectDO.class)
.eq(TenantDbConnectDO::getHost, req.getHost())
.eq(TenantDbConnectDO::getPort, req.getPort())
.eq(TenantDbConnectDO::getUsername, req.getUsername())
.ne(id != null, TenantDbConnectDO::getId, id)), "数据库连接已存在");
}
@Override
protected void beforeUpdate(TenantDbConnectReq req, Long id) {
TenantConnectTypeEnum connectTypeEnum = TenantConnectTypeEnum.getByOrdinal(req.getType());
if (TenantConnectTypeEnum.MYSQL.equals(connectTypeEnum)) {
DbConnectUtil.getMysqlDataSource(req.getHost(), req.getPort(), req.getUsername(), req
.getPassword(), null, null);
checkRepeat(req, id);
}
}
@Override
protected void beforeDelete(List<Long> ids) {
CheckUtils.throwIf(tenantMapper.selectCount(Wrappers.lambdaQuery(TenantDO.class)
.in(TenantDO::getDbConnectId, ids)) > 0, "存在关联租户无法删除");
}
@Override
protected void afterUpdate(TenantDbConnectReq req, TenantDbConnectDO entity) {
RedisUtils.delete(CacheConstants.DB_CONNECT_KEY_PREFIX + entity.getId());
}
@Override
protected void afterDelete(List<Long> ids) {
ids.forEach(id -> RedisUtils.delete(CacheConstants.DB_CONNECT_KEY_PREFIX + id));
}
@Override
public JdbcTemplate getConnectJdbcTemplateById(Long id) {
TenantDbConnectDetailResp dbConnectReq = get(id);
TenantConnectTypeEnum connectTypeEnum = TenantConnectTypeEnum.getByOrdinal(dbConnectReq.getType());
if (TenantConnectTypeEnum.MYSQL.equals(connectTypeEnum)) {
DataSource dataSource = DbConnectUtil.getMysqlDataSource(dbConnectReq.getHost(), dbConnectReq
.getPort(), dbConnectReq.getUsername(), dbConnectReq.getPassword(), null, null);
return new JdbcTemplate(dataSource);
}
return null;
}
}

View File

@@ -1,71 +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.json.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.admin.common.base.service.BaseServiceImpl;
import top.continew.admin.tenant.mapper.TenantMapper;
import top.continew.admin.tenant.mapper.TenantPackageMapper;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.entity.TenantPackageDO;
import top.continew.admin.tenant.model.query.TenantPackageQuery;
import top.continew.admin.tenant.model.req.TenantPackageReq;
import top.continew.admin.tenant.model.resp.TenantPackageDetailResp;
import top.continew.admin.tenant.model.resp.TenantPackageResp;
import top.continew.admin.tenant.service.TenantPackageService;
import top.continew.starter.core.util.validation.CheckUtils;
import java.util.List;
/**
* 租户套餐业务实现
*
* @author 小熊
* @since 2024/11/26 11:25
*/
@Service
@RequiredArgsConstructor
public class TenantPackageServiceImpl extends BaseServiceImpl<TenantPackageMapper, TenantPackageDO, TenantPackageResp, TenantPackageDetailResp, TenantPackageQuery, TenantPackageReq> implements TenantPackageService {
private final TenantMapper tenantMapper;
@Override
public TenantPackageDetailResp get(Long id) {
TenantPackageDO tenantPackageDO = getById(id);
TenantPackageDetailResp packageDetailResp = BeanUtil
.copyProperties(tenantPackageDO, TenantPackageDetailResp.class);
packageDetailResp.setMenuIds(new JSONArray(tenantPackageDO.getMenuIds()).toList(Long.class));
fill(packageDetailResp);
return packageDetailResp;
}
@Override
protected void beforeCreate(TenantPackageReq req) {
CheckUtils.throwIf(baseMapper.selectCount(Wrappers.lambdaQuery(TenantPackageDO.class)
.eq(TenantPackageDO::getName, req.getName())) > 0, "租户套餐名称不能重复");
}
@Override
protected void beforeDelete(List<Long> ids) {
CheckUtils.throwIf(tenantMapper.selectCount(Wrappers.lambdaQuery(TenantDO.class)
.in(TenantDO::getPackageId, ids)) > 0, "存在关联租户无法删除");
}
}

View File

@@ -1,82 +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.util.StrUtil;
import com.zaxxer.hikari.HikariConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.resp.TenantDbConnectDetailResp;
import top.continew.admin.tenant.service.TenantDbConnectService;
import top.continew.admin.tenant.service.TenantService;
import top.continew.admin.tenant.util.DbConnectUtil;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.extension.tenant.config.TenantDataSource;
import top.continew.starter.extension.tenant.config.TenantProvider;
import top.continew.starter.extension.tenant.context.TenantContext;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
/**
* @description: 租户数据源提供者实现
* @author: 小熊
* @create: 2024-12-12 15:35
*/
@Service
@RequiredArgsConstructor
public class TenantProviderImpl implements TenantProvider {
private final TenantService tenantService;
private final TenantDbConnectService tenantDbConnectService;
@Override
public TenantContext getByTenantId(String tenantId, boolean verify) {
TenantContext context = new TenantContext();
if (StrUtil.isNotEmpty(tenantId) && !SysConstants.DEFAULT_TENANT.equals(tenantId)) {
Long longTenantId = Long.valueOf(tenantId);
TenantDO tenantDO = tenantService.getTenantById(longTenantId);
CheckUtils.throwIfNull(tenantDO, "租户[{}]不存在", tenantId);
CheckUtils.throwIf(verify && DisEnableStatusEnum.DISABLE.getValue()
.equals(tenantDO.getStatus()), "租户[{}]已被禁用", tenantId);
context.setTenantId(longTenantId);
TenantIsolationLevel isolationLevel = TenantIsolationLevel.DATASOURCE.ordinal() == tenantDO
.getIsolationLevel() ? TenantIsolationLevel.DATASOURCE : TenantIsolationLevel.LINE;
context.setIsolationLevel(isolationLevel);
if (isolationLevel.equals(TenantIsolationLevel.DATASOURCE)) {
TenantDbConnectDetailResp dbConnectReq = tenantDbConnectService.get(tenantDO.getDbConnectId());
String dbName = SysConstants.TENANT_DB_PREFIX + tenantDO.getTenantSn();
HikariConfig hikariConfig = DbConnectUtil.formatHikariConfig(dbConnectReq.getHost(), dbConnectReq
.getPort(), dbConnectReq.getUsername(), dbConnectReq.getPassword(), dbName, DbConnectUtil
.getDefaultMysqlConnectParameter());
TenantDataSource source = new TenantDataSource();
source.setPoolName(tenantId);
source.setDriverClassName(hikariConfig.getDriverClassName());
source.setUrl(hikariConfig.getJdbcUrl());
source.setUsername(hikariConfig.getUsername());
source.setPassword(hikariConfig.getPassword());
context.setDataSource(source);
}
} else {
context.setTenantId(Long.valueOf(SysConstants.DEFAULT_TENANT));
context.setIsolationLevel(TenantIsolationLevel.LINE);
}
return context;
}
}

View File

@@ -18,185 +18,157 @@ package top.continew.admin.tenant.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.extra.spring.SpringUtil;
import com.alicp.jetcache.anno.Cached;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
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.properties.TenantProperties;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.common.constant.SysConstants;
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;
import top.continew.admin.tenant.handler.TenantDataHandler;
import top.continew.admin.tenant.mapper.TenantMapper;
import top.continew.admin.tenant.mapper.TenantPackageMapper;
import top.continew.admin.tenant.model.entity.PackageDO;
import top.continew.admin.tenant.model.entity.TenantDO;
import top.continew.admin.tenant.model.entity.TenantPackageDO;
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.TenantDbConnectService;
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;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
import top.continew.starter.extension.tenant.TenantHandler;
import java.util.Arrays;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 租户业务实现
*
* @author 小熊
* @author Charles7c
* @since 2024/11/26 17:20
*/
@Service
@RequiredArgsConstructor
public class TenantServiceImpl extends BaseServiceImpl<TenantMapper, TenantDO, TenantResp, TenantDetailResp, TenantQuery, TenantReq> implements TenantService {
private final TenantPackageMapper packageMapper;
private final TenantProperties tenantProperties;
private final TenantDbConnectService dbConnectService;
private final IdGeneratorProvider idGeneratorProvider;
private final PackageMenuService packageMenuService;
private final PackageService packageService;
private final DatasourceService datasourceService;
private final TenantDataHandler tenantDataHandler;
@Override
protected void beforeCreate(TenantReq req) {
//租户名称不能重复
ValidationUtils.throwIf(baseMapper.exists(Wrappers.lambdaQuery(TenantDO.class)
.eq(TenantDO::getName, req.getName())), "重复的租户名称");
//录入随机的六位租户编号
req.setTenantSn(generateTenantSn());
}
/**
* 生成六位随机不重复的编号
*/
private String generateTenantSn() {
String tenantSn;
do {
tenantSn = RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER_LOWER, 6);
} while (baseMapper.exists(Wrappers.lambdaQuery(TenantDO.class).eq(TenantDO::getTenantSn, tenantSn)));
return tenantSn;
}
@Override
protected void afterCreate(TenantReq req, TenantDO entity) {
//数据源级别的租户需要创建数据库
if (entity.getIsolationLevel().equals(TenantIsolationLevel.DATASOURCE.ordinal())) {
JdbcTemplate jdbcTemplate = dbConnectService.getConnectJdbcTemplateById(entity.getDbConnectId());
String dbName = SysConstants.TENANT_DB_PREFIX + entity.getTenantSn();
//建库
jdbcTemplate.execute(StrUtil
.format("CREATE DATABASE {} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;", dbName));
jdbcTemplate.execute(StrUtil.format("USE {};", dbName));
//建表
Resource resource = new ClassPathResource("db/changelog/mysql/tenant_table.sql");
String tableSql = resource.readUtf8Str();
Arrays.stream(tableSql.split(";"))
.map(String::trim)
.filter(sql -> !sql.isEmpty())
.forEach(jdbcTemplate::execute);
@DSTransactional(rollbackFor = Exception.class)
public Long create(TenantReq req) {
this.checkNameRepeat(req.getName(), null);
// 检查租户套餐
List<Long> menuIds = packageMenuService.listMenuIdsByPackageId(req.getPackageId());
CheckUtils.throwIfEmpty(menuIds, "所选套餐无可用菜单");
// 生成租户编码
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);
return id;
}
@Override
public void beforeUpdate(TenantReq req, Long id) {
this.checkNameRepeat(req.getName(), id);
}
@Override
public void afterUpdate(TenantReq req, TenantDO entity) {
// 更新租户缓存
RedisUtils.set(TenantCacheConstants.TENANT_KEY_PREFIX + entity.getId(), entity);
}
@Override
public void beforeDelete(List<Long> ids) {
// 在租户中执行数据清除
for (Long id : ids) {
SpringUtil.getBean(TenantHandler.class).execute(id, tenantDataHandler::clear);
}
}
@Override
public void afterDelete(List<Long> ids) {
ids.forEach(id -> RedisUtils.delete(TenantCacheConstants.TENANT_KEY_PREFIX + id));
}
@Override
@Cached(name = TenantCacheConstants.TENANT_KEY_PREFIX, key = "#id")
public TenantDO getById(Serializable id) {
return super.getById(id);
}
@Override
public TenantDO checkStatus(Long id) {
TenantDO tenant = this.getById(id);
if (!tenantProperties.isEnabled() || id.equals(tenantProperties.getSuperTenantId())) {
return tenant;
}
CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE.getValue(), 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;
}
@Override
public List<TenantAvailableResp> getAvailableList() {
List<TenantDO> tenantDOS = baseMapper.selectList(Wrappers.lambdaQuery(TenantDO.class)
List<TenantDO> tenantList = baseMapper.selectList(Wrappers.lambdaQuery(TenantDO.class)
.select(TenantDO::getName, BaseIdDO::getId, TenantDO::getDomain)
.eq(TenantDO::getStatus, DisEnableStatusEnum.ENABLE.getValue())
.and(t -> t.isNull(TenantDO::getExpireTime).or().ge(TenantDO::getExpireTime, DateUtil.date())));
return BeanUtil.copyToList(tenantDOS, TenantAvailableResp.class);
return BeanUtil.copyToList(tenantList, TenantAvailableResp.class);
}
@Override
public PageResp<TenantResp> page(TenantQuery query, PageQuery pageQuery) {
QueryWrapper queryWrapper = Wrappers.query(TenantQuery.class)
.eq(query.getPackageId() != null, "package_id", query.getPackageId())
.like(StrUtil.isNotEmpty(query.getName()), "sys_tenant.name", query.getName());
this.sort(queryWrapper, pageQuery);
IPage<TenantResp> list = baseMapper.listTenant(new Page<>(pageQuery.getPage(), pageQuery
.getSize()), queryWrapper);
PageResp<TenantResp> pageResp = PageResp.build(list, TenantResp.class);
return pageResp;
/**
* 名称是否存在
*
* @param name 名称
* @param id ID
*/
private void checkNameRepeat(String name, Long id) {
CheckUtils.throwIf(baseMapper.lambdaQuery()
.eq(TenantDO::getName, name)
.ne(id != null, TenantDO::getId, id)
.exists(), "名称为 [{}] 的租户已存在", name);
}
@Override
public TenantDetailResp get(Long id) {
TenantDetailResp detailResp = new TenantDetailResp();
TenantDO tenantDO = getById(id);
if (tenantDO != null) {
BeanUtil.copyProperties(tenantDO, detailResp);
TenantPackageDO packageDO = packageMapper.selectById(tenantDO.getPackageId());
if (packageDO != null) {
detailResp.setPackageName(packageDO.getName());
detailResp.setMenuIds(new JSONArray(packageDO.getMenuIds()).toList(Long.class));
}
}
fill(detailResp);
return detailResp;
/**
* 生成租户编码
*
* @return 租户编码
*/
private String generateCode() {
String code;
do {
code = idGeneratorProvider.getRequired(TenantConstants.CODE_GENERATOR_KEY).generateAsString();
} while (baseMapper.lambdaQuery().eq(TenantDO::getCode, code).exists());
return code;
}
@Override
public void bindUser(Long tenantId, Long userId) {
update(Wrappers.lambdaUpdate(TenantDO.class).set(TenantDO::getUserId, userId).eq(BaseIdDO::getId, tenantId));
TenantDO entity = getById(tenantId);
RedisUtils.set(CacheConstants.TENANT_KEY + tenantId, entity);
}
@Override
public void checkStatus() {
if (tenantProperties.isEnabled()) {
Long tenantId = TenantContextHolder.getTenantId();
CheckUtils.throwIfNull(tenantId, "未选择租户");
if (tenantId != 0) {
TenantDO tenantDO = baseMapper.selectById(tenantId);
CheckUtils.throwIfNull(tenantDO, "租户不存在");
CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE.getValue(), tenantDO.getStatus(), "此租户已被禁用");
//租户过期
CheckUtils.throwIf(tenantDO.getExpireTime() != null && tenantDO.getExpireTime()
.isBefore(DateUtil.date().toLocalDateTime()), "租户已过期");
//套餐状态
TenantPackageDO packageDO = packageMapper.selectById(tenantDO.getPackageId());
CheckUtils.throwIfNull(tenantDO, "套餐不存在");
CheckUtils.throwIfNotEqual(DisEnableStatusEnum.ENABLE.getValue(), packageDO.getStatus(), "此租户套餐已被禁用");
}
}
}
@Override
@Cached(name = CacheConstants.TENANT_KEY, key = "#id")
public TenantDO getTenantById(Long id) {
return baseMapper.selectById(id);
}
@Override
protected void afterUpdate(TenantReq req, TenantDO entity) {
RedisUtils.set(CacheConstants.TENANT_KEY + entity.getId(), entity);
}
@Override
protected void afterDelete(List<Long> ids) {
ids.forEach(id -> RedisUtils.delete(CacheConstants.TENANT_KEY + id));
}
@Override
public TenantDO getTenantByUserId(Long userId) {
return baseMapper.selectOne(Wrappers.lambdaQuery(TenantDO.class).eq(TenantDO::getUserId, userId));
}
}

View File

@@ -1,106 +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.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import top.continew.starter.core.exception.BusinessException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
/**
* @description: 数据连接工具类
* @author: 小熊
* @create: 2024-12-15 18:54
*/
public class DbConnectUtil {
/**
* 格式化HikariConfig
*/
public static HikariConfig formatHikariConfig(String host,
Integer port,
String username,
String password,
String dbName,
Map<String, String> parameter) {
String activeProfile = SpringUtil.getActiveProfile();
String jdbcUrl = StrUtil.format("jdbc:mysql://{}:{}", host, port);
String driverClassName = "com.mysql.cj.jdbc.Driver";
if ("dev".equals(activeProfile)) {
jdbcUrl = StrUtil.format("jdbc:p6spy:mysql://{}:{}", host, port);
driverClassName = "com.p6spy.engine.spy.P6SpyDriver";
}
if (StrUtil.isNotEmpty(dbName)) {
jdbcUrl = StrUtil.format("{}/{}", jdbcUrl, dbName);
}
if (parameter != null) {
jdbcUrl = StrUtil.format("{}?{}", jdbcUrl, MapUtil.join(parameter, "&", "="));
}
HikariConfig configuration = new HikariConfig();
configuration.setJdbcUrl(jdbcUrl);
configuration.setDriverClassName(driverClassName);
configuration.setUsername(username);
configuration.setPassword(password);
configuration.setConnectionTimeout(3000L);
return configuration;
}
/**
* 验证mysql连接有效性并返回数据源
*/
public static DataSource getMysqlDataSource(String host,
Integer port,
String username,
String password,
String dbName,
Map<String, String> parameter) {
try {
DataSource dataSource = new HikariDataSource(formatHikariConfig(host, port, username, password, dbName, parameter));
Connection connection = dataSource.getConnection();
connection.close();
return dataSource;
} catch (Exception e) {
throw new BusinessException("数据库连接失败,请检查基础配置信息");
}
}
/**
* 默认的mysql连接参数
*
* @return
*/
public static Map<String, String> getDefaultMysqlConnectParameter() {
Map<String, String> parameter = new HashMap<>();
parameter.put("serverTimezone", "Asia/Shanghai");
parameter.put("useUnicode", "true");
parameter.put("characterEncoding", "utf8");
parameter.put("useSSL", "false");
parameter.put("allowMultiQueries", "true");
parameter.put("autoReconnect", "true");
parameter.put("maxReconnects", "10");
parameter.put("failOverReadOnly", "false");
return parameter;
}
}