## 主要更改 ### 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 <noreply@anthropic.com>
7.9 KiB
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`) -- ⚠️ 联合索引
);
关键发现
- 唯一约束包含租户ID:
uk_closure_ancestor_descendant索引包含了customer_id - 所有索引都包含租户ID: 所有查询索引都设计为租户隔离
- 数据隔离设计: 相同的
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);
}
问题:
- 额外查询开销: 需要先查
sys_org_units获取customer_id - 安全风险: 无法保证查询的
orgId属于当前用户的租户 - 索引效率低: 无法利用
(ancestor_id, customer_id)联合索引 - 缓存复杂性: 缓存键无法区分不同租户的相同orgId
方案2: 从 orgId 推导 customerId
// 理论上的实现
List<SysOrgUnits> findAllDescendants(Long orgId) {
// 1. 先查询获取 customerId
Long customerId = sysOrgUnitsMapper.getCustomerIdByOrgId(orgId);
// 2. 再执行闭包查询
return sysOrgClosureMapper.findAllDescendants(orgId, customerId);
}
问题:
- N+1 查询问题: 每次都需要额外查询获取
customerId - 性能下降: 两次数据库访问 vs 一次访问
- 一致性风险: 两次查询之间数据可能发生变化
- 权限检查复杂: 需要额外验证用户是否有权访问该 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 参数?
- 数据安全第一: 防止跨租户数据泄露
- 性能最优: 利用联合索引,避免额外查询
- 架构一致性: 所有多租户接口都遵循相同模式
- 业务清晰: 调用方明确知道在操作哪个租户的数据
替代方案的成本
| 方案 | 查询次数 | 安全性 | 性能 | 复杂度 |
|---|---|---|---|---|
| 当前方案 (需要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 参数是必要且正确的设计决策,主要原因:
- 多租户数据隔离是系统安全的基本要求
- 性能优化需要利用包含租户ID的联合索引
- 业务清晰性要求调用方明确指定操作范围
- 架构一致性保证所有多租户接口的统一性
这个设计虽然增加了一个参数,但换来了更好的安全性、性能和维护性,是值得的架构权衡。