# 租户字段添加指南 ## 概述 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 { // ... 其他字段 ... /** * 租户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 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 implements WorkcaseService { @Override public List listByStatus(String status) { // 租户拦截器会自动添加 tenant_id 条件 return this.list(new QueryWrapper() .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 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 listAllTenants() { // 使用 @InterceptorIgnore 注解忽略租户拦截 return workcaseMapper.selectAllWithoutTenant(); } } // Mapper 接口 @Mapper public interface WorkcaseMapper extends BaseMapper { @InterceptorIgnore(tenantLine = "true") @Select("SELECT * FROM tb_workcase") List selectAllWithoutTenant(); } ``` ### 6.2 定时任务中的租户处理 ```java @Component public class WorkcaseScheduledTask { @Autowired private WorkcaseService workcaseService; /** * 定时任务:为每个租户执行任务 */ @Scheduled(cron = "0 0 2 * * ?") public void processAllTenants() { // 获取所有租户列表 List 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 tenant1List = workcaseService.list(); // 模拟租户2的用户 mockTenant(2L); List 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索引 - [ ] 测试租户隔离功能 - [ ] 测试跨租户查询权限 - [ ] 文档更新和团队培训