# 执行计划：无纸化模拟器 Token 获取

> **对应 PRD**: `_PRD_需求优化_无纸化模拟器Token获取.md`
> **创建日期**: 2026-08-20
> **状态**: 待执行

---

## 阶段一：扩展 token 客户端 —— 新增无纸化 Token 获取支持

**目标**：使 `DoorTokenClient`（或新建子类）支持无纸化设备的 token 获取，处理 API 路径和 regUdid 格式差异。

### 任务清单

| # | 任务 | 状态 |
|---|------|------|
| 1.1 | **新增无纸化 API 路径常量**：`PAPERLESS_TOKEN_API_PATH = "/api/api-android/token/getTokenInfoByToken"`（注意：比门口屏少 `/exapi` 前缀） | ✅ |
| 1.2 | **新增无纸化 regUdid 派生函数** `generate_paperless_reg_udid(device_id)`：`md5(device_id)[:16]`（纯 16 位 hex，无 `reg_` 前缀） | ✅ |
| 1.3 | **修改 `DoorTokenClient.__init__`**：新增可选参数 `token_api_path: str = TOKEN_API_PATH`，允许无纸化传入不同的 API 路径 | ✅ |
| 1.4 | **修改 `DoorTokenClient.get_token`**：使用 `self._token_api_path` 替代硬编码的 `TOKEN_API_PATH` 常量 | ✅ |
| 1.5 | **修改 `DoorTokenClient.__init__` 中的 regUdid 生成**：新增可选参数 `reg_udid_override: Optional[str] = None`，允许外部传入已派生的 regUdid（跳过内部 `generate_reg_udid`） | ✅ |
| 1.6 | **新增 `PaperlessTokenPool` 类或修改 `TokenPool.allocate`**：支持无纸化 appToken 格式（`ANP-xxx-xxxx`），可复用门口屏的登记池，无需单独新建 | ✅ |

### 关键设计决策

1. **复用 `DoorTokenClient`，不新建类**：
   - 无纸化与门口屏的请求头格式（X-SIGN/X-TIMESTAMP/X-RANDOM）完全一致
   - 响应解析逻辑（`_parse_response`）完全一致
   - 唯一差异：API 路径和 regUdid 格式
   - 通过参数化方式支持两种模式，避免代码重复

2. **API 路径差异**：
   - 门口屏：`/exapi/api-android/token/getTokenInfoByToken`
   - 无纸化：`/api/api-android/token/getTokenInfoByToken`
   - 差异仅在 `/exapi` 前缀，通过 `token_api_path` 参数控制

3. **regUdid 格式差异**：
   - 门口屏：`reg_{md5(device_id)[:16]}`（含 `reg_` 前缀）
   - 无纸化：`{md5(device_id)[:16]}`（纯 16 位 hex，无前缀）
   - 通过 `reg_udid_override` 参数由外部派生后传入

4. **首调即验**：实现后先本地直接调用真实接口验证（appToken=ANP-NRY-0022 + regUdid=54035332b33e1e30）

---

## 阶段二：PaperlessSimulator 集成 —— start() 接入 token 获取

**目标**：无纸化模拟器启动时调用 token 接口，获取 Authorization 并回填 topic_params。

### 任务清单

| # | 任务 | 状态 |
|---|------|------|
| 2.1 | **修改 `PaperlessSimulator.__init__`**：新增 `_token_info: Optional[dict] = None` 和 `_token_client: Optional[object] = None`、`_authorization: str = ""` 属性 | ✅ |
| 2.2 | **重写 `PaperlessSimulator.start()`**：在调用父类 `start()` 之前插入 token 获取流程（参考门口屏 `DoorSimulator.start()`） | ✅ |
| 2.3 | **token 获取流程**：检查 `self.token_api_host` → 创建 `DoorTokenClient`（传入 `token_api_path=PAPERLESS_TOKEN_API_PATH` + `reg_udid_override`） → 调 `get_token()` → 保存 `_token_client`/`_authorization`/`_token_info` → 回填 `topic_params` | ✅ |
| 2.4 | **topic_params 回填**：`company_id ← token.companyNumber`、`room_id ← token.cnum`、`conference_id ← conference.conferenceId`、`conference_name ← conference.conferenceName`、`app_token ← 导入时的 appToken` | ✅ |
| 2.5 | **重新解析主题**：回填 `topic_params` 后调用 `self._resolve_topics()` 重新生成 MQTT 主题 | ✅ |
| 2.6 | **错误处理**：token 获取失败 → 记录错误日志 → `_notify_report` 上报 `token_get` failed → 启动终止返回 False | ✅ |
| 2.7 | **未配置 token_api_host 时**：跳过 token 获取，仅 warning 日志，继续正常启动（向后兼容） | ✅ |

### 关键设计决策

1. **启动流程顺序**（与门口屏一致）：
   ```
   start()
     ↓
   ① 获取 token（如配置了 token_api_host）
   ② 回填 topic_params
   ③ 重新解析主题 _resolve_topics()
   ④ 调用父类 start() 执行 MQTT 注册/订阅/上报
   ```

2. **复用门口屏的 token 客户端逻辑**：
   - 传入 `token_api_path="/api/api-android/token/getTokenInfoByToken"`
   - 传入 `reg_udid_override=generate_paperless_reg_udid(device_id)`

3. **regUdid 派生**：在 `door_token_client.py` 中新增函数 `generate_paperless_reg_udid(device_id)`，供无纸化模拟器调用

4. **appToken 登记**：复用门口屏的 `allocate_app_token(device_id, app_token)` 全局函数（`TokenPool` 全局单例）

5. **回填时机**：token 获取成功后立即回填，再重新解析主题，确保 MQTT 订阅/发布使用正确的主题路径

---

## 阶段三：验证

| # | 验证项 | 验证方式 | 状态 |
|---|--------|----------|------|
| 3.1 | Python 语法检查 | `python -m py_compile backend/app/simulators/door_token_client.py` | ✅ |
| 3.2 | Python 语法检查 | `python -m py_compile backend/app/simulators/paperless_simulator.py` | ✅ |
| 3.3 | 本地真实调用验证 | 使用 appToken=ANP-NRY-0022 + regUdid=54035332b33e1e30 调用无纸化 token 接口 HTTP 200 | ⬜ |
| 3.4 | topic_params 回填验证 | 日志确认 company_id=CN-P7J-UBAINS、room_id=248d49ca...、conferenceId=4 | ⬜ |
| 3.5 | 未提供 appToken 验证 | 报错"未配置 appToken，不可自动生成" | ⬜ |
| 3.6 | 未配置 token_api_host 验证 | warning 日志 + 设备正常启动（不调 token 接口） | ⬜ |

### 验证过程记录（待填写）

1. 本地脚本调用验证：通过 `DoorTokenClient` + `token_api_path` 调用无纸化 token 接口
2. 确认 Authorization 为无空格格式（`BearereyJ...`）
3. 确认 companyNumber=CN-P7J-UBAINS、cnum=248d49ca... 等字段正确提取
4. 端到端验证：无纸化模拟器启动 → token 获取 → topic_params 回填 → MQTT 连接成功

---

## 影响范围

| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `backend/app/simulators/door_token_client.py` | 修改 | 新增 `PAPERLESS_TOKEN_API_PATH` 常量 + `generate_paperless_reg_udid()` 函数 + `DoorTokenClient.__init__` 新增 `token_api_path`/`reg_udid_override` 参数 + `get_token()` 使用实例路径 |
| `backend/app/simulators/paperless_simulator.py` | 修改 | `start()` 接入 token 获取流程 + `_token_info`/`_token_client`/`_authorization` 属性 |
| `backend/app/simulators/base_simulator.py` | 无需修改 | 已支持 `token_api_host` 参数 |
| `backend/app/simulators/__init__.py` | 无需修改 | 已透传 `token_api_host` |
| `backend/app/services/device_sim_service.py` | 无需修改 | 已从环境配置读取 `token_api_host` |

---

## 八、后续工作（本期不做）

无纸化模拟器在 token 获取成功后，还有一个**定期查询会议消息接口** `POST /api/message/getMessageByAppToken`（真实设备日志已提供），见：
- `_PRD_需求优化_无纸化模拟器消息定时拉取.md`
- `_执行计划_无纸化模拟器消息定时拉取.md`