# PRD 需求优化文档 — 环境配置多主题与设备接收记录优化

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

---

## 一、需求背景

### 1.1 当前问题

设备模拟模块（Devices Sim）已完成基础开发，涵盖环境配置 CRUD、4 种设备模拟器（门口屏/无纸化/中控/集控客户端）、MQTT 连接管理、上报记录等功能。但在实际使用和评审过程中，发现以下两个关键缺口：

#### 问题一：环境配置仅支持单主题前缀

当前 `EnvConfig` 模型中 `topic_prefix` 字段为单字符串（如 `"device"`），所有设备类型的主题均固定拼接为 `{prefix}/{type}/{id}/...`。但在实际 IoT 场景中：

- 不同设备类型可能需要不同的主题结构（如门口屏用 `device/door`，集控客户端用 `device/client`）
- 不同环境可能共用 Broker 但使用不同的主题前缀来隔离设备类型
- 用户可能需要在同一环境配置下管理多个主题前缀

当前各模拟器的 `get_topic_prefix()` 方法均为硬编码的字符串（如 `"device/door"`、`"device/paperless"`），并未真正读取环境配置中的主题前缀，导致环境配置的 `topic_prefix` 字段形同虚设。

**相关文件：**

| 文件 | 行号 | 问题 |
|------|------|------|
| `backend/app/models/device_sim.py` | L51 | `topic_prefix: str` 为单字符串，无法表达多主题 |
| `backend/app/schemas/device_sim.py` | L27 | `topic_prefix` 字段限定 `max_length=200` |
| `backend/app/simulators/door_simulator.py` | L58 | 硬编码 `return "device/door"` |
| `backend/app/simulators/paperless_simulator.py` | L65 | 硬编码 `return "device/paperless"` |
| `backend/app/simulators/central_simulator.py` | L113 | 硬编码 `return "device/central"` |
| `backend/app/simulators/client_simulator.py` | L63 | 硬编码 `return "device/client"` |
| `frontend/src/views/device-sim/EnvConfig.vue` | L61-63 | 仅一个"主题前缀"输入框 |
| `frontend/src/types/device.ts` | L35 | 接口字段 `topicPrefix: string` |

#### 问题二：设备缺少接收记录（subscribe 方向）

当前 `ReportLog` 表字段 `direction` 支持 `publish`（上报）和 `subscribe`（接收）两个方向，前端 `ReportLog.vue` 组件也已用 Tag 标签区分显示「上报」/「接收」。但后端实际写入时，**只有 publish 方向**的记录被写入：

- 各模拟器在 `publish` 消息时通过 `_notify_report()` 回调写入记录，`direction` 默认为 `"publish"`
- 各模拟器的 `_on_command()` 方法收到平台下发的指令后，仅对指令响应（`command_response` 主题）做 `_notify_report`，而**没有将收到的原始指令本身记录为接收记录**

这意味着：
1. 数据库 `device_report_logs` 表中几乎不存在 `direction='subscribe'` 的记录
2. 前端「接收」标签永远不会显示数据
3. 无法追溯平台向设备下发了哪些指令

**相关文件：**

| 文件 | 行号 | 代码 | 问题 |
|------|------|------|------|
| `backend/app/simulators/door_simulator.py` | L90-147 | `_on_command()` | 入口处未记录接收到的指令 |
| `backend/app/simulators/paperless_simulator.py` | L94-170 | `_on_command()` | 同上 |
| `backend/app/simulators/central_simulator.py` | L152-224 | `_on_command()` | 同上 |
| `backend/app/simulators/client_simulator.py` | L99-192 | `_on_command()` | 同上 |
| `backend/app/models/device_sim.py` | L121 | `direction = "publish"` | 默认值正确，但 subscribe 方向从未被写入 |
| `frontend/src/components/device/ReportLog.vue` | L37-39 | 方向标签 | UI 已支持但后端无数据 |

### 1.2 目标用户

| 用户角色 | 使用场景 | 核心需求 |
|---------|---------|---------|
| 测试工程师 | 配置设备模拟环境 | 能为不同设备类型配置不同的主题前缀，查看设备收到的完整消息记录 |
| 测试工程师 | 验证平台下发指令 | 查看平台下发了哪些指令、指令内容、设备响应是否正确 |
| 开发工程师 | 调试设备接入逻辑 | 追溯完整的 MQTT 消息交互链路（上报 + 接收） |

### 1.3 需求目标

1. **多主题支持**：将环境配置中的单主题前缀升级为可配置的多个主题前缀，让各模拟器从环境配置中读取对应类型的主题
2. **接收记录补齐**：在所有模拟器收到平台下发指令时，将指令本身记录为 `direction=subscribe` 的接收记录
3. **API 兼容**：向后兼容，已有的单主题前缀数据自动迁移为多主题数组

---

## 二、功能需求

### 2.1 环境配置多主题支持

#### 2.1.1 功能描述

将环境配置的 `topic_prefix` 字段从单字符串升级为 `topics` 字符串数组，支持配置多个主题前缀。每个设备类型从 `topics` 数组中获取对应的主题前缀。

#### 2.1.2 字段变更

| 字段 | 变更前 | 变更后 |
|------|--------|--------|
| `topic_prefix` | `String(200)` 单前缀 | → 废弃，迁移到 `topics` |
| `topics` | 不存在 | `JSON` 数组，如 `["device/door", "device/paperless", "device/central", "device/client"]` |

#### 2.1.3 前端 UI 变更

**当前：** 单行输入框「主题前缀」

**变更后：** 可增删的「主题列表」组件

```
┌──────────────────────────────────────┐
│  ▼ 主题配置                           │
│                                      │
│  ┌────────────────────────────────┐  │
│  │ 门口屏:   [device/door______]  │  │
│  ├────────────────────────────────┤  │
│  │ 无纸化:   [device/paperless_]  │  │
│  ├────────────────────────────────┤  │
│  │ 中控设备: [device/central____]  │  │
│  ├────────────────────────────────┤  │
│  │ 集控客户端: [device/client____] │  │
│  └────────────────────────────────┘  │
│                                      │
│  默认使用设备类型名作为路径段，         │
│  可自定义每个设备类型的前缀。          │
└──────────────────────────────────────┘
```

#### 2.1.4 主题映射规则

环境配置中存储一个 `key-value` 映射（JSON 对象）：

```json
{
  "door": "device/door",
  "paperless": "device/paperless",
  "central": "device/central",
  "client": "device/client"
}
```

- **Key**：设备类型标识（`door`/`paperless`/`central`/`client`）
- **Value**：该设备类型对应的主题前缀
- 当设备类型未配置时，使用默认值 `device/{type}`

#### 2.1.5 各模拟器主题前缀读取逻辑

当前各模拟器的 `get_topic_prefix()` 返回硬编码字符串，优化后改为从环境配置中读取：

```
get_topic_prefix() → 从 env_config 的 topics 中获取 self.device_type 对应的前缀
                     → 如果未配置，fallback 到 "device/{device_type}"
```

这个改动需要：
1. 模拟器 `__init__` 接收 `topics` 参数（或 `env_config` 完整对象）
2. `DeviceSimService.start_simulator()` 将环境配置的 `topics` 传递给模拟器
3. 模拟器工厂 `create_simulator()` 支持传递 `topics` 参数

### 2.2 设备接收记录补齐

#### 2.2.1 功能描述

在四个模拟器的 `_on_command()` 方法中，收到平台下发的指令后，先调用 `_notify_report()` 记录收到的指令（direction=subscribe），再处理指令逻辑和发送响应。

#### 2.2.2 变更逻辑

**当前流程：**
```
平台 --command--→ 模拟器._on_command()
                    → 处理指令
                    → _notify_report(command_response)  ← 仅记录响应（publish）
```

**优化后流程：**
```
平台 --command--→ 模拟器._on_command()
                    → _notify_report(command_topic, payload, direction="subscribe")  ← 新增：记录接收的指令
                    → 处理指令
                    → _notify_report(command_response)  ← 记录响应（publish）
```

#### 2.2.3 修改范围

| 模拟器 | 方法 | 新增代码位置 |
|--------|------|-------------|
| `door_simulator.py` | `_on_command()` | L103 后，`command = payload.get("command", "")` 之后 |
| `paperless_simulator.py` | `_on_command()` | L107 后 |
| `central_simulator.py` | `_on_command()` | L165 后 |
| `client_simulator.py` | `_on_command()` | L115 后 |

每个模拟器新增的代码模式一致：

```python
# 记录接收到的指令
self._notify_report(
    topic,  # 原始指令主题
    payload,  # 原始指令内容
    direction="subscribe",
    status="success"
)
```

#### 2.2.4 接收记录筛选 API

当前 `GET /api/device-sim/report-logs` 不支持按 `direction` 筛选，需新增 `direction` 参数以支持按方向查询：

| 参数 | 类型 | 说明 |
|------|------|------|
| `direction` | `Optional[str]` | 筛选方向：`publish` / `subscribe`，不传则查全部 |

#### 2.2.5 前端展示优化

当前 `ReportLog.vue` 已支持方向标签显示，但需增加：
- 方向筛选下拉框（全部 / 上报 / 接收）
- 设备列表页增加「接收记录」Tab 或切换按钮

---

## 三、非功能需求

### 3.1 兼容性要求

| 要求 | 说明 |
|------|------|
| 旧数据兼容 | 已有 `topic_prefix` 字段数据自动迁移为 `topics` 数组格式 |
| 向后兼容 | 旧 API 调用 `topic_prefix` 参数仍可识别（服务端做兼容） |
| 前端兼容 | 已有环境配置编辑时，自动将单前缀转为数组格式展示 |

### 3.2 性能要求

| 指标 | 要求 |
|------|------|
| 接收记录写入延迟 | 从收到指令到写入数据库 ≤ 500ms |
| 方向筛选查询 | 按 direction 筛选的查询响应 ≤ 1s（10 万条记录以内） |

### 3.3 数据安全

| 要求 | 说明 |
|------|------|
| 数据完整性 | 接收记录一旦写入不可修改（仅可清理过期数据） |
| 保留周期 | 接收记录与上报记录共用保留周期（默认 7 天） |

---

## 四、数据结构变更

### 4.1 环境配置表（device_env_configs）

| 字段 | 变更 | 说明 |
|------|------|------|
| `topic_prefix` | 不变（保留，标注为 deprecated） | 已有数据兼容，后续可移除 |
| `topics` | **新增** JSON 字段 | 主题前缀映射，如 `{"door":"device/door","paperless":"device/paperless",...}` |

### 4.2 上报记录表（device_report_logs）

| 字段 | 变更 | 说明 |
|------|------|------|
| `direction` | 不变 | 已有字段，`publish`/`subscribe`，原设计正确 |

---

## 五、技术方案

### 5.1 多主题实现方案

#### 后端模型变更

```python
# backend/app/models/device_sim.py 新增字段
topics: Mapped[Optional[dict]] = mapped_column(
    JSON, nullable=True,
    comment="多主题前缀映射，如 {'door': 'device/door', 'paperless': 'device/paperless'}"
)
```

#### Schema 变更

```python
# backend/app/schemas/device_sim.py
class EnvConfigBase(BaseModel):
    # topic_prefix 保留（向后兼容）
    topic_prefix: str = Field(default="device", max_length=200, description="主题前缀（已弃用，建议使用 topics）")
    topics: Optional[Dict[str, str]] = Field(
        default=None,
        description="多主题前缀映射，key为设备类型，value为主题前缀"
    )
```

#### 模拟器主题读取逻辑

```python
# base_simulator.py 新增方法
def get_topic_prefix(self) -> str:
    """
    从环境配置的 topics 中获取当前设备类型的主题前缀
    """
    if self.topics and self.device_type in self.topics:
        return self.topics[self.device_type]
    return f"device/{self.device_type}"
```

#### 向后兼容策略

1. 创建环境配置时，`topics` 未传入时，自动从 `topic_prefix` 生成默认映射
2. `topic_prefix` 字段保留，但标记为 deprecated
3. 已有数据升级：`topic_prefix="device"` → `topics={"door":"device/door","paperless":"device/paperless","central":"device/central","client":"device/client"}`

### 5.2 接收记录实现方案

在 `base_simulator.py` 的 `_on_command` 抽象方法中增加通用处理逻辑：

```python
# base_simulator.py
def _on_command(self, topic: str, payload: dict) -> None:
    """
    处理平台下发的指令（基类默认实现）
    
    1. 记录接收到的指令（direction=subscribe）
    2. 调用子类特有的处理逻辑
    """
    # 记录接收到的指令
    self._notify_report(
        topic, payload,
        direction="subscribe",
        status="success"
    )
    # 调用子类处理
    self._handle_command(topic, payload)

@abstractmethod
def _handle_command(self, topic: str, payload: dict) -> None:
    """子类实现具体的指令处理逻辑"""
    pass
```

或者更简洁的方式：在各子类 `_on_command()` 方法第一行调用 `_notify_report` 记录接收指令。

### 5.3 API 新增

| 方法 | 路径 | 变更类型 | 说明 |
|------|------|---------|------|
| GET | `/api/device-sim/report-logs` | 修改 | 新增 `direction` 查询参数 |

---

## 六、变更文件清单

### 6.1 后端变更

| 文件 | 变更类型 | 说明 |
|------|---------|------|
| `backend/app/models/device_sim.py` | 修改 | `EnvConfig` 新增 `topics` JSON 字段 |
| `backend/app/schemas/device_sim.py` | 修改 | `EnvConfigBase` 新增 `topics` 字段，`EnvConfigResponse` 包含 `topics` |
| `backend/app/services/device_sim_service.py` | 修改 | 创建环境配置时自动生成默认 topics；启动模拟器时传递 topics |
| `backend/app/services/mqtt_manager.py` | 无需修改 | 主题由模拟器层决定，MQTT 管理器只负责发布/订阅 |
| `backend/app/simulators/__init__.py` | 修改 | `create_simulator()` 工厂新增 `topics` 参数 |
| `backend/app/simulators/base_simulator.py` | 修改 | 新增 `topics` 属性；`get_topic_prefix()` 改为从 topics 读取 |
| `backend/app/simulators/door_simulator.py` | 修改 | 移除 `get_topic_prefix()` 硬编码；`_on_command()` 开头增加接收记录 |
| `backend/app/simulators/paperless_simulator.py` | 修改 | 同上 |
| `backend/app/simulators/central_simulator.py` | 修改 | 同上 |
| `backend/app/simulators/client_simulator.py` | 修改 | 同上 |
| `backend/app/routers/device_sim.py` | 修改 | `GET /report-logs` 新增 `direction` 查询参数 |

### 6.2 前端变更

| 文件 | 变更类型 | 说明 |
|------|---------|------|
| `frontend/src/types/device.ts` | 修改 | `EnvConfig` 接口 `topicPrefix` → `topics: Record<string, string>` |
| `frontend/src/api/deviceSim.ts` | 修改 | `listReportLogs` 参数新增 `direction` |
| `frontend/src/views/device-sim/EnvConfig.vue` | 修改 | 表单从单行输入改为多主题列表编辑 |
| `frontend/src/components/device/ReportLog.vue` | 修改 | 新增方向筛选下拉框 |
| `frontend/src/views/device-sim/DoorSim.vue` | 无需修改 | 侧边栏联动 |
| `frontend/src/views/device-sim/PaperlessSim.vue` | 无需修改 | 同上 |
| `frontend/src/views/device-sim/CentralSim.vue` | 无需修改 | 同上 |
| `frontend/src/views/device-sim/ClientSim.vue` | 无需修改 | 同上 |

---

## 七、验收标准

| 验收项 | 预期结果 | 涉及模块 |
|--------|---------|---------|
| 环境配置支持多主题 | 创建时可为每个设备类型配置不同的主题前缀 | 环境配置 |
| 旧数据兼容 | 已有 `topic_prefix` 数据自动转为 `topics` 格式，不丢失 | 环境配置 |
| 门口屏主题读取 | 门口屏模拟器启动时使用环境配置中的多主题设置 | 门口屏模拟 |
| 无纸化主题读取 | 无纸化模拟器启动时使用环境配置中的多主题设置 | 无纸化模拟 |
| 中控主题读取 | 中控设备模拟器启动时使用环境配置中的多主题设置 | 中控设备模拟 |
| 集控客户端主题读取 | 集控客户端模拟器启动时使用环境配置中的多主题设置 | 集控客户端模拟 |
| 门口屏接收记录 | 平台下发指令后，记录显示为「接收」方向 | 门口屏模拟 |
| 无纸化接收记录 | 平台下发指令后，记录显示为「接收」方向 | 无纸化模拟 |
| 中控接收记录 | 平台下发指令后，记录显示为「接收」方向 | 中控设备模拟 |
| 集控客户端接收记录 | 平台下发指令后，记录显示为「接收」方向 | 集控客户端模拟 |
| 方向筛选 | 上报记录列表可按方向筛选（全部/上报/接收） | 上报记录 |
| 前端多主题编辑 | 环境配置页面可编辑多主题列表，支持增删改 | 环境配置 |

---

## 八、工作量估算

| 阶段 | 内容 | 预计工时 |
|------|------|---------|
| 阶段一 | 后端模型 + Schema + 迁移（多主题） | 1 天 |
| 阶段二 | 模拟器主题读取改造 + 向后兼容 | 1 天 |
| 阶段三 | 接收记录补齐（4 个模拟器） | 0.5 天 |
| 阶段四 | 接收记录 API + 方向筛选 | 0.5 天 |
| 阶段五 | 前端多主题编辑 UI | 1 天 |
| 阶段六 | 前端方向筛选 + 联调验证 | 0.5 天 |
| **总计** | | **4.5 天** |

---

*本文档由 Claude Code 生成，用于指导设备模拟模块的优化开发。*