提交 695460eb authored 作者: 陈泽健's avatar 陈泽健

feat(device-sim): 门口屏补全全局配置定时拉取接口 + 更新 HANDOFF

- door_token_client: 新增 EX_GLOBAL_CONFIG_API_PATH + get_ex_global_config()(GET, query companyNumber)
- door_simulator: 定时循环扩充为 3 个接口(消息列表/人脸页面/全局配置)
- 全局配置接口实测验证通过(HTTP 200 + success=true)
- 公司编号来源:token 响应 token.companyNumber → topic_params.company_id
- 新增 PRD + 执行计划文档
- 更新 HANDOFF 设备模拟会话 G 记录
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 39cfb764
# 执行计划:门口屏模拟器全局配置定时拉取
> **对应 PRD**: `_PRD_需求优化_门口屏全局配置定时拉取.md`
> **创建日期**: 2026-08-19
> **状态**: 待执行
---
## 阶段一:扩展 DoorTokenClient —— 新增 get_ex_global_config()
**目标**:在 `door_token_client.py` 中新增全局配置接口调用能力。
### 任务清单
| # | 任务 | 状态 |
|---|------|------|
| 1.1 | 新增 `EX_GLOBAL_CONFIG_API_PATH = "/exapi/systemConfiguration/exGlobalConfig/encrypt"` 常量 | ✅ |
| 1.2 | 新增 `get_ex_global_config(authorization, company_number)` 实例方法:GET 请求 + 动态签名头 + companyNumber 查询参数 | ✅ |
| 1.3 | 查询参数构造:`companyNumber={token响应中的 companyNumber}` | ✅ |
| 1.4 | 动态签名复用 `_generate_dynamic_sign(params, bearer_token)`(已实现) | ✅ |
| 1.5 | 响应校验:仅判断 HTTP 200 + success=true,不解析加密 result | ✅ |
| 1.6 | 错误处理:网络错误/超时不重试(下次定时再试),业务失败警告日志 | ✅ |
### 关键设计决策
1. **GET 请求 + query 参数**:与消息列表接口(GET)模式一致,与人脸页面接口(POST + form-urlencoded)不同
2. **Content-Type: application/json**:日志显示该接口 Content-Type 为 `application/json`(虽然是 GET 无 body,但保留该头与真实设备一致)
3. **查询参数 companyNumber**:直接从 token 响应 `token.companyNumber` 取值,已回填到 `topic_params.company_id`
4. **Authorization 格式**:与消息列表/人脸页面一致,使用 `BearereyJ...`(无空格格式)
---
## 阶段二:DoorSimulator 集成 —— 定时循环中追加全局配置调用
**目标**:在现有定时循环中,每次循环追加一次全局配置接口调用。
### 任务清单
| # | 任务 | 状态 |
|---|------|------|
| 2.1 | 新增 `_call_ex_global_config_once()` 方法:单次调用全局配置接口 + 记录 ReportLog | ✅ |
| 2.2 | 修改 `_message_list_loop()`:每次循环追加调用 `_call_ex_global_config_once()` | ✅ |
| 2.3 | 修改 `_start_message_list_polling()`:首次立即调用时也包含全局配置接口 | ✅ |
| 2.4 | 更新线程日志描述(消息列表/人脸页面 → 三个接口) | ✅ |
### 关键设计决策
1. **共用同一定时线程**:三个接口(消息列表、人脸页面、全局配置)在同一个循环中串行调用
2. **调用顺序**:消息列表 → 人脸页面 → 全局配置(顺序不重要,三个接口独立)
3. **失败不中断**:任一接口失败不影响其他接口的调用,也不影响下一次定时循环
4. **首次立即执行**:在 `_start_message_list_polling()` 中首次调用时,三个接口都立即执行一次
---
## 阶段三:验证
| # | 验证项 | 验证方式 | 状态 |
|---|--------|----------|------|
| 3.1 | Python 语法检查 | `python -m py_compile backend/app/simulators/door_token_client.py` | ✅ 通过 |
| 3.2 | Python 语法检查 | `python -m py_compile backend/app/simulators/door_simulator.py` | ✅ 通过 |
| 3.3 | 本地真实调用验证:token → 全局配置接口 HTTP 200 + success=true | 本地脚本调用 | ✅ 通过(code=200, success=True) |
### 验证过程记录
- **端到端验证**:通过 `DoorTokenClient.get_token()` + `get_ex_global_config()` 完整链路调用成功
- **参数确认**:只需 `companyNumber` 一个查询参数(GET 请求),与日志一致
- **Content-Type**:设为 `application/json` 与真实设备日志一致(GET 无 body,但头信息保留)
---
## 影响范围
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `backend/app/simulators/door_token_client.py` | 修改 | 新增 `get_ex_global_config()` 方法 + 常量 |
| `backend/app/simulators/door_simulator.py` | 修改 | 定时循环中追加全局配置调用 |
\ No newline at end of file
# PRD 需求文档:门口屏模拟器全局配置定时拉取
> **版本**: v1.0
> **创建日期**: 2026-08-19
> **状态**: 待实现
---
## 一、需求概述
### 1.1 背景
在门口屏模拟器获取 token 成功后,真实设备除了定时调用消息列表接口(`getMsgPageList`)和今日会议人脸页面接口(`getTodayMeetingFacePage`),还会定时调用 `GET /exapi/systemConfiguration/exGlobalConfig/encrypt` 接口拉取全局配置数据。
当前模拟器已实现 token 获取(会话 E)、消息列表定时拉取(会话 F)和今日会议人脸页面定时拉取(本次会话),但缺失此全局配置接口的定时调用。
### 1.2 目标
门口屏模拟器在获取 token 成功后,与消息列表/人脸页面定时拉取同步,按照设备导入时配置的**上报间隔**(默认 30 秒)定时调用全局配置接口,仅确保**调用成功**(HTTP 200),不解析/不处理加密的响应数据。
### 1.3 真实设备日志参考
```
08-19 18:38:36.949 16621 12295 I UbGview : UbRxRetrofit -- ----------Request Start----------
08-19 18:38:36.949 16621 12295 I UbGview : UbRxRetrofit -- GET https://192.168.5.48/exapi/systemConfiguration/exGlobalConfig/encrypt?companyNumber=CN-34H-UBAINS
08-19 18:38:36.949 16621 12295 I UbGview : UbRxRetrofit -- Content-Type: application/json
08-19 18:38:36.949 16621 12295 I UbGview : UbRxRetrofit -- Authorization: BearereyJ...
08-19 18:38:36.949 16621 12295 I UbGview : UbRxRetrofit -- X-SIGN: azYIfdNhtzt6opqItgub4FeZHV5rmu4cmjJ6ym7a+N4t5IeJsbgmC5oIocBB5aFdcqCky1WObDTtjC7OnLfqLz7DmsQT8VEedV3E2U+/e1Y=
08-19 18:38:36.949 16621 12295 I UbGview : UbRxRetrofit -- X-TIMESTAMP: 1787135916
08-19 18:38:36.949 16621 12295 I UbGview : UbRxRetrofit -- X-RANDOM: 8tMFhCcJpylaN0Jo
08-19 18:38:37.332 16621 12295 I UbGview : UbRxRetrofit -- Response Body: {"success":true,"code":"200","message":"OPERATION_SUCCESSFUL","result":"VkjgBfDWAHNOGT...(密文, 较长)"}
08-19 18:38:37.333 16621 12295 I UbGview : UbRxRetrofit -- Time:377 ms
```
---
## 二、功能需求
### 2.1 接口调用
| 项目 | 说明 |
|------|------|
| 接口路径 | `GET /exapi/systemConfiguration/exGlobalConfig/encrypt` |
| 请求方式 | GET |
| 请求头 | `Authorization: Bearer {token}`(无空格格式 `BearereyJ...`)、`Content-Type: application/json``X-SIGN`/`X-TIMESTAMP`/`X-RANDOM`(动态签名,与消息列表算法一致) |
| 查询参数 | `companyNumber`(来自 token 响应的 `token.companyNumber`,已回填到 `topic_params.company_id`) |
| 响应处理 | 仅校验 HTTP 200 + `success=true`**不解密 result 字段** |
### 2.2 定时周期
- 调用间隔 = **设备导入时填写的上报间隔**`report_config.interval`,默认 30 秒)
- **与消息列表/人脸页面共用同一定时循环**,每次循环依次调用三个接口
- 首次调用在 token 获取成功后立即执行一次
### 2.3 参数来源
| 参数 | 来源 |
|------|------|
| Bearer token | `DoorTokenClient.get_token()` 返回的 `authorization` 字段(无空格格式) |
| companyNumber | token 响应中 `token.companyNumber`(已回填到 `topic_params.company_id`) |
| 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` | 修改 | 新增 `EX_GLOBAL_CONFIG_API_PATH` 常量 + `get_ex_global_config()` 方法 |
| `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 | 本地脚本验证 |
\ No newline at end of file
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
> **生成时间**: 2026-08-19 > **生成时间**: 2026-08-19
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **最近提交**: `1a3d52ad` fix(executor): 修复执行中无法取消(MySQL 1205 行锁)并加固 SUT URL 校验 > **最近提交**: `39cfb764` fix(device-sim): 门口屏人脸页面接口补传 companyNumber 参数
> **未提交改动**: 设备模拟相关改动 + 其他模块(见 git status > **未提交改动**: 无(已全部提交
--- ---
...@@ -796,4 +796,83 @@ if existing and existing.connected and existing.client: ...@@ -796,4 +796,83 @@ if existing and existing.connected and existing.client:
--- ---
### 2026-08-19 会话 G:门口屏补全最后两个 HTTP 接口定时调用(今日会议人脸页面 + 全局配置,已实现 + 真实调用验证通过,未提交未部署)
**会话目标**:为门口屏模拟器补全剩余的定时 HTTP 接口调用 —— ① `POST /exapi/manageUser/getTodayMeetingFacePage/encrypt`(今日会议人脸页面) ② `GET /exapi/systemConfiguration/exGlobalConfig/encrypt`(全局配置)。至此门口屏启动后的 3 个 HTTP 定时接口全部实现。
**状态**:✅ 文档 + 代码实现完成 + 真实接口调用验证通过(两个接口均 HTTP 200 + success=true);⚠️ 未提交 git、未部署
**背景**:真实门口屏设备(UbGview 应用)启动获取 token 后,除了消息列表接口,还会定时调用今日会议人脸页面和全局配置接口。模拟器此前只实现了 token 获取(会话 E)和消息列表拉取(会话 F)。
---
#### ① 新增接口 1:今日会议人脸页面(`DoorTokenClient.get_today_meeting_face_page()`)
- **接口**`POST {scheme}://{host}/exapi/manageUser/getTodayMeetingFacePage/encrypt`
- **请求方式**:POST + form-urlencoded(Content-Type: `application/x-www-form-urlencoded; charset=utf-8`
- **请求体(实测关键发现)**:body 必须含 **`companyNumber`**,否则返回 `A0002 缺少参数`
- 仅传 `conferenceId=50``A0002 缺少参数`
-`companyNumber=CN-34H-UBAINS``200 OPERATION_SUCCESSFUL`(conferenceId 可选,同时传不影响,result_len=172)
- **公司编号来源**:token 响应 `token.companyNumber`,已在 token 获取流程回填到 `topic_params.company_id`,每次调用实时读取
- **签名**:复用 `_generate_dynamic_sign()` 动态签名(与消息列表一致)
#### ② 新增接口 2:全局配置(`DoorTokenClient.get_ex_global_config()`)
- **接口**`GET {scheme}://{host}/exapi/systemConfiguration/exGlobalConfig/encrypt?companyNumber={companyNumber}`
- **请求方式**:GET + query 参数(Content-Type: `application/json` 与真实日志一致)
- **参数**:仅 `companyNumber`(来自 token 响应 `token.companyNumber`),实测直接成功
- **签名**:复用 `_generate_dynamic_sign()` 动态签名
#### ③ DoorSimulator 集成(定时常量扩充为 3 个接口)
- `_message_list_loop()` 每次循环依次调用:
1. `_call_message_list_once()` — 消息列表(已有)
2. `_call_face_page_once()` — 今日会议人脸页面(新增)
3. `_call_ex_global_config_once()` — 全局配置(新增)
- `_start_message_list_polling()` 首次立即执行时三个接口都调用一次
- 三个接口共用同一定时线程,间隔 = `self._report_interval`(默认 30s),不新增线程
- 任一接口失败不影响其他接口,也不影响下次定时循环
#### 验证结果
| 验证项 | 结果 |
|--------|------|
| 真实调用:token → 人脸页面接口 | ✅ HTTP 200 + OPERATION_SUCCESSFUL,result_len=172 |
| 参数探测:仅传 conferenceId | ✅ 返回 A0002 缺少参数(确认 companyNumber 必传) |
| 真实调用:token → 全局配置接口 | ✅ HTTP 200 + OPERATION_SUCCESSFUL |
| 公司编号来源链路 | ✅ token 响应 token.companyNumber → topic_params.company_id → 两个接口调用 |
| 三接口共用同一定时线程 | ✅ 消息列表/人脸页面/全局配置串行调用,不影响间隔 |
| Python 语法检查 | ✅ 两个文件 py_compile 通过 |
**修改文件清单(本次会话)**
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `backend/app/simulators/door_token_client.py` | 修改 | 新增 `FACE_PAGE_API_PATH` / `EX_GLOBAL_CONFIG_API_PATH` 常量 + `get_today_meeting_face_page()`(POST, body 含 companyNumber)+ `get_ex_global_config()`(GET, query companyNumber) |
| `backend/app/simulators/door_simulator.py` | 修改 | 新增 `_call_face_page_once()` / `_call_ex_global_config_once()`,定时循环与首次调用扩充为 3 个接口 |
| `Docs/PRD/设备模拟/需求文档/_PRD_需求优化_门口屏今日会议人脸页面定时拉取.md` | 新增 | PRD 需求文档 |
| `Docs/PRD/设备模拟/执行计划/_执行计划_门口屏今日会议人脸页面定时拉取.md` | 新增 | 执行计划文档 |
| `Docs/PRD/设备模拟/需求文档/_PRD_需求优化_门口屏全局配置定时拉取.md` | 新增 | PRD 需求文档 |
| `Docs/PRD/设备模拟/执行计划/_执行计划_门口屏全局配置定时拉取.md` | 新增 | 执行计划文档 |
**门口屏模拟器完整 HTTP 调用链(至此全部实现)**
```
启动
① getTokenInfoByToken(POST,会话 E)→ 拿 authorization + 回填 company_id/room_id/conference_id/app_token
② 定时循环(间隔 = 上报间隔,默认 30s,独立线程):
├─ getMsgPageList/encrypt(GET,会话 F)— 消息列表
├─ getTodayMeetingFacePage/encrypt(POST,本次会话)— 今日会议人脸页面(body 含 companyNumber)
└─ exGlobalConfig/encrypt(GET,本次会话)— 全局配置(query companyNumber)
```
**待办**
- ⚠️ 本次改动(含此前会话 B/C/D/E/F 未提交的设备模拟改动)尚未提交 git,下次会话 `/GitCommit` 提交
- ⚠️ 未部署。需要门口屏环境配置在 `EnvConfig.default_topic_params` 里配 `token_api_host` 才会触发 token 获取及后续 3 个定时接口
- 后续迭代:动态 X-SIGN 算法完全对齐(P2)、消息内容解密(P2)、消息列表结果与 MQTT 主题联动(P3)
---
*本文档记录设备模拟模块开发状态,供下次会话快速恢复上下文。* *本文档记录设备模拟模块开发状态,供下次会话快速恢复上下文。*
...@@ -437,14 +437,15 @@ class DoorSimulator(BaseSimulator): ...@@ -437,14 +437,15 @@ class DoorSimulator(BaseSimulator):
def _message_list_loop(self) -> None: def _message_list_loop(self) -> None:
""" """
消息列表 + 今日会议人脸页面定时拉取循环 消息列表 + 人脸页面 + 全局配置定时拉取循环
在独立线程中运行,interval 秒调用一次: 在独立线程中运行,interval 秒调用一次:
1. _call_message_list_once() — 消息列表接口 1. _call_message_list_once() — 消息列表接口
2. _call_face_page_once() — 今日会议人脸页面接口 2. _call_face_page_once() — 今日会议人脸页面接口
3. _call_ex_global_config_once() — 全局配置接口
调用失败仅记日志,不影响下次定时执行。 调用失败仅记日志,不影响下次定时执行。
""" """
logger.info(f"消息列表/人脸页面定时线程启动: {self.device_id}, 间隔={self._report_interval}s") logger.info(f"消息列表/人脸页面/全局配置定时线程启动: {self.device_id}, 间隔={self._report_interval}s")
while self._msg_running: while self._msg_running:
for _ in range(self._report_interval): for _ in range(self._report_interval):
if not self._msg_running: if not self._msg_running:
...@@ -456,8 +457,9 @@ class DoorSimulator(BaseSimulator): ...@@ -456,8 +457,9 @@ class DoorSimulator(BaseSimulator):
self._call_message_list_once() self._call_message_list_once()
self._call_face_page_once() self._call_face_page_once()
self._call_ex_global_config_once()
logger.info(f"消息列表/人脸页面定时线程结束: {self.device_id}") logger.info(f"消息列表/人脸页面/全局配置定时线程结束: {self.device_id}")
def trigger_event(self, event_type: str = "random") -> bool: def trigger_event(self, event_type: str = "random") -> bool:
""" """
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论