Commit b16f58b0 by luoqi

docs(auth): 登录对外契约(集团模型)

host→PAC 登录输入规范:host_id + group_id + user{id,name,role} +
source_units(品牌数据范围,空=不限)+ clinic_ids;PAC 派生 permissions/
dictionary,signAccessToken 签发新契约 token。mock 与真 host 共用 signAccessToken
+ 同一 token 形状,差别仅输入来源。含隔离保证映射。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 927021a0
# PAC 登录对外契约(集团模型)
> host 接入 PAC 的登录输入规范。mock 登录 = 本契约的开发期模拟实现(同一套派生 + token)。
> 关联:[tenant-group-org-model-refactor.md](tenant-group-org-model-refactor.md)
---
## 0. 一句话
host 在用户登录时,把「**身份 + 可见范围**」按本契约传给 PAC;PAC 派生权限、签发 token。
**host 只声明它唯一知道的(谁、什么角色、能看哪些品牌/诊所),结构性的东西 PAC 自己派生。**
---
## 1. 输入:Host → PAC(登录时)
```jsonc
{
"host_id": "jvs-dw", // 接入宿主(对应 PAC hosts.name)
"group_id": "ruier-grp", // 集团 = PAC 租户(稳定 slug,各环境一致;不是 UUID)
"user": {
"id": "wx_woqj...", // 宿主侧用户唯一 id
"name": "韩医生", // 展示名
"role": "leader" // staff | leader | admin(PAC 规范角色)
},
"source_units": ["瑞尔"], // 用户可见品牌(数据范围)。空数组 = 不限品牌(集团级角色)
"clinic_ids": ["C1","C2"] // 用户可达诊所
}
```
字段说明:
- **`group_id`**:集团 = 租户,稳定 slug(`ruier-grp`),**不用 UUID**(各环境一致,不写死)。
- **`source_units`**:用户能看哪些品牌。瑞尔员工=`["瑞尔"]`;集团管理员=`[]`(不限,看全集团)。
这就是「品牌共享/数据权限」——要不要跨品牌,由 host 在这里给。**PAC 不另设共享开关。**
- **`clinic_ids`**:诊所数据范围(plan 按 `target_clinic_id` 圈)。
- **`role`**:决定功能权限(能否分配/回收/看统计),PAC 用 `ROLE_PERMISSIONS` 静态码表派生。
> **可选增强(P2,需要时)**:host 若只知道「用户挂在哪个组织节点」(如区域),可改传
> `org_scope: ["R-east"]`,PAC 用 org_tree(yaml)下展成 `clinic_ids` + 上溯成 `source_units`。
> 当前最简契约让 host 直接给 `source_units` + `clinic_ids`,不依赖 org_tree。
---
## 2. PAC 派生(host 不用管)
| host 给的 | PAC 派生 | 怎么派生 |
|---|---|---|
| `group_id` | `tenantId` | 直接用 |
| `role` | `permissions` | `ROLE_PERMISSIONS[role]`(代码级,固定词表) |
| `source_units` | token.sourceUnits | 直接带(空=不限) |
| `clinic_ids` | token.clinicIds | 直接带 |
| (names) | dictionary | clinic/user 名走 host 字典;品牌/集团名走 yaml |
---
## 3. 输出:Token(`AccessTokenPayload`)
```jsonc
{
"sub": "wx_woqj...", "hostId": "<uuid>",
"tenantId": "ruier-grp", // 集团
"sourceUnits": ["瑞尔"], // 品牌数据范围(空=不限)
"clinicIds": ["C1","C2"],
"role": "leader", "permissions": ["plan:view_all", ...],
"dictionary": { "clinics": {...}, "users": {...} },
"iat": ..., "exp": ...
}
```
每请求 → `tenant-scope.interceptor` 取出 → `scope`(供 service + Prisma 纵深防御)。
**契约 token 构造器 = `auth.service.signAccessToken(payload)`** —— mock 登录、refresh 重签、
未来真 host 端点,**都调它**,单一来源。
---
## 4. 实现现状
| 角色 | 实现 | 输入来源 |
|---|---|---|
| **mock 登录**(dev) | ✅ 已实现,产出本契约 token | 预设:brand slug → group_id/source_units/clinic_ids |
| **真 host SSO**(prod) | host 接入时实现(薄适配器) | host SSO → 本契约字段 → `signAccessToken` |
mock 和真 host **共用 `signAccessToken` + 同一 token 形状**;差别仅在"输入来源"。
→ 真 host 接入 = 写一个「host SSO 数据 → 契约字段」的映射 + 一个端点调 `signAccessToken`,**输出不变**
---
## 5. 隔离保证(token 怎么落到数据范围)
- **集团隔离(硬)**:`tenantId` 进每张表唯一键 + 每条查询 where;Prisma 纵深防御扩展对漏写的读查询自动注入 `tenantId`(ALS 上下文)。
- **品牌范围(数据权限)**:`sourceUnits` → 患者粒度过滤(搜索/列表/by-id + 扩展自动注入 source_unit 模型)。空 = 不限(集团级角色)。
- **诊所范围**:`clinicIds` → plan 按 `target_clinic_id` 圈。
- **功能权限**:`role → permissions`,`PermissionsGuard` 路由级校验。
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