# PRD 需求文档：门口屏模拟器今日会议人脸页面定时拉取

> **版本**: v1.0
> **创建日期**: 2026-08-19
> **状态**: 待实现

---

## 一、需求概述

### 1.1 背景

在门口屏模拟器获取 token 成功后，真实设备除了定时调用消息列表接口（`getMsgPageList`），还会定时调用 `POST /exapi/manageUser/getTodayMeetingFacePage/encrypt` 接口拉取今日会议的人脸页面数据。

当前模拟器已实现 token 获取（会话 E）和消息列表定时拉取（会话 F），但缺失此接口的定时调用。

### 1.2 目标

门口屏模拟器在获取 token 成功后，与消息列表定时拉取同步，按照设备导入时配置的**上报间隔**（默认 30 秒）定时调用今日会议人脸页面接口，仅确保**调用成功**（HTTP 200），不解析/不处理加密的响应数据。

### 1.3 真实设备日志参考

```
08-19 18:38:36.518 16621 17020 I UbGview : UbRxRetrofit -- ----------Request Start----------
08-19 18:38:36.518 16621 17020 I UbGview : UbRxRetrofit -- POST https://192.168.5.48/exapi/manageUser/getTodayMeetingFacePage/encrypt
08-19 18:38:36.518 16621 17020 I UbGview : UbRxRetrofit -- Authorization: BearereyJhbGciOiJIUzUxMiJ9...
08-19 18:38:36.519 16621 17020 I UbGview : UbRxRetrofit -- X-SIGN: eKLNJLsIluCpZlUWUWnIz5dScabXc1N7Ja427ocmHWWC2uVz/pULpRsOm2uy0awHLibDRrDu23XfMhllf1Siz5b6KiCCt/kV2tBsPPJvCTk=
08-19 18:38:36.519 16621 17020 I UbGview : UbRxRetrofit -- X-TIMESTAMP: 1787135916
08-19 18:38:36.519 16621 17020 I UbGview : UbRxRetrofit -- X-RANDOM: b6UWKorg2AJp3zkN
08-19 18:38:36.519 16621 17020 I UbGview : UbRxRetrofit -- Params:Print content-type is not supported:application/x-www-form-urlencoded; charset=utf-8
08-19 18:38:36.932 16621 17020 I UbGview : UbRxRetrofit -- Response Body: {"success":true,"code":"200","message":"OPERATION_SUCCESSFUL","result":"VkjxFfHBF2tbT2CMaJPAZzZoPCEAkWr6GKTWiTuVCP9yZulJaZYPmXkfBVqvyKXWPvY6Bu+vSdmbi9zXLy5XouYGWWKXtmYOH8+Nhyxca1a1ad9cH5vfAcDscUSw586XBKXQP9sFh5ROPoGq4zXMgLCp/Yea4WblZ6kPyQXVWc8="}
08-19 18:38:36.932 16621 17020 I UbGview : UbRxRetrofit -- Time:413 ms
08-19 18:38:36.932 16621 17020 I UbGview : UbRxRetrofit -- ----------Request End----------
```

---

## 二、功能需求

### 2.1 接口调用

| 项目 | 说明 |
|------|------|
| 接口路径 | `POST /exapi/manageUser/getTodayMeetingFacePage/encrypt` |
| 请求方式 | POST |
| 请求头 | `Authorization: Bearer {token}`（无空格格式 `BearereyJ...`）、`X-SIGN`/`X-TIMESTAMP`/`X-RANDOM`（动态签名，与消息列表算法一致） |
| Content-Type | `application/x-www-form-urlencoded; charset=utf-8` |
| 请求体 | form-urlencoded 格式，必须含 `companyNumber`（来自 token 响应的 `token.companyNumber`，已回填到 `topic_params.company_id`），可选含 `conferenceId` |

### 2.2 参数说明（实测修正）

**【实测发现】人脸页面接口的 body 必须包含 `companyNumber` 字段：**

- 仅传 `conferenceId` → `A0002 缺少参数`
- 传 `companyNumber=CN-34H-UBAINS` → `200 OPERATION_SUCCESSFUL`（conferenceId 可选，不影响）
- companyNumber 来自 token 响应的 `token.companyNumber`，已在 token 获取流程中回填到 `topic_params.company_id`
| 响应处理 | 仅校验 HTTP 200 + `success=true`，**不解密 result 字段** |

### 2.2 定时周期

- 调用间隔 = **设备导入时填写的上报间隔**（`report_config.interval`，默认 30 秒）
- **与消息列表定时拉取共用同一定时循环**，每次循环先调消息列表接口，再调人脸页面接口（或反之，顺序不重要）
- 首次调用在 token 获取成功后立即执行一次（与消息列表首次调用同步）

### 2.3 参数来源

| 参数 | 来源 |
|------|------|
| Bearer token | `DoorTokenClient.get_token()` 返回的 `authorization` 字段（无空格格式） |
| conferenceId | token 响应中 `conference.conferenceId`（已回填到 `topic_params.conference_id`） |
| **companyNumber** | **token 响应中 `token.companyNumber`（已回填到 `topic_params.company_id`），必传，缺省返回 A0002** |
| X-SIGN/X-TIMESTAMP/X-RANDOM | 动态签名算法（与消息列表 `_generate_dynamic_sign()` 一致） |

### 2.4 签名算法

与消息列表接口完全一致（复用 `DoorTokenClient._generate_dynamic_sign()`）：

1. 生成随机 x_random（8-16 位字母数字）
2. x_timestamp = 当前**秒级**时间戳
3. sign_str = timestamp + JSON.stringify(params) + random（有参数时）
4. sign_hash = SHA256(sign_str)
5. aes_key_source = SHA256(bearer_token)（用完整的 Bearer 字符串）
6. aes_key = aes_key_source[16:32]（16 字符）
7. aes_iv = aes_key_source[0:8] + aes_key_source[-8:]（16 字符）
8. x_sign = AES-CBC 加密(sign_hash, aes_key, aes_iv) → Base64 编码

### 2.5 错误处理

| 场景 | 行为 |
|------|------|
| HTTP 200 + success=true | 日志记录成功，不处理 result |
| HTTP 非 200 | 日志警告，不影响下次定时调用 |
| 网络错误/超时 | 日志警告，下次定时继续尝试 |
| 请求异常 | 捕获异常，不影响设备运行 |

---

## 三、非功能需求

### 3.1 性能

- 与消息列表共用同一定时线程，不额外增加线程数
- 每次调用超时 5 秒

### 3.2 兼容性

- 仅门口屏（door）设备需要此行为
- 未配置 `token_api_host` 时不触发（与 token 获取逻辑一致）
- 未获取到 `authorization` 时不触发（与消息列表逻辑一致）

---

## 四、影响范围

### 4.1 修改文件

| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `backend/app/simulators/door_token_client.py` | 修改 | 新增 `get_today_meeting_face_page()` 方法 |
| `backend/app/simulators/door_simulator.py` | 修改 | 在消息列表定时循环中增加人脸页面接口调用 |

### 4.2 不变的文件

- `base_simulator.py` — 不改动基类
- 前端文件 — 无改动
- 数据库/Schema — 无改动
- `__init__.py` — 无改动

---

## 五、依赖

- **pycryptodome**（已安装）— AES-CBC 加密签名
- **requests**（已安装）— HTTP 请求
- **DoorTokenClient.get_token()** 返回的 token 信息（已实现）
- **DoorTokenClient._generate_dynamic_sign()**（已实现）

---

## 六、验收标准

| # | 验收项 | 验证方式 |
|---|--------|----------|
| 1 | 启动门口屏模拟器后，token 获取成功后立即调用一次人脸页面接口 | 日志查看 |
| 2 | 之后每 N 秒（与消息列表同步）调用一次 | 日志查看时间间隔 |
| 3 | HTTP 200 响应成功，不解密 result | 日志不报错即可 |
| 4 | 未配置 token_api_host 时不触发 | 日志无相关调用 |
| 5 | 设备停止时线程自动退出 | 日志确认 |
| 6 | 真实调用验证：token → 人脸页面接口 HTTP 200 + success=true | 本地脚本验证 |