## 主要更改 ### 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]>
228 lines
7.9 KiB
Markdown
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. **架构一致性**保证所有多租户接口的统一性
|
|
|
|
这个设计虽然增加了一个参数,但换来了更好的安全性、性能和维护性,是值得的架构权衡。 |