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

228 lines
7.9 KiB
Markdown

# 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<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
```java
// 理论上的实现
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. 性能优势
```java
@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. 安全优势
```java
// 调用方必须明确提供租户信息
List<SysUser> users = sysUserService.getUsersByOrgId(orgId, customerId);
```
**优势**:
- **显式租户控制**: 调用方必须明确指定租户,防止越权访问
- **权限边界清晰**: 在接口层面就确保了租户隔离
- **审计友好**: 日志中可以清楚看到租户信息
### 3. 业务逻辑清晰
```java
public Result<SysUserMapVO> 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<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. **架构一致性**保证所有多租户接口的统一性
这个设计虽然增加了一个参数,但换来了更好的安全性、性能和维护性,是值得的架构权衡。