# 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` 路由级校验。
