# PRD 需求优化文档 — 门口屏模拟器 Token 获取

> **文档类型**: PRD 需求优化文档
> **创建日期**: 2026-08-19
> **作者**: czj
> **优先级**: P0
> **状态**: 待评审
> **关联文档**: `_PRD_需求文档_设备模拟模块.md`、`_执行计划_门口屏模拟器Token获取.md`

---

## 一、需求背景

### 1.1 当前问题

门口屏（DoorSimulator）模拟器当前启动流程为：连接 MQTT Broker → 发布注册消息 → 订阅主题 → 发布初始状态 → 启动周期上报。**整个过程不涉及任何 HTTP 接口调用**。

但在真实 Android 门口屏设备（UbGview 应用）中，应用启动后的**第一个动作**是调用 HTTP 接口 `POST /exapi/api-android/token/getTokenInfoByToken` 获取 token，成功后才会进行后续的 MQTT 连接、主题订阅和数据上报。

当前模拟器与真实设备存在以下差距：

| 对比项 | 真实设备 | 当前模拟器 |
|--------|---------|-----------|
| 启动第一步 | HTTP 调用获取 token | 直接连接 MQTT |
| token 管理 | 从接口响应获取 Authorization + token 对象 | 无 token 概念 |
| company_id / room_id | 从接口响应动态获取（companyNumber / cnum） | 手动在 topic_params 中配置 |
| 签名机制 | X-SIGN / X-TIMESTAMP / X-RANDOM | 无 |

### 1.2 目标用户

| 用户角色 | 使用场景 | 核心需求 |
|---------|---------|---------|
| 测试工程师 | 模拟门口屏设备启动 | 模拟真实设备启动流程，先调用 token 接口 |
| 测试工程师 | 多设备并发模拟 | 每个设备独立获取 token，regUdid 不重复 |
| 开发工程师 | 调试 token 获取逻辑 | 查看接口调用日志，验证 token 获取是否成功 |

### 1.3 需求目标

1. **启动流程补全**：门口屏模拟器启动时，第一步调用 `getTokenInfoByToken` 接口获取 token
2. **Token 持久化**：成功获取 token 后保存 Authorization 及响应中的必要字段
3. **regUdid 唯一性**：多设备场景下 regUdid 不能重复，由 device_id 派生
4. **参数回填**：将接口返回的 companyNumber → company_id、cnum → room_id 存入 topic_params

### 1.4 本期范围与排除

**本期范围：**

| 模块 | 说明 |
|------|------|
| Token 接口调用 | 启动时调用 `getTokenInfoByToken`，使用日志中的固定 X-SIGN |
| 响应解析与保存 | 解析 Authorization、token、conference、company 信息并保存 |
| regUdid 唯一性 | 多设备场景下 regUdid 由 device_id 派生，保证不重复 |
| 参数回填 | 将接口返回的 companyNumber / cnum 回填到 topic_params |

**本期不涉及（后续迭代）：**

| 项目 | 说明 |
|------|------|
| MQTT 主题解析 | 根据回填后的 company_id + room_id 解析 MQTT 主题 |
| 会议服务主题订阅 | 订阅 `/iot/v1/conference/service/request/{company_id}/{room_id}/` 等主题 |
| MQTT 周期上报 | 启动 MQTT 周期上报任务 |
| 动态 X-SIGN 算法 | 日志中固定 X-SIGN 的生成算法（需逆向 APK 确认） |

---

## 二、功能需求

### 2.1 接口规格

#### 2.1.1 基本信息

| 项目 | 值 |
|------|-----|
| 接口 URL | `POST https://{host}/exapi/api-android/token/getTokenInfoByToken` |
| 请求头 | Content-Type: application/json; charset=utf-8 |
| 请求头 | X-SIGN: 固定值（从日志中获取） |
| 请求头 | X-TIMESTAMP: 当前 Unix 秒级时间戳 |
| 请求头 | X-RANDOM: 16 位随机字符串 |

#### 2.1.2 请求体

```json
{
  "appToken": "AND-34H-0101",
  "regUdid": "{device_id 派生值}"
}
```

| 字段 | 说明 | 来源 |
|------|------|------|
| appToken | 应用令牌 | 固定值 `AND-34H-0101`（从日志中获取） |
| regUdid | 设备注册唯一标识 | 由 device_id 派生，多设备不重复 |

#### 2.1.3 响应体（成功）

```json
{
  "code": 200,
  "message": "OPERATION_SUCCESSFUL",
  "success": true,
  "result": {
    "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9...",
    "token": {
      "tokenId": 7186,
      "companyNumber": "CN-34H-UBAINS",
      "appToken": "AND-34H-0101",
      "regUdid": "79f18c1a9a3bcfb1",
      "cnum": "upjp7gzkjmgsu15y06aoahff55xh6c53",
      "startTime": "2026-04-28 17:34:38",
      "endTime": "2099-12-31 23:59:59",
      "state": 1,
      "annotation": "彭甘宇在2026-05-08 02:02:17绑定了 设备"
    },
    "conference": {
      "conferenceId": 50,
      "conferenceName": "PGY测试会议室1",
      "conferenceNumber": "upjp7gzkjmgsu15y06aoahff55xh6c53",
      "qrCode": "https://192.168.5.48/group1/M00/00/00/wKgFMGn9rkqAb4dYAAAlXncPlGo276.png",
      "generalField": "{...}"
    },
    "company": {
      "companyId": 466,
      "companyName": "马来西亚项目测试",
      "companyNumber": "CN-34H-UBAINS",
      "ubainsKey": "ubains76zu81x6vu",
      "ubainsSecret": "gKv2nq2O-nPA9S3yp-SPJeRxVH-N9HBSF4y"
    }
  }
}
```

#### 2.1.4 字段映射规则

| 响应字段 | 映射目标 | 说明 |
|---------|---------|------|
| `result.Authorization` | token_authorization | Bearer JWT，MQTT 和后续接口使用 |
| `result.token.tokenId` | token_id | Token 标识 |
| `result.token.companyNumber` | topic_params.company_id | 公司编号（如 `CN-34H-UBAINS`） |
| `result.token.cnum` | topic_params.room_id | 会议室编号（32 位字符串，非旧格式短编号） |
| `result.token.appToken` | topic_params.app_token | 应用令牌 |
| `result.token.regUdid` | token_reg_udid | 设备注册标识 |
| `result.conference` | token_conference | 会议信息（conferenceId, conferenceName 等） |
| `result.company` | token_company | 公司信息（companyId, companyName, ubainsKey, ubainsSecret） |

### 2.2 签名机制（本期）

#### 2.2.1 当前方案

由于动态 X-SIGN 生成算法未知（需要逆向 APK 确认），**本期使用日志中已验证通过的固定值**：

| 请求头 | 值 |
|--------|-----|
| X-SIGN | `uEkHrug...`（从日志中获取的固定值，已验证可调用成功） |
| X-TIMESTAMP | 当前 Unix 秒级时间戳（动态生成，跟随当前时间） |
| X-RANDOM | 16 位随机字符串 |

**注意**：由于 X-SIGN 是固定值，与 X-TIMESTAMP / X-RANDOM 的校验关系未知。当前方案的前提是服务端**不校验** X-SIGN 与时间戳/随机数的绑定关系，或该校验已关闭（测试环境特性）。

#### 2.2.2 后续计划

| 阶段 | 内容 | 优先级 |
|------|------|--------|
| 本期 | 使用日志固定 X-SIGN | P0 |
| 后续 | 逆向 APK 确认签名算法，实现动态生成 | P2 |

### 2.3 regUdid 唯一性

#### 2.3.1 规则定义

**约束**：多设备场景下，regUdid 不能重复（目标环境会校验唯一性）。

**策略**：regUdid 由 device_id 派生，确保每个设备有独立的 regUdid。

```
regUdid = f"{device_id}_{random_suffix}"
```

| 场景 | device_id | regUdid 示例 |
|------|-----------|-------------|
| 单设备（默认） | DS001 | DS001_a3b7k9m2 |
| 多设备 | DS001 | DS001_a3b7k9m2 |
| 多设备 | DS002 | DS002_x5p8q1w4 |
| 多设备 | DS003 | DS003_j2n6r4t8 |

#### 2.3.2 生成逻辑

1. 取 `device_id` 作为前缀（如 `DS001`）
2. 拼接 `_` + 8 位随机字母数字
3. 最终格式：`{device_id}_{8位随机}`
4. 每次启动时重新生成（固定后缀可能导致服务端缓存冲突）

### 2.4 Token 存储与持久化

#### 2.4.1 存储字段

| 字段 | 类型 | 说明 |
|------|------|------|
| token_authorization | str | Bearer JWT |
| token_id | int | token 记录 ID |
| token_reg_udid | str | 设备注册标识 |
| token_conference | dict | 会议信息（JSON 对象） |
| token_company | dict | 公司信息（JSON 对象） |

#### 2.4.2 存储位置

| 存储方式 | 用途 | 说明 |
|---------|------|------|
| 模拟器实例属性 | 运行时使用 | 直接保存在 DoorSimulator 实例中 |
| topic_params（部分字段） | 参数回填 | company_id / room_id / app_token 回填到 topic_params |
| 数据库（可选） | 持久化 | 可选保存到 DeviceSimulator 记录的 extra_attrs 字段 |

### 2.5 启动流程变更

#### 2.5.1 当前流程

```
DoorSimulator.start()
  ├── 连接 MQTT Broker
  ├── 发布注册消息（reboot_response）
  ├── 订阅全部 publish topics
  ├── 发布初始状态
  └── 启动周期上报（_auto_report_loop）
```

#### 2.5.2 改造后流程

```
DoorSimulator.start()
  ├── 第一步：HTTP 调用 getTokenInfoByToken
  │   ├── 构建请求头（X-SIGN 固定值，X-TIMESTAMP 当前秒，X-RANDOM 16位随机）
  │   ├── 构建请求体（appToken 固定值，regUdid 由 device_id 派生）
  │   ├── 发送 POST 请求
  │   ├── 解析响应（code=200, success=true）
  │   ├── 保存 Authorization / token / conference / company
  │   ├── 回填 topic_params（company_id, room_id, app_token）
  │   └── 失败则抛出异常，启动终止
  ├── 连接 MQTT Broker（使用 MQTT 配置）
  ├── 发布注册消息（reboot_response）
  ├── 订阅全部 publish topics
  ├── 发布初始状态
  └── 启动周期上报（_auto_report_loop）  ← 本期不实现
```

**本期改造范围**：仅实现 `第一步` 的 HTTP 调用 + token 保存 + 参数回填。
**MQTT 相关步骤**（连接/注册/订阅/状态发布/周期上报）：保持现有逻辑不变，但需注意启动顺序（token 获取成功后，下一期才接入 MQTT 步骤）。

---

## 三、技术方案

### 3.1 后端改动

#### 3.1.1 新增 HTTP 客户端

在 `backend/app/simulators/` 目录下新增 `door_token_client.py`：

| 方法 | 说明 |
|------|------|
| `DoorTokenClient.__init__(host, device_id, app_token)` | 初始化客户端 |
| `DoorTokenClient.get_token()` | 调用 getTokenInfoByToken 接口 |
| `DoorTokenClient._build_headers()` | 构造请求头（X-SIGN 固定值, X-TIMESTAMP, X-RANDOM） |
| `DoorTokenClient._build_body()` | 构造请求体（appToken, regUdid） |
| `DoorTokenClient._generate_reg_udid()` | 由 device_id 生成唯一 regUdid |
| `DoorTokenClient._parse_response()` | 解析响应，提取 Authorization / token / conference / company |

#### 3.1.2 修改 DoorSimulator

在 `DoorSimulator.start()` 方法头部增加 token 获取逻辑：

| 改动 | 说明 |
|------|------|
| `start()` 方法开头 | 调用 `DoorTokenClient.get_token()` |
| 新增属性 `_token_info` | 保存 token 响应信息 |
| `topic_params` 回填 | 将 companyNumber → company_id, cnum → room_id, appToken → app_token |

#### 3.1.3 配置项

在 `EnvConfig` 或 `DeviceSimulator` 中增加：

| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| `token_api_host` | Token 接口主机地址 | 从环境配置中获取（目标系统 IP） |
| `app_token` | 应用令牌 | `AND-34H-0101` |
| `x_sign_fixed` | 固定 X-SIGN 值 | 从日志中获取 |
| `token_enabled` | 是否启用 token 获取 | `true` |

### 3.2 异常处理

#### 3.2.1 失败场景

| 场景 | 处理方式 |
|------|---------|
| 网络不可达（连接拒绝/超时） | 记录错误日志，抛出异常，启动终止 |
| HTTP 非 200 状态码 | 记录错误日志，抛出异常，启动终止 |
| 响应 `success: false` | 记录错误日志（含 message），抛出异常，启动终止 |
| 响应缺少必需字段 | 记录错误日志，抛出异常，启动终止 |

#### 3.2.2 重试策略

| 策略 | 值 |
|------|-----|
| 最大重试次数 | 3 次 |
| 重试间隔 | 2 秒（指数退避） |
| 重试条件 | 仅网络错误可重试，业务错误不重试 |

---

## 四、非功能需求

### 4.1 性能要求

- Token 接口调用耗时 < 5s（含网络延迟）
- 启动总耗时增加 < 10s（含 3 次重试）
- 不影响已启动设备的运行性能

### 4.2 兼容性要求

- 向后兼容：已创建的门禁设备升级后，启动时自动获取 token
- 单设备兼容：单设备场景下 regUdid 唯一性不影响
- 多设备兼容：多设备场景下 regUdid 自动派生，无需手动配置

### 4.3 可扩展性

- 签名算法扩展：后续支持动态 X-SIGN 生成时，替换 `_build_headers()` 方法即可
- 设备类型扩展：其他设备类型（如无纸化屏）需要 token 时，可复用 `DoorTokenClient`

---

## 五、验收标准

### 5.1 功能验收

| 编号 | 验收项 | 预期结果 |
|------|--------|---------|
| F01 | 门口屏启动时调用 token 接口 | 日志显示 `POST /exapi/api-android/token/getTokenInfoByToken` 调用记录 |
| F02 | 接口调用成功 | 响应 `code=200, success=true`，Authorization 已保存 |
| F03 | Token 信息保存 | 模拟器实例中可访问 `_token_info`，包含 Authorization / token / conference / company |
| F04 | topic_params 回填 | `company_id` 和 `room_id` 自动回填为接口返回的值 |
| F05 | regUdid 唯一性 | 多设备启动时，各设备的 regUdid 不同（前缀不同） |
| F06 | 接口调用失败 | 启动终止，日志显示错误原因，设备状态为 `error` |

### 5.2 边界验收

| 编号 | 边界场景 | 预期结果 |
|------|---------|---------|
| B01 | 网络不可达 | 3 次重试后失败，启动终止 |
| B02 | 接口返回 `success: false` | 立即失败，不重试，启动终止 |
| B03 | 响应缺少 Authorization 字段 | 解析异常，启动终止 |
| B04 | 单设备启动 | regUdid 正常生成，token 获取成功 |
| B05 | 多设备（10 台）同时启动 | 各设备独立获取 token，regUdid 均不重复 |

---

## 六、附录

### 6.1 参考文档

- 真实门口屏设备日志（UbGview 应用，2026-08-19）
- `_PRD_需求文档_设备模拟模块.md`
- `backend/app/simulators/base_simulator.py` — BaseSimulator 启动流程
- `backend/app/simulators/door_simulator.py` — DoorSimulator 实现
- `backend/app/executors/http_client.py` — 现有签名算法（与日志不匹配，仅供参考）

### 6.2 术语说明

| 术语 | 说明 |
|------|------|
| X-SIGN | 请求签名，本期使用日志固定值 |
| X-TIMESTAMP | 请求时间戳，秒级 Unix 时间戳 |
| X-RANDOM | 请求随机数，16 位随机字符串 |
| regUdid | 设备注册唯一标识，需保证多设备不重复 |
| topic_params | 设备模拟器的主题动态参数，存储在 DeviceSimulator 记录中 |
| companyNumber | 接口返回的公司编号，映射为 topic_params.company_id |
| cnum | 接口返回的会议室编号，映射为 topic_params.room_id |

### 6.3 已确认的决策记录

| 决策 | 内容 | 依据 |
|------|------|------|
| X-SIGN 使用固定值 | 本期复用日志中已验证通过的 X-SIGN | 动态算法未知，日志固定值已可调用成功 |
| regUdid 由 device_id 派生 | 格式 `{device_id}_{8位随机}` | 多设备场景下不能重复 |
| 只保存 token，不做 MQTT 操作 | 本期仅获取并保存 token | 用户明确要求 |
| 使用日志中的 appToken | `AND-34H-0101` | 与 X-SIGN 配套 |

---

*文档结束*