Commit 8d74885e by luoqi

docs(refactor): 权限模型三概念分离 + org_units 推到 P2 + 去子表冗余

- 新增 §7.5 权限模型:功能权限(角色,代码级,保持)/ 数据范围(应绑 org 子树,
  非角色逃生门)/ 数据共享策略(集团级结构配置)三者正交,现状把前两者搅在一起
- org_units 整体推到 P2:P0/P1 用 source_unit 字符串列 + 集团级品牌共享策略即可,
  不需要建树;org_units 只在多级组织/层级数据范围/rollup 时才引入
- 子表不冗余 source_unit:召回 SQL 与 plan 列表本就 join patient,过滤近乎免费
- scope 分期标注:brandSharingPolicy=P1,orgUnitSubtree=P2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent d7d0f342
......@@ -228,26 +228,51 @@ if (policy.shareAcrossBrand === false) where.brand = scope.brand;
- token(`AccessTokenPayload`,`packages/types/src/schemas/auth.ts`):`{ sub, hostId, tenantId, clinicIds[], role, permissions, dictionary }`
- scope(`tenant-scope.decorator.ts`):`{ hostId, tenantId, clinicIds[], userId }`,锚单 tenant。
### 目标
- token 新增:`orgUnitSubtree: string[]`(用户可达的 org 节点子树)、`brandSharingPolicy?`
> **分期**:`brandSharingPolicy`(数据共享策略)是 **P1**;`orgUnitSubtree`(数据范围迁 org 子树)是 **P2**(随 org_units)。
> **P0/P1 的 scope 基本不变**(只是 tenant 语义变集团 + P1 加一个 `brandSharingPolicy`);下面是 **P2 完成态**的目标形状。
### 目标(P2 完成态)
- token 新增:`brandSharingPolicy?`(**P1**)、`orgUnitSubtree: string[]`(**P2**)。
- scope 扩形:
```ts
interface TenantScopeContext {
hostId: string;
tenantId: string; // = 集团
orgUnitSubtree: string[]; // 新增:org 子树(集团/品牌/区域/诊所角色)
clinicIds: string[]; // 现状保留:org 子树里的诊所叶子(派生)
clinicIds: string[]; // 现状保留(P2 后 = org 子树里的诊所叶子,派生)
userId: string;
brandSharingPolicy?: { shareAcrossBrand?: boolean; visibleBrands?: string[] }; // 新增
brandSharingPolicy?: { shareAcrossBrand?: boolean; visibleBrands?: string[] }; // P1
orgUnitSubtree?: string[]; // P2:org 子树(集团/品牌/区域/诊所层级角色)
}
```
- `jwt.strategy.ts:18` 校验加 `orgUnitSubtree` 非空。
- `auth.service.ts` 签发 + mock 登录预设:品牌 slug → **集团 tenantId + brand + org 子树**
- **集团级角色**(看全部品牌)= org 子树挂 group 节点 + `shareAcrossBrand=true`,自然实现,无需破墙。
- (P2)`jwt.strategy.ts:18` 校验加 `orgUnitSubtree` 非空。
- `auth.service.ts` 签发 + mock 登录预设:品牌 slug → **集团 tenantId + source_unit**(P1);+ org 子树(P2)。
- **集团级角色**(看全部品牌)= P1 用 `shareAcrossBrand=true`;P2 后 = org 子树挂 group 节点,自然实现,无需破墙。
---
## 7.5 ⭐ 权限模型:三个正交概念(别再搅在一起)
PAC 现状把三件事搅在两样东西里(角色 + clinicIds 扁平),要拆清:
| 概念 | 管什么 | 现状 | 目标 | 阶段 |
|---|---|---|---|---|
| **功能权限** | 能做什么动作(分配/回收/看统计) | `Permission` 枚举 + `ROLE_PERMISSIONS` 静态码表 + `PermissionsGuard`(全代码,无表) | **保持代码级**(权限词表是产品功能决定的固定词汇,不该让租户改) | 不变 |
| **数据范围** | 能看哪些组织单元的数据 | `clinicIds[]` 扁平 + 角色逃生门 `PLAN_VIEW_ALL`(越过 clinic 看全 tenant) | **绑 org 位置(子树)**:挂哪个节点→看那个子树。替掉"两档"逃生门 | **P2** |
| **数据共享策略** | 集团内跨品牌通不通 | 不存在 | **集团级结构配置**(`brandSharingPolicy`),不绑角色不绑个人 | **P1** |
**判定规则(目标态)**:
> 能看到一条 plan = (它的 org 单元 ∈ 你的 org 子树) **OR** (集团共享策略允许 + 你有跨单元权限)
> 角色只决定动作(assign/recycle/stats),**不决定数据范围**。
**为什么"数据范围用角色"不合理**:现在只有"看本诊所 / 看全部"两档,正因为拿角色(`PLAN_VIEW_ALL`)当数据范围开关。"区域经理看本区 8 家诊所"这种中间档角色给不了,**org 子树才给得了** → 这正是 P2 引入 org_units 的真实驱动。
**P0/P1 不动权限**:功能权限保持代码级;数据范围维持现状(clinicIds 扁平 + 角色逃生门),只是 tenant 语义变集团;新增的只有集团级"品牌共享策略"一个配置。**数据范围迁 org 子树留到 P2**
> 未来若集团要**自定义角色**(如"区域经理"配权限集),再加薄表 `roles(tenant_id, name, permissions[])`。现在不需要。
---
## 8. clinic_id → org_units + 前端
## 8. clinic_id → org_units + 前端(**P2**)
### 后端
- 5 处 clinic 列保留(`target_clinic_id` / `clinic_id` / `executor_clinic_id` …),**额外挂 org_units 叶子**:
......@@ -281,24 +306,27 @@ interface TenantScopeContext {
## 10. 分期实施
| 阶段 | 内容 | 产出 |
|---|---|---|
| **P0 地基** | `patients.brand` + 唯一键 + tenant 重映射 + manifest 静态 group + upsert 落 brand | tenant=集团跑通,行为同现状 |
| **P1 隔离** | 品牌共享策略(默认 closed)+ Prisma 中间件兜底 + scope 加 orgUnitSubtree | 墙在集团、可配置共享、有纵深防御 |
| **P2 组织** | `org_units` 树 + clinic 挂叶 + 权限子树化 + 前端组织树筛选 | 集团/品牌/区域/诊所角色 + rollup |
| **P3(独立)** | MPI:姓名+生日+性别(+身份证)跨品牌归一 golden 客户 | 集团级统一患者 |
| 阶段 | 内容 | 产出 | 用 org_units? |
|---|---|---|---|
| **P0 地基** | `patients.source_unit` + 唯一键 + tenant 重映射 + manifest(tenant_map→集团 + identity_namespace_field)+ 4 处 upsert | tenant=集团跑通,行为同现状 | ❌ |
| **P1 隔离** | 集团级**品牌共享策略**(默认 closed,一个配置)+ Prisma `$extends` 兜底 | 墙在集团、可配置共享、有纵深防御 | ❌ |
| **P2 组织(按需)** | `org_units` 树 + clinic 挂叶 + **数据范围迁 org 子树**(替掉 PLAN_VIEW_ALL 逃生门)+ rollup + 前端组织树 | 集团/品牌/区域/诊所层级角色 + 任意层级 rollup | ✅ |
| **P3(独立)** | MPI:姓名+生日+性别(+身份证)跨品牌归一 golden 客户 | 集团级统一患者 | — |
P0+P1 即满足"租户=集团、集团数据隔离(跟原 SaaS 一致)、品牌共享可配置"的核心目标。P2 解决深组织/角色。P3 是另一个项目(对齐 CDP)。
- **P0+P1 不需要 org_units**,只用 `source_unit` 字符串列 + 集团级一个共享策略 + 维持现状 clinicIds 扁平权限,即满足"租户=集团、集团数据隔离(跟原 SaaS 一致)、品牌共享可配置"的核心目标。
- **P2 才引入 org_units**,且只在真要"多级组织 / 区域层级数据范围 / 任意层级 rollup"时做。加它不用回头改 P0/P1。
- P3 是另一个项目(对齐 CDP)。
---
## 11. 待决策 / 开放问题
1. **org_units 存不存 name**?建议不存(沿用"宿主字典"原则),但若要做集团级报表/树形 UI,可能需要 PAC 侧缓存名。→ 倾向:结构 PAC 拥有,名走字典。**待你定。**
2. **子表要不要冗余 source_unit 列**?查询过滤性能 vs 冗余。→ 倾向:`patient_facts`/`followup_plans` 冗余 source_unit,省 join;其余不加。**待你定。**
1. ~~org_units 存不存 name?~~ **✅ 已定:org_units 整体推到 P2**,P0/P1 不引入(见 §10);存名问题随之延后(届时倾向结构 PAC 拥有、名走字典)。
2. ~~子表冗余 source_unit?~~ **✅ 已定:不冗余**。召回 SQL 与 plan 列表本就 join patient,`p.source_unit` 过滤近乎免费,无需冗余。
3. ~~多集团 host 先锁 1:1?~~ **✅ 已定:支持多集团**,schema 本就支持(tenant_id=集团多值),代码直接按 1:N 实现。
4. **集团 UUID 谁定**?→ **✅ 已定:新发**集团 UUID,不复用品牌 UUID;品牌→集团归属在 manifest `tenant_map` 配置。
5. ~~纵深防御选哪个?~~ **✅ 已定:Prisma `$extends` + AsyncLocalStorage**,RLS 作 P2+ 硬化(见 §6.3)。
6. **权限模型**(见 §7.5):功能权限保持代码级;数据范围 P2 迁 org 子树;数据共享策略 P1 加集团级配置。**✅ 已定方向。**
---
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment