Files
AquaControlAI/开发文档/spec-历史数据.md
T
2026-07-11 08:19:19 +08:00

28 KiB
Raw Blame History

历史数据模块 — 开发规格说明

本文档属于《污水厂智能控制平台》的一部分,详细描述历史数据模块的功能、数据模型、API接口和业务逻辑,细度可达直接开发级别。 本模块与 数据管理模块 紧密关联,依赖其中的设备、采集点、TDengine 超级表等设计。


目录


1. 概述

1.1 模块定位

历史数据模块负责展示平台中所有已存储的历史时序数据,提供两种查看方式:

  • 曲线模式:多条数据在同一时间轴上的变化趋势对比,支持游标查看
  • 表格模式:按固定时间间隔展示数据,支持导出CSV

1.2 模块边界

交互对象 方向 内容
Web前端 返回 历史点位树、历史数据查询结果、CSV文件
TDengine 查询 读取 collection_data 超级表下的子表数据
PostgreSQL 查询 读取设备、采集点配置,构建树形结构
数据管理模块 依赖 采集点配置、设备配置、分组信息

1.3 页面层级

历史数据与数据管理在同一层级,位于左侧导航栏:

┌──────────┐
│  ▽ 数据管理 │
│    设备管理 │
│    数据采集 │
│    数据写入 │
│            │
│  ○ 历史数据 │  ← 同层级,无子菜单
│            │
│  ○ 实时监控  │
│  ○ 智能控制  │
│  ○ 系统配置  │
└──────────┘

2. 数据来源

2.1 当前数据来源:采集点历史数据

来自数据管理模块中启用了 store_history=true 的采集点,数据存储在 TDengine 的 collection_data 超级表中。

-- 已在数据管理模块中定义的超级表
CREATE STABLE IF NOT EXISTS collection_data (
    ts          TIMESTAMP,
    value       DOUBLE,
    quality     INT,              -- 0=good, 1=bad
    point_id    VARCHAR(32),
    point_name  VARCHAR(128)
) TAGS (
    device_id   VARCHAR(32),
    device_name VARCHAR(128),
    data_type   VARCHAR(16),
    unit        VARCHAR(32)
);

2.2 预留数据来源:非采集点位(内部数据)

未来的扩展数据(如AI模型推理的建议值、人工录入的补充数据等),统一归属为内部数据

预留数据模型:新增 computed_data 超级表

-- 预留:非采集点位的时序数据
CREATE STABLE IF NOT EXISTS computed_data (
    ts          TIMESTAMP,
    value       DOUBLE,
    quality     INT,              -- 0=good, 1=bad
    point_id    VARCHAR(32),
    point_name  VARCHAR(128),
    source      VARCHAR(32)       -- 数据来源: 'ai_model', 'manual_input', 'external' 等
) TAGS (
    category    VARCHAR(32),      -- 类别,如 'aeration', 'dosing', 'prediction'
    unit        VARCHAR(32)
);

当前阶段只实现采集点历史数据展示,内部数据在树形结构中占位,不可勾选,显示"暂无数据"。

2.3 历史数据点位判定规则

一个采集点是否出现在历史数据树形结构中,取决于:

采集点出现在历史树中 ⇔ collection_points.enabled = true
                     AND collection_points.deleted = false
                     AND collection_points.store_history = true
                     AND 关联的 devices.enabled = true
                     AND 关联的 devices.deleted = false

3. 历史点位树形结构

3.1 树结构定义

所有历史点位
 ├── 设备1
 │    ├── 分组A
 │    │    ├── ☐ 采集点1(最新值,质量戳)
 │    │    ├── ☐ 采集点2(最新值,质量戳)
 │    │    └── ...
 │    ├── 分组B
 │    └── ...
 ├── 设备2
 │    └── ...
 └── 内部数据(预留)
      └── (暂无数据)

3.2 构建规则

层级 数据来源 说明
第一层:设备 PostgreSQL devices 只有旗下有可用历史数据点的设备才显示
第二层:分组 PostgreSQL collection_points 表的 group_name 按设备分组,只有该设备下有历史数据点的分组才显示
第三层:点位 PostgreSQL collection_points 满足 store_history=true 且 enabled 的采集点
独立节点:内部数据 硬编码占位 当前阶段显示"暂无数据",后续扩展

3.3 节点属性

每个采集点节点应包含以下信息(用于展示和查询):

{
    "id": "point-uuid",
    "name": "曝气池DO_01",
    "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": "曝气池"
}

3.4 复选框行为

操作 行为
勾选点位 该点位数据加入曲线/表格
取消勾选点位 该点位数据从曲线/表格中移除
勾选上限 建议最多同时勾选20个点位(前端友好性考虑)
勾选状态 切换曲线/表格模式时,勾选状态保持不变

4. 数据保留策略

4.1 配置方式

在系统配置中增加一个全局参数:

参数 类型 范围 默认值 说明
history_retention_days int 1~730 365 历史数据保留天数

4.2 实现方式

通过 TDengine 的数据库级 KEEP 参数控制:

-- 创建数据库时指定保留天数
CREATE DATABASE IF NOT EXISTS water_plant 
  KEEP 365                     -- 数据保留天数(对应配置值)
  DAYS 10                      -- 每10天一个文件
  BLOCKS 100;

-- 修改保留天数(当用户在系统配置中修改时执行)
ALTER DATABASE water_plant KEEP 730;

4.3 注意

  • 修改保留天数后,TDengine 会自动清理超出保留期的数据
  • 保留天数对 collection_datacomputed_data 两个超级表同时生效
  • 建议在系统配置页面提供"立即清理过期数据"按钮,但通常不需要手动操作

5. RESTful API

5.1 接口列表

方法 路径 说明
GET /api/v1/history/tree 获取历史点位树形结构
POST /api/v1/history/query 查询历史数据(曲线模式)
POST /api/v1/history/query-table 查询历史数据(表格模式)
POST /api/v1/history/export 导出历史数据为CSV

5.2 接口详细定义

5.2.1 GET /api/v1/history/tree — 获取历史点位树

无需参数,从 PostgreSQL 查询所有符合条件的历史点位,按设备→分组→点位构建树。

Response (200)

{
    "tree": [
        {
            "id": "device-uuid-1",
            "name": "一期曝气柜PLC",
            "type": "device",
            "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": "internal-data",
            "name": "内部数据",
            "type": "reserved",
            "children": [
                {
                    "id": "placeholder",
                    "name": "暂无数据",
                    "type": "placeholder",
                    "disabled": true
                }
            ]
        }
    ]
}

5.2.2 POST /api/v1/history/query — 曲线模式查询

Request body

{
    "point_ids": ["point-uuid-1", "point-uuid-2"],
    "start_time": "2026-07-09T00:00:00+08:00",
    "end_time": "2026-07-09T01:00:00+08:00"
}
参数 类型 必填 说明
point_ids string[] 要查询的点位ID列表,最少1个,最多20个
start_time string 开始时间,ISO 8601格式
end_time string 结束时间,ISO 8601格式

后端逻辑:

1. 根据 point_ids 获取每个点位的子表名(从 point_id 映射)
2. 对每个点位,查询 TDengine
   SELECT ts, value, quality 
   FROM {subtable_name}
   WHERE ts >= start_time AND ts <= end_time
   ORDER BY ts ASC
3. 聚合结果,返回

Response (200)

{
    "series": [
        {
            "point_id": "point-uuid-1",
            "point_name": "曝气池DO_01",
            "unit": "mg/L",
            "data_type": "REAL",
            "data": [
                { "ts": "2026-07-09T00:00:01+08:00", "value": 2.35, "quality": "good" },
                { "ts": "2026-07-09T00:00:02+08:00", "value": 2.36, "quality": "good" },
                { "ts": "2026-07-09T00:00:05+08:00", "value": 2.38, "quality": "good" },
                { "ts": "2026-07-09T00:00:08+08:00", "value": 2.30, "quality": "bad" },
                { "ts": "2026-07-09T00:00:10+08:00", "value": 2.40, "quality": "good" }
            ]
        },
        {
            "point_id": "point-uuid-2",
            "point_name": "曝气池温度",
            "unit": "℃",
            "data_type": "REAL",
            "data": [
                { "ts": "2026-07-09T00:00:01+08:00", "value": 25.1, "quality": "good" },
                { "ts": "2026-07-09T00:00:05+08:00", "value": 25.2, "quality": "good" },
                { "ts": "2026-07-09T00:00:10+08:00", "value": 25.0, "quality": "good" }
            ]
        }
    ]
}

5.2.3 POST /api/v1/history/query-table — 表格模式查询

Request body

{
    "point_ids": ["point-uuid-1", "point-uuid-2"],
    "start_time": "2026-07-09T00:00:00+08:00",
    "end_time": "2026-07-09T01:00:00+08:00",
    "interval_minutes": 10
}
参数 类型 必填 说明
point_ids string[] 要查询的点位ID列表,最少1个,最多20个
start_time string 开始时间
end_time string 结束时间
interval_minutes int 间隔分钟数,范围1~1440,必须能被60整除(建议限制为1, 2, 5, 10, 15, 20, 30, 60

后端逻辑(重点 — 最近邻匹配):

1. 根据 point_ids 获取每个点位的子表名
2. 对每个点位,查询 TDengine 获取 start_time ~ end_time 范围内的所有原始数据
3. 生成目标时间序列:从 start_time 开始,每隔 interval_minutes 生成一个目标时间点
   - 例如:00:00, 00:10, 00:20, ..., 01:00
4. 对每个目标时间点 T,执行最近邻匹配:
   a. 定义匹配窗口 = interval_minutes / 2(向上取整,单位分钟)
   b. 在原始数据中,找时间戳距离 T 最近的数据点
   c. 如果最近距离 ≤ 匹配窗口 → 匹配成功,使用该数据点的 value 和 quality
   d. 如果最近距离 > 匹配窗口 → 匹配失败,标记为 null
5. 返回按目标时间序列对齐的多条数据

举例: interval_minutes=10,匹配窗口=5分钟

目标时间 原始数据 最近距离 匹配结果
00:00:00 有 00:00:01 的数据 1秒 2.35 (good)
00:10:00 有 00:00:08 和 00:10:02 的数据 2秒(00:10:02 2.40 (good)
00:20:00 最近数据在 00:14:00,距离6分钟 6分钟 > 5分钟 null(显示—)

Response (200)

{
    "time_column": [
        "2026-07-09T00:00:00+08:00",
        "2026-07-09T00:10:00+08:00",
        "2026-07-09T00:20:00+08:00",
        "2026-07-09T00:30:00+08:00",
        "2026-07-09T00:40:00+08:00",
        "2026-07-09T00:50:00+08:00",
        "2026-07-09T01:00:00+08:00"
    ],
    "columns": [
        {
            "point_id": "point-uuid-1",
            "point_name": "曝气池DO_01",
            "unit": "mg/L",
            "data": [
                { "value": 2.35, "quality": "good" },
                { "value": 2.40, "quality": "good" },
                null,
                { "value": 2.38, "quality": "good" },
                { "value": 2.42, "quality": "good" },
                null,
                { "value": 2.36, "quality": "good" }
            ]
        },
        {
            "point_id": "point-uuid-2",
            "point_name": "曝气池温度",
            "unit": "℃",
            "data": [
                { "value": 25.1, "quality": "good" },
                { "value": 25.0, "quality": "good" },
                { "value": 25.3, "quality": "good" },
                null,
                { "value": 25.1, "quality": "good" },
                { "value": 25.2, "quality": "good" },
                { "value": 25.0, "quality": "good" }
            ]
        }
    ]
}

5.2.4 POST /api/v1/history/export — 导出CSV

Request body 与 query-table 一致:

{
    "point_ids": ["point-uuid-1", "point-uuid-2"],
    "start_time": "2026-07-09T00:00:00+08:00",
    "end_time": "2026-07-09T01:00:00+08:00",
    "interval_minutes": 10
}

ResponseCSV文件(Content-Type: text/csv; charset=utf-8-sig

CSV格式:

时间,曝气池DO_01(mg/L),曝气池DO_01_质量,曝气池温度(℃),曝气池温度_质量
2026-07-09 00:00:00,2.35,good,25.1,good
2026-07-09 00:10:00,2.40,good,25.0,good
2026-07-09 00:20:00,,,25.3,good
2026-07-09 00:30:00,2.38,good,,

注意:

  • CSV使用UTF-8 with BOM编码
  • 首行为标题行,包含时间、每个点位的数据列和质量列
  • 质量戳为 bad 时,数据列显示为 (全角破折号),质量列显示 bad
  • 未匹配到数据(null)时,数据列显示为 ,质量列显示

6. 曲线模式(前端)

6.1 布局

┌───────────┬──────────────────────────────────────┐
│ 点位树     │  时间范围选择器  [过去1小时] [今天] [自定义]  │
│ (复选框)   │                                        │
│            │  ┌──────────────────────────────────┐  │
│  ☑ 设备1   │  │                                  │  │
│    ☑ 分组A  │  │        曲线图区域                  │  │
│      ☑ DO  │  │    (ECharts 折线图)               │  │
│      ☐ 温度 │  │                                  │  │
│    ☐ 分组B  │  │                                  │  │
│  ☐ 设备2   │  └──────────────────────────────────┘  │
│            │  ┌──────────┬──────────┬──────────┐   │
│  内部数据   │  │ 点位名称   │ 时间     │ 数值     │   │
│  ☐ (暂无)  │  │ DO        │ 00:00:05 │ 2.38    │   │
│            │  │ 温度      │ 00:00:05 │ 25.2    │   │
│            │  └──────────┴──────────┴──────────┘   │
│  [曲线] [表格]│                                      │
└───────────┴──────────────────────────────────────┘

6.2 时间范围选择

提供快捷选择 + 自定义:

选项 说明
过去1小时 快速查看最近1小时
过去6小时 快速查看最近6小时
过去24小时 快速查看最近1天
过去7天 快速查看最近1周
自定义 自由选择起止时间(精确到秒)

6.3 曲线图规则

6.3.1 横轴(X轴)

  • 类型:时间轴
  • 自适应:根据选择的时间范围自动调整刻度
  • 刻度标签格式:
    • 1小时内:HH:mm:ss
    • 1小时~1天:HH:mm
    • 1天以上:MM-dd HH:mm

6.3.2 纵轴(Y轴)— 单轴分段自适应

核心需求:一个Y轴,刻度不按数值均匀分布,而是根据数据分布自动调整疏密——数据密集的区域刻度变密(放大),数据稀疏的区域刻度变疏(压缩),让不同量级的多条曲线都能在同一个Y轴上看出变化趋势。

场景举例

同时展示两条曲线,一条 DO 值在 2.03.0 mg/L,一条温度在 2030℃。如果Y轴从0均匀到30,DO曲线会被压缩成一条几乎水平的直线,完全看不出变化。

实现方式 — 分段映射法

原始Y轴(均匀)        显示Y轴(分段自适应)
30 ─                   30 ─
                        │
                        │  温度变化区域(20~30)
                        │  该区域被适当压缩
20 ─                   20 ─
                        │
                        │  ─ ─ ─ 中间区域(3~20)
                        │  该区域数据稀疏,被大幅压缩
                        │
 3 ─                    3 ─
                        │
                        │  DO变化区域(2.0~3.0
                        │  该区域数据密集,被放大展开
 2 ─                    2 ─
                        │
 0 ─                    0 ─

技术实现

  1. 后端(或前端)对查询到的所有曲线数据进行全局分析,找出数据分布
  2. 将Y轴划分为若干段,每段内使用线性映射,段间不等距
  3. 段划分规则:
    • 找出所有曲线的数据值,按值聚类
    • 每个聚类形成一个"密度段",该段在Y轴上占据较大比例的高度
    • 聚类之间的空白区域形成一个"稀疏段",该段在Y轴上占据较小比例的高度
  4. 数据值 → 显示坐标的映射公式:
对于每个数据值 v,计算其在Y轴上的显示位置 p

p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segment_start) * segment_height

其中 segment_height 根据该段的数据密度分配:
- 密度段(包含大量数据点):分配较高比例(如60%的Y轴高度)
- 稀疏段(几乎无数据点):分配较低比例(如10%的Y轴高度)
  1. Y轴刻度标签显示原始值,位置按映射后的坐标放置

ECharts 实现方案

方案一(推荐 — 数据变换):后端将数据值按分段映射关系转换为显示坐标,前端使用线性Y轴绘制,Y轴通过 axisLabel.formatter 显示原始值标签,tooltip.formatter 显示原始值。

方案二(纯前端):前端获取原始数据后,使用 ECharts 的 custom 系列或数据变换插件,在前端完成映射计算。

Y轴标签规则

规则 说明
单Y轴 物理上只有一个Y轴,位于图表左侧
数值标签 显示原始数值,不显示映射后的坐标值
刻度位置 刻度线在Y轴上的物理位置由映射函数决定,不是均匀分布
刻度数量 自动控制,保持5~10个刻度标签,避免过密
单位标注 Y轴标题显示 数值 (单位),如 mg/L
分段视觉提示 在Y轴背景上用浅色横条标记不同分段区域,帮助用户识别分段边界

6.3.3 折线规则

数据质量 显示方式
连续 good 数据 实线折线,正常连接
连续 bad 数据 虚线连接(-- 样式)
good → bad 过渡 实线连接到 bad 点,bad 点之后变为虚线
数据缺失(gap 断线,不连接

6.3.4 游标功能

游标是一条垂直的十字线,跟随鼠标移动。

行为:

鼠标在图表区域移动
    │
    ▼
游标垂直线跟随鼠标位置
    │
    ▼
对每条曲线,找到游标所在时刻最近的数据点
    │
    ├── 找到数据点 → 显示该点的值
    └── 未找到数据点 → 显示 "--"
    │
    ▼
游标信息显示在右侧表格中

右侧游标信息表格:

点位名称 时间 数值 质量
曝气池DO_01 00:00:05 2.38 good
曝气池温度 00:00:05 25.2 good

游标交互细节:

  • 游标跟随鼠标移动,实时更新
  • 鼠标离开图表区域时,游标消失
  • 游标所在时刻精确到最近的数据点,不插值
  • 游标表格中,质量戳为 bad 时,数值显示为

7. 表格模式(前端)

7.1 布局

┌───────────┬──────────────────────────────────────┐
│ 点位树     │  时间范围选择器  [自定义]               │
│ (复选框)   │  间隔: [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    │ —      │  │
│  内部数据   │  │ ...          │ ...     │ ...    │  │
│  ☐ (暂无)  │  └──────────────────────────────────┘  │
│            │  [导出CSV]                              │
│  [曲线] [表格]│                                      │
└───────────┴──────────────────────────────────────┘

7.2 时间间隔选择

参数 说明
间隔时间 下拉选择:1分钟、2分钟、5分钟、10分钟、15分钟、20分钟、30分钟、60分钟
默认值 10分钟

7.3 表格数据规则

规则 说明
第一列 时间序列,从 start_time 开始,按 interval 递增
数据列 每个勾选的点位对应一列,列标题为 点位名称(单位)
数据对齐 使用最近邻匹配(见 5.2.3 后端逻辑)
bad 质量 数据单元格显示 (全角破折号)
无匹配数据 数据单元格显示 (全角破折号)
空值处理 空白单元格统一显示 ,不显示空单元格

7.4 导出CSV

点击"导出CSV"按钮,触发 POST /api/v1/history/export 接口。

  • 导出内容与当前表格显示内容一致(相同的时间范围、间隔、点位)
  • 文件名格式:历史数据_{start_time}_{end_time}_{interval}min.csv
  • 导出完成后浏览器自动下载

8. 前端页面结构

8.1 页面布局

┌─────────────────────────────────────────────────────┐
│  ┌──────────┐  ┌──────────────────────────────────┐ │
│  │  ▽ 数据管理 │  │                                    │ │
│  │    设备管理 │  │                                    │ │
│  │    数据采集 │  │           内容区域                  │ │
│  │    数据写入 │  │                                    │ │
│  │          │  │  ┌──────────────────────────────┐  │ │
│  │  ○ 历史数据 │  │  │  ☐ 设备1                     │  │ │
│  │          │  │  │    ☐ 分组A                    │  │ │
│  │  ○ 实时监控  │  │  │      ☑ 曝气池DO_01  2.35   │  │ │
│  │  ○ 智能控制  │  │  │      ☐ 曝气池温度  25.1   │  │ │
│  │  ○ 系统配置  │  │  │  ☐ 分组B                    │  │ │
│  │          │  │  │  ☐ 设备2                      │  │ │
│  └──────────┘  │  │  ───────                     │  │ │
│                 │  │  内部数据                      │  │ │
│                 │  │  ☐ (暂无数据)                  │  │ │
│                 │  └──────────────────────────────┘  │ │
│                 │  [曲线] [表格] 切换                  │ │
│                 │  ┌──────────────────────────────┐  │ │
│                 │  │     曲线图/表格内容区域         │  │ │
│                 │  │                              │  │ │
│                 │  └──────────────────────────────┘  │ │
│                 └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

8.2 模式切换

  • 曲线模式和表格模式通过 Tab 按钮切换
  • 切换时,勾选的点位状态保持不变
  • 切换时,时间范围保持不变
  • 切换时,不重复请求数据,按需加载(点击曲线Tab时请求曲线数据,点击表格Tab时请求表格数据)

8.3 交互流程

用户进入历史数据页面
    │
    ▼
加载历史点位树(GET /api/v1/history/tree
    │
    ▼
左侧树渲染,用户勾选点位
    │
    ▼
用户选择时间范围
    │
    ├── 曲线模式 → 点击查询 → POST /api/v1/history/query → 渲染曲线图
    └── 表格模式 → 选择间隔 → 点击查询 → POST /api/v1/history/query-table → 渲染表格
    │
    ▼
用户勾选/取消勾选点位 → 自动重新查询(保持时间范围不变)
用户切换模式 → 自动重新查询(保持点位和时间范围不变)

附录A:关键业务规则一览

编号 规则 说明
H001 历史数据来源 仅展示 store_history=true 且设备/点位均启用的采集点数据
H002 树形结构 按设备→分组→点位三层组织,外加"内部数据"预留节点
H003 最大勾选数 同时勾选的点位不超过20个
H004 数据保留 可配置1~730天,通过TDengine KEEP参数实现
H005 表格时间对齐 最近邻匹配,匹配窗口 = interval/2
H006 曲线Y轴 单Y轴分段自适应,数据密集区放大,稀疏区压缩
H007 bad质量显示 曲线:虚线;表格:;游标:
H008 数据缺失 曲线:断线;表格:
H009 模式切换 保持点位勾选状态和时间范围不变
H010 CSV导出 使用UTF-8 with BOM编码,与表格显示内容一致

本文档版本:v1.0 最后更新:2026-07-09