feat(plugin/tenant): 新增多租户插件模块 (#175)

This commit is contained in:
小熊
2025-07-10 20:38:59 +08:00
committed by GitHub
parent 72493f8161
commit ed6dd65a51
70 changed files with 3539 additions and 65 deletions

View File

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

View File

@@ -0,0 +1,40 @@
/*
* 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.common.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import top.continew.starter.core.constant.PropertiesConstants;
import java.util.List;
/**
* @description: 多租户配置
* @author: 小熊
* @create: 2024-11-29 12:05
*/
@Component
@ConfigurationProperties(prefix = PropertiesConstants.TENANT)
@Data
public class TenantProperties {
private boolean enabled;
private List<Long> ignoreMenus;
}

View File

@@ -71,6 +71,16 @@ public class CacheConstants {
*/
public static final String DATA_IMPORT_KEY = "SYSTEM" + DELIMITER + "DATA_IMPORT" + DELIMITER;
/**
* 数据连接键前缀
*/
public static final String DB_CONNECT_KEY_PREFIX = "DB_CONNECT" + DELIMITER;
/**
* 租户信息前缀
*/
public static final String TENANT_KEY = "TENANT" + DELIMITER;
private CacheConstants() {
}
}

View File

@@ -84,6 +84,31 @@ public class SysConstants {
*/
public static final String LOGOUT_URI = "/auth/logout";
/**
* 描述类字段后缀
*/
public static final String DESCRIPTION_FIELD_SUFFIX = "String";
/**
* 租户数据库前缀
*/
public static final String TENANT_DB_PREFIX = "tenant_";
/**
* 默认租户
*/
public static final String DEFAULT_TENANT = "0";
/**
* 默认数据源
*/
public static final String DEFAULT_DATASOURCE = "master";
/**
* 租户管理员角色编码
*/
public static final String TENANT_ADMIN_CODE = "tenant_admin";
private SysConstants() {
}
}

View File

@@ -80,7 +80,7 @@ public class UserContext implements Serializable {
*/
private Set<RoleContext> roles;
/**
/*
* 客户端类型
*/
private String clientType;
@@ -90,6 +90,11 @@ public class UserContext implements Serializable {
*/
private String clientId;
/**
* 租户 ID
*/
private Long tenantId;
public UserContext(Set<String> permissions, Set<RoleContext> roles, Integer passwordExpirationDays) {
this.permissions = permissions;
this.setRoles(roles);
@@ -129,4 +134,5 @@ public class UserContext implements Serializable {
}
return this.pwdResetTime.plusDays(this.passwordExpirationDays).isBefore(LocalDateTime.now());
}
}

View File

@@ -180,4 +180,11 @@ public class UserContextHolder {
StpUtil.checkLogin();
return getContext().isAdmin();
}
/**
* 获取租户ID
*/
public static Long getTenantId() {
return ExceptionUtils.exToNull(() -> getContext().getTenantId());
}
}

View File

@@ -55,4 +55,5 @@ public interface AppService extends BaseService<AppResp, AppDetailResp, AppQuery
* @return 应用信息
*/
AppDO getByAccessKey(String accessKey);
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew.admin</groupId>
<artifactId>continew-plugin</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-plugin-tenant</artifactId>
<description>多租户插件</description>
<dependencies>
<dependency>
<groupId>top.continew.admin</groupId>
<artifactId>continew-system</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,211 @@
/*
* 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.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 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.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.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.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.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 小熊
* @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})
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 = "多租户通用信息查询")
public TenantCommonResp common() {
TenantCommonResp commonResp = new TenantCommonResp();
commonResp.setIsEnabled(tenantProperties.isEnabled());
commonResp.setAvailableList(baseService.getAvailableList());
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());
}
});
}
/**
* 查询所有租户套餐
*/
@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

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

@@ -0,0 +1,116 @@
/*
* 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,28 @@
/*
* 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 top.continew.admin.tenant.model.entity.TenantDbConnectDO;
import top.continew.starter.data.mapper.BaseMapper;
/**
* 租户数据连接 Mapper
*
* @author 小熊
* @since 2024/12/12 19:13
*/
public interface TenantDbConnectMapper extends BaseMapper<TenantDbConnectDO> {}

View File

@@ -0,0 +1,44 @@
/*
* 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 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;
/**
* 租户 Mapper
*
* @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

@@ -0,0 +1,29 @@
/*
* 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 top.continew.starter.data.mapper.BaseMapper;
import top.continew.admin.tenant.model.entity.TenantPackageDO;
/**
* 租户套餐 Mapper
*
* @author 小熊
* @since 2024/11/26 11:25
*/
public interface TenantPackageMapper extends BaseMapper<TenantPackageDO> {
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import top.continew.admin.common.base.model.entity.BaseDO;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 租户实体
*
* @author 小熊
* @since 2024/11/26 17:20
*/
@Data
@TableName("sys_tenant")
public class TenantDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
*/
private String name;
/**
* 绑定的域名
*/
private String domain;
/**
* 租户套餐编号
*/
private Long packageId;
/**
* 状态1启用2禁用
*/
private Integer status;
/**
* 租户过期时间
*/
private LocalDateTime expireTime;
/**
* 用户ID
*/
private Long userId;
/**
* 租户编号
*/
private String tenantSn;
/**
* 隔离级别
*/
private Integer isolationLevel;
/**
* 数据连接ID
*/
private Long dbConnectId;
}

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.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import top.continew.admin.common.base.model.entity.BaseDO;
import java.io.Serial;
/**
* 租户数据连接实体
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Data
@TableName("sys_tenant_db_connect")
public class TenantDbConnectDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
*/
private String connectName;
/**
* 连接类型
*/
private Integer type;
/**
* 连接主机地址
*/
private String host;
/**
* 连接端口
*/
private Integer port;
/**
* 连接用户名
*/
private String username;
/**
* 连接密码
*/
private String password;
}

View File

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

View File

@@ -0,0 +1,38 @@
/*
* 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.AllArgsConstructor;
import lombok.Getter;
import top.continew.starter.core.exception.BusinessException;
@Getter
@AllArgsConstructor
public enum TenantConnectTypeEnum {
MYSQL;
public static TenantConnectTypeEnum getByOrdinal(Integer ordinal) {
for (TenantConnectTypeEnum item : TenantConnectTypeEnum.values()) {
if (item.ordinal() == ordinal) {
return item;
}
}
throw new BusinessException("未知的连接类型");
}
}

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.query;
import java.io.Serial;
import java.io.Serializable;
import java.time.*;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import top.continew.starter.data.annotation.Query;
import top.continew.starter.data.enums.QueryType;
/**
* 租户数据连接查询条件
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Data
@Schema(description = "租户数据连接查询条件")
public class TenantDbConnectQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
*/
@Schema(description = "连接名称")
@Query(type = QueryType.EQ)
private String connectName;
}

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.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 top.continew.starter.data.annotation.Query;
import top.continew.starter.data.enums.QueryType;
/**
* 租户套餐查询条件
*
* @author 小熊
* @since 2024/11/26 11:25
*/
@Data
@Schema(description = "租户套餐查询条件")
public class TenantPackageQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
*/
@Schema(description = "套餐名称")
@Query(type = QueryType.EQ)
private String name;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
@Query(type = QueryType.EQ)
private Integer 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.query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.starter.data.annotation.Query;
import top.continew.starter.data.enums.QueryType;
import java.io.Serial;
import java.io.Serializable;
/**
* 租户查询条件
*
* @author 小熊
* @since 2024/11/26 17:20
*/
@Data
@Schema(description = "租户查询条件")
public class TenantQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
*/
@Schema(description = "租户名称")
@Query(type = QueryType.LIKE)
private String name;
/**
* 租户套餐编号
*/
@Schema(description = "租户套餐编号")
@Query(type = QueryType.EQ)
private Long packageId;
}

View File

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

@@ -0,0 +1,56 @@
/*
* 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.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
/**
* @description: 租户登录用户信息
* @author: 小熊
* @create: 2024-12-02 20:41
*/
@Data
public class TenantLoginUserInfoReq implements Serializable {
/**
* 租户id
*/
@NotNull(message = "租户ID不能为空")
private Long tenantId;
/**
* 登录用户名
*/
@NotEmpty(message = "登录用户名不能为空")
private String username;
/**
* 登录密码
*/
private String password;
/**
* ID
*/
@Schema(hidden = true)
private Long id;
}

View File

@@ -0,0 +1,76 @@
/*
* 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 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;
/**
* 创建或修改租户套餐参数
*
* @author 小熊
* @since 2024/11/26 11:25
*/
@Data
@Schema(description = "创建或修改租户套餐参数")
public class TenantPackageReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
*/
@Schema(description = "套餐名称")
@NotBlank(message = "套餐名称不能为空")
@Length(max = 64, message = "套餐名称长度不能超过 {max} 个字符")
private String name;
/**
* 关联的菜单ids
*/
@Schema(description = "关联的菜单ids")
private Long[] menuIds;
/**
* 菜单选择是否父子节点关联
*/
@Schema(description = "菜单选择是否父子节点关联")
private Boolean menuCheckStrictly;
/**
* 状态
*/
@Schema(description = "状态")
@NotNull(message = "状态不能为空")
private Integer status;
/**
* ID
*/
@Schema(hidden = true)
private Long id;
}

View File

@@ -0,0 +1,120 @@
/*
* 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.Future;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.common.constant.RegexConstants;
import top.continew.starter.extension.crud.validation.CrudValidationGroup;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 创建或修改租户参数
*
* @author 小熊
* @since 2024/11/26 17:20
*/
@Data
@Schema(description = "创建或修改租户参数")
public class TenantReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
*/
@Schema(description = "租户名称")
@NotBlank(message = "租户名称不能为空")
@Length(max = 64, message = "租户名称长度不能超过 {max} 个字符")
private String name;
/**
* 绑定的域名
*/
@Schema(description = "绑定的域名")
@Length(max = 128, message = "绑定的域名长度不能超过 {max} 个字符")
private String domain;
/**
* 租户套餐编号
*/
@Schema(description = "租户套餐编号")
@NotNull(message = "租户套餐编号不能为空")
private Long packageId;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态")
@NotNull(message = "状态不能为空")
private Integer status;
/**
* 租户过期时间
*/
@Schema(description = "租户过期时间")
@Future(message = "过期时间必须是未来时间")
private LocalDateTime expireTime;
/**
* 用户名
*/
@Schema(description = "用户名", example = "zhangsan")
@NotBlank(message = "用户名不能为空", groups = CrudValidationGroup.Create.class)
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头")
private String username;
/**
* 密码(加密)
*/
@Schema(description = "密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
@NotBlank(message = "密码不能为空", groups = CrudValidationGroup.Create.class)
private String password;
/**
* 租户编号
*/
private String tenantSn;
/**
* 隔离级别
*/
@Schema(description = "隔离级别")
@NotNull(message = "隔离级别不能为空", groups = CrudValidationGroup.Create.class)
private Integer isolationLevel;
/**
* 数据连接ID
*/
@Schema(description = "数据连接ID")
private Long dbConnectId;
/**
* ID
*/
@Schema(hidden = true)
private Long id;
}

View File

@@ -0,0 +1,30 @@
/*
* 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 lombok.Data;
@Data
public class TenantAvailableResp {
private Long id;
private String name;
private String domain;
}

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.model.resp;
import lombok.Data;
import java.util.List;
/**
* @description: 租户通用信息返回
* @author: 小熊
* @create: 2024-11-28 09:53
*/
@Data
public class TenantCommonResp {
/**
* 是否开启了多租户
*/
private Boolean isEnabled;
/**
* 可用租户列表
*/
private List<TenantAvailableResp> availableList;
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.resp;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.common.base.model.resp.BaseDetailResp;
import java.io.Serial;
/**
* 租户数据连接详情信息
*
* @author 小熊
* @since 2024/12/12 19:13
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "租户数据连接详情信息")
public class TenantDbConnectDetailResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 连接名称
*/
@Schema(description = "连接名称")
@ExcelProperty(value = "连接名称")
private String connectName;
/**
* 连接类型
*/
@Schema(description = "连接类型")
@ExcelProperty(value = "连接类型")
private Integer type;
/**
* 连接主机地址
*/
@Schema(description = "连接主机地址")
@ExcelProperty(value = "连接主机地址")
private String host;
/**
* 连接端口
*/
@Schema(description = "连接端口")
@ExcelProperty(value = "连接端口")
private Integer port;
/**
* 连接用户名
*/
@Schema(description = "连接用户名")
@ExcelProperty(value = "连接用户名")
private String username;
/**
* 连接密码
*/
@Schema(description = "连接密码")
@ExcelProperty(value = "连接密码")
private String password;
}

View File

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

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.tenant.model.resp;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.common.base.model.resp.BaseDetailResp;
import java.io.Serial;
import java.time.LocalDateTime;
import java.util.List;
/**
* 租户详情信息
*
* @author 小熊
* @since 2024/11/26 17:20
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "租户详情信息")
public class TenantDetailResp extends BaseDetailResp {
@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;
}

View File

@@ -0,0 +1,72 @@
/*
* 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 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;
/**
* 租户套餐详情信息
*
* @author 小熊
* @since 2024/11/26 11:25
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "租户套餐详情信息")
public class TenantPackageDetailResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
*/
@Schema(description = "套餐名称")
@ExcelProperty(value = "套餐名称")
private String name;
/**
* 关联的菜单ids
*/
@Schema(description = "关联的菜单ids")
@ExcelProperty(value = "关联的菜单ids")
private List<Long> menuIds;
/**
* 菜单选择是否父子节点关联
*/
@Schema(description = "菜单选择是否父子节点关联")
@ExcelProperty(value = "菜单选择是否父子节点关联")
private Boolean menuCheckStrictly;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
@ExcelProperty(value = "状态1启用2禁用")
private Integer status;
}

View File

@@ -0,0 +1,65 @@
/*
* 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 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 小熊
* @since 2024/11/26 11:25
*/
@Data
@Schema(description = "租户套餐信息")
public class TenantPackageResp extends BaseResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 套餐名称
*/
@Schema(description = "套餐名称")
private String name;
/**
* 关联的菜单ids
*/
@Schema(description = "关联的菜单ids")
private List<Long> menuIds;
/**
* 菜单选择是否父子节点关联
*/
@Schema(description = "菜单选择是否父子节点关联")
private Boolean menuCheckStrictly;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
private Integer status;
}

View File

@@ -0,0 +1,91 @@
/*
* 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;
import java.time.LocalDateTime;
/**
* 租户信息
*
* @author 小熊
* @since 2024/11/26 17:20
*/
@Data
@Schema(description = "租户信息")
public class TenantResp extends BaseResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户名称
*/
@Schema(description = "租户名称")
private String name;
/**
* 绑定的域名
*/
@Schema(description = "绑定的域名")
private String domain;
/**
* 租户套餐编号
*/
@Schema(description = "租户套餐编号")
private Long packageId;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
private Integer status;
/**
* 租户过期时间
*/
@Schema(description = "租户过期时间")
private LocalDateTime expireTime;
/**
* 绑定的套餐名称
*/
@Schema(description = "绑定的套餐名称")
private String packageName;
/**
* 租户编号
*/
private String tenantSn;
/**
* 隔离级别
*/
@Schema(description = "隔离级别")
private Integer isolationLevel;
/**
* 数据连接ID
*/
@Schema(description = "数据连接ID")
private Long dbConnectId;
}

View File

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

@@ -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.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;
/**
* 租户套餐业务接口
*
* @author 小熊
* @since 2024/11/26 11:25
*/
public interface TenantPackageService extends BaseService<TenantPackageResp, TenantPackageDetailResp, TenantPackageQuery, TenantPackageReq> {}

View File

@@ -0,0 +1,63 @@
/*
* 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.TenantDO;
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.starter.data.service.IService;
import java.util.List;
/**
* 租户业务接口
*
* @author 小熊
* @since 2024/11/26 17:20
*/
public interface TenantService extends BaseService<TenantResp, TenantDetailResp, TenantQuery, TenantReq>, IService<TenantDO> {
/**
* 获取所有可用的租户列表
*/
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,120 @@
/*
* 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

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

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

@@ -0,0 +1,202 @@
/*
* 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.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 com.alicp.jetcache.anno.Cached;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
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 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.enums.DisEnableStatusEnum;
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.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.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 java.util.Arrays;
import java.util.List;
/**
* 租户业务实现
*
* @author 小熊
* @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;
@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);
}
}
@Override
public List<TenantAvailableResp> getAvailableList() {
List<TenantDO> tenantDOS = 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);
}
@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;
}
@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;
}
@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

@@ -0,0 +1,106 @@
/*
* 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;
}
}

View File

@@ -19,6 +19,7 @@
<module>continew-plugin-schedule</module>
<module>continew-plugin-open</module>
<module>continew-plugin-generator</module>
<module>continew-plugin-tenant</module>
</modules>
<dependencies>

View File

@@ -64,6 +64,12 @@
<artifactId>liquibase-core</artifactId>
</dependency>
<!-- 多租户插件 -->
<dependency>
<groupId>top.continew.admin</groupId>
<artifactId>continew-plugin-tenant</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -23,6 +23,7 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
@@ -41,6 +42,8 @@ import top.continew.admin.system.service.UserService;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.StrUtils;
import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.model.LogRecord;
import top.continew.starter.log.model.LogRequest;
@@ -85,7 +88,10 @@ public class LogDaoLocalImpl implements LogDao {
logDO.setCreateTime(LocalDateTime.ofInstant(logRecord.getTimestamp(), ZoneId.systemDefault()));
// 设置操作人
this.setCreateUser(logDO, logRequest, logResponse);
Long tenantId = TenantContextHolder.getTenantId();
SpringUtil.getBean(TenantHandler.class).execute(tenantId, () -> {
logMapper.insert(logDO);
});
}
/**

View File

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

View File

@@ -11,17 +11,23 @@ server:
--- ### 数据源配置
spring.datasource:
type: com.zaxxer.hikari.HikariDataSource
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
url: jdbc:p6spy:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
## 动态数据源配置可配多主多从m1、s1...纯粹多库mysql、oracle...混合配置m1、s1、oracle...
dynamic:
# 是否启用 P6SpySQL 性能分析组件,该插件有性能损耗,不建议生产环境使用)
p6spy: true
# 设置默认的数据源或者数据源组默认master
primary: master
# 严格匹配数据源true未匹配到指定数据源时抛异常false使用默认数据源默认 false
strict: false
datasource:
# 主库配置(可配多个,构成多主)
master:
url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&autoReconnect=true&maxReconnects=10&failOverReadOnly=false
username: ${DB_USER:root}
password: ${DB_PWD:123456}
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# # PostgreSQL 配置
# url: jdbc:p6spy:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:5432}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&stringtype=unspecified
# username: ${DB_USER:postgres}
# password: ${DB_PWD:123456}
# driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# Hikari 连接池配置
driver-class-name: com.mysql.cj.jdbc.Driver
type: ${spring.datasource.type}
# Hikari 连接池配置完整配置请参阅https://github.com/brettwooldridge/HikariCP
hikari:
# 最大连接数量(默认 10根据实际环境调整
# 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒
@@ -34,6 +40,8 @@ spring.datasource:
keepaliveTime: 30000
# 连接最大生存时间(默认 1800000 毫秒30 分钟)
max-lifetime: 1800000
## Liquibase 配置
spring.liquibase:
# 是否启用
@@ -168,7 +176,7 @@ captcha:
expirationInMinutes: 5
--- ### 短信配置
## 提示:配置文件方式和 [系统管理/系统配置/短信配置] 功能可任选其一方式使用,也可共同使用,但实际开发时建议选择一种,注释或删除另一方
## 提示:配置文件方式和 [短信配置] 功能可任选其一方式使用,也可共同使用,但实际开发时建议选择一种,注释或删除另一方
sms:
http-log: true
is-print: true
@@ -184,7 +192,7 @@ sms:
# sdk-app-id: 你的应用ID
--- ### 邮件配置
## 提示:配置文件方式和 [系统管理/系统配置/邮件配置] 功能可任选其一方式使用,实际开发时请注释或删除另一方
## 提示:配置文件方式和 [邮件配置] 功能可任选其一方式使用,实际开发时请注释或删除另一方
#spring.mail:
# # 根据需要更换
# host: smtp.126.com

View File

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

View File

@@ -261,7 +261,6 @@ mybatis-plus:
# 分页插件配置
pagination:
enabled: true
db-type: MYSQL
--- ### CosId 配置
cosid:
@@ -299,3 +298,32 @@ auth:
- /auth/logout
- /system/user/password
#多租户配置
continew-starter.tenant:
enabled: true
# 多租户忽略的表
ignore-tables:
- gen_config # 代码生成
- gen_field_config
- sys_dict # 字典表
- sys_dict_item
- sys_option #参数
- sys_storage # 存储配置
- sys_tenant # 租户
- sys_tenant_package
- sys_tenant_db_connect
- sys_app #应用
- sys_client #客户端管理
- sys_sms_config
- sys_sms_log
#租户不能使用的菜单
ignore-menus:
- 1130 #字典管理
- 1140
- 1150 #系统配置
- 2050 #短信日志
- 3000 #任务调度
- 9000 #代码生成
- 7000 #能力开放
- 7010 #应用管理
- 10010 #租户管理

View File

@@ -11,6 +11,8 @@ databaseChangeLog:
file: db/changelog/mysql/plugin/plugin_open.sql
- include:
file: db/changelog/mysql/plugin/plugin_generator.sql
- include:
file: db/changelog/mysql/plugin/plugin_tenant.sql
# PostgreSQL
# - include:
# file: db/changelog/postgresql/main_table.sql

View File

@@ -0,0 +1,145 @@
-- liquibase formatted sql
-- changeset 小熊:1
-- comment 初始化表结构
-- ----------------------------
-- Table structure for sys_tenant
-- ----------------------------
DROP TABLE IF EXISTS `sys_tenant`;
CREATE TABLE `sys_tenant` (
`id` bigint NOT NULL COMMENT 'ID',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '租户名称',
`tenant_sn` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户编号',
`user_id` bigint DEFAULT NULL COMMENT '租户对应的用户',
`domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '绑定的域名',
`package_id` bigint NOT NULL COMMENT '租户套餐编号',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态1启用2禁用',
`expire_time` datetime DEFAULT NULL COMMENT '租户过期时间',
`create_user` bigint NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`isolation_level` tinyint unsigned NOT NULL COMMENT '隔离级别',
`db_connect_id` bigint DEFAULT NULL COMMENT '数据连接ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户表';
-- ----------------------------
-- Table structure for sys_tenant_package
-- ----------------------------
DROP TABLE IF EXISTS `sys_tenant_package`;
CREATE TABLE `sys_tenant_package` (
`id` bigint NOT NULL COMMENT 'ID',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '套餐名称',
`menu_ids` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '关联的菜单ids',
`menu_check_strictly` bit(1) DEFAULT b'0' COMMENT '菜单选择是否父子节点关联',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态1启用2禁用',
`create_user` bigint NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户套餐表';
-- ----------------------------
-- Table structure for sys_tenant_db_connect
-- ----------------------------
CREATE TABLE `sys_tenant_db_connect` (
`id` bigint NOT NULL COMMENT 'ID',
`connect_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '连接名称',
`type` tinyint(1) NOT NULL COMMENT '连接类型',
`host` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '连接主机地址',
`port` smallint NOT NULL COMMENT '连接端口',
`username` varchar(128) NOT NULL COMMENT '连接用户名',
`password` varchar(128) NOT NULL COMMENT '连接密码',
`create_user` bigint NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户数据库连接表';
-- changeset 小熊:2
-- comment 添加租户列和索引
ALTER TABLE sys_app ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_app_tenant_id ON sys_app(tenant_id);
ALTER TABLE sys_dept ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_dept_tenant_id ON sys_dept(tenant_id);
ALTER TABLE sys_file ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_file_tenant_id ON sys_file(tenant_id);
ALTER TABLE sys_log ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_log_tenant_id ON sys_log(tenant_id);
ALTER TABLE sys_menu ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_menu_tenant_id ON sys_menu(tenant_id);
ALTER TABLE sys_message ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_message_tenant_id ON sys_message(tenant_id);
ALTER TABLE sys_message_log ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_message_log_tenant_id ON sys_message_log(tenant_id);
ALTER TABLE sys_notice ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_notice_tenant_id ON sys_notice(tenant_id);
ALTER TABLE sys_notice_log ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_notice_log_tenant_id ON sys_notice_log(tenant_id);
ALTER TABLE sys_role ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_role_tenant_id ON sys_role(tenant_id);
ALTER TABLE sys_user ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_user_tenant_id ON sys_user(tenant_id);
ALTER TABLE sys_role_dept ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_role_dept_tenant_id ON sys_role_dept(tenant_id);
ALTER TABLE sys_role_menu ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_role_menu_tenant_id ON sys_role_menu(tenant_id);
ALTER TABLE sys_user_password_history ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_user_password_history_tenant_id ON sys_user_password_history(tenant_id);
ALTER TABLE sys_user_role ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_user_role_tenant_id ON sys_user_role(tenant_id);
ALTER TABLE sys_user_social ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_sys_user_social_tenant_id ON sys_user_social(tenant_id);
-- changeset 小熊:3
-- comment 唯一索引变更
ALTER TABLE sys_app DROP INDEX `uk_access_key`;
ALTER TABLE sys_app ADD UNIQUE INDEX `uk_access_key` (`access_key`, `tenant_id`);
ALTER TABLE sys_dept DROP INDEX `uk_name_parent_id`;
ALTER TABLE sys_dept ADD UNIQUE INDEX `uk_name_parent_id` (`name`, `parent_id`, `tenant_id`);
ALTER TABLE sys_menu DROP INDEX `uk_title_parent_id`;
ALTER TABLE sys_menu ADD UNIQUE INDEX `uk_title_parent_id` (`title`, `parent_id`, `tenant_id`);
ALTER TABLE sys_role DROP INDEX `uk_name`;
ALTER TABLE sys_role ADD UNIQUE INDEX `uk_name` (`name`, `tenant_id`);
ALTER TABLE sys_role DROP INDEX `uk_code`;
ALTER TABLE sys_role ADD UNIQUE INDEX `uk_code` (`code`, `tenant_id`);
ALTER TABLE sys_user DROP INDEX `uk_username`;
ALTER TABLE sys_user ADD UNIQUE INDEX `uk_username` (`username`, `tenant_id`);
ALTER TABLE sys_user DROP INDEX `uk_email`;
ALTER TABLE sys_user ADD UNIQUE INDEX `uk_email` (`email`, `tenant_id`);
ALTER TABLE sys_user DROP INDEX `uk_phone`;
ALTER TABLE sys_user ADD UNIQUE INDEX `uk_phone` (`phone`, `tenant_id`);
-- changeset 小熊:4
-- comment 菜单录入
INSERT INTO `sys_menu`
(`id`, `title`, `parent_id`, `type`, `path`, `name`, `component`, `redirect`, `icon`, `is_external`, `is_cache`, `is_hidden`, `permission`, `sort`, `status`, `create_user`, `create_time`, `update_user`, `update_time`, `tenant_id`)
VALUES
(10010, '租户管理', 0, 1, '/tenant', 'Tenant', 'Layout', '/tenant/user', 'user-group', b'0', b'0', b'0', NULL, 6, 1, 1, NOW(), NULL, NULL, 0),
(10015, '租户套餐', 10010, 2, '/tenant/package', 'TenantPackage', 'tenant/package/index', NULL, 'menu', b'0', b'0', b'0', NULL, 2, 1, 1, NOW(), NULL, NULL, 0),
(10016, '列表', 10015, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:list', 1, 1, 1, NOW(), NULL, NULL, 0),
(10017, '详情', 10015, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:detail', 2, 1, 1, NOW(), NULL, NULL, 0),
(10018, '新增', 10015, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:add', 3, 1, 1, NOW(), NULL, NULL, 0),
(10019, '修改', 10015, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:update', 4, 1, 1, NOW(), NULL, NULL, 0),
(10020, '删除', 10015, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:package:delete', 5, 1, 1, NOW(), NULL, NULL, 0),
(10022, '租户管理', 10010, 2, '/tenant/user', 'TenantUser', 'tenant/user/index', NULL, 'user-group', b'0', b'0', b'0', NULL, 1, 1, 1, NOW(), NULL, NULL, 0),
(10023, '列表', 10022, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:user:list', 1, 1, 1, NOW(), NULL, NULL, 0),
(10024, '详情', 10022, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:user:detail', 2, 1, 1, NOW(), NULL, NULL, 0),
(10025, '新增', 10022, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:user:add', 3, 1, 1, NOW(), NULL, NULL, 0),
(10026, '修改', 10022, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:user:update', 4, 1, 1, NOW(), NULL, NULL, 0),
(10027, '删除', 10022, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'tenant:user:delete', 5, 1, 1, NOW(), NULL, NULL, 0),
(10028, '账号更新', 10022, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:user:editLoginUserInfo', 6, 1, 1, NOW(), NULL, NULL, 0),
(10040, '数据连接', 10010, 2, '/tenant/dbConnect', 'TenantDbConnect', 'tenant/tenantDbConnect/index', NULL, 'storage', b'0', b'0', b'0', NULL, 3, 1, 1, NOW(), NULL, NULL, 0),
(10041, '列表', 10040, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:dbConnect:list', 1, 1, 1, NOW(), NULL, NULL, 0),
(10042, '详情', 10040, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:dbConnect:detail', 2, 1, 1, NOW(), NULL, NULL, 0),
(10043, '新增', 10040, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:dbConnect:add', 3, 1, 1, NOW(), NULL, NULL, 0),
(10044, '修改', 10040, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:dbConnect:update', 4, 1, 1, NOW(), NULL, NULL, 0),
(10045, '删除', 10040, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'tenant:dbConnect:delete', 5, 1, 1, NOW(), NULL, NULL, 0);

View File

@@ -0,0 +1,241 @@
-- 数据源级别租户表结构
CREATE TABLE IF NOT EXISTS `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`title` varchar(30) NOT NULL COMMENT '标题',
`parent_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '上级菜单ID',
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型1目录2菜单3按钮',
`path` varchar(255) DEFAULT NULL COMMENT '路由地址',
`name` varchar(50) DEFAULT NULL COMMENT '组件名称',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`redirect` varchar(255) DEFAULT NULL COMMENT '重定向地址',
`icon` varchar(50) DEFAULT NULL COMMENT '图标',
`is_external` bit(1) DEFAULT b'0' COMMENT '是否外链',
`is_cache` bit(1) DEFAULT b'0' COMMENT '是否缓存',
`is_hidden` bit(1) DEFAULT b'0' COMMENT '是否隐藏',
`permission` varchar(100) DEFAULT NULL COMMENT '权限标识',
`sort` int NOT NULL DEFAULT 999 COMMENT '排序',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用2禁用',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `uk_title_parent_id`(`title`, `parent_id`),
INDEX `idx_parent_id`(`parent_id`),
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
CREATE TABLE IF NOT EXISTS `sys_dept` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(30) NOT NULL COMMENT '名称',
`parent_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '上级部门ID',
`ancestors` varchar(512) NOT NULL DEFAULT '' COMMENT '祖级列表',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`sort` int NOT NULL DEFAULT 999 COMMENT '排序',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用2禁用',
`is_system` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为系统内置数据',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `uk_name_parent_id`(`name`, `parent_id`),
INDEX `idx_parent_id`(`parent_id`),
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';
CREATE TABLE IF NOT EXISTS `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(30) NOT NULL COMMENT '名称',
`code` varchar(30) NOT NULL COMMENT '编码',
`data_scope` tinyint(1) NOT NULL DEFAULT 4 COMMENT '数据权限1全部数据权限2本部门及以下数据权限3本部门数据权限4仅本人数据权限5自定义数据权限',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`sort` int NOT NULL DEFAULT 999 COMMENT '排序',
`is_system` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为系统内置数据',
`menu_check_strictly` bit(1) DEFAULT b'0' COMMENT '菜单选择是否父子节点关联',
`dept_check_strictly` bit(1) DEFAULT b'0' COMMENT '部门选择是否父子节点关联',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `uk_name`(`name`),
UNIQUE INDEX `uk_code`(`code`),
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE IF NOT EXISTS `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`username` varchar(64) NOT NULL COMMENT '用户名',
`nickname` varchar(30) NOT NULL COMMENT '昵称',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`gender` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '性别0未知12',
`email` varchar(255) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(255) DEFAULT NULL COMMENT '手机号码',
`avatar` longtext DEFAULT NULL COMMENT '头像',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用2禁用',
`is_system` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为系统内置数据',
`pwd_reset_time` datetime DEFAULT NULL COMMENT '最后一次修改密码时间',
`dept_id` bigint(20) NOT NULL COMMENT '部门ID',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `uk_username`(`username`),
UNIQUE INDEX `uk_email`(`email`),
UNIQUE INDEX `uk_phone`(`phone`),
INDEX `idx_dept_id`(`dept_id`),
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
CREATE TABLE IF NOT EXISTS `sys_user_password_history` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`password` varchar(255) NOT NULL COMMENT '密码',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
INDEX `idx_user_id`(`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户历史密码表';
CREATE TABLE IF NOT EXISTS `sys_user_social` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`source` varchar(255) NOT NULL COMMENT '来源',
`open_id` varchar(255) NOT NULL COMMENT '开放ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`meta_json` text DEFAULT NULL COMMENT '附加信息',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `uk_source_open_id`(`source`, `open_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户社会化关联表';
CREATE TABLE IF NOT EXISTS `sys_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
UNIQUE INDEX `uk_user_id_role_id`(`user_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户和角色关联表';
CREATE TABLE IF NOT EXISTS `sys_role_menu` (
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`role_id`, `menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色和菜单关联表';
CREATE TABLE IF NOT EXISTS `sys_role_dept` (
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`dept_id` bigint(20) NOT NULL COMMENT '部门ID',
PRIMARY KEY (`role_id`, `dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色和部门关联表';
CREATE TABLE IF NOT EXISTS `sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`trace_id` varchar(255) DEFAULT NULL COMMENT '链路ID',
`description` varchar(255) NOT NULL COMMENT '日志描述',
`module` varchar(100) NOT NULL COMMENT '所属模块',
`request_url` varchar(512) NOT NULL COMMENT '请求URL',
`request_method` varchar(10) NOT NULL COMMENT '请求方式',
`request_headers` text DEFAULT NULL COMMENT '请求头',
`request_body` text DEFAULT NULL COMMENT '请求体',
`status_code` int NOT NULL COMMENT '状态码',
`response_headers` text DEFAULT NULL COMMENT '响应头',
`response_body` mediumtext DEFAULT NULL COMMENT '响应体',
`time_taken` bigint(20) NOT NULL COMMENT '耗时ms',
`ip` varchar(100) DEFAULT NULL COMMENT 'IP',
`address` varchar(255) DEFAULT NULL COMMENT 'IP归属地',
`browser` varchar(100) DEFAULT NULL COMMENT '浏览器',
`os` varchar(100) DEFAULT NULL COMMENT '操作系统',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1成功2失败',
`error_msg` text DEFAULT NULL COMMENT '错误信息',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
INDEX `idx_module`(`module`),
INDEX `idx_ip`(`ip`),
INDEX `idx_address`(`address`),
INDEX `idx_create_time`(`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表';
CREATE TABLE IF NOT EXISTS `sys_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`title` varchar(50) NOT NULL COMMENT '标题',
`content` text DEFAULT NULL COMMENT '内容',
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型1系统消息2安全消息',
`path` varchar(255) DEFAULT NULL COMMENT '跳转路径',
`scope` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '通知范围1所有人2指定用户',
`users` json DEFAULT NULL COMMENT '通知用户',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';
CREATE TABLE IF NOT EXISTS `sys_message_log` (
`message_id` bigint(20) NOT NULL COMMENT '消息ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`read_time` datetime DEFAULT NULL COMMENT '读取时间',
PRIMARY KEY (`message_id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息日志表';
CREATE TABLE IF NOT EXISTS `sys_notice` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`title` varchar(150) NOT NULL COMMENT '标题',
`content` mediumtext NOT NULL COMMENT '内容',
`type` varchar(30) NOT NULL COMMENT '分类',
`notice_scope` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '通知范围1所有人2指定用户',
`notice_users` json DEFAULT NULL COMMENT '通知用户',
`notice_methods` json DEFAULT NULL COMMENT '通知方式1系统消息2登录弹窗',
`is_timing` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否定时',
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
`is_top` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否置顶',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1草稿2待发布3已发布',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表';
CREATE TABLE IF NOT EXISTS `sys_notice_log` (
`notice_id` bigint(20) NOT NULL COMMENT '公告ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`read_time` datetime DEFAULT NULL COMMENT '读取时间',
PRIMARY KEY (`notice_id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告日志表';
CREATE TABLE IF NOT EXISTS `sys_file` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(255) NOT NULL COMMENT '名称',
`original_name` varchar(255) NOT NULL COMMENT '原始名称',
`size` bigint(20) DEFAULT NULL COMMENT '大小(字节)',
`parent_path` varchar(512) NOT NULL DEFAULT '/' COMMENT '上级目录',
`path` varchar(512) NOT NULL COMMENT '路径',
`extension` varchar(32) DEFAULT NULL COMMENT '扩展名',
`content_type` varchar(255) DEFAULT NULL COMMENT '内容类型',
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型0: 目录1其他2图片3文档4视频5音频',
`sha256` varchar(256) DEFAULT NULL COMMENT 'SHA256值',
`metadata` text DEFAULT NULL COMMENT '元数据',
`thumbnail_name` varchar(255) DEFAULT NULL COMMENT '缩略图名称',
`thumbnail_size` bigint(20) DEFAULT NULL COMMENT '缩略图大小(字节)',
`thumbnail_metadata` text DEFAULT NULL COMMENT '缩略图元数据',
`storage_id` bigint(20) NOT NULL COMMENT '存储ID',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
INDEX `idx_type`(`type`),
INDEX `idx_sha256`(`sha256`),
INDEX `idx_storage_id`(`storage_id`),
INDEX `idx_create_user`(`create_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表';

View File

@@ -19,6 +19,7 @@ package top.continew.admin.auth;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -36,10 +37,13 @@ import top.continew.admin.system.service.DeptService;
import top.continew.admin.system.service.OptionService;
import top.continew.admin.system.service.RoleService;
import top.continew.admin.system.service.UserService;
import top.continew.starter.core.util.ServletUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.Validator;
import top.continew.starter.core.util.ServletUtils;
import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -90,10 +94,21 @@ public abstract class AbstractLoginHandler<T extends LoginReq> implements LoginH
protected String authenticate(UserDO user, ClientResp client) {
// 获取权限、角色、密码过期天数
Long userId = user.getId();
CompletableFuture<Set<String>> permissionFuture = CompletableFuture.supplyAsync(() -> roleService
.listPermissionByUserId(userId), threadPoolTaskExecutor);
CompletableFuture<Set<RoleContext>> roleFuture = CompletableFuture.supplyAsync(() -> roleService
.listByUserId(userId), threadPoolTaskExecutor);
Long tenantId = TenantContextHolder.getTenantId();
CompletableFuture<Set<String>> permissionFuture = CompletableFuture.supplyAsync(() -> {
Set<String> permissions = new HashSet<>();
SpringUtil.getBean(TenantHandler.class).execute(tenantId, () -> {
permissions.addAll(roleService.listPermissionByUserId(userId));
});
return permissions;
}, threadPoolTaskExecutor);
CompletableFuture<Set<RoleContext>> roleFuture = CompletableFuture.supplyAsync(() -> {
Set<RoleContext> roles = new HashSet<>();
SpringUtil.getBean(TenantHandler.class).execute(tenantId, () -> {
roles.addAll(roleService.listByUserId(userId));
});
return roles;
}, threadPoolTaskExecutor);
CompletableFuture<Integer> passwordExpirationDaysFuture = CompletableFuture.supplyAsync(() -> optionService
.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name()));
CompletableFuture.allOf(permissionFuture, roleFuture, passwordExpirationDaysFuture);
@@ -108,6 +123,7 @@ public abstract class AbstractLoginHandler<T extends LoginReq> implements LoginH
userContext.setClientType(client.getClientType());
loginParameter.setExtra(CLIENT_ID, client.getClientId());
userContext.setClientId(client.getClientId());
userContext.setTenantId(tenantId);
// 登录并缓存用户信息
StpUtil.login(userContext.getId(), loginParameter.setExtraData(BeanUtil
.beanToMap(new UserExtraContext(ServletUtils.getRequest()))));

View File

@@ -123,4 +123,5 @@ public class AuthServiceImpl implements AuthService {
});
return BeanUtil.copyToList(treeList, RouteResp.class);
}
}

View File

@@ -29,12 +29,14 @@ import org.springframework.stereotype.Service;
import top.continew.admin.auth.model.query.OnlineUserQuery;
import top.continew.admin.auth.model.resp.OnlineUserResp;
import top.continew.admin.auth.service.OnlineUserService;
import top.continew.admin.common.config.properties.TenantProperties;
import top.continew.admin.common.context.UserContext;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.common.context.UserExtraContext;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import java.time.LocalDateTime;
import java.util.*;
@@ -50,6 +52,8 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class OnlineUserServiceImpl implements OnlineUserService {
private final TenantProperties tenantProperties;
@Override
@AutoOperate(type = OnlineUserResp.class, on = "list")
public PageResp<OnlineUserResp> page(OnlineUserQuery query, PageQuery pageQuery) {
@@ -88,6 +92,12 @@ public class OnlineUserServiceImpl implements OnlineUserService {
.isMatchClientId(query.getClientId(), userContext)) {
continue;
}
//租户数据过滤
if (tenantProperties.isEnabled()) {
if (!TenantContextHolder.getTenantId().equals(userContext.getTenantId())) {
continue;
}
}
List<LocalDateTime> loginTimeList = query.getLoginTime();
entry.getValue().parallelStream().forEach(token -> {
UserExtraContext extraContext = UserContextHolder.getExtraContext(token);

View File

@@ -16,6 +16,8 @@
package top.continew.admin.system.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import top.continew.admin.common.constant.SysConstants;
import org.apache.ibatis.annotations.Mapper;
import top.continew.admin.system.model.entity.StorageDO;
import top.continew.starter.data.mapper.BaseMapper;
@@ -26,6 +28,7 @@ import top.continew.starter.data.mapper.BaseMapper;
* @author Charles7c
* @since 2023/12/26 22:09
*/
@DS(SysConstants.DEFAULT_DATASOURCE)
@Mapper
public interface StorageMapper extends BaseMapper<StorageDO> {
}

View File

@@ -108,4 +108,10 @@ public class UserDO extends BaseDO {
* 部门 ID
*/
private Long deptId;
/**
* 租户 ID
*/
@TableField(select = false)
private Long tenantId;
}

View File

@@ -25,6 +25,7 @@ import top.continew.starter.data.enums.QueryType;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 菜单查询条件
@@ -56,4 +57,12 @@ public class MenuQuery implements Serializable {
public MenuQuery(DisEnableStatusEnum status) {
this.status = status;
}
/**
* 排除的菜单
*/
@Schema(description = "排除的菜单")
@Query(columns = "id", type = QueryType.NOT_IN)
private List<Long> excludeMenuIdList;
}

View File

@@ -56,4 +56,12 @@ public interface DeptService extends BaseService<DeptResp, DeptResp, DeptQuery,
* @return 部门数量
*/
int countByNames(List<String> deptNames);
/**
* 初始化租户部门
*
* @param deptName
* @return 部门ID
*/
Long initTenantDept(String deptName);
}

View File

@@ -34,6 +34,13 @@ import java.util.Set;
*/
public interface MenuService extends BaseService<MenuResp, MenuResp, MenuQuery, MenuReq>, IService<MenuDO> {
/**
* 查询全部菜单
*
* @return 菜单列表
*/
List<MenuResp> listAll(Long tenantId);
/**
* 根据用户 ID 查询
*
@@ -43,10 +50,35 @@ public interface MenuService extends BaseService<MenuResp, MenuResp, MenuQuery,
Set<String> listPermissionByUserId(Long userId);
/**
* 根据角色 ID 查询
* 根据角色id查询
*
* @param roleId 角色 ID
* @param roleId 角色id
* @return 菜单列表
*/
List<MenuResp> listByRoleId(Long roleId);
/**
* 递归初始化菜单
*
* @param menuList 需要初始化的菜单ID
* @param oldParentId 原来的父级ID
* @param newParentId 新的父级ID
*/
void menuInit(List<MenuDO> menuList, Long oldParentId, Long newParentId);
/**
* 删除租户菜单
*
* @param menuList
*/
void deleteTenantMenus(List<MenuDO> menuList);
/**
* 新增租户菜单
*
* @param menu 新增的菜单
* @param pMenu 新增菜单的父级别
*/
void addTenantMenu(MenuDO menu, MenuDO pMenu);
}

View File

@@ -16,6 +16,9 @@
package top.continew.admin.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import top.continew.admin.system.model.entity.RoleMenuDO;
import java.util.List;
/**
@@ -24,7 +27,7 @@ import java.util.List;
* @author Charles7c
* @since 2023/2/19 10:40
*/
public interface RoleMenuService {
public interface RoleMenuService extends IService<RoleMenuDO> {
/**
* 新增

View File

@@ -100,4 +100,12 @@ public interface RoleService extends BaseService<RoleResp, RoleDetailResp, RoleQ
* @return 角色数量
*/
int countByNames(List<String> roleNames);
/**
* 初始化租户角色
*
* @return 角色ID
*/
Long initTenantRole();
}

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.system.service;
/**
* @description: 多租户系统数据接口
* @author: 小熊
* @create: 2024-12-02 20:08
*/
public interface TenantSysDataService {
/**
* 清除所有系统数据
*/
void clear();
}

View File

@@ -155,4 +155,13 @@ public interface UserService extends BaseService<UserResp, UserDetailResp, UserQ
* @return 用户数量
*/
Long countByDeptIds(List<Long> deptIds);
/**
* 初始化租户管理员
*
* @param username
* @param password
* @return 管理员id
*/
Long initTenantUser(String username, String password, Long deptId);
}

View File

@@ -214,4 +214,25 @@ public class DeptServiceImpl extends BaseServiceImpl<DeptMapper, DeptDO, DeptRes
}
baseMapper.updateById(list);
}
/**
* 初始化租户部门
*
* @param deptName
* @return 部门ID
*/
@Override
public Long initTenantDept(String deptName) {
//部门添加
DeptDO deptDO = new DeptDO();
deptDO.setName(deptName);
deptDO.setParentId(0l);
deptDO.setAncestors("0");
deptDO.setDescription("系统初始部门");
deptDO.setSort(1);
deptDO.setStatus(DisEnableStatusEnum.ENABLE);
baseMapper.insert(deptDO);
return deptDO.getId();
}
}

View File

@@ -19,6 +19,7 @@ package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.alicp.jetcache.anno.Cached;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -28,15 +29,20 @@ import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.system.enums.MenuTypeEnum;
import top.continew.admin.system.mapper.MenuMapper;
import top.continew.admin.system.mapper.RoleMapper;
import top.continew.admin.system.model.entity.MenuDO;
import top.continew.admin.system.model.entity.RoleDO;
import top.continew.admin.system.model.entity.RoleMenuDO;
import top.continew.admin.system.model.query.MenuQuery;
import top.continew.admin.system.model.req.MenuReq;
import top.continew.admin.system.model.resp.MenuResp;
import top.continew.admin.system.service.MenuService;
import top.continew.admin.system.service.RoleMenuService;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.validation.CheckUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -50,6 +56,9 @@ import java.util.Set;
@RequiredArgsConstructor
public class MenuServiceImpl extends BaseServiceImpl<MenuMapper, MenuDO, MenuResp, MenuResp, MenuQuery, MenuReq> implements MenuService {
private final RoleMenuService roleMenuService;
private final RoleMapper roleMapper;
@Override
public Long create(MenuReq req) {
String title = req.getTitle();
@@ -90,12 +99,17 @@ public class MenuServiceImpl extends BaseServiceImpl<MenuMapper, MenuDO, MenuRes
RedisUtils.deleteByPattern(CacheConstants.ROLE_MENU_KEY_PREFIX + StringConstants.ASTERISK);
}
@Override
@Cached(key = "'ALL' + #tenantId", name = CacheConstants.ROLE_MENU_KEY_PREFIX)
public List<MenuResp> listAll(Long tenantId) {
return super.list(new MenuQuery(DisEnableStatusEnum.ENABLE), null);
}
@Override
public Set<String> listPermissionByUserId(Long userId) {
return baseMapper.selectPermissionByUserId(userId);
}
@Override
@Cached(key = "#roleId", name = CacheConstants.ROLE_MENU_KEY_PREFIX)
public List<MenuResp> listByRoleId(Long roleId) {
if (SysConstants.SUPER_ROLE_ID.equals(roleId)) {
@@ -107,6 +121,63 @@ public class MenuServiceImpl extends BaseServiceImpl<MenuMapper, MenuDO, MenuRes
return list;
}
@Override
public void menuInit(List<MenuDO> menuList, Long oldParentId, Long newParentId) {
List<MenuDO> children = menuList.stream().filter(menuDO -> menuDO.getParentId().equals(oldParentId)).toList();
for (MenuDO menuDO : children) {
Long oldId = menuDO.getId();
menuDO.setId(null);
menuDO.setParentId(newParentId);
save(menuDO);
menuInit(menuList, oldId, menuDO.getId());
}
}
@Override
public void deleteTenantMenus(List<MenuDO> menuList) {
if (!menuList.isEmpty()) {
List<Long> delIds = new ArrayList<>();
for (MenuDO menuDO : menuList) {
MenuDO tMenu = getOne(Wrappers.query(MenuDO.class)
.eq(menuDO.getType().equals(MenuTypeEnum.BUTTON.getValue()), "CONCAT(title,permission)", menuDO
.getTitle() + menuDO.getPermission())
.eq(!menuDO.getType().equals(MenuTypeEnum.BUTTON.getValue()), "name", menuDO.getName()));
if (tMenu != null) {
delIds.add(tMenu.getId());
}
}
if (!delIds.isEmpty()) {
//菜单删除
delete(delIds);
//绑定关系删除
roleMenuService.remove(Wrappers.lambdaQuery(RoleMenuDO.class).in(RoleMenuDO::getMenuId, delIds));
}
}
}
@Override
public void addTenantMenu(MenuDO menu, MenuDO pMenu) {
Long pId = 0l;
if (pMenu != null) {
MenuDO tPMenu = getOne(Wrappers.query(MenuDO.class)
.eq(pMenu.getType().equals(MenuTypeEnum.BUTTON.getValue()), "CONCAT(title,permission)", pMenu
.getTitle() + pMenu.getPermission())
.eq(!pMenu.getType().equals(MenuTypeEnum.BUTTON.getValue()), "name", pMenu.getName()));
pId = tPMenu.getId();
}
menu.setId(null);
menu.setParentId(pId);
//菜单新增
save(menu);
//管理员绑定菜单
RoleDO roleDO = roleMapper.selectOne(Wrappers.lambdaQuery(RoleDO.class)
.eq(RoleDO::getCode, SysConstants.TENANT_ADMIN_CODE));
RoleMenuDO roleMenuDO = new RoleMenuDO();
roleMenuDO.setRoleId(roleDO.getId());
roleMenuDO.setMenuId(menu.getId());
roleMenuService.save(roleMenuDO);
}
/**
* 标题是否存在
*

View File

@@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.system.mapper.RoleMenuMapper;
import top.continew.admin.system.model.entity.RoleMenuDO;
import top.continew.admin.system.service.RoleMenuService;
import top.continew.starter.data.service.impl.ServiceImpl;
import java.util.ArrayList;
import java.util.List;
@@ -36,7 +37,7 @@ import java.util.stream.Collectors;
*/
@Service
@RequiredArgsConstructor
public class RoleMenuServiceImpl implements RoleMenuService {
public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenuDO> implements RoleMenuService {
private final RoleMenuMapper baseMapper;

View File

@@ -69,6 +69,8 @@ public class RoleServiceImpl extends BaseServiceImpl<RoleMapper, RoleDO, RoleRes
CheckUtils.throwIf(this.isNameExists(name, null), "新增失败,[{}] 已存在", name);
String code = req.getCode();
CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code);
// 防止租户添加超管
CheckUtils.throwIf(SysConstants.SUPER_ROLE_CODE.equals(code), "新增失败,[{}] 禁止使用", code);
// 新增信息
Long roleId = super.create(req);
// 保存角色和部门关联
@@ -247,4 +249,24 @@ public class RoleServiceImpl extends BaseServiceImpl<RoleMapper, RoleDO, RoleRes
}
});
}
/**
* 初始化租户角色
*
* @return 角色ID
*/
@Override
public Long initTenantRole() {
RoleDO roleDO = new RoleDO();
roleDO.setName("系统管理员");
roleDO.setCode(SysConstants.TENANT_ADMIN_CODE);
roleDO.setDataScope(DataScopeEnum.ALL);
roleDO.setDescription("系统初始角色");
roleDO.setSort(1);
roleDO.setIsSystem(true);
roleDO.setMenuCheckStrictly(false);
roleDO.setDeptCheckStrictly(false);
baseMapper.insert(roleDO);
return roleDO.getId();
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.system.service.impl;
import cn.dev33.satoken.stp.StpUtil;
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.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.user.UserDO;
import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.TenantSysDataService;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import java.util.List;
/**
* @description: 多租户系统数据接口
* @author: 小熊
* @create: 2024-12-02 20:12
*/
@RequiredArgsConstructor
@Service
public class TenantSysDataServiceImpl implements TenantSysDataService {
private final DeptMapper deptMapper;
private final FileService fileService;
private final LogMapper logMapper;
private final MenuMapper menuMapper;
private final MessageMapper messageMapper;
private final MessageMapper messageUserMapper;
private final NoticeMapper noticeMapper;
private final RoleMapper roleMapper;
private final RoleDeptMapper roleDeptMapper;
private final RoleMenuMapper roleMenuMapper;
private final UserMapper userMapper;
private final UserPasswordHistoryMapper userPasswordHistoryMapper;
private final UserRoleMapper userRoleMapper;
private final UserSocialMapper userSocialMapper;
@Override
@Transactional
public void clear() {
//所有用户退出
List<UserDO> userDOS = userMapper.selectList(null);
for (UserDO userDO : userDOS) {
StpUtil.logout(userDO.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);
}
}

View File

@@ -23,10 +23,7 @@ import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.*;
import cn.hutool.extra.validation.ValidationUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
@@ -57,6 +54,7 @@ import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.auth.service.OnlineUserService;
import top.continew.admin.common.base.service.BaseServiceImpl;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.common.constant.RegexConstants;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.context.UserContext;
import top.continew.admin.common.context.UserContextHolder;
@@ -80,8 +78,10 @@ import top.continew.admin.system.service.*;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.FileUploadUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
@@ -734,6 +734,26 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
}
}
@Override
public Long initTenantUser(String username, String password, Long deptId) {
//密码验证
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(password));
ValidationUtils.throwIfNull(rawPassword, "密码解密失败");
ValidationUtils.throwIf(!ReUtil
.isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
UserDO userDO = new UserDO();
userDO.setUsername(username);
userDO.setNickname("系统管理员");
userDO.setPassword(rawPassword);
userDO.setGender(GenderEnum.UNKNOWN);
userDO.setDescription("系统初始用户");
userDO.setStatus(DisEnableStatusEnum.ENABLE);
userDO.setIsSystem(true);
userDO.setDeptId(deptId);
baseMapper.insert(userDO);
return userDO.getId();
}
/**
* 根据 ID 获取用户信息(数据权限)
*

View File

@@ -76,6 +76,13 @@
<version>${revision}</version>
</dependency>
<!-- 多租户插件 -->
<dependency>
<groupId>top.continew.admin</groupId>
<artifactId>continew-plugin-tenant</artifactId>
<version>4.0.0-SNAPSHOT</version>
</dependency>
<!-- 代码生成器插件(后续会改造为独立插件) -->
<dependency>
<groupId>top.continew.admin</groupId>