# SysOrgClosureService 为什么需要 customerId 参数分析 ## 🔍 核心问题分析 你提出了一个很重要的架构设计问题:为什么 `SysOrgClosureService.findAllDescendants` 需要 `customerId` 作为入参,不能仅凭 `orgId` 查询? ## 📊 数据库表结构分析 ### sys_org_closure 表结构 ```sql CREATE TABLE `sys_org_closure` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `ancestor_id` BIGINT NOT NULL COMMENT '祖先节点ID', `descendant_id` BIGINT NOT NULL COMMENT '后代节点ID', `depth` INT NOT NULL DEFAULT 0 COMMENT '层级深度', `customer_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID', -- ⚠️ 关键字段 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_closure_ancestor_descendant` (`ancestor_id`, `descendant_id`, `customer_id`), -- ⚠️ 包含租户ID KEY `idx_closure_ancestor` (`ancestor_id`, `customer_id`), -- ⚠️ 联合索引 KEY `idx_closure_descendant` (`descendant_id`, `customer_id`) -- ⚠️ 联合索引 ); ``` ### 关键发现 1. **唯一约束包含租户ID**: `uk_closure_ancestor_descendant` 索引包含了 `customer_id` 2. **所有索引都包含租户ID**: 所有查询索引都设计为租户隔离 3. **数据隔离设计**: 相同的 `ancestor_id` 和 `descendant_id` 在不同租户中可以存在 ## 🏢 多租户架构的必要性 ### 1. 租户数据隔离 ```sql -- 优化后的查询SQL SELECT org.* FROM sys_org_closure c INNER JOIN sys_org_units org ON c.descendant_id = org.id WHERE c.ancestor_id = #{ancestorId} AND c.customer_id = #{customerId} -- ⚠️ 租户隔离的关键 AND c.depth > 0 ``` **为什么必要?** - **数据安全**: 防止租户A查询到租户B的组织结构 - **业务隔离**: 不同租户的组织ID可能重复 - **性能优化**: 索引设计基于租户隔离,查询效率更高 ### 2. 组织ID重复问题 #### 场景示例 ```sql -- 可能的数据情况 sys_org_closure 表数据: | ancestor_id | descendant_id | depth | customer_id | |------------|---------------|-------|-------------| | 1000 | 1001 | 1 | 100 | -- 租户A的组织关系 | 1000 | 1002 | 1 | 100 | -- 租户A的组织关系 | 1000 | 1001 | 1 | 200 | -- 租户B的组织关系 (相同的org_id!) | 1000 | 1003 | 1 | 200 | -- 租户B的组织关系 ``` **如果只用 orgId 查询会发生什么?** ```sql -- 🚫 错误查询 (没有租户隔离) SELECT org.* FROM sys_org_closure c INNER JOIN sys_org_units org ON c.descendant_id = org.id WHERE c.ancestor_id = 1000; -- 会返回所有租户的数据! -- 结果: 租户A会看到租户B的组织数据 - 严重的安全问题! ``` ## 🔧 技术架构考虑 ### 1. 索引性能优化 ```sql -- 当前的索引设计 KEY `idx_closure_ancestor` (`ancestor_id`, `customer_id`) ``` **分析**: - 联合索引 `(ancestor_id, customer_id)` 比单独的 `ancestor_id` 索引更高效 - 查询时同时过滤租户ID,减少了扫描的数据量 - 利用索引覆盖优化,避免回表查询 ### 2. 数据库分区策略 ```sql -- 闭包表可能按 customer_id 进行分区 PARTITION BY HASH(customer_id) PARTITIONS 10; ``` **优势**: - 每个租户的数据物理隔离 - 查询时只需访问特定分区 - 提高并发性能和数据安全性 ## 💡 替代方案分析 ### 方案1: 仅使用 orgId 查询 ```java // 假设的实现 List findAllDescendants(Long orgId) { // 问题: 如何确定该 orgId 属于哪个租户? // 需要额外查询 sys_org_units 表获取 customer_id return sysOrgClosureMapper.findByOrgIdOnly(orgId); } ``` **问题**: 1. **额外查询开销**: 需要先查 `sys_org_units` 获取 `customer_id` 2. **安全风险**: 无法保证查询的 `orgId` 属于当前用户的租户 3. **索引效率低**: 无法利用 `(ancestor_id, customer_id)` 联合索引 4. **缓存复杂性**: 缓存键无法区分不同租户的相同orgId ### 方案2: 从 orgId 推导 customerId ```java // 理论上的实现 List findAllDescendants(Long orgId) { // 1. 先查询获取 customerId Long customerId = sysOrgUnitsMapper.getCustomerIdByOrgId(orgId); // 2. 再执行闭包查询 return sysOrgClosureMapper.findAllDescendants(orgId, customerId); } ``` **问题**: 1. **N+1 查询问题**: 每次都需要额外查询获取 `customerId` 2. **性能下降**: 两次数据库访问 vs 一次访问 3. **一致性风险**: 两次查询之间数据可能发生变化 4. **权限检查复杂**: 需要额外验证用户是否有权访问该 orgId ## 🎯 当前设计的优势 ### 1. 性能优势 ```java @Cacheable(value = CACHE_NAME, key = "'all_descendants_' + #ancestorId + '_' + #customerId") public List findAllDescendants(Long ancestorId, Long customerId) { // 直接使用联合索引,一次查询完成 return sysOrgClosureMapper.findAllDescendants(ancestorId, customerId); } ``` **优势**: - **单次查询**: 避免了额外的 `customerId` 查询 - **索引优化**: 充分利用 `(ancestor_id, customer_id)` 联合索引 - **缓存精确**: 缓存键区分了不同租户,避免数据混乱 ### 2. 安全优势 ```java // 调用方必须明确提供租户信息 List users = sysUserService.getUsersByOrgId(orgId, customerId); ``` **优势**: - **显式租户控制**: 调用方必须明确指定租户,防止越权访问 - **权限边界清晰**: 在接口层面就确保了租户隔离 - **审计友好**: 日志中可以清楚看到租户信息 ### 3. 业务逻辑清晰 ```java public Result getUsersByOrgId( @RequestParam String orgId, @RequestParam Long customerId) { // 明确要求前端传递租户ID // 业务逻辑清晰:在指定租户下查询指定组织的用户 } ``` ## 📋 设计决策总结 ### 为什么需要 customerId 参数? 1. **数据安全第一**: 防止跨租户数据泄露 2. **性能最优**: 利用联合索引,避免额外查询 3. **架构一致性**: 所有多租户接口都遵循相同模式 4. **业务清晰**: 调用方明确知道在操作哪个租户的数据 ### 替代方案的成本 | 方案 | 查询次数 | 安全性 | 性能 | 复杂度 | |------|----------|--------|------|--------| | 当前方案 (需要customerId) | 1次 | 高 | 优 | 低 | | 仅orgId查询 | 2次 | 低 | 差 | 高 | | 推导customerId | 2次 | 中 | 差 | 中 | ## 🔮 架构建议 ### 短期保持当前设计 当前的设计是正确的,因为: - 符合多租户架构最佳实践 - 性能和安全性都达到最优 - 代码逻辑清晰,维护成本低 ### 可能的优化方向 如果确实想简化调用,可以考虑: ```java // 在上下文中获取当前用户的租户信息 @Component public class TenantContextHolder { private static final ThreadLocal CURRENT_CUSTOMER_ID = new ThreadLocal<>(); public static Long getCurrentCustomerId() { return CURRENT_CUSTOMER_ID.get(); } } // 提供重载方法 public List findAllDescendants(Long ancestorId) { Long customerId = TenantContextHolder.getCurrentCustomerId(); if (customerId == null) { throw new BizException("无法获取当前租户信息"); } return findAllDescendants(ancestorId, customerId); } ``` 但这种方式增加了复杂性,当前的显式传参方式更安全可靠。 ## 🎉 结论 `SysOrgClosureService.findAllDescendants` 需要 `customerId` 参数是**必要且正确的设计决策**,主要原因: 1. **多租户数据隔离**是系统安全的基本要求 2. **性能优化**需要利用包含租户ID的联合索引 3. **业务清晰性**要求调用方明确指定操作范围 4. **架构一致性**保证所有多租户接口的统一性 这个设计虽然增加了一个参数,但换来了更好的安全性、性能和维护性,是值得的架构权衡。