Skip to content

Commit ed6dd65

Browse files
author
小熊
authored
feat(plugin/tenant): 新增多租户插件模块 (#175)
1 parent 72493f8 commit ed6dd65

70 files changed

Lines changed: 3539 additions & 65 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

continew-common/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,5 +167,18 @@
167167
<groupId>top.continew.starter</groupId>
168168
<artifactId>continew-starter-extension-crud-mp</artifactId>
169169
</dependency>
170+
171+
<!-- 多租户组件-->
172+
<dependency>
173+
<groupId>top.continew.starter</groupId>
174+
<artifactId>continew-starter-extension-tenant-mp</artifactId>
175+
</dependency>
176+
177+
<!-- Dynamic Datasource(基于 Spring Boot 的快速集成多数据源的启动器) -->
178+
<dependency>
179+
<groupId>com.baomidou</groupId>
180+
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
181+
</dependency>
182+
170183
</dependencies>
171184
</project>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.admin.common.config.properties;
18+
19+
import lombok.Data;
20+
import org.springframework.boot.context.properties.ConfigurationProperties;
21+
import org.springframework.stereotype.Component;
22+
import top.continew.starter.core.constant.PropertiesConstants;
23+
24+
import java.util.List;
25+
26+
/**
27+
* @description: 多租户配置
28+
* @author: 小熊
29+
* @create: 2024-11-29 12:05
30+
*/
31+
@Component
32+
@ConfigurationProperties(prefix = PropertiesConstants.TENANT)
33+
@Data
34+
public class TenantProperties {
35+
36+
private boolean enabled;
37+
38+
private List<Long> ignoreMenus;
39+
40+
}

continew-common/src/main/java/top/continew/admin/common/constant/CacheConstants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ public class CacheConstants {
7171
*/
7272
public static final String DATA_IMPORT_KEY = "SYSTEM" + DELIMITER + "DATA_IMPORT" + DELIMITER;
7373

74+
/**
75+
* 数据连接键前缀
76+
*/
77+
public static final String DB_CONNECT_KEY_PREFIX = "DB_CONNECT" + DELIMITER;
78+
79+
/**
80+
* 租户信息前缀
81+
*/
82+
public static final String TENANT_KEY = "TENANT" + DELIMITER;
83+
7484
private CacheConstants() {
7585
}
7686
}

continew-common/src/main/java/top/continew/admin/common/constant/SysConstants.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,31 @@ public class SysConstants {
8484
*/
8585
public static final String LOGOUT_URI = "/auth/logout";
8686

87+
/**
88+
* 描述类字段后缀
89+
*/
90+
public static final String DESCRIPTION_FIELD_SUFFIX = "String";
91+
92+
/**
93+
* 租户数据库前缀
94+
*/
95+
public static final String TENANT_DB_PREFIX = "tenant_";
96+
97+
/**
98+
* 默认租户
99+
*/
100+
public static final String DEFAULT_TENANT = "0";
101+
102+
/**
103+
* 默认数据源
104+
*/
105+
public static final String DEFAULT_DATASOURCE = "master";
106+
107+
/**
108+
* 租户管理员角色编码
109+
*/
110+
public static final String TENANT_ADMIN_CODE = "tenant_admin";
111+
87112
private SysConstants() {
88113
}
89114
}

continew-common/src/main/java/top/continew/admin/common/context/UserContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public class UserContext implements Serializable {
8080
*/
8181
private Set<RoleContext> roles;
8282

83-
/**
83+
/*
8484
* 客户端类型
8585
*/
8686
private String clientType;
@@ -90,6 +90,11 @@ public class UserContext implements Serializable {
9090
*/
9191
private String clientId;
9292

93+
/**
94+
* 租户 ID
95+
*/
96+
private Long tenantId;
97+
9398
public UserContext(Set<String> permissions, Set<RoleContext> roles, Integer passwordExpirationDays) {
9499
this.permissions = permissions;
95100
this.setRoles(roles);
@@ -129,4 +134,5 @@ public boolean isPasswordExpired() {
129134
}
130135
return this.pwdResetTime.plusDays(this.passwordExpirationDays).isBefore(LocalDateTime.now());
131136
}
137+
132138
}

continew-common/src/main/java/top/continew/admin/common/context/UserContextHolder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,11 @@ public static boolean isAdmin() {
180180
StpUtil.checkLogin();
181181
return getContext().isAdmin();
182182
}
183+
184+
/**
185+
* 获取租户ID
186+
*/
187+
public static Long getTenantId() {
188+
return ExceptionUtils.exToNull(() -> getContext().getTenantId());
189+
}
183190
}

continew-plugin/continew-plugin-open/src/main/java/top/continew/admin/open/service/AppService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ public interface AppService extends BaseService<AppResp, AppDetailResp, AppQuery
5555
* @return 应用信息
5656
*/
5757
AppDO getByAccessKey(String accessKey);
58+
5859
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>top.continew.admin</groupId>
8+
<artifactId>continew-plugin</artifactId>
9+
<version>${revision}</version>
10+
</parent>
11+
12+
<artifactId>continew-plugin-tenant</artifactId>
13+
<description>多租户插件</description>
14+
<dependencies>
15+
<dependency>
16+
<groupId>top.continew.admin</groupId>
17+
<artifactId>continew-system</artifactId>
18+
</dependency>
19+
</dependencies>
20+
21+
</project>
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.admin.tenant.controller;
18+
19+
import cn.dev33.satoken.annotation.SaCheckPermission;
20+
import cn.dev33.satoken.annotation.SaIgnore;
21+
import cn.dev33.satoken.annotation.SaMode;
22+
import cn.hutool.core.collection.ListUtil;
23+
import cn.hutool.core.util.StrUtil;
24+
import cn.hutool.extra.spring.SpringUtil;
25+
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
26+
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
27+
import io.swagger.v3.oas.annotations.Operation;
28+
import io.swagger.v3.oas.annotations.tags.Tag;
29+
import jakarta.validation.Valid;
30+
import lombok.AllArgsConstructor;
31+
import org.springframework.validation.annotation.Validated;
32+
import org.springframework.web.bind.annotation.*;
33+
import top.continew.admin.common.base.controller.BaseController;
34+
import top.continew.admin.common.config.properties.TenantProperties;
35+
import top.continew.admin.common.util.SecureUtils;
36+
import top.continew.admin.system.model.entity.MenuDO;
37+
import top.continew.admin.system.model.entity.user.UserDO;
38+
import top.continew.admin.system.model.req.user.UserPasswordResetReq;
39+
import top.continew.admin.system.service.*;
40+
import top.continew.admin.tenant.model.entity.TenantDO;
41+
import top.continew.admin.tenant.model.query.TenantQuery;
42+
import top.continew.admin.tenant.model.req.TenantLoginUserInfoReq;
43+
import top.continew.admin.tenant.model.req.TenantReq;
44+
import top.continew.admin.tenant.model.resp.*;
45+
import top.continew.admin.tenant.service.TenantDbConnectService;
46+
import top.continew.admin.tenant.service.TenantPackageService;
47+
import top.continew.admin.tenant.service.TenantService;
48+
import top.continew.starter.core.util.ExceptionUtils;
49+
import top.continew.starter.core.util.validation.CheckUtils;
50+
import top.continew.starter.core.util.validation.ValidationUtils;
51+
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
52+
import top.continew.starter.extension.crud.enums.Api;
53+
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
54+
import top.continew.admin.common.base.model.resp.BaseResp;
55+
import top.continew.starter.extension.crud.model.req.IdsReq;
56+
import top.continew.starter.extension.crud.model.resp.IdResp;
57+
import top.continew.starter.extension.tenant.TenantHandler;
58+
59+
import java.util.List;
60+
61+
/**
62+
* 租户管理 API
63+
*
64+
* @author 小熊
65+
* @since 2024/11/26 17:20
66+
*/
67+
@Tag(name = "租户管理 API")
68+
@RestController
69+
@AllArgsConstructor
70+
@CrudRequestMapping(value = "/tenant/user", api = {Api.PAGE, Api.GET, Api.CREATE, Api.UPDATE, Api.DELETE})
71+
public class TenantController extends BaseController<TenantService, TenantResp, TenantDetailResp, TenantQuery, TenantReq> {
72+
73+
private final TenantProperties tenantProperties;
74+
private final DeptService deptService;
75+
private final MenuService menuService;
76+
private final TenantPackageService packageService;
77+
private final RoleService roleService;
78+
private final UserService userService;
79+
private final TenantSysDataService tenantSysDataService;
80+
private final RoleMenuService roleMenuService;
81+
private final TenantDbConnectService dbConnectService;
82+
83+
@GetMapping("/common")
84+
@SaIgnore
85+
@Operation(summary = "多租户通用信息查询", description = "多租户通用信息查询")
86+
public TenantCommonResp common() {
87+
TenantCommonResp commonResp = new TenantCommonResp();
88+
commonResp.setIsEnabled(tenantProperties.isEnabled());
89+
commonResp.setAvailableList(baseService.getAvailableList());
90+
return commonResp;
91+
}
92+
93+
@Override
94+
@DSTransactional
95+
public IdResp<Long> create(TenantReq req) {
96+
//套餐菜单
97+
TenantPackageDetailResp detailResp = packageService.get(req.getPackageId());
98+
CheckUtils.throwIf(detailResp.getMenuIds().isEmpty(), "该套餐无可用菜单");
99+
List<MenuDO> menuRespList = menuService.listByIds(detailResp.getMenuIds());
100+
//租户添加
101+
IdResp<Long> baseIdResp = super.create(req);
102+
//在租户中执行数据插入
103+
SpringUtil.getBean(TenantHandler.class).execute(baseIdResp.getId(), () -> {
104+
//租户部门初始化
105+
Long deptId = deptService.initTenantDept(req.getName());
106+
//租户菜单初始化
107+
menuService.menuInit(menuRespList, 0L, 0L);
108+
//租户角色初始化
109+
Long roleId = roleService.initTenantRole();
110+
//角色绑定菜单
111+
roleMenuService.add(menuService.listAll(baseIdResp.getId()).stream().map(BaseResp::getId).toList(), roleId);
112+
//管理用户初始化
113+
Long userId = userService.initTenantUser(req.getUsername(), req.getPassword(), deptId);
114+
//用户绑定角色
115+
roleService.assignToUsers(roleId, ListUtil.of(userId));
116+
//租户绑定用户
117+
baseService.bindUser(baseIdResp.getId(), userId);
118+
});
119+
return baseIdResp;
120+
}
121+
122+
@Override
123+
public void delete(Long id) {
124+
SpringUtil.getBean(TenantHandler.class).execute(id, () -> {
125+
//系统数据清除
126+
tenantSysDataService.clear();
127+
});
128+
super.delete(id);
129+
}
130+
131+
@Override
132+
public void batchDelete(@Valid IdsReq ids) {
133+
for (Long id : ids.getIds()) {
134+
//在租户中执行数据清除
135+
SpringUtil.getBean(TenantHandler.class).execute(id, () -> {
136+
//系统数据清除
137+
tenantSysDataService.clear();
138+
});
139+
}
140+
super.batchDelete(ids);
141+
}
142+
143+
/**
144+
* 获取租户管理账号用户名
145+
*/
146+
@GetMapping("/loginUser/{tenantId}")
147+
@Operation(summary = "获取租户管理账号信息", description = "获取租户管理账号信息")
148+
@SaCheckPermission("tenant:user:editLoginUserInfo")
149+
public String loginUserInfo(@PathVariable Long tenantId) {
150+
TenantDO tenantDO = baseService.getTenantById(tenantId);
151+
CheckUtils.throwIfNull(tenantDO, "租户不存在");
152+
StringBuilder username = new StringBuilder();
153+
SpringUtil.getBean(TenantHandler.class).execute(tenantDO.getId(), () -> {
154+
UserDO userDO = userService.getById(tenantDO.getUserId());
155+
CheckUtils.throwIfNull(userDO, "租户管理用户不存在");
156+
username.append(userDO.getUsername());
157+
});
158+
return username.toString();
159+
}
160+
161+
/**
162+
* 租户管理账号信息更新
163+
*/
164+
@PutMapping("/loginUser")
165+
@Operation(summary = "租户管理账号信息更新", description = "租户管理账号信息更新")
166+
@SaCheckPermission("tenant:user:editLoginUserInfo")
167+
@DSTransactional
168+
public void editLoginUserInfo(@Validated @RequestBody TenantLoginUserInfoReq req) {
169+
TenantDO tenantDO = baseService.getTenantById(req.getTenantId());
170+
CheckUtils.throwIfNull(tenantDO, "租户不存在");
171+
SpringUtil.getBean(TenantHandler.class).execute(tenantDO.getId(), () -> {
172+
UserDO userDO = userService.getById(tenantDO.getUserId());
173+
CheckUtils.throwIfNull(userDO, "用户不存在");
174+
//修改用户名
175+
if (!req.getUsername().equals(userDO.getUsername())) {
176+
userService.update(Wrappers.lambdaUpdate(UserDO.class)
177+
.set(UserDO::getUsername, req.getUsername())
178+
.eq(BaseIdDO::getId, userDO.getId()));
179+
}
180+
//修改密码
181+
if (StrUtil.isNotEmpty(req.getPassword())) {
182+
String password = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword()));
183+
ValidationUtils.throwIfNull(password, "新密码解密失败");
184+
UserPasswordResetReq passwordResetReq = new UserPasswordResetReq();
185+
passwordResetReq.setNewPassword(password);
186+
userService.resetPassword(passwordResetReq, userDO.getId());
187+
}
188+
});
189+
}
190+
191+
/**
192+
* 查询所有租户套餐
193+
*/
194+
@GetMapping("/all/package")
195+
@Operation(summary = "查询所有租户套餐", description = "查询所有租户套餐")
196+
@SaCheckPermission(value = {"tenant:user:add", "tenant:user:update"}, mode = SaMode.OR)
197+
public List<TenantPackageResp> packageList() {
198+
return packageService.list(null, null);
199+
}
200+
201+
/**
202+
* 查询所有数据库连接
203+
*/
204+
@GetMapping("/all/dbConnect")
205+
@Operation(summary = "获取租户数据连接列表", description = "获取租户数据连接列表")
206+
@SaCheckPermission(value = {"tenant:user:add", "tenant:user:update"}, mode = SaMode.OR)
207+
public List<TenantDbConnectResp> dbConnectList() {
208+
return dbConnectService.list(null, null);
209+
}
210+
211+
}

0 commit comments

Comments
 (0)