---
name: uap-api
description: 在业务系统中对接统一认证平台（UAP / Unified Authentication Platform）：含 UAP_API_BASE / UAP_WEB_BASE 配置、三类认证（Simple Auth Body 签名 / 消息 Header 签名 / M2M Access Token）、HMAC-SHA256 签名规则、登录、SSO、用户查询（关键词搜索 / 按平台用户 ID 查档案）、映射同步、系统通知与广播。当需要实现 UAP 登录或 SSO、服务端调用 UAP 接口、用户映射同步、或给平台用户推送通知时使用。详细字段与示例以同目录 LLM_UAP_INTEGRATION.md 为准。
---

# UAP 对接

## 何时使用

业务系统需要对接 **UAP（统一认证平台）**：实现登录/SSO、服务端调用 M2M 接口、用户同步或消息通知。

## 向用户询问（必须）

同目录 `LLM_UAP_INTEGRATION.md` §2 中的 **App ID、App Secret、Access Token** 等均为占位，**不得**臆造或把文档示例当真实值。在编写或修改对接代码、配置 `.env` 前：

1. **若仓库内尚无有效配置**（如无 `.env`、或缺少关键变量）：在对话中**向用户询问**，请其从 **UAP 管理后台** 提供至少：
   - `APP_ID`
   - `APP_SECRET`（仅用于服务端；提醒用户勿贴到公开渠道）
   - `APP_ACCESS_TOKEN`（M2M 用，与 App Secret 不同）
2. **若使用独立域名或非文档默认环境**：询问或确认 `UAP_API_BASE`、`UAP_WEB_BASE`（须含 `/api/v1` 等约定见长文 §1）。
3. **涉及登录 / SSO / 回调**：询问或确认已在 UAP 后台配置的 **Redirect URI** 是否与实际访问地址一致（本地开发常见为 `http://localhost:端口/callback` 等）。
4. 用户已在本机填好 `.env` 且仅调试验证时，可读 `.env`（勿把内容写回仓库）；**仍缺项时继续询问用户**，不要留空提交或填假值。

## 权威文档（必须先读）

**完整接口、字段、流程、HTTP 示例与错误处理以项目内下文为准，不要凭通用 OAuth 经验臆测：**

| 路径 | 说明 |
|------|------|
| `./LLM_UAP_INTEGRATION.md`（与本 `SKILL.md` 同目录） | 独立成篇的对接说明，面向自动化阅读 |

实现具体接口前，用 Read 工具打开该文件对应章节，按文档中的路径与「参与签名的字段」编写代码。若该文件不存在，请向用户询问获取方式（通常由 UAP 运维方提供下载链接）。

## 两类基础地址

| 变量 | 含义 |
|------|------|
| `UAP_API_BASE` | HTTP(S) **API 根路径**，必须包含 **`/api/v1`** |
| `UAP_WEB_BASE` | 浏览器访问的**前端站点根**（跳转登录页等） |

接口路径均相对 `UAP_API_BASE` 拼接。登录页跳转示例（详见长文）：PC `{UAP_WEB_BASE}/login?app_id=...`，H5 `{UAP_WEB_BASE}/mobile/login?app_id=...`。

## 对接前配置（来自 UAP 管理后台）

| 变量 | 用途 |
|------|------|
| `APP_ID` | 应用标识 |
| `APP_SECRET` | **仅服务端** HMAC 签名用，**禁止**写入前端或提交仓库 |
| `APP_ACCESS_TOKEN` | M2M：`X-App-Access-Token`，**不是** App Secret |
| `REDIRECT_URIS` | 后台配置的回调列表；SSO / exchange 等依赖合法回调 |

环境变量放在 `.env`，示例在 `.env.example`；**绝不**把真实凭据写入 Skill、README 或提交的源码。

## 三类认证（勿混用）

| 方式 | 场景 | 要点 |
|------|------|------|
| **A. Simple Auth** | `/simple/login`、`/simple/validate`、`/simple/exchange` 等 | JSON Body 含 `timestamp`、`sign`；对**除 `sign` 外**参与字段按 ASCII 键名排序拼接后 HMAC |
| **B. 消息 Header 签名** | `POST /messages/` 等 | `X-App-Id`、`X-Timestamp`、`X-Sign`；待签串仅为 `app_id=<APP_ID>&timestamp=<TIMESTAMP>` |
| **C. M2M Access Token** | 用户搜索、**按平台用户 ID 查用户详情**、映射同步、全量拉取等 | `X-App-Access-Token: <APP_ACCESS_TOKEN>` |

## 签名算法要点（细节与每接口字段见长文第 4 节）

- 算法：**HMAC-SHA256**，密钥为 UTF-8 的 `APP_SECRET`，输出 **小写十六进制**（64 位）。
- **Simple Auth**：键名 **ASCII 升序**，`key1=value1&key2=value2`，**不含** `sign`；`None` 值通常不参与。
- **消息接口**：只对 `app_id` 与 `timestamp` 拼接签名；`X-Timestamp` 与参与签名的字符串一致。
- **时间戳**：Unix **秒**；服务端对时钟偏差约有 **300 秒**容差。

## 两种同名 `app_id` 的区分（易错点）

- 请求侧 `app_id` / `X-App-Id`：**字符串** App ID（如 `app_xxxxxxxx`），用于签名与鉴权。
- 响应体里 `MessageResponse.app_id`、`MappingResponse.app_id`：**整数**（应用数据库主键）。
- 两者**不可混用**。对接方通常只关心字符串 App ID；整数 `app_id` 仅作为消息/映射记录的内部标识读取。

## 工作流程建议

1. **识别意图**：用户说"对接一键登录 / SSO / 登录跳转"→ 走 [§A 一键登录](#a-一键登录--sso-redirect-sso)；说"推送 / 站内通知 / 广播"→ 走 [§B 消息通知](#b-消息通知系统通知--广播)；说"账号同步 / 用户拉取 / 建映射"→ 走 [§C 用户同步](#c-用户同步--映射绑定)。
2. **凭据与地址**：按上文「向用户询问」收集或确认；缺什么问什么，不要臆造。
3. **查对应章节**：按下文意图清单里的「长文锚点」打开 `LLM_UAP_INTEGRATION.md` 对应节，严格按「参与签名的字段」实现，再发请求。
4. **区分认证**：开始写签名前，先确认属于三类认证中的哪一种（Simple Auth Body / 消息 Header / M2M Token）。

## 按意图的最短操作清单（命中 skill 后必读）

> 只有把**前置确认**清单全部勾上，才能开始写代码；缺项先问用户。
>
> **三意图的依赖关系（务必先看）**：
>
> - §A 一键登录要**真正跳进本系统**，前提是目标用户手机号**在该 `APP_ID` 下已建立映射**——`POST /simple/validate` 响应里 `mapped_key` 必须非 `null`，业务侧才能据此定位本系统账号。
> - 也就是说：**§C 用户同步是 §A 一键登录的前置**。上线 SSO 之前，通常要先把本系统用户通过 `POST /apps/mapping/sync` 批量或按需推到 UAP 该 `APP_ID` 下。没有映射的手机号即使能在 UAP 登录，到 callback 侧也会拿到 `mapped_key=null`，无法一键进入本系统。
> - §B 若按 `app_user_id` 投递消息依赖映射存在；按平台 `receiver_id` 投递不依赖映射。**广播**的接收者为「当前 `APP_ID` 下有效映射 ∩ 平台活跃用户」，同样依赖映射已建立。

### A. 一键登录 / SSO（Redirect SSO）

**前置确认**（缺任一项不写代码，先向用户问）：

- [ ] 已拿到 `APP_ID` + `APP_SECRET`（Secret 仅用于后端签名）。
- [ ] 业务侧回调 URL（如 `https://biz.example.com/uap/callback`）已登记到 UAP 后台 `REDIRECT_URIS`。
- [ ] `UAP_API_BASE`（含 `/api/v1`）与 `UAP_WEB_BASE` 已确认。
- [ ] **映射前置**：目标用户手机号已在该 `APP_ID` 下建立映射（否则 `validate` 响应 `mapped_key=null`，无法落地到本系统账号）。若尚未同步，先走 §C 补齐；SSO 上线前建议批量同步一次全量。

**代码产出（对照长文 §6.1 流程 + §5.3 字段）**：

1. **前端跳登录页**：把浏览器导向 `{UAP_WEB_BASE}/login?app_id=<APP_ID>`（H5 用 `/mobile/login?app_id=<APP_ID>`）。
2. **后端 callback 路由**（如 `GET /uap/callback`）：从 query 读 `ticket`；为空直接 400。
3. **后端调用 `POST {UAP_API_BASE}/simple/validate`**：
   - Body 字段：`{ ticket, app_id, timestamp }`，`timestamp` 为当前 Unix **秒**。
   - **签名**：对 `app_id=<APP_ID>&ticket=<TICKET>&timestamp=<TS>`（键名 ASCII 升序，**不含** `sign`、**不含** `None`）做 **HMAC-SHA256**，密钥为 UTF-8 的 `APP_SECRET`，输出 **小写 64 位 hex**，写入 Body 的 `sign` 字段。
   - `Content-Type: application/json`，POST 最终 Body `{ ticket, app_id, timestamp, sign }`。
4. **处理响应 `TicketValidateResponse`**：
   - `valid=false` → 提示票据失效并跳回登录；
   - `valid=true` 且 `mapped_key` **非空** → 用 `mapped_key` 定位本系统账号、建立会话（cookie / JWT 由业务自定）；可取的辅助字段有 `user_id`（平台 ID）、`mobile`、`mapped_email`、`english_name`。
   - `valid=true` 但 `mapped_key=null`（该手机号在本 `APP_ID` 下**无映射**）→ **不可**直接放行。按业务选择：(a) 拒绝并提示联系管理员同步账号；(b) 若允许自助绑定，引导用户绑定本系统账号后调 §C 同步；(c) 管理员控制台触发批量 / 按手机号即时同步后再让用户重试。
   - 账号绑定策略见下文「账号与 `mapped_key`」。

**常见坑**：

- 用错 Secret：`sign` 必须用**消费票据的应用**（即当前 callback 所属应用）的 `APP_SECRET`。
- 时间戳非 Unix 秒，或偏差超 **300s** 容差。
- 签名时把 `sign` 自身也加进去、或没按 ASCII 升序。
- 回调 URL 未登记到 `REDIRECT_URIS`，导致跳转失败。

### B. 消息通知（三类场景）

> 消息通知按**业务语义**分三类，字段组合与前端会话样式**不同**，不要一把梭；识别意图后对号入座。
>
> | 场景 | 何时用 | 前端会话样式 |
> |------|-------|-------------|
> | **B-1 平台自动 → 用户** | 平台定时任务 / 触发器自动发：告警、工单未审核提醒、任务超时提醒、备份完成回执等 | 系统通知 |
> | **B-2 用户 → 用户**（业务流） | 人类用户在应用里操作触发，平台代发给另一人：OA 请假 / 报销 / 审批通过 / 任务下发 / 评论 @ 等 | 发起人的私信（会话聚合到 A） |
> | **B-3 应用内广播** | 面向本应用全体**已映射**用户：维护公告、版本须知等 | 系统通知（按应用维度分发） |

**前置确认（三类都要）**：

- [ ] 已拿到 `APP_ID` + `APP_SECRET`（服务端签名用）。
- [ ] 已识别场景属于 B-1 / B-2 / B-3 中哪一类。
- [ ] 非广播时接收者定位方式：平台 `user_id`（整数）或本应用 `app_user_id`（即 `mapped_key`，字符串）。
- [ ] 若是 **B-2**：**发起人本人**在该 `APP_ID` 下已有映射（否则 `sender_app_user_id` 会 404）。

**共用 Header 签名**：

- `X-App-Id: <APP_ID>` / `X-Timestamp: <TS>` / `X-Sign = HMAC-SHA256(APP_SECRET, "app_id=<APP_ID>&timestamp=<TS>")` → 小写 hex。
- 签名字段**只有** `app_id` + `timestamp` 两个键，不要套 Simple Auth 规则。

**共用端点**：`POST {UAP_API_BASE}/messages/`，`Content-Type: application/json`。

---

#### B-1 平台自动 → 用户（系统通知）

**特征**：由本系统定时任务 / 触发器自动生成，没有"人类发起者"；前端会话显示为**系统通知**样式。

**字段组合**：

- `type: "NOTIFICATION"`；
- `content_type: "TEXT"`（普通）或 `"USER_NOTIFICATION"`（需结构化 / 富文本时）；
- **不传** `sender_app_user_id` → `sender_id` 为空 → 显示系统通知；
- 接收者：`app_id + app_user_id` 或 `receiver_id`；
- 需点击跳转时配 `auto_sso:true + target_url`，平台会把 `action_url` 改写成 `/api/v1/simple/sso/jump?...`。

**Body 模板**：

```json
{
  "type": "NOTIFICATION",
  "content_type": "TEXT",
  "title": "有 5 单待审核",
  "content": "您有 5 条工单超过 24 小时未处理",
  "app_id": "<APP_ID>",
  "app_user_id": "<接收人 mapped_key>",
  "auto_sso": true,
  "target_url": "https://biz.example.com/tickets/pending",
  "action_text": "去审核"
}
```

**典型场景**：告警阈值触发、累计待办聚合提醒、定时汇总、系统事件回执（备份完成、任务完成）。

---

#### B-2 用户 → 用户（业务流通知）

**特征**：用户 A 在应用内触发动作（提交申请 / 指派任务 / 审批通过），平台代 A 给用户 B 发通知；前端会话**聚合到 A 的私信**，B 能回溯到谁发起的。

**字段组合（关键差异）**：

- `type: "NOTIFICATION"`（应用侧**仍用 NOTIFICATION**，**不要**改 `MESSAGE`——`MESSAGE` 是用户登录态自己发的私信，应用身份调用会被拒）；
- `content_type: "USER_NOTIFICATION"`（推荐：申请单样式，`content` 可传**对象**，服务端自动 JSON 序列化存储，前端可 `JSON.parse` 结构化渲染）；
- **必传** `sender_app_user_id`：发起人 A 在本 `APP_ID` 下的 `mapped_key`，平台解析成 `sender_id` 写入消息（无映射 → 404）；
- 接收者：`app_id + app_user_id`（B 的 `mapped_key`）或 `receiver_id`；
- 强烈建议配 `auto_sso:true + target_url`：点击消息 SSO 跳到业务详情页。

**Body 模板（OA 请假申请）**：

```json
{
  "type": "NOTIFICATION",
  "content_type": "USER_NOTIFICATION",
  "title": "请假申请待审批",
  "content": {
    "applicant": "张三",
    "days": 3,
    "reason": "年假",
    "request_id": "LEAVE-2026-000123"
  },
  "app_id": "<APP_ID>",
  "app_user_id": "<审批人 mapped_key>",
  "sender_app_user_id": "<申请人 mapped_key>",
  "auto_sso": true,
  "target_url": "https://oa.example.com/hr/leave/LEAVE-2026-000123",
  "action_text": "去审批"
}
```

**典型场景**：

- OA：请假 / 报销 / 加班 / 审批通过回执；
- 工单 / 任务：任务下发、完成反馈；
- 物料 / 采购：物料申请、到货提醒（业务人员触发）；
- 协作：@提及、评论回复。

---

#### B-3 应用内广播（独立功能）

**特征**：一次调用向**当前签名应用**下所有「存在有效用户映射且平台账号活跃」的用户各建一条通知；**仅应用身份可调**。

**约束（后端强制）**：

- `is_broadcast: true`；
- `type` **必须** `"NOTIFICATION"`（否则 403 "广播模式仅支持系统通知"）；
- **不**传 `receiver_id` / `app_user_id`；
- **不**传 `sender_app_user_id`（广播通常是应用级公告，显示系统通知样式）；
- 该应用下无可投递用户时返回 `400`（无有效映射活跃用户）。

**Body 模板**：

```json
{
  "is_broadcast": true,
  "type": "NOTIFICATION",
  "content_type": "TEXT",
  "title": "系统维护通知",
  "content": "今晚 22:00-24:00 进行系统维护，期间服务可能中断。"
}
```

**最佳实践：独立成一条"发广播"入口**：

1. 广播影响面极大，应在管理后台**单独设页面**（权限控制 + 二次确认弹窗），**不要**把它和 B-1 / B-2 的发消息接口混进同一个前端按钮或后端函数。
2. 留操作审计：记录发起管理员、广播时间、内容快照、影响用户数。
3. 节流 / 防重：前端禁用重复点击，后端按操作者+内容哈希做短时间内防重。

---

**三类速查表**：

| 维度 | B-1 平台→用户 | B-2 用户→用户 | B-3 广播 |
|------|--------------|--------------|----------|
| `type` | `NOTIFICATION` | `NOTIFICATION` | `NOTIFICATION`（强制） |
| `content_type` | `TEXT` 为主 | **`USER_NOTIFICATION`** | `TEXT` |
| `sender_app_user_id` | **不传** | **必传**（发起人 mapped_key） | **不传** |
| 接收者字段 | `app_user_id` / `receiver_id` | 同左 | `is_broadcast:true`，其他都不传（范围=本应用有效映射用户） |
| 前端会话归类 | 系统通知 | 发起人的私信会话 | 系统通知 |
| 触发方 | 定时任务 / 事件监听 | 用户在应用内操作 | 管理员手动（独立入口） |

**常见坑**：

- 把 Simple Auth 的「所有字段 ASCII 升序」规则套到消息接口（消息接口**仅** `app_id` + `timestamp` 参与签名）。
- 把响应里**整数** `app_id`（数据库主键）当请求参数回填。
- B-2 忘传 `sender_app_user_id` → 退化成 B-1 样式，OA 审批变"系统通知"、对话回溯不到谁发起；反之 B-1 / B-3 误传 `sender_app_user_id` → 变"用户私信"样式，归类错乱。
- B-2 发起人在 UAP 该 `APP_ID` 下**没有映射** → 404（先走 §C 建映射）。
- 应用身份想用 `type:"MESSAGE"` 代用户发私信——平台只允许 `NOTIFICATION`，应改走 B-2 的 `USER_NOTIFICATION`。
- 广播时误传 `receiver_id` / `app_user_id`，或把 `type` 写成 `MESSAGE` → 403 / 400。
- 把广播入口和 B-1 / B-2 合并 → 误点击一次影响该应用全体映射用户。

### C. 用户同步 / 映射绑定

> **用途与地位**：把本系统用户（以手机号定位）在 UAP 该 `APP_ID` 下建立 / 维护**映射**（`mobile ↔ mapped_key`）。这是 §A 一键登录能落地到本系统账号的**硬前置**——`/simple/validate` 响应里的 `mapped_key` 就来自这里的同步结果；同时也是 §B 按 `app_user_id` 投递消息的前提。**没有同步过的手机号，即便能在 UAP 登录，也拿不到本系统的身份。**

**前置确认**：

- [ ] 已拿到 `APP_ACCESS_TOKEN`（注意**不是** `APP_SECRET`，两者不可互换）。
- [ ] 本系统 `mapped_key` 取值策略已确定（见下文「账号与 `mapped_key`」）。

**代码产出（对照长文 §7）**：

1. **先查（二选一）**：
   - **有关键词**（手机号、姓名等）：`GET {UAP_API_BASE}/users/search?q=<mobile|name>&limit=20`，Header `X-App-Access-Token: <APP_ACCESS_TOKEN>`。
   - **已持有平台 `user_id`**（如 `POST /simple/validate`、`POST /apps/mapping/sync` 响应中的平台用户主键）：`GET {UAP_API_BASE}/users/m2m/by-platform-id/<user_id>`，**仅** Header `X-App-Access-Token`（路径中的 ID 是平台 `users` 表主键，**不是** `mapped_key`）。**勿**在同一请求中带有效的用户 `Authorization: Bearer`，否则鉴权优先解析为用户 JWT，本接口会返回 **403**「仅支持应用凭证访问」（见长文 **§7.2.1**）。
2. **再同步**：`POST {UAP_API_BASE}/apps/mapping/sync`，Header `X-App-Access-Token`，`Content-Type: application/json`：
   - **UPSERT**（新增 / 更新映射；若平台无此手机号会连同建用户）：`{ mobile, name, mapped_key, mapped_email?, is_active?, sync_action:"UPSERT" }`；
   - **DELETE**（仅删映射，不删平台用户）：`{ mobile, mapped_key, sync_action:"DELETE" }`。
3. **读响应**：关注 `new_user_created`（是否新建平台用户）、`generated_password`（新用户初始密码，可能存在）、`is_active`。

#### C.1 最佳实践：本地用户 CRUD 与 UAP 映射保持一致

> **目标**：本系统的「新建 / 编辑 / 删除用户」每一个动作，都**同步**更新 UAP 该 `APP_ID` 下的映射，让 SSO、消息投递、对账三者始终可用。

**① 新建本地用户（Create）— UAP 预填 + UPSERT**

1. 管理员在「新建用户」表单里输入**手机号**（或姓名 / 英文名关键词）。
2. 前端点「从 UAP 查询」→ 后端以 `X-App-Access-Token` 调 `GET /users/search?q=<mobile>&limit=1`。
3. 取返回首条的 `name` / `english_name` / `mobile` / `organization_name` 等**回填表单**，管理员确认或补录本系统专属字段（角色、权限、业务档案）。
4. 提交保存时（**同一工作单元内**）：
   - 先在本系统落库产生本地 `id`；
   - 随即调 `POST /apps/mapping/sync`（`UPSERT`）：`{ mobile, name, mapped_key: <本系统账号/id>, mapped_email?, is_active: true, sync_action: "UPSERT" }`；
   - 若本次**同时**新建了 UAP 平台用户（响应 `new_user_created: true`），把响应里的 `generated_password` 通过安全通道告知用户或提示管理员（切勿写日志 / 明文显示）。
5. **失败处理**：UAP 同步失败时二选一——回滚本地创建 / 把本地用户标记为 `pending_sync` 并入队异步补偿，避免两侧漂移。

**② 编辑本地用户（Update）— 仅同步映射层字段**

1. 允许通过本接口同步的仅限**映射字段**：`mapped_key`、`mapped_email`、`is_active`（以及本地业务字段不发 UAP）。
2. 保存时调 `POST /apps/mapping/sync`（`UPSERT`）：`mobile` 作为用户定位键保持不变，只把变更字段发过去。
3. **禁止**通过此接口改 UAP 侧的 `name` / `mobile` 等基础档案（平台会拒绝）——如确需变更，指引管理员到 UAP 后台处理，或走管理端 `PUT /users/{id}`（需超级管理员）。
4. 本地「停用」操作 → 对应把 `is_active: false` UPSERT 到 UAP，保证 SSO 侧与本地状态一致（停用用户登不进）。

**③ 删除本地用户（Delete）— DELETE 映射 或 软停用**

1. **解绑映射（推荐硬删除场景）**：`POST /apps/mapping/sync` → `{ mobile, mapped_key, sync_action: "DELETE" }`。只删**本 `APP_ID` 下**的映射，不影响 UAP 平台用户本身与其他应用。
2. **软停用**（保留映射便于恢复）：用 `UPSERT` 把 `is_active` 设为 `false`。
3. **顺序要正确**：先调 UAP DELETE 成功再删本地用户，避免本地已删、UAP 侧留下孤儿映射（再用此 `mapped_key` 投消息会发到已不存在的本地用户上）。

**④ 批量对账（可选 / 兜底）**

- 定时拉 `GET /apps/mapping/users?skip=&limit=` 与本地用户表做 diff：
  - 本地有 / UAP 无 → 补 `UPSERT`；
  - UAP 有 / 本地无 → 按策略选择 `DELETE` UAP 映射或在本地回填。
- 用于覆盖同步失败未补偿、UAP 后台手工改动等漂移场景。

**设计要点**：

- 本地 CRUD 与 UAP 同步**同一事务边界**；无法强一致时用「本地成功 → MQ → 异步 UAP 同步 + 重试」的最终一致方案，并留可观察的失败表。
- `mapped_key` 一旦对外使用（SSO / 消息）就**不要随意改**；需换键时：先 UPSERT 新 key → 业务引用切换 → 再 DELETE 旧 key。
- 所有 M2M 调用只在**服务端**完成，`APP_ACCESS_TOKEN` 不下发前端。

**常见坑**：

- 用 `APP_SECRET` 去发 M2M 请求（M2M 用 `X-App-Access-Token`，不签名）。
- 想用 `/apps/mapping/sync` 改已存在用户的 `name` / `mobile`（平台禁止，应到后台改）。
- `mapped_key` 超过 100 字符未截断。
- 本地删了用户但**忘了调** `DELETE` 映射 → UAP 侧残留孤儿映射，给已不存在的本地账号发消息 / 让 SSO 跳到失效账号。
- 本地 CRUD 成功但 UAP 同步失败**无回滚 / 无补偿** → 两侧漂移，下次 SSO 出 `mapped_key=null`。
- 调 **`GET /users/m2m/by-platform-id/{user_id}`** 时误带用户 **Bearer** → **403**；用户已删 / 非 `ACTIVE` → **404**。

## 账号与 `mapped_key`（可选策略）

从 **`GET /users/search`** 返回的用户行中，业务侧常见做法是取一个稳定、可读的字段作为本系统「账号」字段和 **`POST /apps/mapping/sync`** 里的 **`mapped_key`**。可选策略（按需采纳，不强制）：

- 优先用 **`english_name`**（若存在）：人类可读、跨系统稳定；
- 其次用行内已有 **`mapped_key`**（表示之前已建立过映射）；
- 再回退为本库用户 id 的字符串形式，保证唯一性。

**约束：**

- **`mapped_key`** 发往 UAP 时须满足平台文档中的长度上限（见长文 §7.3）。
- 若 `POST /simple/validate` 响应中包含 `english_name`，可与上述优先级对齐后再写入本系统用户。

## 组织信息（用户附带返回）

- `GET /users/search` 与 **`GET /users/m2m/by-platform-id/{user_id}`** 的响应均带 **`organization_id`** 与 **`organization_name`**（无归属为 `null`），可直接用于对接方展示部门归属，**不必**再二次请求组织接口。

## 自检

- [ ] 已识别用户意图并走对应清单（§A 一键登录 / §B 消息通知 / §C 用户同步），而非泛泛串代码。
- [ ] 清单里的**前置确认**项全部已向用户确认，未自行填假值。
- [ ] 凭据来自用户或已验证的本地 `.env`，未使用文档中的占位符当真实值。
- [ ] `UAP_API_BASE` 含 `/api/v1`。
- [ ] `APP_SECRET` 仅出现在服务端；前端只用公开信息（如 `APP_ID`、跳转 URL）。
- [ ] 未将 Simple Auth 与消息 Header 两种签名规则混用；M2M 接口未误用 `APP_SECRET`。
- [ ] 消息场景已按 B-1 / B-2 / B-3 分类：B-2 必传 `sender_app_user_id` 且优先 `USER_NOTIFICATION`；B-1 / B-3 不传 `sender_app_user_id`；广播仅 `NOTIFICATION` 且不传接收者。
- [ ] 广播功能有独立入口、权限控制、二次确认与操作审计，未与 B-1 / B-2 的发送逻辑混用。
- [ ] 未把字符串 App ID 与响应中的整数 `app_id` 主键混用。
- [ ] 一键登录：callback 路由已处理 `ticket` 缺失 / `valid=false` / **`mapped_key=null`**（无映射）三种分支；`validate` 签名字段为 `app_id + ticket + timestamp`，使用**消费票据应用**的 Secret。
- [ ] 上线 SSO 前：本系统用户到 UAP 该 `APP_ID` 下的**映射已同步**（或已设计好 `mapped_key=null` 的回退策略）。
- [ ] 若本系统有用户 CRUD：**创建 / 编辑 / 删除**均对应调用 `POST /apps/mapping/sync`（UPSERT / UPSERT / DELETE），创建时先 `GET /users/search` 回填表单；同步失败有回滚或补偿策略（见 §C.1）。
- [ ] 若涉及账号、`mapped_key` 或映射同步：已对照长文 `/users/search`、**§7.2.1** `/users/m2m/by-platform-id/{user_id}`、`POST /apps/mapping/sync` 章节。
- [ ] 若展示组织/部门信息：优先用 `/users/search` 或按 ID 查询返回的 `organization_name`。
- [ ] M2M 按平台 ID 查用户：服务端仅带 `X-App-Access-Token`，未与用户 JWT 混用；理解 **403**（应用凭证强制）与 **404**（用户不存在或不可用）语义。
