ai-engineering-init 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/skill-forced-eval.js +46 -62
- package/.claude/settings.json +10 -1
- package/.claude/skills/api-development/SKILL.md +179 -130
- package/.claude/skills/architecture-design/SKILL.md +102 -212
- package/.claude/skills/backend-annotations/SKILL.md +166 -220
- package/.claude/skills/bug-detective/SKILL.md +225 -186
- package/.claude/skills/code-patterns/SKILL.md +127 -244
- package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.claude/skills/crud-development/SKILL.md +226 -307
- package/.claude/skills/data-permission/SKILL.md +131 -202
- package/.claude/skills/database-ops/SKILL.md +158 -355
- package/.claude/skills/error-handler/SKILL.md +224 -285
- package/.claude/skills/file-oss-management/SKILL.md +174 -169
- package/.claude/skills/git-workflow/SKILL.md +123 -341
- package/.claude/skills/json-serialization/SKILL.md +121 -137
- package/.claude/skills/performance-doctor/SKILL.md +83 -89
- package/.claude/skills/redis-cache/SKILL.md +134 -185
- package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
- package/.claude/skills/security-guard/SKILL.md +168 -276
- package/.claude/skills/sms-mail/SKILL.md +266 -228
- package/.claude/skills/social-login/SKILL.md +257 -195
- package/.claude/skills/tenant-management/SKILL.md +172 -188
- package/.claude/skills/utils-toolkit/SKILL.md +214 -222
- package/.claude/skills/websocket-sse/SKILL.md +251 -172
- package/.claude/skills/workflow-engine/SKILL.md +178 -250
- package/.codex/skills/api-development/SKILL.md +179 -130
- package/.codex/skills/architecture-design/SKILL.md +102 -212
- package/.codex/skills/backend-annotations/SKILL.md +166 -220
- package/.codex/skills/bug-detective/SKILL.md +225 -186
- package/.codex/skills/code-patterns/SKILL.md +127 -244
- package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.codex/skills/crud-development/SKILL.md +226 -307
- package/.codex/skills/data-permission/SKILL.md +131 -202
- package/.codex/skills/database-ops/SKILL.md +158 -355
- package/.codex/skills/error-handler/SKILL.md +224 -285
- package/.codex/skills/file-oss-management/SKILL.md +174 -169
- package/.codex/skills/git-workflow/SKILL.md +123 -341
- package/.codex/skills/json-serialization/SKILL.md +121 -137
- package/.codex/skills/performance-doctor/SKILL.md +83 -89
- package/.codex/skills/redis-cache/SKILL.md +134 -185
- package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
- package/.codex/skills/security-guard/SKILL.md +168 -276
- package/.codex/skills/sms-mail/SKILL.md +266 -228
- package/.codex/skills/social-login/SKILL.md +257 -195
- package/.codex/skills/tenant-management/SKILL.md +172 -188
- package/.codex/skills/utils-toolkit/SKILL.md +214 -222
- package/.codex/skills/websocket-sse/SKILL.md +251 -172
- package/.codex/skills/workflow-engine/SKILL.md +178 -250
- package/.cursor/hooks/cursor-skill-eval.js +66 -6
- package/.cursor/skills/api-development/SKILL.md +179 -130
- package/.cursor/skills/architecture-design/SKILL.md +102 -212
- package/.cursor/skills/backend-annotations/SKILL.md +166 -220
- package/.cursor/skills/bug-detective/SKILL.md +225 -186
- package/.cursor/skills/code-patterns/SKILL.md +127 -244
- package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.cursor/skills/crud-development/SKILL.md +226 -307
- package/.cursor/skills/data-permission/SKILL.md +131 -202
- package/.cursor/skills/database-ops/SKILL.md +158 -355
- package/.cursor/skills/error-handler/SKILL.md +224 -285
- package/.cursor/skills/file-oss-management/SKILL.md +174 -169
- package/.cursor/skills/git-workflow/SKILL.md +123 -341
- package/.cursor/skills/json-serialization/SKILL.md +121 -137
- package/.cursor/skills/performance-doctor/SKILL.md +83 -89
- package/.cursor/skills/redis-cache/SKILL.md +134 -185
- package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
- package/.cursor/skills/security-guard/SKILL.md +168 -276
- package/.cursor/skills/sms-mail/SKILL.md +266 -228
- package/.cursor/skills/social-login/SKILL.md +257 -195
- package/.cursor/skills/tenant-management/SKILL.md +172 -188
- package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
- package/.cursor/skills/websocket-sse/SKILL.md +251 -172
- package/.cursor/skills/workflow-engine/SKILL.md +178 -250
- package/AGENTS.md +49 -540
- package/CLAUDE.md +73 -119
- package/README.md +37 -6
- package/bin/index.js +5 -1
- package/package.json +1 -1
- package/src/skills/api-development/SKILL.md +179 -130
- package/src/skills/architecture-design/SKILL.md +102 -212
- package/src/skills/backend-annotations/SKILL.md +166 -220
- package/src/skills/bug-detective/SKILL.md +225 -186
- package/src/skills/code-patterns/SKILL.md +127 -244
- package/src/skills/collaborating-with-codex/SKILL.md +96 -113
- package/src/skills/crud-development/SKILL.md +226 -307
- package/src/skills/data-permission/SKILL.md +131 -202
- package/src/skills/database-ops/SKILL.md +158 -355
- package/src/skills/error-handler/SKILL.md +224 -285
- package/src/skills/file-oss-management/SKILL.md +174 -169
- package/src/skills/git-workflow/SKILL.md +123 -341
- package/src/skills/json-serialization/SKILL.md +121 -137
- package/src/skills/performance-doctor/SKILL.md +83 -89
- package/src/skills/redis-cache/SKILL.md +134 -185
- package/src/skills/scheduled-jobs/SKILL.md +187 -224
- package/src/skills/security-guard/SKILL.md +168 -276
- package/src/skills/sms-mail/SKILL.md +266 -228
- package/src/skills/social-login/SKILL.md +257 -195
- package/src/skills/tenant-management/SKILL.md +172 -188
- package/src/skills/utils-toolkit/SKILL.md +214 -222
- package/src/skills/websocket-sse/SKILL.md +251 -172
- package/src/skills/workflow-engine/SKILL.md +178 -250
- package/.claude/skills/skill-creator/LICENSE.txt +0 -202
- package/.claude/skills/skill-creator/SKILL.md +0 -479
- package/.claude/skills/skill-creator/agents/analyzer.md +0 -274
- package/.claude/skills/skill-creator/agents/comparator.md +0 -202
- package/.claude/skills/skill-creator/agents/grader.md +0 -223
- package/.claude/skills/skill-creator/assets/eval_review.html +0 -146
- package/.claude/skills/skill-creator/eval-viewer/generate_review.py +0 -471
- package/.claude/skills/skill-creator/eval-viewer/viewer.html +0 -1325
- package/.claude/skills/skill-creator/references/schemas.md +0 -430
- package/.claude/skills/skill-creator/scripts/__init__.py +0 -0
- package/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
- package/.claude/skills/skill-creator/scripts/generate_report.py +0 -326
- package/.claude/skills/skill-creator/scripts/improve_description.py +0 -248
- package/.claude/skills/skill-creator/scripts/package_skill.py +0 -136
- package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -103
- package/.claude/skills/skill-creator/scripts/run_eval.py +0 -310
- package/.claude/skills/skill-creator/scripts/run_loop.py +0 -332
- package/.claude/skills/skill-creator/scripts/utils.py +0 -47
|
@@ -1,179 +1,212 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tenant-management
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
通用多租户架构设计指南。涵盖字段隔离、Schema 隔离、物理隔离三种模式的对比与实现要点。
|
|
6
5
|
触发场景:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- 配置排除租户过滤的表
|
|
6
|
+
- 设计多租户 SaaS 系统架构
|
|
7
|
+
- 选择租户隔离方案
|
|
8
|
+
- 处理跨租户数据操作
|
|
11
9
|
- 定时任务遍历所有租户
|
|
10
|
+
- 配置 Redis / 缓存租户隔离
|
|
11
|
+
触发词:多租户、租户隔离、SaaS、tenantId、跨租户、租户切换、数据隔离、Schema隔离、物理隔离、字段隔离
|
|
12
|
+
注意:如果项目有专属技能(如 `leniu-data-permission` 或 `leniu-tenant`),优先使用专属版本。
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# 多租户架构设计指南
|
|
16
|
+
|
|
17
|
+
> 通用模板。如果项目有专属技能,优先使用。
|
|
18
|
+
|
|
19
|
+
## 设计原则
|
|
20
|
+
|
|
21
|
+
1. **隔离性**:租户数据必须完全隔离,不可跨租户访问。
|
|
22
|
+
2. **透明性**:业务代码尽量不感知多租户,由框架层自动处理。
|
|
23
|
+
3. **可扩展**:新增租户不需要改代码,配置即可上线。
|
|
24
|
+
4. **运维友好**:备份恢复、数据迁移应以租户为单位。
|
|
12
25
|
|
|
13
|
-
触发词:多租户、租户隔离、TenantEntity、TenantHelper、tenantId、跨租户、ignore租户、动态租户、租户切换、SaaS、数据隔离
|
|
14
26
|
---
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
## 三种隔离模式对比
|
|
29
|
+
|
|
30
|
+
| 维度 | 字段隔离 | Schema 隔离 | 物理隔离(独立库) |
|
|
31
|
+
|------|---------|------------|-----------------|
|
|
32
|
+
| **实现方式** | 每张表加 `tenant_id` 字段 | 每个租户独立 Schema | 每个租户独立数据库 |
|
|
33
|
+
| **隔离强度** | 低(逻辑隔离) | 中(Schema 级) | 高(物理隔离) |
|
|
34
|
+
| **数据安全** | 依赖应用层正确过滤 | 数据库级隔离 | 最安全 |
|
|
35
|
+
| **资源利用** | 高(共享一切) | 中(共享实例) | 低(独占资源) |
|
|
36
|
+
| **扩展性** | 单库上限制约 | 单实例 Schema 数限制 | 水平扩展灵活 |
|
|
37
|
+
| **运维复杂度** | 低 | 中 | 高 |
|
|
38
|
+
| **备份恢复** | 复杂(需过滤) | 简单(按 Schema) | 最简单(按库) |
|
|
39
|
+
| **租户定制** | 困难 | 可支持 | 完全独立 |
|
|
40
|
+
| **成本** | 最低 | 中等 | 最高 |
|
|
41
|
+
| **适用场景** | 中小 SaaS、租户量大 | 中等规模、需一定隔离 | 大客户、强合规需求 |
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
---
|
|
19
44
|
|
|
20
|
-
##
|
|
45
|
+
## 实现模式
|
|
21
46
|
|
|
22
|
-
###
|
|
47
|
+
### 模式一:字段隔离(最常见)
|
|
23
48
|
|
|
24
49
|
```java
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// BaseEntity 字段:createDept, createBy, createTime, updateBy, updateTime, params
|
|
50
|
+
// 1. Entity 携带 tenant_id
|
|
51
|
+
public class [你的租户基类] extends [你的基础Entity] {
|
|
52
|
+
private String tenantId;
|
|
53
|
+
}
|
|
30
54
|
|
|
31
|
-
@Data
|
|
32
|
-
@EqualsAndHashCode(callSuper = true)
|
|
33
55
|
@TableName("biz_order")
|
|
34
|
-
public class BizOrder extends
|
|
35
|
-
@TableId(value = "id")
|
|
56
|
+
public class BizOrder extends [你的租户基类] {
|
|
36
57
|
private Long id;
|
|
37
58
|
private String orderNo;
|
|
38
|
-
private String status;
|
|
39
59
|
}
|
|
40
|
-
```
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
55
|
-
del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志',
|
|
56
|
-
PRIMARY KEY (id),
|
|
57
|
-
KEY idx_tenant_id (tenant_id)
|
|
58
|
-
);
|
|
61
|
+
// 2. 数据库表必须含 tenant_id
|
|
62
|
+
// CREATE TABLE biz_order (
|
|
63
|
+
// id BIGINT NOT NULL,
|
|
64
|
+
// tenant_id VARCHAR(20) DEFAULT '000000' COMMENT '租户ID',
|
|
65
|
+
// order_no VARCHAR(64) NOT NULL,
|
|
66
|
+
// PRIMARY KEY (id),
|
|
67
|
+
// KEY idx_tenant_id (tenant_id)
|
|
68
|
+
// );
|
|
69
|
+
|
|
70
|
+
// 3. MyBatis 拦截器自动追加条件
|
|
71
|
+
// SELECT * FROM biz_order WHERE tenant_id = '000001' AND ...
|
|
72
|
+
// INSERT INTO biz_order (tenant_id, ...) VALUES ('000001', ...)
|
|
59
73
|
```
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## 二、TenantHelper 工具类
|
|
64
|
-
|
|
65
|
-
**位置**:`org.dromara.common.tenant.helper.TenantHelper`
|
|
66
|
-
|
|
67
|
-
### API 速查
|
|
68
|
-
|
|
69
|
-
| 方法 | 说明 | 返回值 |
|
|
70
|
-
|------|------|--------|
|
|
71
|
-
| `isEnable()` | 租户功能是否启用 | boolean |
|
|
72
|
-
| `getTenantId()` | 获取当前租户ID(优先动态租户) | String |
|
|
73
|
-
| `ignore(Runnable)` | 忽略租户执行(无返回值) | void |
|
|
74
|
-
| `ignore(Supplier<T>)` | 忽略租户执行(有返回值) | T |
|
|
75
|
-
| `dynamic(tenantId, Runnable)` | 切换租户执行(无返回值) | void |
|
|
76
|
-
| `dynamic(tenantId, Supplier<T>)` | 切换租户执行(有返回值) | T |
|
|
77
|
-
| `setDynamic(tenantId)` | 手动设置动态租户(需配合 clearDynamic) | void |
|
|
78
|
-
| `setDynamic(tenantId, true)` | 设置全局动态租户(跨请求,存 Redis) | void |
|
|
79
|
-
| `clearDynamic()` | 清除动态租户 | void |
|
|
80
|
-
|
|
81
|
-
### 核心用法
|
|
75
|
+
**核心工具类模式**:
|
|
82
76
|
|
|
83
77
|
```java
|
|
84
|
-
|
|
78
|
+
public class [你的租户工具类] {
|
|
85
79
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
return userMapper.selectList(null); // 不追加 tenant_id 条件
|
|
89
|
-
});
|
|
80
|
+
// 获取当前租户ID(从请求上下文/ThreadLocal)
|
|
81
|
+
public static String getTenantId() { ... }
|
|
90
82
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
// 忽略租户过滤(查全量数据)
|
|
84
|
+
public static <T> T ignore(Supplier<T> supplier) {
|
|
85
|
+
// 临时关闭拦截器的租户条件追加
|
|
86
|
+
try {
|
|
87
|
+
setIgnore(true);
|
|
88
|
+
return supplier.get();
|
|
89
|
+
} finally {
|
|
90
|
+
setIgnore(false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
95
93
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
// 切换到指定租户执行
|
|
95
|
+
public static void dynamic(String tenantId, Runnable runnable) {
|
|
96
|
+
String original = getTenantId();
|
|
97
|
+
try {
|
|
98
|
+
setTenantId(tenantId);
|
|
99
|
+
runnable.run();
|
|
100
|
+
} finally {
|
|
101
|
+
setTenantId(original);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 带返回值的租户切换
|
|
106
|
+
public static <T> T dynamic(String tenantId, Supplier<T> supplier) {
|
|
107
|
+
String original = getTenantId();
|
|
108
|
+
try {
|
|
109
|
+
setTenantId(tenantId);
|
|
110
|
+
return supplier.get();
|
|
111
|
+
} finally {
|
|
112
|
+
setTenantId(original);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
102
115
|
}
|
|
103
116
|
```
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
## 三、配置
|
|
118
|
+
**配置排除表**(不需要租户过滤的共享表):
|
|
108
119
|
|
|
109
120
|
```yaml
|
|
110
|
-
# application.yml
|
|
111
121
|
tenant:
|
|
112
122
|
enable: true
|
|
113
|
-
excludes:
|
|
123
|
+
excludes:
|
|
114
124
|
- sys_menu
|
|
115
125
|
- sys_dict_type
|
|
116
126
|
- sys_dict_data
|
|
117
|
-
- sys_oss_config
|
|
118
127
|
- sys_config
|
|
119
128
|
```
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
### 模式二:Schema 隔离
|
|
131
|
+
|
|
132
|
+
```java
|
|
133
|
+
// 动态数据源 / 动态 Schema 切换
|
|
134
|
+
public class TenantSchemaResolver {
|
|
135
|
+
public String resolveSchema(String tenantId) {
|
|
136
|
+
return "tenant_" + tenantId; // tenant_000001, tenant_000002
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 通过 AbstractRoutingDataSource 或 MyBatis 拦截器切换 Schema
|
|
141
|
+
// SET search_path TO tenant_000001; (PostgreSQL)
|
|
142
|
+
// USE tenant_000001; (MySQL)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 模式三:物理隔离(独立库)
|
|
146
|
+
|
|
147
|
+
```java
|
|
148
|
+
// 每个租户对应独立的数据源配置
|
|
149
|
+
// 通过 AbstractRoutingDataSource 或动态数据源框架切换
|
|
150
|
+
|
|
151
|
+
public class TenantDataSourceRouter extends AbstractRoutingDataSource {
|
|
152
|
+
@Override
|
|
153
|
+
protected Object determineCurrentLookupKey() {
|
|
154
|
+
return [你的租户工具类].getTenantId();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 典型用法
|
|
159
|
+
[你的租户工具类].doInTenant(tenantId, () -> {
|
|
160
|
+
// 自动路由到该租户的数据库
|
|
161
|
+
orderMapper.insert(order);
|
|
162
|
+
});
|
|
122
163
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
| `TenantSaTokenDao` | Sa-Token 会话租户隔离 |
|
|
164
|
+
[你的租户工具类].doInSystem(() -> {
|
|
165
|
+
// 路由到系统公共库
|
|
166
|
+
configMapper.selectList(null);
|
|
167
|
+
});
|
|
168
|
+
```
|
|
129
169
|
|
|
130
170
|
---
|
|
131
171
|
|
|
132
|
-
##
|
|
172
|
+
## 常见场景
|
|
133
173
|
|
|
134
174
|
### 场景 1:管理员查看所有租户数据
|
|
135
175
|
|
|
136
176
|
```java
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return TenantHelper.ignore(() -> userMapper.selectVoList(null));
|
|
177
|
+
public List<UserVo> listAllTenantUsers() {
|
|
178
|
+
return [你的租户工具类].ignore(() -> userMapper.selectVoList(null));
|
|
140
179
|
}
|
|
141
180
|
```
|
|
142
181
|
|
|
143
182
|
### 场景 2:跨租户数据同步
|
|
144
183
|
|
|
145
184
|
```java
|
|
146
|
-
public void syncConfigToAllTenants(
|
|
147
|
-
List<String> tenantIds =
|
|
185
|
+
public void syncConfigToAllTenants(Config config) {
|
|
186
|
+
List<String> tenantIds = [你的租户工具类].ignore(() ->
|
|
148
187
|
tenantMapper.selectList(null).stream()
|
|
149
|
-
.map(
|
|
188
|
+
.map(Tenant::getTenantId).collect(Collectors.toList())
|
|
150
189
|
);
|
|
151
190
|
for (String tenantId : tenantIds) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (existing == null) configMapper.insert(config);
|
|
155
|
-
else configMapper.updateById(config);
|
|
191
|
+
[你的租户工具类].dynamic(tenantId, () -> {
|
|
192
|
+
configMapper.insertOrUpdate(config);
|
|
156
193
|
});
|
|
157
194
|
}
|
|
158
195
|
}
|
|
159
196
|
```
|
|
160
197
|
|
|
161
|
-
### 场景 3
|
|
162
|
-
|
|
163
|
-
> 详细示例见 `references/tenant-scenarios.md`
|
|
198
|
+
### 场景 3:定时任务遍历所有租户
|
|
164
199
|
|
|
165
200
|
```java
|
|
166
201
|
@Scheduled(cron = "0 0 2 * * ?")
|
|
167
202
|
public void cleanupExpiredOrders() {
|
|
168
|
-
List<
|
|
169
|
-
tenantMapper.
|
|
203
|
+
List<Tenant> tenants = [你的租户工具类].ignore(() ->
|
|
204
|
+
tenantMapper.selectByStatus("ACTIVE")
|
|
170
205
|
);
|
|
171
|
-
for (
|
|
206
|
+
for (Tenant tenant : tenants) {
|
|
172
207
|
try {
|
|
173
|
-
|
|
174
|
-
orderMapper.
|
|
175
|
-
.eq(Order::getStatus, "CANCELLED")
|
|
176
|
-
.lt(Order::getCreateTime, DateUtils.addDays(new Date(), -30)));
|
|
208
|
+
[你的租户工具类].dynamic(tenant.getTenantId(), () -> {
|
|
209
|
+
orderMapper.deleteExpired(30);
|
|
177
210
|
});
|
|
178
211
|
} catch (Exception e) {
|
|
179
212
|
log.error("清理租户 {} 订单失败: {}", tenant.getTenantId(), e.getMessage());
|
|
@@ -182,9 +215,7 @@ public void cleanupExpiredOrders() {
|
|
|
182
215
|
}
|
|
183
216
|
```
|
|
184
217
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
## 五、Redis 缓存隔离
|
|
218
|
+
### 场景 4:Redis 缓存隔离
|
|
188
219
|
|
|
189
220
|
```
|
|
190
221
|
原始 Key: user:info:1001
|
|
@@ -192,97 +223,50 @@ public void cleanupExpiredOrders() {
|
|
|
192
223
|
实际 Key: 000001:user:info:1001 (租户 000001)
|
|
193
224
|
```
|
|
194
225
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
```java
|
|
198
|
-
import org.dromara.common.core.constant.GlobalConstants;
|
|
226
|
+
全局缓存(不隔离):使用特定前缀标识全局 Key。
|
|
199
227
|
|
|
200
|
-
|
|
201
|
-
String globalKey = GlobalConstants.GLOBAL_REDIS_KEY + "config:site_name";
|
|
202
|
-
RedisUtils.setCacheObject(globalKey, "网站名称");
|
|
228
|
+
---
|
|
203
229
|
|
|
204
|
-
|
|
205
|
-
RedisUtils.setCacheObject("user:info:" + userId, userInfo);
|
|
206
|
-
```
|
|
230
|
+
## 选型建议
|
|
207
231
|
|
|
208
|
-
|
|
232
|
+
| 维度 | 字段隔离 | Schema 隔离 | 物理隔离 |
|
|
233
|
+
|------|---------|------------|---------|
|
|
234
|
+
| 租户数量 | 大量(100+) | 中等(10-100) | 少量(<20) |
|
|
235
|
+
| 数据量级 | 单租户数据量小 | 中等 | 大 |
|
|
236
|
+
| 合规要求 | 一般 | 中等 | 严格(金融、政务) |
|
|
237
|
+
| 预算 | 有限 | 中等 | 充足 |
|
|
238
|
+
| 定制化 | 几乎不需要 | 少量 | 高度定制 |
|
|
209
239
|
|
|
210
240
|
---
|
|
211
241
|
|
|
212
|
-
##
|
|
213
|
-
|
|
214
|
-
### 1. 忘记继承 TenantEntity
|
|
242
|
+
## 常见错误
|
|
215
243
|
|
|
216
244
|
```java
|
|
217
|
-
//
|
|
218
|
-
public class BizOrder extends BaseEntity { }
|
|
219
|
-
|
|
220
|
-
// ✅ 继承 TenantEntity
|
|
221
|
-
public class BizOrder extends TenantEntity { }
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### 2. 数据库表缺少 tenant_id
|
|
225
|
-
|
|
226
|
-
```sql
|
|
227
|
-
-- ❌ 表没有 tenant_id
|
|
228
|
-
CREATE TABLE biz_order (id BIGINT NOT NULL, order_no VARCHAR(64));
|
|
229
|
-
|
|
230
|
-
-- ✅ 添加 tenant_id
|
|
231
|
-
CREATE TABLE biz_order (id BIGINT NOT NULL, tenant_id VARCHAR(20) DEFAULT '000000', order_no VARCHAR(64));
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### 3. setDynamic 后忘记 clearDynamic
|
|
245
|
+
// 1. Entity 忘记继承租户基类 / 表缺少 tenant_id(字段隔离模式)
|
|
246
|
+
public class BizOrder extends BaseEntity { } // 缺少 tenantId
|
|
247
|
+
// 表中也没有 tenant_id 字段 -> 数据不隔离
|
|
235
248
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
TenantHelper.setDynamic("000001");
|
|
249
|
+
// 2. 手动设置租户后忘记清理
|
|
250
|
+
[你的租户工具类].setTenantId("000001");
|
|
239
251
|
userMapper.insert(user);
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
// ✅ 推荐使用 dynamic() 方法(自动清理)
|
|
243
|
-
TenantHelper.dynamic("000001", () -> userMapper.insert(user));
|
|
244
|
-
```
|
|
252
|
+
// 忘记恢复原租户ID -> 后续请求使用错误租户
|
|
253
|
+
// 应使用 dynamic() 方法(自动恢复)
|
|
245
254
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
```java
|
|
249
|
-
// ❌ 事务内切换租户导致数据错乱
|
|
255
|
+
// 3. 事务中切换租户(字段隔离模式下可能数据错乱)
|
|
250
256
|
@Transactional
|
|
251
257
|
public void wrongMethod() {
|
|
252
|
-
orderMapper.insert(order);
|
|
253
|
-
|
|
258
|
+
orderMapper.insert(order); // 租户 A
|
|
259
|
+
[你的租户工具类].dynamic("B", () -> logMapper.insert(log)); // 租户 B
|
|
260
|
+
// 如果回滚,租户 B 的数据不会回滚(不同事务)
|
|
254
261
|
}
|
|
255
|
-
|
|
256
|
-
// ✅ 使用独立事务
|
|
262
|
+
// 应使用独立事务
|
|
257
263
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
|
258
|
-
public void
|
|
259
|
-
TenantHelper.dynamic(tenantId, () -> logMapper.insert(log));
|
|
260
|
-
}
|
|
261
|
-
```
|
|
264
|
+
public void saveToOtherTenant(String tenantId, Log log) { ... }
|
|
262
265
|
|
|
263
|
-
|
|
266
|
+
// 4. 共享表未排除租户过滤
|
|
267
|
+
// sys_dict_type 等共享表被加上 tenant_id 条件 -> 查不到数据
|
|
268
|
+
// 应在配置中排除这些表
|
|
264
269
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
tenant:
|
|
268
|
-
excludes:
|
|
269
|
-
- biz_order
|
|
270
|
-
|
|
271
|
-
# ✅ 只排除共享的系统表
|
|
272
|
-
tenant:
|
|
273
|
-
excludes:
|
|
274
|
-
- sys_menu
|
|
275
|
-
- sys_dict_type
|
|
270
|
+
// 5. 物理隔离模式下使用 tenant_id 字段(画蛇添足)
|
|
271
|
+
// 物理隔离已通过数据源区分租户,表中不需要 tenant_id
|
|
276
272
|
```
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
|
|
280
|
-
## 七、参考代码位置
|
|
281
|
-
|
|
282
|
-
| 类型 | 位置 |
|
|
283
|
-
|------|------|
|
|
284
|
-
| 租户基类 | `ruoyi-common/ruoyi-common-tenant/.../core/TenantEntity.java` |
|
|
285
|
-
| 租户助手 | `ruoyi-common/ruoyi-common-tenant/.../helper/TenantHelper.java` |
|
|
286
|
-
| 租户处理器 | `ruoyi-common/ruoyi-common-tenant/.../handle/PlusTenantLineHandler.java` |
|
|
287
|
-
| Redis Key 处理 | `ruoyi-common/ruoyi-common-tenant/.../handle/TenantKeyPrefixHandler.java` |
|
|
288
|
-
| 配置文件 | `ruoyi-admin/src/main/resources/application.yml` |
|