diff --git a/开发文档/spec-历史数据.md b/开发文档/spec-历史数据.md index a330670..2d2f0c7 100644 --- a/开发文档/spec-历史数据.md +++ b/开发文档/spec-历史数据.md @@ -48,10 +48,6 @@ │ 数据写入 │ │ │ │ ○ 历史数据 │ ← 同层级,无子菜单 -│ │ -│ ○ 实时监控 │ -│ ○ 智能控制 │ -│ ○ 系统配置 │ └──────────┘ ``` @@ -122,26 +118,26 @@ CREATE STABLE IF NOT EXISTS computed_data ( ``` 所有历史点位 - ├── 设备1 - │ ├── 分组A - │ │ ├── ☐ 采集点1(最新值,质量戳) - │ │ ├── ☐ 采集点2(最新值,质量戳) - │ │ └── ... - │ ├── 分组B + ├── 分组A + │ ├── ☐ 采集点1(最新值,质量戳) + │ ├── ☐ 采集点2(最新值,质量戳) │ └── ... - ├── 设备2 + ├── 分组B + │ ├── ☐ 采集点3(最新值,质量戳) │ └── ... + ├── ...(其他分组) └── 内部数据(预留) └── (暂无数据) ``` +分组名称直接取自数据采集点配置中的 `group_name`,同一分组下的点位可来自不同设备。 + ### 3.2 构建规则 | 层级 | 数据来源 | 说明 | |------|----------|------| -| 第一层:设备 | PostgreSQL `devices` 表 | 只有旗下有可用历史数据点的设备才显示 | -| 第二层:分组 | PostgreSQL `collection_points` 表的 group_name | 按设备分组,只有该设备下有历史数据点的分组才显示 | -| 第三层:点位 | PostgreSQL `collection_points` | 满足 store_history=true 且 enabled 的采集点 | +| 第一层:分组 | PostgreSQL `collection_points` 表的 `group_name` | 去重后作为树的顶层节点,只有包含可用历史数据点的分组才显示 | +| 第二层:点位 | PostgreSQL `collection_points` | 满足 store_history=true 且 enabled 的采集点,按 group_name 归属到对应分组下 | | 独立节点:内部数据 | 硬编码占位 | 当前阶段显示"暂无数据",后续扩展 | ### 3.3 节点属性 @@ -155,15 +151,17 @@ CREATE STABLE IF NOT EXISTS computed_data ( "type": "collection", // 或 'computed'(预留) "data_type": "REAL", "unit": "mg/L", - "latest_value": 2.35, - "latest_quality": "good", - "latest_ts": "2026-07-09T10:00:01+08:00", "device_id": "device-uuid", "device_name": "一期曝气柜PLC", - "group_name": "曝气池" + "group_name": "曝气池", + "latest_value": 2.35, + "latest_quality": "good", + "latest_ts": "2026-07-09T10:00:01+08:00" } ``` +> 由于树结构按分组平铺,点位节点需额外携带 `device_id` 和 `device_name` 以便前端识别点位所属设备。 + ### 3.4 复选框行为 | 操作 | 行为 | @@ -231,36 +229,33 @@ Response (200): { "tree": [ { - "id": "device-uuid-1", - "name": "一期曝气柜PLC", - "type": "device", + "id": "group-aeration", + "name": "曝气池", + "type": "group", "children": [ { - "id": "group-aeration", - "name": "曝气池", - "type": "group", - "children": [ - { - "id": "point-uuid-1", - "name": "曝气池DO_01", - "type": "collection", - "data_type": "REAL", - "unit": "mg/L", - "latest_value": 2.35, - "latest_quality": "good", - "latest_ts": "2026-07-09T10:00:01+08:00" - }, - { - "id": "point-uuid-2", - "name": "曝气池温度", - "type": "collection", - "data_type": "REAL", - "unit": "℃", - "latest_value": 25.1, - "latest_quality": "good", - "latest_ts": "2026-07-09T10:00:01+08:00" - } - ] + "id": "point-uuid-1", + "name": "曝气池DO_01", + "type": "collection", + "data_type": "REAL", + "unit": "mg/L", + "device_id": "device-uuid-1", + "device_name": "一期曝气柜PLC", + "latest_value": 2.35, + "latest_quality": "good", + "latest_ts": "2026-07-09T10:00:01+08:00" + }, + { + "id": "point-uuid-2", + "name": "曝气池温度", + "type": "collection", + "data_type": "REAL", + "unit": "℃", + "device_id": "device-uuid-1", + "device_name": "一期曝气柜PLC", + "latest_value": 25.1, + "latest_quality": "good", + "latest_ts": "2026-07-09T10:00:01+08:00" } ] }, @@ -475,15 +470,14 @@ CSV格式: │ 点位树 │ 时间范围选择器 [过去1小时] [今天] [自定义] │ │ (复选框) │ │ │ │ ┌──────────────────────────────────┐ │ -│ ☑ 设备1 │ │ │ │ -│ ☑ 分组A │ │ 曲线图区域 │ │ -│ ☑ DO │ │ (ECharts 折线图) │ │ -│ ☐ 温度 │ │ │ │ -│ ☐ 分组B │ │ │ │ -│ ☐ 设备2 │ └──────────────────────────────────┘ │ -│ │ ┌──────────┬──────────┬──────────┐ │ -│ 内部数据 │ │ 点位名称 │ 时间 │ 数值 │ │ -│ ☐ (暂无) │ │ DO │ 00:00:05 │ 2.38 │ │ +│ ☑ 曝气池 │ │ │ │ +│ ☑ DO │ │ 曲线图区域 │ │ +│ ☐ 温度 │ │ (ECharts 折线图) │ │ +│ ☐ 加药间 │ │ │ │ +│ │ └──────────────────────────────────┘ │ +│ 内部数据 │ ┌──────────┬──────────┬──────────┐ │ +│ ☐ (暂无) │ │ 点位名称 │ 时间 │ 数值 │ │ +│ │ │ DO │ 00:00:05 │ 2.38 │ │ │ │ │ 温度 │ 00:00:05 │ 25.2 │ │ │ │ └──────────┴──────────┴──────────┘ │ │ [曲线] [表格]│ │ @@ -637,17 +631,16 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme │ 点位树 │ 时间范围选择器 [自定义] │ │ (复选框) │ 间隔: [10分钟] ▼ │ │ │ │ -│ ☑ 设备1 │ ┌──────────────────────────────────┐ │ -│ ☑ 分组A │ │ 时间 │ DO(mg/L)│ 温度(℃)│ │ -│ ☑ DO │ │──────────────┼─────────┼────────│ │ -│ ☐ 温度 │ │ 00:00:00 │ 2.35 │ 25.1 │ │ -│ ☐ 分组B │ │ 00:10:00 │ 2.40 │ 25.0 │ │ -│ ☐ 设备2 │ │ 00:20:00 │ — │ 25.3 │ │ -│ │ │ 00:30:00 │ 2.38 │ — │ │ -│ 内部数据 │ │ ... │ ... │ ... │ │ -│ ☐ (暂无) │ └──────────────────────────────────┘ │ +│ ☑ 曝气池 │ ┌──────────────────────────────────┐ │ +│ ☑ DO │ │ 时间 │ DO(mg/L)│ 温度(℃)│ │ +│ ☐ 温度 │ │──────────────┼─────────┼────────│ │ +│ ☐ 加药间 │ │ 00:00:00 │ 2.35 │ 25.1 │ │ +│ │ │ 00:10:00 │ 2.40 │ 25.0 │ │ +│ 内部数据 │ │ 00:20:00 │ — │ 25.3 │ │ +│ ☐ (暂无) │ │ 00:30:00 │ 2.38 │ — │ │ +│ │ │ ... │ ... │ ... │ │ +│ [曲线] [表格]│ └──────────────────────────────────┘ │ │ │ [导出CSV] │ -│ [曲线] [表格]│ │ └───────────┴──────────────────────────────────────┘ ``` @@ -684,30 +677,28 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme ### 8.1 页面布局 ``` -┌─────────────────────────────────────────────────────┐ -│ ┌──────────┐ ┌──────────────────────────────────┐ │ -│ │ ▽ 数据管理 │ │ │ │ -│ │ 设备管理 │ │ │ │ -│ │ 数据采集 │ │ 内容区域 │ │ -│ │ 数据写入 │ │ │ │ -│ │ │ │ ┌──────────────────────────────┐ │ │ -│ │ ○ 历史数据 │ │ │ ☐ 设备1 │ │ │ -│ │ │ │ │ ☐ 分组A │ │ │ -│ │ ○ 实时监控 │ │ │ ☑ 曝气池DO_01 2.35 │ │ │ -│ │ ○ 智能控制 │ │ │ ☐ 曝气池温度 25.1 │ │ │ -│ │ ○ 系统配置 │ │ │ ☐ 分组B │ │ │ -│ │ │ │ │ ☐ 设备2 │ │ │ -│ └──────────┘ │ │ ─────── │ │ │ -│ │ │ 内部数据 │ │ │ -│ │ │ ☐ (暂无数据) │ │ │ -│ │ └──────────────────────────────┘ │ │ -│ │ [曲线] [表格] 切换 │ │ -│ │ ┌──────────────────────────────┐ │ │ -│ │ │ 曲线图/表格内容区域 │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────┘ │ │ -│ └──────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────┐ +│ ┌──────────┐ ┌───────────────────────┐ │ +│ │ ▽ 数据管理 │ │ │ │ +│ │ 设备管理 │ │ │ │ +│ │ 数据采集 │ │ 内容区域 │ │ +│ │ 数据写入 │ │ │ │ +│ │ │ │ ┌───────────────────┐ │ │ +│ │ ○ 历史数据 │ │ │ ☑ 曝气池 │ │ │ +│ │ │ │ │ ☑ DO │ │ │ +│ └──────────┘ │ │ ☐ 温度 │ │ │ +│ │ │ ☐ 加药间 │ │ │ +│ │ │ ─────── │ │ │ +│ │ │ 内部数据 │ │ │ +│ │ │ ☐ (暂无数据) │ │ │ +│ │ └───────────────────┘ │ │ +│ │ [曲线] [表格] 切换 │ │ +│ │ ┌───────────────────┐ │ │ +│ │ │ 曲线图/表格内容区域 │ │ │ +│ │ │ │ │ │ +│ │ └───────────────────┘ │ │ +│ └───────────────────────┘ │ +└──────────────────────────────────────────┘ ``` ### 8.2 模式切换 @@ -723,7 +714,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme 用户进入历史数据页面 │ ▼ -加载历史点位树(GET /api/v1/history/tree) +加载历史点位树(GET /api/v1/history/tree),按分组→点位两层构建 │ ▼ 左侧树渲染,用户勾选点位 @@ -746,7 +737,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme | 编号 | 规则 | 说明 | |------|------|------| | H001 | 历史数据来源 | 仅展示 store_history=true 且设备/点位均启用的采集点数据 | -| H002 | 树形结构 | 按设备→分组→点位三层组织,外加"内部数据"预留节点 | +| H002 | 树形结构 | 按分组→点位两层组织,分组名称取自采集点配置的 group_name,外加"内部数据"预留节点 | | H003 | 最大勾选数 | 同时勾选的点位不超过20个 | | H004 | 数据保留 | 可配置1~730天,通过TDengine KEEP参数实现 | | H005 | 表格时间对齐 | 最近邻匹配,匹配窗口 = interval/2 | diff --git a/开发文档/spec-数据管理.md b/开发文档/spec-数据管理.md index ff621a6..c9ea676 100644 --- a/开发文档/spec-数据管理.md +++ b/开发文档/spec-数据管理.md @@ -141,12 +141,27 @@ CREATE UNIQUE INDEX idx_devices_name ON devices(name) WHERE deleted = FALSE; #### 2.1.4 设备的业务状态 +**持久化状态(数据库)**: + | 状态 | 说明 | |------|------| | enabled=true | 设备启用,采集引擎会为该设备建立连接并执行采集任务 | | enabled=false | 设备禁用,采集引擎跳过该设备,已有连接断开 | | deleted=true | 逻辑删除,数据保留但不再显示和使用 | +**运行时连接状态(采集引擎内存维护,不落库)**: + +| 连接状态 | 枚举值 | 说明 | +|---------|--------|------| +| 连接成功 | `connected` | 设备已启用(enabled=true)且与PLC连接成功,可正常采集 | +| 连接失败 | `disconnected` | 设备已启用(enabled=true)但连接断开或连接失败(初次连接或重连均失败) | +| 连接禁用 | `disabled` | 设备未启用(enabled=false),不进行连接。采集引擎不维护该设备的连接状态 | + +说明: +- `connection_status` 是运行时状态,由采集引擎维护在内存中,**不存储到数据库** +- 设备逻辑删除(deleted=true)时,在接口响应中表现为 `disabled` +- 采集引擎未运行时,所有 enabled=true 的设备均返回 `disconnected` + ### 2.2 RESTful API #### 2.2.1 接口列表 @@ -217,6 +232,9 @@ Response (201): - 与新增使用相同的请求体结构 - 修改后,如果设备正在运行中,采集引擎应自动重新连接 +- 如果修改了 enabled 字段(启用/禁用),响应中的 `connection_status` 同步变化: + - 从 enabled=false 改为 true → 采集引擎尝试连接,`connection_status` 在异步连接完成前为 `disconnected` + - 从 enabled=true 改为 false → `connection_status` 立即变为 `disabled` **DELETE /api/v1/devices/{id}** — 逻辑删除 @@ -288,6 +306,7 @@ Response (200): "connect_timeout": 5, "reconnect_interval": 10, "protocol_config": { "rack": 0, "slot": 1 }, + "connection_status": "connected", "created_at": "...", "updated_at": "..." } @@ -295,6 +314,12 @@ Response (200): } ``` +`connection_status` 字段说明: +- 由 API Handler 从采集引擎运行时状态中获取后组装到响应中 +- 枚举值:`connected`、`disconnected`、`disabled` +- enabled=false 时始终为 `disabled`,无需查询引擎运行时状态 +- enabled=true 时,如果采集引擎未运行或查询不到该设备状态,统一返回 `disconnected` + --- ## 3. 数据采集点管理 @@ -903,6 +928,35 @@ USING collection_data TAGS ( - 如果 store_history=false,不写入TDengine - 写入频率由 history_interval 控制(例如 history_interval=5,则每5分钟写入一条) +### 5.8 运行时状态查询接口 + +采集引擎需要对外提供设备连接状态的查询能力,供 API Handler 层在组装设备列表/详情响应时获取。 + +``` +采集引擎内部维护: + deviceConnections map[string]ConnectionState // key: device_id + + type ConnectionState struct { + Status string // "connected" | "disconnected" + Since time.Time // 状态持续起始时间 + } + +对外暴露的查询方法(在采集引擎实例上): + func (m *CollectorManager) GetDeviceStatus(deviceID string) string + func (m *CollectorManager) GetDeviceStatusMap() map[string]string +``` + +状态查询规则: + +| 条件 | 返回状态 | +|------|---------| +| 设备不在映射表中(引擎未初始化该设备) | `disconnected` | +| 设备在映射表中且连接成功 | `connected` | +| 设备在映射表中但连接已断开 | `disconnected` | +| 引擎未启动或不可用 | `disconnected` | + +> 采集引擎以单例模式运行在服务端进程中,API Handler 通过依赖注入获取引擎实例的引用,直接调用 `GetDeviceStatusMap()` 查询状态。 + --- ## 6. 写入引擎(运行时) @@ -1051,22 +1105,17 @@ func GetProtocol(protocolType string) (ProtocolDriver, error) { ## 附录A:前端页面结构参考 -### A.1 页面布局(左侧导航栏) +### A.1 页面布局 ``` -┌─────────────────────────────────────────────────────┐ -│ ┌──────────┐ ┌──────────────────────────────────┐ │ -│ │ ▽ 数据管理 │ │ │ │ -│ │ 设备管理 │ │ 内容区域 │ │ -│ │ 数据采集 │ │ │ │ -│ │ 数据写入 │ │ │ │ -│ │ │ │ │ │ -│ │ ○ 实时监控 │ │ │ │ -│ │ ○ 智能控制 │ │ │ │ -│ │ ○ 历史分析 │ │ │ │ -│ │ ○ 系统配置 │ │ │ │ -│ └──────────┘ └──────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────┐ +│ ┌──────────┐ ┌───────────────────────┐ │ +│ │ ▽ 数据管理 │ │ │ │ +│ │ 设备管理 │ │ 内容区域 │ │ +│ │ 数据采集 │ │ │ │ +│ │ 数据写入 │ │ │ │ +│ └──────────┘ └───────────────────────┘ │ +└──────────────────────────────────────────┘ ``` 左侧导航栏说明: @@ -1074,7 +1123,6 @@ func GetProtocol(protocolType string) (ProtocolDriver, error) { - 当前展开的主菜单:**数据管理** - 展开后显示三个子菜单项:**设备管理**、**数据采集**、**数据写入** - 点击子菜单项,右侧内容区域切换对应页面 -- 其他主菜单(实时监控、智能控制、历史分析、系统配置)为后续模块占位,当前不可展开 ### A.2 子页面:设备管理 diff --git a/系统设计图/历史数据趋势分析-深色科技风.png b/系统设计图/历史数据趋势分析-深色科技风.png deleted file mode 100644 index 336978e..0000000 Binary files a/系统设计图/历史数据趋势分析-深色科技风.png and /dev/null differ