Files
java-template/SysOrgClosureService_CustomerId_Analysis.md
T
BrunoGaoandClaude 3248bb0a2a feat: 完成消息系统V2优化 - 基于user_id的高性能索引重构
## 主要更改

### 1. 数据库表结构增强
- **t_device_message_v2**: 新增7个关键字段
  - priority_level: 优先级数值(1-5)
  - channels: 分发渠道JSON
  - require_ack: 是否需要确认
  - metadata: 扩展元数据JSON
  - target_count: 目标用户总数
  - version: 乐观锁版本号

- **t_device_message_detail_v2**: 新增8个关键字段
  - distribution_id: 分发唯一ID
  - device_sn: 设备序列号
  - user_id: 用户ID(核心查询字段)
  - response_duration: 响应耗时
  - delivery_details: 分发详情JSON
  - client_info: 客户端信息JSON
  - location_info: 位置信息JSON
  - last_retry_time: 最后重试时间

### 2. 索引优化重构 (基于user_id)
- 删除原有device_sn相关索引
- 新建5个user_id高性能复合索引:
  - idx_customer_user_time: 客户+用户+时间
  - idx_customer_user_status: 客户+用户+状态
  - idx_customer_user_type: 客户+用户+类型
  - idx_user_org_priority: 用户+组织+优先级
  - idx_user_status_time: 用户+状态+时间

- detail表新建4个user_id相关索引:
  - uk_message_user_target: 唯一约束
  - idx_customer_user_status: 客户+用户+状态
  - idx_user_channel_time: 用户+渠道+时间
  - idx_user_delivery_status: 用户+分发状态

### 3. 数据迁移和初始化
- 自动生成distribution_id
- 根据urgency设置priority_level
- 初始化默认channels配置
- 保持现有数据完整性

### 4. 性能提升预期
- 用户消息查询: 预计10-20倍性能提升
- 确认状态查询: 支持快速人员清单检索
- 存储空间: JSON字段替代冗余VARCHAR
- 查询覆盖: 所有user_id查询都能命中索引

### 5. 文档和脚本
- 完整的V2优化SQL脚本
- 消息机制设计文档
- 数据库备份(22MB)
- 性能验证查询

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2025-09-15 07:56:03 +08:00

7.9 KiB

SysOrgClosureService 为什么需要 customerId 参数分析

🔍 核心问题分析

你提出了一个很重要的架构设计问题:为什么 SysOrgClosureService.findAllDescendants 需要 customerId 作为入参,不能仅凭 orgId 查询?

📊 数据库表结构分析

sys_org_closure 表结构

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
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重复问题

场景示例

-- 可能的数据情况
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 查询会发生什么?

-- 🚫 错误查询 (没有租户隔离)
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. 索引性能优化

-- 当前的索引设计
KEY `idx_closure_ancestor` (`ancestor_id`, `customer_id`)

分析:

  • 联合索引 (ancestor_id, customer_id) 比单独的 ancestor_id 索引更高效
  • 查询时同时过滤租户ID,减少了扫描的数据量
  • 利用索引覆盖优化,避免回表查询

2. 数据库分区策略

-- 闭包表可能按 customer_id 进行分区
PARTITION BY HASH(customer_id) PARTITIONS 10;

优势:

  • 每个租户的数据物理隔离
  • 查询时只需访问特定分区
  • 提高并发性能和数据安全性

💡 替代方案分析

方案1: 仅使用 orgId 查询

// 假设的实现
List<SysOrgUnits> 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

// 理论上的实现
List<SysOrgUnits> 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. 性能优势

@Cacheable(value = CACHE_NAME, key = "'all_descendants_' + #ancestorId + '_' + #customerId")
public List<SysOrgUnits> findAllDescendants(Long ancestorId, Long customerId) {
    // 直接使用联合索引,一次查询完成
    return sysOrgClosureMapper.findAllDescendants(ancestorId, customerId);
}

优势:

  • 单次查询: 避免了额外的 customerId 查询
  • 索引优化: 充分利用 (ancestor_id, customer_id) 联合索引
  • 缓存精确: 缓存键区分了不同租户,避免数据混乱

2. 安全优势

// 调用方必须明确提供租户信息
List<SysUser> users = sysUserService.getUsersByOrgId(orgId, customerId);

优势:

  • 显式租户控制: 调用方必须明确指定租户,防止越权访问
  • 权限边界清晰: 在接口层面就确保了租户隔离
  • 审计友好: 日志中可以清楚看到租户信息

3. 业务逻辑清晰

public Result<SysUserMapVO> getUsersByOrgId(
    @RequestParam String orgId,
    @RequestParam Long customerId) {  // 明确要求前端传递租户ID
    // 业务逻辑清晰:在指定租户下查询指定组织的用户
}

📋 设计决策总结

为什么需要 customerId 参数?

  1. 数据安全第一: 防止跨租户数据泄露
  2. 性能最优: 利用联合索引,避免额外查询
  3. 架构一致性: 所有多租户接口都遵循相同模式
  4. 业务清晰: 调用方明确知道在操作哪个租户的数据

替代方案的成本

方案 查询次数 安全性 性能 复杂度
当前方案 (需要customerId) 1次 高 优 低
仅orgId查询 2次 低 差 高
推导customerId 2次 中 差 中

🔮 架构建议

短期保持当前设计

当前的设计是正确的,因为:

  • 符合多租户架构最佳实践
  • 性能和安全性都达到最优
  • 代码逻辑清晰,维护成本低

可能的优化方向

如果确实想简化调用,可以考虑:

// 在上下文中获取当前用户的租户信息
@Component
public class TenantContextHolder {
    private static final ThreadLocal<Long> CURRENT_CUSTOMER_ID = new ThreadLocal<>();
    
    public static Long getCurrentCustomerId() {
        return CURRENT_CUSTOMER_ID.get();
    }
}

// 提供重载方法
public List<SysOrgUnits> 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. 架构一致性保证所有多租户接口的统一性

这个设计虽然增加了一个参数,但换来了更好的安全性、性能和维护性,是值得的架构权衡。