# 执行计划 — 中控设备多类型主题上报

> **文档类型**: 执行计划文档
> **创建日期**: 2026-08-06
> **关联 PRD**: `_PRD_需求优化_中控设备多类型主题上报.md` v1.0
> **负责人**: czj
> **优先级**: P1
> **状态**: 待评审

---

## 一、执行概述

### 1.1 需求概要

中控设备模拟器当前仅支持一种主题上报格式，需扩展为支持 7 种主题类型（会议室在线、设备在线、音频/视频/控制/网络/电源系统），实现：

1. **Excel 批量导入**：通过 Excel 新增"设备类型"列，一次性配置中控设备上报类型
2. **界面动态切换**：设备启动后可在界面上切换上报类型，实时生效
3. **消息体格式规范**：每种类型有独立的消息体格式，自动填充模拟数据

### 1.2 技术变更范围

| 层级 | 影响文件 | 变更类型 |
|------|---------|---------|
| 后端主题模板 | `backend/app/simulators/topic_templates.py` | 修改：中控主题模板扩展为 7 种类型 |
| 后端中控模拟器 | `backend/app/simulators/central_simulator.py` | 修改：新增 7 种消息体构建方法，根据 device_type 动态选择 |
| 后端模拟器基类 | `backend/app/simulators/base_simulator.py` | 修改：支持根据 device_type 动态选择主题模板 |
| 后端导入服务 | `backend/app/services/device_sim_service.py` | 修改：Excel 解析新增"设备类型"列，存入 topic_params |
| 后端导入路由 | `backend/app/routers/device_sim.py` | 修改：Excel 模板新增"设备类型"列 |
| 前端类型定义 | `frontend/src/types/device.ts` | 修改：新增中控设备类型枚举 |
| 前端 API 封装 | `frontend/src/api/deviceSim.ts` | 修改：新增切换设备类型 API |
| 前端设备列表 | `frontend/src/components/device/DeviceList.vue` | 修改：新增设备类型显示列 + 切换下拉框 |

### 1.3 预计工期

| 阶段 | 内容 | 预计工时 |
|------|------|---------|
| 阶段一 | 后端主题模板 + 模拟器消息体（7 种类型） | 1 天 |
| 阶段二 | 后端 Excel 导入 + API 适配 | 0.5 天 |
| 阶段三 | 前端设备类型显示 + 切换 UI | 1 天 |
| 阶段四 | 联调测试 + 部署验证 | 0.5 天 |
| **总计** | | **3 天** |

---

## 二、任务分解与实施计划

### 2.1 阶段一：后端主题模板 + 模拟器消息体（1 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| CEN-001 | 主题模板扩展 | `backend/app/simulators/topic_templates.py` | 为中控设备定义 7 种主题模板，每种包含独立的订阅/发布主题和 device_type 标识 | P0 |
| CEN-002 | 消息体构建方法 | `backend/app/simulators/central_simulator.py` | 新增 7 种消息体构建方法，覆盖所有主题类型 | P0 |
| CEN-003 | 动态选择逻辑 | `backend/app/simulators/central_simulator.py` | `build_status_payload()` 根据 `topic_params.device_type` 动态选择消息体 | P0 |
| CEN-004 | 基类主题选择 | `backend/app/simulators/base_simulator.py` | `_resolve_topics()` 支持根据 device_type 筛选对应主题模板 | P1 |

#### 详细实施

**CEN-001：主题模板扩展**

在 `topic_templates.py` 中将中控主题从 2 种扩展为 7 种：

```python
"central": [
    # 会议室在线（发布主题不同）
    {
        "key": "room_online",
        "template_subscribe": "/maintain/room/master/{room_id}/",
        "template_publish": "/maintain/room/online/{room_id}/",
        "label": "会议室在线",
        "device_type": "room_online",
        "params": ["room_id"],
        "param_labels": {"room_id": "会议室编号"},
        "param_defaults": {"room_id": "A101"},
    },
    # 设备在线
    {
        "key": "device_online",
        "template_subscribe": "/maintain/room/master/{room_id}/",
        "template_publish": "/maintain/room/master/client/",
        "label": "设备在线",
        "device_type": "device_online",
        "params": ["room_id"],
        "param_labels": {"room_id": "会议室编号"},
        "param_defaults": {"room_id": "A101"},
    },
    # 音频系统
    {
        "key": "audio",
        "template_subscribe": "/maintain/room/master/{room_id}/",
        "template_publish": "/maintain/room/master/client/",
        "label": "音频系统",
        "device_type": "audio",
        "params": ["room_id"],
        "param_labels": {"room_id": "会议室编号"},
        "param_defaults": {"room_id": "A101"},
    },
    # 视频系统
    {
        "key": "video",
        "template_subscribe": "/maintain/room/master/{room_id}/",
        "template_publish": "/maintain/room/master/client/",
        "label": "视频系统",
        "device_type": "video",
        "params": ["room_id"],
        "param_labels": {"room_id": "会议室编号"},
        "param_defaults": {"room_id": "A101"},
    },
    # 控制系统
    {
        "key": "control",
        "template_subscribe": "/maintain/room/master/{room_id}/",
        "template_publish": "/maintain/room/master/client/",
        "label": "控制系统",
        "device_type": "control",
        "params": ["room_id"],
        "param_labels": {"room_id": "会议室编号"},
        "param_defaults": {"room_id": "A101"},
    },
    # 网络系统
    {
        "key": "network",
        "template_subscribe": "/maintain/room/master/{room_id}/",
        "template_publish": "/maintain/room/master/client/",
        "label": "网络系统",
        "device_type": "network",
        "params": ["room_id"],
        "param_labels": {"room_id": "会议室编号"},
        "param_defaults": {"room_id": "A101"},
    },
    # 电源系统
    {
        "key": "power",
        "template_subscribe": "/maintain/room/master/{room_id}/",
        "template_publish": "/maintain/room/master/client/",
        "label": "电源系统",
        "device_type": "power",
        "params": ["room_id"],
        "param_labels": {"room_id": "会议室编号"},
        "param_defaults": {"room_id": "A101"},
    },
]
```

**CEN-002：消息体构建方法**

在 `central_simulator.py` 中新增 7 个方法：

```python
def build_room_online_payload(self) -> dict:
    """会议室在线消息体"""
    return {
        "udid": str(uuid.uuid4()),
        "action": "online",
        "value": 1
    }

def build_device_online_payload(self) -> dict:
    """设备在线消息体"""
    room_id = self.topic_params.get("room_id", self.device_id)
    device_number = self.topic_params.get("device_number", self.device_id)
    return {
        "action": "_updatestatus",
        "client_udid": room_id,
        "data": [
            {"device_udid": device_number, "power": 1, "online": 1, "watt": 10000, "run": "在线"},
            {"device_udid": device_number, "power": 1, "online": 1, "watt": 1000, "run": "在线"}
        ]
    }

def build_audio_payload(self) -> dict:
    """音频系统消息体"""
    device_number = self.topic_params.get("device_number", self.device_id)
    # ... 完整的音频通道数据结构

def build_video_payload(self) -> dict:
    """视频系统消息体"""
    # ... 视频通道数据结构

def build_control_payload(self) -> dict:
    """控制系统消息体"""
    # ... COM/IR/IO/REL/BUS/NET 数据结构

def build_network_payload(self) -> dict:
    """网络系统消息体"""
    # ... 网络通道数据结构

def build_power_payload(self) -> dict:
    """电源系统消息体"""
    # ... 电源通道数据结构
```

**CEN-003：动态选择逻辑**

```python
# 消息体构建方法映射
_PAYLOAD_BUILDERS = {
    "room_online": "build_room_online_payload",
    "device_online": "build_device_online_payload",
    "audio": "build_audio_payload",
    "video": "build_video_payload",
    "control": "build_control_payload",
    "network": "build_network_payload",
    "power": "build_power_payload",
}

def build_status_payload(self) -> dict:
    """根据 device_type 动态选择消息体"""
    device_type = self.topic_params.get("device_type", "device_online")
    method_name = self._PAYLOAD_BUILDERS.get(device_type, "build_device_online_payload")
    builder = getattr(self, method_name)
    return builder()
```

**CEN-004：基类主题选择**

修改 `base_simulator.py` 的 `_resolve_topics()` 方法，支持根据 `topic_params.device_type` 筛选中控主题：

```python
def _resolve_topics(self) -> None:
    # ... 现有逻辑
    if self._has_real_topics:
        params_with_device_id = dict(self.topic_params)
        if 'device_id' not in params_with_device_id:
            params_with_device_id['device_id'] = self.device_id

        # 中控设备：根据 device_type 筛选对应主题
        if self.device_type == "central" and "device_type" in self.topic_params:
            self._resolved_topics = resolve_topics_by_device_type(
                self.device_type, self.topic_params
            )
        else:
            self._resolved_topics = resolve_all_topics(self.device_type, params_with_device_id)
```

新增工具函数 `resolve_topics_by_device_type()`：

```python
def resolve_topics_by_device_type(device_type: str, topic_params: dict) -> Dict[str, dict]:
    """根据 device_type 筛选对应主题模板"""
    central_device_type = topic_params.get("device_type", "device_online")
    templates = get_topic_templates(device_type)
    resolved = {}
    for tmpl in templates:
        if tmpl.get("device_type") == central_device_type:
            resolved[tmpl["key"]] = {
                "topic": resolve_topic(tmpl["template_publish"], topic_params or {}),
                "direction": "publish",
                "label": tmpl["label"],
            }
            # 订阅主题
            if "template_subscribe" in tmpl:
                resolved[tmpl["key"] + "_sub"] = {
                    "topic": resolve_topic(tmpl["template_subscribe"], topic_params or {}),
                    "direction": "subscribe",
                    "label": tmpl["label"],
                }
            break  # 只取匹配的一种
    return resolved
```

---

### 2.2 阶段二：后端 Excel 导入 + API 适配（0.5 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| CEN-005 | Excel 列映射扩展 | `backend/app/services/device_sim_service.py` | 新增"设备类型"列解析，存入 topic_params.device_type | P0 |
| CEN-006 | Excel 模板更新 | `backend/app/routers/device_sim.py` | 中控模板新增"设备类型"列，含下拉选项 | P0 |
| CEN-007 | 设备编号存储 | `backend/app/services/device_sim_service.py` | 设备编号存入 topic_params.device_number | P0 |
| CEN-008 | 切换类型 API | `backend/app/routers/device_sim.py` | 新增 `PATCH /devices/{device_id}/type` 接口 | P1 |

#### 详细实施

**CEN-005：Excel 列映射扩展**

```python
COLUMN_MAP = {
    # ... 现有映射
    "设备类型": "central_device_type",
}

# 设备类型校验
CENTRAL_DEVICE_TYPES = {
    "会议室在线": "room_online",
    "设备在线": "device_online",
    "音频系统": "audio",
    "视频系统": "video",
    "控制系统": "control",
    "网络系统": "network",
    "电源系统": "power",
}
```

**CEN-006：Excel 模板更新**

```python
if device_type == "central":
    headers.append("会议室编号")
    headers.append("设备类型")

# 示例行
if device_type == "central":
    example.append("A101")
    example.append("设备在线")
```

**CEN-007：设备编号存储**

```python
# topic_params 构建逻辑
topic_params = {}
room_id = row_data.get("room_id")
if room_id:
    topic_params["room_id"] = str(room_id).strip()
# 设备编号 = device_id，用于消息体中 client_udid
topic_params["device_number"] = str(row_data["device_id"]).strip()
# 设备类型
central_device_type = row_data.get("central_device_type")
if central_device_type:
    topic_params["device_type"] = CENTRAL_DEVICE_TYPES.get(
        str(central_device_type).strip(), "device_online"
    )
```

**CEN-008：切换类型 API**

```python
@router.patch("/devices/{device_id}/type")
async def switch_device_type(
    device_id: str,
    device_type_name: str = Query(..., description="设备类型名称"),
    service: DeviceSimService = Depends(get_device_sim_service),
):
    """切换中控设备上报类型"""
    # 1. 更新 topic_params.device_type
    # 2. 如果设备正在运行，重启设备
    # 3. 返回更新结果
```

---

### 2.3 阶段三：前端设备类型显示 + 切换 UI（1 天）

#### 任务清单

| 任务ID | 任务名称 | 文件 | 说明 | 优先级 |
|--------|---------|------|------|--------|
| CEN-009 | 类型枚举定义 | `frontend/src/types/device.ts` | 新增 `CentralDeviceType` 枚举和映射 | P0 |
| CEN-010 | 切换类型 API | `frontend/src/api/deviceSim.ts` | 新增 `switchDeviceType()` 调用 | P1 |
| CEN-011 | 设备类型显示列 | `frontend/src/components/device/DeviceList.vue` | 设备列表新增"设备类型"列 | P0 |
| CEN-012 | 切换下拉框 | `frontend/src/components/device/DeviceList.vue` | 操作列新增"切换类型"下拉框 | P1 |
| CEN-013 | 导入弹窗提示 | `frontend/src/components/device/DeviceList.vue` | 中控导入弹窗新增"设备类型"说明 | P0 |

#### 详细实施

**CEN-009：类型枚举定义**

```typescript
// frontend/src/types/device.ts
export const CENTRAL_DEVICE_TYPES: Record<string, string> = {
  room_online: '会议室在线',
  device_online: '设备在线',
  audio: '音频系统',
  video: '视频系统',
  control: '控制系统',
  network: '网络系统',
  power: '电源系统',
}
```

**CEN-011：设备类型显示列**

```html
<!-- 仅中控设备显示 -->
<el-table-column v-if="deviceType === 'central'" label="设备类型" min-width="100">
  <template #default="{ row }">
    {{ CENTRAL_DEVICE_TYPES[row.topicParams?.device_type] || '设备在线' }}
  </template>
</el-table-column>
```

**CEN-012：切换下拉框**

```html
<!-- 仅中控设备显示 -->
<el-dropdown v-if="deviceType === 'central'" @command="(cmd) => handleSwitchType(row, cmd)">
  <span class="el-dropdown-link">
    切换类型 <el-icon><ArrowDown /></el-icon>
  </span>
  <template #dropdown>
    <el-dropdown-menu>
      <el-dropdown-item
        v-for="(label, key) in CENTRAL_DEVICE_TYPES"
        :key="key"
        :command="key"
      >
        {{ label }}
      </el-dropdown-item>
    </el-dropdown-menu>
  </template>
</el-dropdown>
```

---

### 2.4 阶段四：联调测试 + 部署验证（0.5 天）

#### 任务清单

| 任务ID | 任务名称 | 说明 | 优先级 |
|--------|---------|------|--------|
| CEN-014 | Excel 导入测试 | 下载中控模板 → 填写 7 种类型 → 导入验证 | P0 |
| CEN-015 | 界面切换测试 | 运行中切换类型 → 消息体格式变化 → 上报日志验证 | P0 |
| CEN-016 | 兼容性测试 | 旧版 Excel（无设备类型列）→ 默认为"设备在线" | P1 |
| CEN-017 | 部署到 192.168.5.60 | 上传修改文件 + 重启容器 | P0 |

---

## 三、执行记录

### 3.1 阶段一执行记录

**开始时间**: 待执行
**结束时间**:
**执行人**:
**完成情况**: ⏳ 待执行

### 3.2 阶段二执行记录

**开始时间**: 待执行
**结束时间**:
**执行人**:
**完成情况**: ⏳ 待执行

### 3.3 阶段三执行记录

**开始时间**: 待执行
**结束时间**:
**执行人**:
**完成情况**: ⏳ 待执行

### 3.4 阶段四执行记录

**开始时间**: 待执行
**结束时间**:
**执行人**:
**完成情况**: ⏳ 待执行

---

## 四、风险与注意事项

### 4.1 已知风险

| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 主题模板结构变更 | `topic_templates.py` 中中控模板从扁平结构改为按 device_type 分组 | 新增 `resolve_topics_by_device_type()` 函数，不影响其他设备类型 |
| 运行中切换类型需重启 | 设备重启期间短暂断连 | 前端提示用户"切换类型将重启设备" |
| 会议室在线发布主题不同 | 其他 6 种发布到 `/maintain/room/master/client/`，会议室在线发布到 `/maintain/room/online/{room_id}/` | 主题模板中分别定义 `template_publish` |

### 4.2 注意事项

1. **中控模板结构变更**：当前中控模板只有 2 个主题（`room_command` + `room_status`），需要替换为 7 种类型的新模板结构
2. **device_type 默认值**：旧设备（无 device_type）默认为"设备在线"（`device_online`），确保向后兼容
3. **Excel 模板兼容**：旧版 Excel（缺少"设备类型"列）导入时默认为"设备在线"
4. **设备编号 vs 设备ID**：Excel 中"设备编号"列对应数据库 `device_id` 字段，同时存入 `topic_params.device_number` 用于消息体 `client_udid`

---

*文档结束*