# 执行计划：无纸化模拟器消息定时拉取

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

---

## 阶段一：扩展 DoorTokenClient —— 新增 get_message_by_app_token()

**目标**：在 `door_token_client.py` 中新增无纸化消息查询接口调用能力。

### 任务清单

| # | 任务 | 状态 |
|---|------|------|
| 1.1 | 新增 `PAPERLESS_MESSAGE_API_PATH = "/api/message/getMessageByAppToken"` 常量 | ✅ |
| 1.2 | 新增 `get_message_by_app_token(authorization, app_token)` 实例方法：POST 请求 + form-urlencoded 体 + 动态签名头 | ✅ |
| 1.3 | 请求体构造：按 `appToken` 参数调用（日志接口名暗示），实测后可调整 | ✅ |
| 1.4 | 动态签名复用 `_generate_dynamic_sign(params, bearer_token)`（已实现） | ✅ |
| 1.5 | 响应校验：HTTP 200 即视为调用成功，业务码非 200 记录日志但不视为失败（如 A0034 会议不存在是正常业务场景） | ✅ |
| 1.6 | 错误处理：网络错误/超时不重试（下次定时再试），业务异常捕获日志 | ✅ |

### 关键设计决策

1. **HTTP 200 即算调用成功**：与门口屏不同，门口屏校验 `success=true` + `code="200"`，但无纸化此接口在**无会议时返回正常业务码 `A0034`**（`success=false`），这是真实设备正常行为，不应视为调用失败
2. **请求体参数待实测确认**：日志显示 `Content-Type: application/x-www-form-urlencoded`，但未打印 body 内容。接口名 `getMessageByAppToken` 暗示按 `appToken` 查询。先实现 `appToken` 参数，实测后调整
3. **复用签名算法**：`_generate_dynamic_sign()` 已支持带 params 的场景

---

## 阶段二：PaperlessSimulator 集成 —— 定时拉取线程

**目标**：无纸化模拟器启动后按上报间隔定时调用消息查询接口。

### 任务清单

| # | 任务 | 状态 |
|---|------|------|
| 2.1 | `PaperlessSimulator.__init__` 新增 `_token_client`、`_authorization`、`_msg_thread`、`_msg_running` 属性 | ✅ |
| 2.2 | `start()` 改造为**四步流程**：①调 token 接口（阶段一）→ ②回填 topic_params → ③解析主题 + 父类 MQTT 启动 → ④启动消息定时拉取线程 | ✅ |
| 2.3 | 新增 `_start_message_polling()`：首次立即调用 + 启动独立线程（间隔 = `self._report_interval`） | ✅ |
| 2.4 | 新增 `_call_message_once()`：单次调用 + 记录 ReportLog | ✅ |
| 2.5 | 新增 `_message_loop()`：定时循环，间隔 = `self._report_interval` | ✅ |
| 2.6 | `stop()` 覆盖：先停止消息拉取线程，再调用父类 stop | ✅ |
| 2.7 | 未配置 `token_api_host` 时跳过消息拉取（向后兼容） | ✅ |

### 关键设计决策

1. **启动流程顺序**（与门口屏一致）：
   ```
   start()
     ↓
   ① 调 token 接口（获取 authorization + 回填 topic_params）
   ② 回填 topic_params + 重新解析主题
   ③ 调用父类 start() 执行 MQTT 注册/订阅/上报
   ④ 启动消息定时拉取线程（仅 authorization 存在时）
   ```

2. **间隔来源**：直接用 `self._report_interval`（设备导入时 Excel 填写的上报间隔），不新增配置项

3. **独立线程**：与门口屏同模式，HTTP 调用与 MQTT 上报分离

4. **失败不影响运行**：消息接口调用失败只记日志，不停止设备

---

## 阶段三：验证

| # | 验证项 | 验证方式 | 状态 |
|---|--------|----------|------|
| 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 | 本地真实调用验证：token → 消息接口 HTTP 200 | 本地脚本调用 | ⬜ |
| 3.4 | 业务码 A0034 不视为失败 | 日志记录正常，不影响定时循环 | ⬜ |
| 3.5 | 端到端验证：启动 → token → 消息定时拉取 | 完整链路 | ⬜ |

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

1. 参数探测：先按 `appToken` 参数调用，验证 body 是否必须含 `conferenceId` 等其他字段
2. 确认 `A0034 会议不存在` 响应不触发错误日志，仅记录 info
3. 确认定时循环持续进行（非单次调用）

---

## 影响范围

| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `backend/app/simulators/door_token_client.py` | 修改 | 新增 `PAPERLESS_MESSAGE_API_PATH` 常量 + `get_message_by_app_token()` 方法 |
| `backend/app/simulators/paperless_simulator.py` | 修改 | `start()` 四步流程 + 新增定时拉取线程 + `stop()` 覆盖 |
| `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` |