505 lines
15 KiB
Markdown
505 lines
15 KiB
Markdown
|
|
# 租户字段添加指南
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
pigx 是一个多租户 SaaS 平台,所有业务数据需要通过租户ID(tenant_id)进行隔离。本指南详细说明如何为所有业务表添加租户字段并实现租户隔离。
|
|||
|
|
|
|||
|
|
## 1. 租户字段规范
|
|||
|
|
|
|||
|
|
### 1.1 字段定义
|
|||
|
|
```sql
|
|||
|
|
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 1.2 字段特性
|
|||
|
|
- **类型**:BIGINT
|
|||
|
|
- **非空**:NOT NULL
|
|||
|
|
- **默认值**:1(默认租户)
|
|||
|
|
- **索引**:建议添加索引以提高查询性能
|
|||
|
|
- **注释**:'租户ID'
|
|||
|
|
|
|||
|
|
## 2. 为现有表添加租户字段
|
|||
|
|
|
|||
|
|
### 2.1 通用 SQL 模板
|
|||
|
|
```sql
|
|||
|
|
-- 添加租户字段
|
|||
|
|
ALTER TABLE `表名`
|
|||
|
|
ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID'
|
|||
|
|
AFTER `某个字段`;
|
|||
|
|
|
|||
|
|
-- 添加索引
|
|||
|
|
ALTER TABLE `表名`
|
|||
|
|
ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
-- 如果需要复合索引(常用查询条件+租户)
|
|||
|
|
ALTER TABLE `表名`
|
|||
|
|
ADD INDEX `idx_status_tenant` (`status`, `tenant_id`);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 批量添加脚本
|
|||
|
|
```sql
|
|||
|
|
-- 工单模块
|
|||
|
|
ALTER TABLE `tb_workcase` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_workcase` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_workcase_process` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_workcase_process` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_workcase_device` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_workcase_device` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_chat_room` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_chat_room` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_chat_room_member` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_chat_room_member` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_chat_room_message` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_chat_room_message` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
-- AI模块
|
|||
|
|
ALTER TABLE `tb_agent` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_agent` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_chat` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_chat` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_chat_message` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_chat_message` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_knowledge` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_knowledge` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
-- 招标模块
|
|||
|
|
ALTER TABLE `tb_bidding_project` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_bidding_project` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_bidding_document` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_bidding_document` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
-- 消息模块
|
|||
|
|
ALTER TABLE `tb_message` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_message` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_message_range` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_message_range` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
|
|||
|
|
ALTER TABLE `tb_message_receiver` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
|
|||
|
|
ALTER TABLE `tb_message_receiver` ADD INDEX `idx_tenant_id` (`tenant_id`);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. 实体类添加租户字段
|
|||
|
|
|
|||
|
|
### 3.1 实体类模板
|
|||
|
|
```java
|
|||
|
|
import com.baomidou.mybatisplus.annotation.TableField;
|
|||
|
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
|||
|
|
|
|||
|
|
@Data
|
|||
|
|
@TableName("tb_workcase")
|
|||
|
|
public class TbWorkcase extends Model<TbWorkcase> {
|
|||
|
|
|
|||
|
|
// ... 其他字段 ...
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 租户ID
|
|||
|
|
*/
|
|||
|
|
@TableField(fill = FieldFill.INSERT)
|
|||
|
|
private Long tenantId;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 MyBatis-Plus 自动填充配置
|
|||
|
|
```java
|
|||
|
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
|||
|
|
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
|
|||
|
|
import org.apache.ibatis.reflection.MetaObject;
|
|||
|
|
import org.springframework.stereotype.Component;
|
|||
|
|
|
|||
|
|
@Component
|
|||
|
|
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void insertFill(MetaObject metaObject) {
|
|||
|
|
// 获取当前用户的租户ID
|
|||
|
|
PigxUser user = SecurityUtils.getUser();
|
|||
|
|
if (user != null) {
|
|||
|
|
// 自动填充租户ID
|
|||
|
|
this.strictInsertFill(metaObject, "tenantId", Long.class, user.getTenantId());
|
|||
|
|
} else {
|
|||
|
|
// 默认租户ID
|
|||
|
|
this.strictInsertFill(metaObject, "tenantId", Long.class, 1L);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 填充其他字段
|
|||
|
|
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
|
|||
|
|
this.strictInsertFill(metaObject, "createBy", String.class, user != null ? user.getUsername() : "system");
|
|||
|
|
this.strictInsertFill(metaObject, "delFlag", String.class, "0");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void updateFill(MetaObject metaObject) {
|
|||
|
|
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
|||
|
|
|
|||
|
|
PigxUser user = SecurityUtils.getUser();
|
|||
|
|
if (user != null) {
|
|||
|
|
this.strictUpdateFill(metaObject, "updateBy", String.class, user.getUsername());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. 配置租户拦截器
|
|||
|
|
|
|||
|
|
### 4.1 MyBatis-Plus 租户拦截器配置
|
|||
|
|
```java
|
|||
|
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
|||
|
|
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
|||
|
|
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
|
|||
|
|
import net.sf.jsqlparser.expression.Expression;
|
|||
|
|
import net.sf.jsqlparser.expression.LongValue;
|
|||
|
|
import net.sf.jsqlparser.schema.Column;
|
|||
|
|
import org.springframework.context.annotation.Bean;
|
|||
|
|
import org.springframework.context.annotation.Configuration;
|
|||
|
|
|
|||
|
|
import java.util.Arrays;
|
|||
|
|
import java.util.List;
|
|||
|
|
|
|||
|
|
@Configuration
|
|||
|
|
public class MybatisPlusConfig {
|
|||
|
|
|
|||
|
|
@Bean
|
|||
|
|
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
|||
|
|
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
|||
|
|
|
|||
|
|
// 添加租户拦截器
|
|||
|
|
interceptor.addInnerInterceptor(tenantLineInnerInterceptor());
|
|||
|
|
|
|||
|
|
// 添加分页拦截器等其他拦截器
|
|||
|
|
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
|||
|
|
|
|||
|
|
return interceptor;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private TenantLineInnerInterceptor tenantLineInnerInterceptor() {
|
|||
|
|
return new TenantLineInnerInterceptor(new TenantLineHandler() {
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public Expression getTenantId() {
|
|||
|
|
// 从当前用户获取租户ID
|
|||
|
|
PigxUser user = SecurityUtils.getUser();
|
|||
|
|
Long tenantId = user != null ? user.getTenantId() : 1L;
|
|||
|
|
return new LongValue(tenantId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public String getTenantIdColumn() {
|
|||
|
|
// 租户ID字段名
|
|||
|
|
return "tenant_id";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public boolean ignoreTable(String tableName) {
|
|||
|
|
// 忽略不需要租户隔离的表
|
|||
|
|
List<String> ignoreTables = Arrays.asList(
|
|||
|
|
"sys_user", // 系统用户表
|
|||
|
|
"sys_role", // 角色表
|
|||
|
|
"sys_menu", // 菜单表
|
|||
|
|
"sys_dict", // 字典表
|
|||
|
|
"sys_log", // 日志表
|
|||
|
|
"temp_user_mapping" // 临时映射表
|
|||
|
|
);
|
|||
|
|
return ignoreTables.contains(tableName);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. Service 层实现租户隔离
|
|||
|
|
|
|||
|
|
### 5.1 查询时自动添加租户条件
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class WorkcaseServiceImpl extends ServiceImpl<WorkcaseMapper, TbWorkcase>
|
|||
|
|
implements WorkcaseService {
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public List<TbWorkcase> listByStatus(String status) {
|
|||
|
|
// 租户拦截器会自动添加 tenant_id 条件
|
|||
|
|
return this.list(new QueryWrapper<TbWorkcase>()
|
|||
|
|
.eq("status", status));
|
|||
|
|
// 实际SQL: SELECT * FROM tb_workcase WHERE status = ? AND tenant_id = ?
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public TbWorkcase getById(String id) {
|
|||
|
|
// 自动添加租户条件,确保不会查询到其他租户的数据
|
|||
|
|
return super.getById(id);
|
|||
|
|
// 实际SQL: SELECT * FROM tb_workcase WHERE id = ? AND tenant_id = ?
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 保存时自动设置租户ID
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class WorkcaseServiceImpl extends ServiceImpl<WorkcaseMapper, TbWorkcase>
|
|||
|
|
implements WorkcaseService {
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
@Transactional(rollbackFor = Exception.class)
|
|||
|
|
public Boolean createWorkcase(TbWorkcase workcase) {
|
|||
|
|
// 通过 MetaObjectHandler 自动填充 tenant_id
|
|||
|
|
// 不需要手动设置
|
|||
|
|
return this.save(workcase);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果需要手动设置(不推荐)
|
|||
|
|
@Override
|
|||
|
|
public Boolean createWorkcaseManual(TbWorkcase workcase) {
|
|||
|
|
PigxUser user = SecurityUtils.getUser();
|
|||
|
|
workcase.setTenantId(user.getTenantId());
|
|||
|
|
workcase.setCreateBy(user.getUsername());
|
|||
|
|
workcase.setCreateTime(LocalDateTime.now());
|
|||
|
|
return this.save(workcase);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. 特殊场景处理
|
|||
|
|
|
|||
|
|
### 6.1 跨租户查询(管理员功能)
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class AdminService {
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private WorkcaseMapper workcaseMapper;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 管理员查询所有租户的数据
|
|||
|
|
* 需要特殊权限
|
|||
|
|
*/
|
|||
|
|
@PreAuthorize("@pms.hasPermission('admin_cross_tenant')")
|
|||
|
|
public List<TbWorkcase> listAllTenants() {
|
|||
|
|
// 使用 @InterceptorIgnore 注解忽略租户拦截
|
|||
|
|
return workcaseMapper.selectAllWithoutTenant();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Mapper 接口
|
|||
|
|
@Mapper
|
|||
|
|
public interface WorkcaseMapper extends BaseMapper<TbWorkcase> {
|
|||
|
|
|
|||
|
|
@InterceptorIgnore(tenantLine = "true")
|
|||
|
|
@Select("SELECT * FROM tb_workcase")
|
|||
|
|
List<TbWorkcase> selectAllWithoutTenant();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 定时任务中的租户处理
|
|||
|
|
```java
|
|||
|
|
@Component
|
|||
|
|
public class WorkcaseScheduledTask {
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private WorkcaseService workcaseService;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 定时任务:为每个租户执行任务
|
|||
|
|
*/
|
|||
|
|
@Scheduled(cron = "0 0 2 * * ?")
|
|||
|
|
public void processAllTenants() {
|
|||
|
|
// 获取所有租户列表
|
|||
|
|
List<Long> tenantIds = getTenantIds();
|
|||
|
|
|
|||
|
|
for (Long tenantId : tenantIds) {
|
|||
|
|
// 设置当前租户上下文
|
|||
|
|
TenantContextHolder.setTenantId(tenantId);
|
|||
|
|
try {
|
|||
|
|
// 执行业务逻辑
|
|||
|
|
processForTenant(tenantId);
|
|||
|
|
} finally {
|
|||
|
|
// 清除租户上下文
|
|||
|
|
TenantContextHolder.clear();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 异步任务中的租户传递
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class AsyncService {
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 异步任务需要传递租户ID
|
|||
|
|
*/
|
|||
|
|
@Async
|
|||
|
|
public void processAsync(Long tenantId, String workcaseId) {
|
|||
|
|
// 在异步线程中设置租户ID
|
|||
|
|
TenantContextHolder.setTenantId(tenantId);
|
|||
|
|
try {
|
|||
|
|
// 执行业务逻辑
|
|||
|
|
doProcess(workcaseId);
|
|||
|
|
} finally {
|
|||
|
|
TenantContextHolder.clear();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用异步方法前获取租户ID
|
|||
|
|
public void callAsync(String workcaseId) {
|
|||
|
|
PigxUser user = SecurityUtils.getUser();
|
|||
|
|
Long tenantId = user.getTenantId();
|
|||
|
|
processAsync(tenantId, workcaseId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 数据迁移时的租户处理
|
|||
|
|
|
|||
|
|
### 7.1 历史数据添加租户ID
|
|||
|
|
```sql
|
|||
|
|
-- 为历史数据设置默认租户ID
|
|||
|
|
UPDATE tb_workcase SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|||
|
|
UPDATE tb_chat_room SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|||
|
|
UPDATE tb_agent SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 根据部门映射租户
|
|||
|
|
```sql
|
|||
|
|
-- 如果有部门与租户的映射关系
|
|||
|
|
UPDATE tb_workcase w
|
|||
|
|
JOIN sys_dept d ON w.dept_id = d.dept_id
|
|||
|
|
SET w.tenant_id = d.tenant_id
|
|||
|
|
WHERE w.tenant_id = 1;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 8. 测试验证
|
|||
|
|
|
|||
|
|
### 8.1 租户隔离测试
|
|||
|
|
```java
|
|||
|
|
@SpringBootTest
|
|||
|
|
@RunWith(SpringRunner.class)
|
|||
|
|
public class TenantIsolationTest {
|
|||
|
|
|
|||
|
|
@Autowired
|
|||
|
|
private WorkcaseService workcaseService;
|
|||
|
|
|
|||
|
|
@Test
|
|||
|
|
@WithMockUser(authorities = {"workcase_view"})
|
|||
|
|
public void testTenantIsolation() {
|
|||
|
|
// 模拟租户1的用户
|
|||
|
|
mockTenant(1L);
|
|||
|
|
List<TbWorkcase> tenant1List = workcaseService.list();
|
|||
|
|
|
|||
|
|
// 模拟租户2的用户
|
|||
|
|
mockTenant(2L);
|
|||
|
|
List<TbWorkcase> tenant2List = workcaseService.list();
|
|||
|
|
|
|||
|
|
// 验证数据隔离
|
|||
|
|
assertNotEquals(tenant1List, tenant2List);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void mockTenant(Long tenantId) {
|
|||
|
|
PigxUser user = new PigxUser();
|
|||
|
|
user.setTenantId(tenantId);
|
|||
|
|
// 设置到安全上下文
|
|||
|
|
SecurityContextHolder.getContext().setAuthentication(
|
|||
|
|
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities())
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 SQL 验证
|
|||
|
|
```sql
|
|||
|
|
-- 验证所有表都有租户字段
|
|||
|
|
SELECT
|
|||
|
|
TABLE_NAME,
|
|||
|
|
COLUMN_NAME
|
|||
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|||
|
|
WHERE
|
|||
|
|
TABLE_SCHEMA = 'your_database'
|
|||
|
|
AND COLUMN_NAME = 'tenant_id'
|
|||
|
|
ORDER BY TABLE_NAME;
|
|||
|
|
|
|||
|
|
-- 验证数据分布
|
|||
|
|
SELECT
|
|||
|
|
tenant_id,
|
|||
|
|
COUNT(*) as record_count
|
|||
|
|
FROM tb_workcase
|
|||
|
|
GROUP BY tenant_id;
|
|||
|
|
|
|||
|
|
-- 验证索引
|
|||
|
|
SHOW INDEX FROM tb_workcase WHERE Column_name = 'tenant_id';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 9. 注意事项
|
|||
|
|
|
|||
|
|
### 9.1 性能考虑
|
|||
|
|
- 租户ID字段必须建立索引
|
|||
|
|
- 常用查询条件可以建立复合索引(如:status + tenant_id)
|
|||
|
|
- 大表可以考虑按租户分区
|
|||
|
|
|
|||
|
|
### 9.2 安全考虑
|
|||
|
|
- 确保租户拦截器正确配置
|
|||
|
|
- 敏感操作需要记录日志
|
|||
|
|
- 跨租户操作需要特殊权限
|
|||
|
|
|
|||
|
|
### 9.3 开发规范
|
|||
|
|
- 不要在代码中硬编码租户ID
|
|||
|
|
- 始终从 SecurityUtils 获取当前租户
|
|||
|
|
- 使用 MyBatis-Plus 的自动填充功能
|
|||
|
|
|
|||
|
|
### 9.4 数据备份
|
|||
|
|
- 迁移前备份原始数据
|
|||
|
|
- 记录租户ID映射关系
|
|||
|
|
- 保留回滚方案
|
|||
|
|
|
|||
|
|
## 10. 故障排查
|
|||
|
|
|
|||
|
|
### 10.1 常见问题
|
|||
|
|
|
|||
|
|
**问题1:查询不到数据**
|
|||
|
|
- 检查租户ID是否正确
|
|||
|
|
- 验证租户拦截器是否生效
|
|||
|
|
- 查看生成的SQL是否包含tenant_id条件
|
|||
|
|
|
|||
|
|
**问题2:插入数据失败**
|
|||
|
|
- 检查tenant_id字段是否为NOT NULL
|
|||
|
|
- 验证自动填充是否配置
|
|||
|
|
- 确认当前用户有租户信息
|
|||
|
|
|
|||
|
|
**问题3:跨租户数据泄露**
|
|||
|
|
- 检查是否所有查询都经过租户拦截
|
|||
|
|
- 验证忽略表配置是否正确
|
|||
|
|
- 审查自定义SQL是否包含租户条件
|
|||
|
|
|
|||
|
|
### 10.2 调试建议
|
|||
|
|
```yaml
|
|||
|
|
# application.yml - 开启SQL日志
|
|||
|
|
logging:
|
|||
|
|
level:
|
|||
|
|
com.pig4cloud.pigx.*.mapper: DEBUG
|
|||
|
|
com.baomidou.mybatisplus: DEBUG
|
|||
|
|
|
|||
|
|
# 查看实际执行的SQL
|
|||
|
|
mybatis-plus:
|
|||
|
|
configuration:
|
|||
|
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 11. 迁移检查清单
|
|||
|
|
|
|||
|
|
- [ ] 所有业务表添加 tenant_id 字段
|
|||
|
|
- [ ] 所有实体类添加 tenantId 属性
|
|||
|
|
- [ ] 配置 MyBatis-Plus 自动填充
|
|||
|
|
- [ ] 配置租户拦截器
|
|||
|
|
- [ ] 配置忽略表列表
|
|||
|
|
- [ ] 历史数据设置默认租户ID
|
|||
|
|
- [ ] 建立租户ID索引
|
|||
|
|
- [ ] 测试租户隔离功能
|
|||
|
|
- [ ] 测试跨租户查询权限
|
|||
|
|
- [ ] 文档更新和团队培训
|