# PII 分级加密扩展设计 > 日期: 2026-04-26 | 状态: 草案 | 作者: iven + Claude ## 目录 1. [背景与动机](#1-背景与动机) 2. [设计目标](#2-设计目标) 3. [当前状态分析](#3-当前状态分析) 4. [分级加密策略](#4-分级加密策略) 5. [加密基础设施提升 (erp-core)](#5-加密基础设施提升) 6. [每租户独立密钥管理](#6-每租户独立密钥管理) 7. [Entity 层改造](#7-entity-层改造) 8. [Service 层集成](#8-service-层集成) 9. [数据迁移](#9-数据迁移) 10. [边界场景处理](#10-边界场景处理) 11. [分步实施计划](#11-分步实施计划) 12. [测试策略](#12-测试策略) 13. [风险与缓解](#13-风险与缓解) --- ## 1. 背景与动机 HMS 健康管理平台当前仅加密了 `patient.id_number`(身份证号)一个字段,使用 AES-256-GCM + HMAC-SHA256 搜索索引。其余 30+ 个 PII 字段(患者姓名、电话、过敏史、病史摘要、咨询内容等)均为明文存储。 **驱动力:** - 体检中心/医疗机构在采购前会进行安全审计,明文 PII 是 dealbreaker - 《个人信息保护法》对医疗健康信息有特殊保护要求 - 多租户 SaaS 架构下,数据隔离需要从访问控制扩展到存储加密 - 三专家评审一致指出 PII 加密范围不足是高优合规风险 **约束:** - 系统前期不涉及实际医疗行为,PII 加密是合规门槛而非实时临床安全 - 保持可搜索性:患者姓名是高频搜索字段,不能因加密丧失搜索能力 - 性能可接受:加密/解密开销不能显著影响 API 响应时间 --- ## 2. 设计目标 | 目标 | 指标 | |------|------| | PII 字段加密覆盖 | 从 1 个字段扩展到 15+ 个高风险字段 | | 多租户密钥隔离 | 每租户独立 DEK,泄漏不影响其他租户 | | 搜索能力保持 | HMAC 精确搜索 + 姓名明文模糊搜索 | | 性能影响 | 单条解密 < 0.1ms,列表页不触发解密 | | 零停机迁移 | 现有明文数据逐步加密,不锁表 | --- ## 3. 当前状态分析 ### 已有加密能力 | 组件 | 位置 | 状态 | |------|------|------| | `HealthCrypto` | `crates/erp-health/src/crypto.rs` | AES-256-GCM + HMAC-SHA256 | | 身份证号加密 | `patient_service.rs` | 已上线 | | 脱敏管道 | `crates/erp-health/src/service/masking.rs` | `mask_id_number`, `mask_phone` | | AI 脱敏层 | `crates/erp-ai/src/sanitization/mod.rs` | DTO 去标识化 | | 密钥配置 | `crates/erp-server/config/default.toml` | 环境变量注入 | ### 明文高风险字段清单 **患者 (patient) — 8 个未加密 PII 字段:** - `name` — 姓名(决定不加密,保持搜索性) - `emergency_contact_phone` — 紧急联系人电话 - `allergy_history` — 过敏史 - `medical_history_summary` — 病史摘要 - `emergency_contact_name` — 紧急联系人姓名 - `birth_date` — 出生日期 - `blood_type` — 血型 - `notes` — 备注 **家庭成员 (patient_family_member) — 2 个:** - `phone` — 电话 - `name` — 姓名 **咨询消息 (consultation_message) — 1 个:** - `content` — 咨询/问诊内容 **随访记录 (follow_up_record) — 3 个:** - `result` — 随访结果 - `patient_condition` — 患者状况 - `medical_advice` — 医嘱 **诊断 (diagnosis) — 1 个:** - `notes` — 诊断备注 **医生档案 (doctor_profile) — 1 个:** - `license_number` — 执业证号 **透析记录 (dialysis_record) — 2 个:** - `symptoms` — 症状 JSON - `complication_notes` — 并发症记录 **化验报告 (lab_report) — 1 个:** - `doctor_notes` — 医生备注 --- ## 4. 分级加密策略 ### Tier 定义 | 级别 | 存储方式 | 搜索方式 | 展示方式 | 适用字段 | |------|---------|---------|---------|---------| | **Tier 1** | AES-256-GCM 加密 | HMAC-SHA256 精确匹配 | 解密后展示/脱敏 | 高敏感、可枚举的字段 | | **Tier 2** | 明文 | 直接 SQL LIKE/模糊 | API 脱敏展示 | 中敏感、高频搜索的字段 | | **Tier 3** | 明文 | 直接 SQL | 原样展示 | 非敏感或非 PII 字段 | ### Tier 1 — 加密存储字段 | Entity | 字段 | HMAC 搜索 | |--------|------|----------| | patient | `emergency_contact_phone` | `emergency_contact_phone_hash` | | patient | `allergy_history` | — | | patient | `medical_history_summary` | — | | patient_family_member | `phone` | `phone_hash` | | consultation_message | `content` | — | **咨询内容搜索说明:** `consultation_message.content` 加密后无法做全文搜索。替代方案:按咨询会话(consultation)维度搜索和过滤(通过状态、时间、医生等非加密字段),进入详情后查看具体消息内容。如未来需要全文搜索,可引入加密索引(如 Order-Preserving Encryption 或专用密文搜索引擎),但 V1 不实施。 | follow_up_record | `result` | — | | follow_up_record | `patient_condition` | — | | follow_up_record | `medical_advice` | — | | doctor_profile | `license_number` | `license_number_hash` | | diagnosis | `notes` | — | | dialysis_record | `complication_notes` | — | | dialysis_record | `symptoms` (JSON) | — | | lab_report | `doctor_notes` | — | **JSON 字段加密说明:** `dialysis_record.symptoms` 类型为 `serde_json::Value`,加密时先序列化为 JSON 字符串再加密,解密后反序列化回 `Value`。 **注:** `patient.id_number` 和 `id_number_hash` 已存在,无需改动。 ### Tier 2 — 明文 + API 脱敏 | Entity | 字段 | 脱敏规则 | |--------|------|---------| | patient | `emergency_contact_name` | 保留首字 + `**`(如 `张**`),两字姓名也适用(`张*`) | | patient | `birth_date` | 仅显示年份 | | patient_family_member | `name` | 保留首字 + `**`,同上 | ### Tier 3 — 明文 `name`, `gender`, `blood_type`, 数值型体征数据, 日期, 状态字段, `icd_code`, `diagnosis_name`, `notes`(patient 和 health_record 的备注字段,内容不可预测且非临床核心)等。 **分类澄清:** `notes` 字段在不同 Entity 中分级不同。`patient.notes` 归入 Tier 3(一般备注),`diagnosis.notes` 归入 Tier 1(临床诊断备注),`follow_up_record` 的 `result`/`patient_condition`/`medical_advice` 归入 Tier 1。具体分级以上面 Tier 1/2/3 表格为准。 **设计决策:患者姓名不加密。** 原因:医疗场景下姓名不是真正意义上的隐私(前台叫号、医生喊名),保持明文可搜索对运营效率至关重要。加密姓名会强制改变搜索交互模式(从"先搜索"变成"先选择"),代价过大。 --- ## 5. 加密基础设施提升 ### 5.1 从 HealthCrypto 到 PiiCrypto 将 `crates/erp-health/src/crypto.rs` 中的 `HealthCrypto` 提升到 `crates/erp-core/src/crypto/`: ``` crates/erp-core/src/crypto/ ├── mod.rs # PiiCrypto 公开接口 ├── engine.rs # AES-256-GCM 加密/解密引擎 ├── hmac.rs # HMAC-SHA256 搜索索引 ├── key_manager.rs # 每租户 DEK 管理 └── masking.rs # 脱敏管道(从 health/service/masking.rs 提升) ``` ### 5.2 PiiCrypto 接口设计 ```rust pub struct PiiCrypto { kek: [u8; 32], // Master KEK dek_cache: DashMap, // 租户 DEK 缓存 } struct CachedDek { dek: [u8; 32], version: u32, loaded_at: Instant, } impl PiiCrypto { /// 加密单个字段,返回 Base64(nonce + ciphertext + tag) pub fn encrypt(&self, dek: &[u8; 32], plaintext: &str) -> Result; /// 解密单个字段 pub fn decrypt(&self, dek: &[u8; 32], ciphertext: &str) -> Result; /// 生成 HMAC-SHA256 搜索索引 pub fn hmac_hash(&self, dek: &[u8; 32], value: &str) -> String; /// 批量解密(避免重复 DEK 加载) pub fn decrypt_batch(&self, dek: &[u8; 32], ciphertexts: &[String]) -> Result>; /// 获取/创建指定租户的 DEK pub async fn get_dek(&self, tenant_id: Uuid, db: &DatabaseConnection) -> Result; /// 轮换指定租户的 DEK pub async fn rotate_dek(&self, tenant_id: Uuid, db: &DatabaseConnection) -> Result<()>; } ``` ### 5.3 依赖关系变化 ``` Before: erp-health → (内部 crypto.rs) After: erp-core (crypto/) ← erp-health (调用 PiiCrypto) ← 未来其他模块 ``` ### 5.4 Cargo.toml 依赖变更 `erp-core/Cargo.toml` 需要新增: ```toml [dependencies] aes-gcm = "0.10" hmac = "0.12" sha2 = "0.10" base64 = "0.22" hex = "0.4" dashmap = "6" ``` `erp-health/Cargo.toml` 可移除 `aes-gcm`, `hmac`, `sha2`, `base64`, `hex`(已提升到 erp-core),改为依赖 `erp-core` 的 crypto 模块。 ### 5.5 erp-ai 依赖关系澄清 当前 erp-ai 通过 `HealthDataProvider` trait 获取**已脱敏**的 DTO(不含 name/phone/id_number),**不需要**直接访问加密数据。PiiCrypto 提升到 erp-core 后,erp-ai 不需要调用 PiiCrypto 解密。AI 脱敏层继续作为独立防线运作。 如果未来 AI 需要原始 PII 数据(如精确匹配患者),可通过新增 `HealthDataProviderWithPii` trait 提供,此时才需要 erp-ai 调用 PiiCrypto。 --- ## 6. 每租户独立密钥管理 ### 6.1 密钥层级 ``` Master KEK (Key Encryption Key) 来源: 环境变量 ERP__CRYPTO__KEK 用途: 加密保护所有租户的 DEK 格式: 32 字节 hex-encoded └── Per-Tenant DEK (Data Encryption Key) 来源: 首次使用时随机生成,用 KEK 加密后存入 DB 用途: 加密该租户的 PII 数据 格式: 32 字节随机 ``` ### 6.2 数据库表 ```sql CREATE TABLE tenant_crypto_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id), encrypted_dek VARCHAR(128) NOT NULL, -- AES-256-GCM 加密的 DEK key_version INTEGER NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID, updated_by UUID, deleted_at TIMESTAMPTZ, version INTEGER NOT NULL DEFAULT 1, UNIQUE(tenant_id, key_version) ); -- 项目规范要求所有表包含: id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version ``` ### 6.3 DEK 缓存策略 | 参数 | 值 | 原因 | |------|-----|------| | 最大缓存数 | 100 个租户 | 典型 SaaS 实例租户数 < 100 | | TTL | 5 分钟 | 平衡安全性和性能 | | 淘汰策略 | LRU | 简单有效 | | 强制刷新 | 密钥轮换后 | 确保新密钥立即生效 | 使用 `DashMap`(并发安全 HashMap)实现缓存,避免每次请求查询数据库。 ### 6.4 密钥轮换流程 1. 管理员调用 `POST /api/v1/admin/tenants/{id}/rotate-key` 2. 生成新 DEK,用 KEK 加密,写入 `tenant_crypto_keys` 表(version +1) 3. 后台任务启动: - 读取该租户所有加密数据 - 用旧 DEK 解密 → 用新 DEK 加密 - 更新 `key_version` 字段 - 分批处理(每批 100 条),每批提交一次 4. 轮换期间,读取侧根据每条记录的 `key_version` 字段精确选择对应 DEK,不做"尝试解密 + 回退"(避免掩盖数据篡改或密钥管理错误) 5. 全部完成后,标记旧 DEK `is_active = false` --- ## 7. Entity 层改造 ### 7.1 新增伴生字段 每个需要 HMAC 搜索的字段,在 SeaORM Entity 中新增 `_hash` 列: | Entity | 新增字段 | 迁移操作 | |--------|---------|---------| | patient | `emergency_contact_phone_hash: Option` | ALTER TABLE ADD COLUMN | | patient_family_member | `phone_hash: Option` | ALTER TABLE ADD COLUMN | | doctor_profile | `license_number_hash: Option` | ALTER TABLE ADD COLUMN | **HMAC hash 列索引:** 每个 `_hash` 列创建普通 B-tree 索引(`CREATE INDEX idx_{table}_{field}_hash ON {table}({field}_hash) WHERE {field}_hash IS NOT NULL`),确保 HMAC 搜索不会退化为全表扫描。 ### 7.2 Entity Model 变化示例 ```rust // crates/erp-health/src/entity/patient.rs // 改造前: pub struct Model { pub id_number: Option, // 已加密 pub id_number_hash: Option, // 已有 pub emergency_contact_phone: Option, // 明文 pub allergy_history: Option, // 明文 // ... } // 改造后: pub struct Model { pub id_number: Option, // 已加密 (不变) pub id_number_hash: Option, // 已有 (不变) pub emergency_contact_phone: Option, // 现在存储加密值 pub emergency_contact_phone_hash: Option, // 新增: HMAC 搜索索引 pub allergy_history: Option, // 现在存储加密值 // ... } ``` **重要:** Entity 字段类型不变(仍为 `Option`),只是存储内容从明文变为密文。这确保 SeaORM 查询层无需改动。 --- ## 8. Service 层集成 ### 8.1 加密/解密注入点 Service 层在每个 CRUD 操作中透明处理加密: | 操作 | 加密处理 | 解密处理 | |------|---------|---------| | Create | 加密 Tier 1 字段 + 生成 HMAC | — | | Update | 重新加密变更的 Tier 1 字段 + 更新 HMAC | — | | Get by ID | — | 解密 Tier 1 字段 | | List | — | 列表不展示加密字段(返回 None) | | Search | HMAC hash 替代明文搜索条件 | — | ### 8.2 PiiCrypto 注入方式 ```rust // erp-server/src/state.rs pub struct HealthState { pub pii_crypto: PiiCrypto, // 替换原来的 HealthCrypto // ... } // patient_service.rs pub async fn create_patient( db: &DatabaseConnection, crypto: &PiiCrypto, tenant_id: Uuid, input: CreatePatientRequest, ) -> Result { let dek = crypto.get_dek(tenant_id, db).await?; // 加密 Tier 1 字段 let encrypted_phone = input.emergency_contact_phone .map(|p| crypto.encrypt(&dek.key, &p)) .transpose()?; let phone_hash = input.emergency_contact_phone .map(|p| crypto.hmac_hash(&dek.key, &p)); // ... 插入数据库 } ``` --- ## 9. 数据迁移 ### 9.1 迁移策略 采用**渐进式迁移**,不锁表,不中断服务: 1. **部署新代码** — 新代码根据 `key_version` 字段判断:`NULL` = 明文(直接返回),有值 = 密文(解密后返回)。不依赖 Base64 格式检测(自由文本字段本身可能是合法 Base64)。 2. **后台迁移任务** — 逐条加密现有明文数据 3. **完成切换** — 所有数据加密后,移除明文兼容路径 ### 9.2 迁移任务设计 ```rust pub async fn migrate_pii_encryption( db: &DatabaseConnection, crypto: &PiiCrypto, tenant_id: Uuid, ) -> Result { let dek = crypto.get_dek(tenant_id, db).await?; let batch_size = 100; loop { // 读取一批未迁移的记录(key_version < current 或 key_version IS NULL) let batch = find_unencrypted_patients(db, tenant_id, batch_size).await?; if batch.is_empty() { break; } for patient in &batch { // 加密明文字段 let encrypted_phone = patient.emergency_contact_phone.as_ref() .map(|p| crypto.encrypt(&dek.key, p)) .transpose()?; let phone_hash = patient.emergency_contact_phone.as_ref() .map(|p| crypto.hmac_hash(&dek.key, p)); // 更新记录 update_patient_encrypted(db, patient.id, encrypted_phone, phone_hash, dek.version).await?; } sleep(Duration::from_millis(50)).await; // 避免压垮数据库 } } ``` ### 9.3 Entity 新增 key_version 字段 所有包含 Tier 1 字段的 Entity 新增 `key_version: Option` 列,用于: - 跟踪每条记录使用的 DEK 版本 - 支持渐进式迁移和轮换 - NULL 表示未加密(明文) ### 9.4 回滚计划 部署后如果需要回滚到旧代码: - **新创建的加密数据:** 旧代码无法读取(`key_version` 非空的记录)。回滚前需要运行"解密迁移"脚本,将加密数据还原为明文。 - **未迁移的明文数据:** 旧代码可正常读取。 - **回滚脚本:** 提供与加密迁移对称的解密迁移脚本,从最新 `key_version` 的 DEK 解密所有字段,清除 `key_version`(设为 NULL)。 - **回滚窗口:** 建议在 Phase A 部署后 24 小时内保持回滚能力,超过 24 小时确认无问题后可移除明文兼容路径。 --- ## 10. 边界场景处理 ### 10.1 列表页性能 **策略:** 列表页不返回 Tier 1 加密字段(设为 `None`),仅展示 Tier 2/3 字段。 这与当前 `model_to_resp` 的设计一致(列表中 `id_number` 已经返回 `None`),无需改变交互模式。 **前端影响评估:** 当前 `model_to_resp` 已经将 `emergency_contact_phone` 做脱敏处理(`mask_phone`)后返回。加密后,列表页的 `model_to_resp` 需要将该字段设为 `None`(因为不解密),详情页的 `model_to_resp_decrypted` 需要解密后再脱敏。前端列表页如果展示了 `emergency_contact_phone`,需要适配为"详情中查看"。`patient_family_member` 和 `doctor_profile` 同理。 ### 10.2 AI 分析管道 erp-ai **不直接调用 PiiCrypto**。它通过 `HealthDataProvider` trait(在 erp-core 中)获取已脱敏的 DTO,不含 name/phone/id_number 等原始 PII。PiiCrypto 对 erp-ai 完全透明——erp-health 在提供数据给 erp-ai 之前已经在 Service 层完成了解密+脱敏。AI 脱敏层(`SanitizationService`)作为二次防护,确保发送给 LLM 的数据不包含原始 PII。 ### 10.3 数据导出 导出操作需要独立的权限码(如 `patient.export`),Service 层解密后输出。审计日志记录所有导出操作。 ### 10.4 小程序端 小程序不直接处理加密数据。所有加密/解密在服务端完成,API 响应中的 Tier 2 字段做脱敏处理。 ### 10.5 跨租户数据泄漏 即使数据库层面发生泄漏(如 SQL 注入),攻击者没有 KEK 就无法解密 DEK,没有 DEK 就无法解密 PII 数据。HMAC hash 字段不可逆,不泄漏原始值。 --- ## 11. 分步实施计划 ### Phase A — 基础设施 + Patient 实体(2-3 天) **目标:** 加密基础设施落地,patient 实体全面加密。 | 步骤 | 内容 | 预估 | |------|------|------| | A1 | 提升加密基础设施到 erp-core/src/crypto/ | 0.5 天 | | A2 | 实现 `tenant_crypto_keys` 表 + DEK 管理 | 0.5 天 | | A3 | 修改 patient Entity(新增 hash 字段 + key_version) | 0.5 天 | | A4 | 修改 patient_service 集成 PiiCrypto | 0.5 天 | | A5 | 数据迁移脚本(patient 表现有数据) | 0.5 天 | | A6 | 单元测试 + 集成测试 | 0.5 天 | **验证标准:** - `cargo check` + `cargo test` 全部通过 - 创建新患者 → 数据库中 phone/allergy 为密文,hash 正确 - 查询患者详情 → API 返回解密后的明文 - 搜索电话号码 → 通过 HMAC hash 匹配 ### Phase B — 全 Entity 扩展 + 密钥轮换(2-3 天) **目标:** 所有 Tier 1 字段加密,管理端支持密钥轮换。 | 步骤 | 内容 | 预估 | |------|------|------| | B1 | consultation_message, follow_up_record 加密 | 0.5 天 | | B2 | patient_family_member, doctor_profile 加密 | 0.5 天 | | B3 | dialysis_record, lab_report 加密 | 0.5 天 | | B4 | 密钥轮换管理端点 + 后台任务 | 0.5 天 | | B5 | DEK 缓存(DashMap LRU) | 0.5 天 | | B6 | 全量集成测试 + 性能基准 | 0.5 天 | **验证标准:** - 所有 Tier 1 字段在数据库中为密文 - 所有现有 API 功能正常(透明加密/解密) - 密钥轮换后数据可正常读写 - 批量解密 50 条记录 < 10ms --- ## 12. 测试策略 ### 12.1 单元测试 | 测试类别 | 覆盖内容 | 数量 | |----------|---------|------| | 加密引擎 | encrypt/decrypt 正确性、随机 nonce 不可预测 | 5 | | HMAC 索引 | 相同输入相同输出、不同输入不同输出 | 3 | | 密钥管理 | DEK 生成/缓存/轮换 | 5 | | 脱敏管道 | 各脱敏规则正确性 | 4 | ### 12.2 集成测试 | 测试类别 | 覆盖内容 | 数量 | |----------|---------|------| | CRUD 加密流 | 创建→加密存储→查询→解密返回 | 8 | | HMAC 搜索 | 精确匹配搜索正确性 | 3 | | 多租户隔离 | 租户 A 的 DEK 无法解密租户 B 的数据 | 2 | | 密钥轮换 | 轮换前后数据可正常访问 | 2 | ### 12.3 性能测试 | 指标 | 基准 | 目标 | |------|------|------| | 单字段加密 | — | < 0.05ms | | 单字段解密 | — | < 0.05ms | | 50 条批量解密 | — | < 10ms | | HMAC 生成 | — | < 0.01ms | --- ## 13. 风险与缓解 | 风险 | 概率 | 影响 | 缓解措施 | |------|------|------|---------| | 加密后搜索性能下降 | 低 | 中 | HMAC 索引 + B-tree 索引 | | 迁移过程中服务中断 | 低 | 高 | 渐进式迁移 + 兼容读取 | | DEK 缓存一致性 | 中 | 中 | TTL + 轮换后强制刷新 | | 密钥泄漏 | 低 | 极高 | KEK 不存数据库 + 环境变量注入 + 泄漏恢复流程(见下) | | 加密字段误漏 | 中 | 中 | 代码审查 + 单元测试覆盖 | | KEK 泄漏灾难恢复 | 极低 | 极高 | 见 §14 灾难恢复 | --- ## 14. KEK 泄漏灾难恢复 **场景:** Master KEK 泄露(如环境变量配置文件意外提交到 Git)。 **恢复流程:** 1. **立即轮换 KEK** — 生成新 KEK,更新环境变量 2. **重新包装所有 DEK** — 用新 KEK 解密并重新加密每个租户的 DEK ```sql -- 不需要重新加密 PII 数据,只需重新加密 DEK UPDATE tenant_crypto_keys SET encrypted_dek = new_kek_encrypt(dek) WHERE is_active = true; ``` 3. **清除 DEK 缓存** — 强制所有实例从数据库重新加载 4. **轮换泄漏渠道** — 轮换所有可能暴露 KEK 的凭据(Git tokens、CI secrets、部署脚本) 5. **审计追溯** — 检查访问日志,确认是否有未授权的数据访问 **关键优势:** KEK 泄漏只需要重新包装 DEK(N 次轻量操作),不需要重新加密所有 PII 数据(可能数百万条记录)。这是 KEK/DEK 两层架构的核心价值。 **预防措施:** - KEK 通过 secrets manager 注入,不写入文件系统 - CI/CD 管道中扫描 KEK 值,防止意外提交 - `.gitignore` 排除 `.env` 文件 --- ## 附录:决策记录 | 决策 | 选项 | 选择 | 原因 | |------|------|------|------| | 加密范围 | 全面/分级/TDE | 分级 | 成本与收益平衡 | | 姓名是否加密 | 加密/不加密 | 不加密 | 医疗场景下姓名不属隐私,搜索性更重要 | | 基础设施位置 | health/core/独立 crate | erp-core | 通用性 + 简洁性平衡 | | 密钥策略 | 全局/per-tenant/渐进 | 每租户独立 | 安全隔离,泄漏影响面最小 | | 实施节奏 | 一次完成/分步/最小可用 | 分步 (A+B) | 风险分散,每步可验证 |