Files
AquaControlAI/开发文档/spec-历史数据.md
T
2026-07-11 18:04:51 +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)
);

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'
);

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

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 树结构定义

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

分组名称直接取自数据采集点配置中的 group_name,同一分组下的点位可来自不同设备。

3.2 构建规则

层级 数据来源 说明
第一层:分组 PostgreSQL collection_points 表的 group_name 去重后作为树的顶层节点,只有包含可用历史数据点的分组才显示
第二层:点位 PostgreSQL collection_points 满足 store_history=true 且 enabled 的采集点,按 group_name 归属到对应分组下
独立节点:内部数据 硬编码占位 当前阶段显示"暂无数据",后续扩展

3.3 节点属性

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

{
    "id": "point-uuid",
    "name": "曝气池DO_01",
    "type": "collection",        // 或 'computed'(预留)
    "data_type": "REAL",
    "device_id": "device-uuid",
    "device_name": "一期曝气柜PLC",
    "group_name": "曝气池",
    "latest_value": {
        "value": 2.35,
        "quality": "good",
        "ts": "2026-07-09T10:00:01+08:00"
    }
}

由于树结构按分组平铺,点位节点需额外携带 device_iddevice_name 以便前端识别点位所属设备。

3.4 复选框行为

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

4. 数据保留策略

4.1 配置方式

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

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

4.2 实现方式

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

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

-- 修改保留天数(当用户在系统配置中修改时执行)
ALTER DATABASE aquacontrolai 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)

{
    "code": 0,
    "message": "success",
    "data": {
        "tree": [
            {
                "id": "group-aeration",
                "name": "曝气池",
                "type": "group",
                "children": [
                    {
                        "id": "point-uuid-1",
                        "name": "曝气池DO_01",
                        "type": "collection",
                        "data_type": "REAL",
                        "device_id": "device-uuid-1",
                        "device_name": "一期曝气柜PLC",
                        "latest_value": {
                            "value": 2.35,
                            "quality": "good",
                            "ts": "2026-07-09T10:00:01+08:00"
                        }
                    },
                    {
                        "id": "point-uuid-2",
                        "name": "曝气池温度",
                        "type": "collection",
                        "data_type": "REAL",
                        "device_id": "device-uuid-1",
                        "device_name": "一期曝气柜PLC",
                        "latest_value": {
                            "value": 25.1,
                            "quality": "good",
                            "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)

{
    "code": 0,
    "message": "success",
    "data": {
        "series": [
            {
                "point_id": "point-uuid-1",
                "point_name": "曝气池DO_01",
                "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": "曝气池温度",
                "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

注意interval_minutes 是表格展示的对齐间隔,与采集点配置的 history_interval(存储间隔,见数据管理模块 §3.1.1)相互独立。如果 interval_minutes 小于选中点位中最大的 history_interval,大部分目标时间点将因无匹配数据而显示为 。建议前端在表格模式下默认将 interval_minutes 的最小可选值限制为不小于所有已勾选点位中最大的 history_interval,或至少在用户选择过小间隔时给出提示。

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

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)

{
    "code": 0,
    "message": "success",
    "data": {
        "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",
                "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": "曝气池温度",
                "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,曝气池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小时] [今天] [自定义]  │
│ (复选框)   │                                        │
│            │  ┌──────────────────────────────────┐  │
│  ☑ 曝气池   │  │                                  │  │
│    ☑ DO    │  │        曲线图区域                  │  │
│    ☐ 温度   │  │    (ECharts 折线图)               │  │
│  ☐ 加药间   │  │                                  │  │
│            │  └──────────────────────────────────┘  │
│  内部数据   │  ┌──────────┬──────────┬──────────┐   │
│  ☐ (暂无)  │  │ 点位名称   │ 时间     │ 数值     │   │
│            │  │ 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,一条温度在 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轴标题显示 数值
分段视觉提示 在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分钟] ▼                     │
│            │                                      │
│  ☑ 曝气池   │  ┌──────────────────────────────────┐  │
│    ☑ DO    │  │ 时间         │ DO      │ 温度    │  │
│    ☐ 温度   │  │──────────────┼─────────┼────────│  │
│  ☐ 加药间   │  │ 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]                              │
└───────────┴──────────────────────────────────────┘

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 页面布局

┌──────────────────────────────────────────┐
│  ┌──────────┐  ┌───────────────────────┐ │
│  │  ▽ 数据管理 │  │                        │ │
│  │    设备管理 │  │                        │ │
│  │    数据采集 │  │       内容区域           │ │
│  │    数据写入 │  │                        │ │
│  │          │  │  ┌───────────────────┐  │ │
│  │  ○ 历史数据 │  │  │  ☑ 曝气池          │  │ │
│  │          │  │  │    ☑ DO           │  │ │
│  └──────────┘  │  │    ☐ 温度          │  │ │
│                 │  │  ☐ 加药间          │  │ │
│                 │  │  ───────          │  │ │
│                 │  │  内部数据           │  │ │
│                 │  │  ☐ (暂无数据)       │  │ │
│                 │  └───────────────────┘  │ │
│                 │  [曲线] [表格] 切换       │ │
│                 │  ┌───────────────────┐  │ │
│                 │  │  曲线图/表格内容区域  │  │ │
│                 │  │                    │  │ │
│                 │  └───────────────────┘  │ │
│                 └───────────────────────┘ │
└──────────────────────────────────────────┘

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 树形结构 按分组→点位两层组织,分组名称取自采集点配置的 group_name,外加"内部数据"预留节点
H003 最大勾选数 同时勾选的点位不超过20个
H004 数据保留 可配置1~730天,通过TDengine KEEP参数实现
H005 表格时间对齐 最近邻匹配,匹配窗口 = interval/2
H006 曲线Y轴 单Y轴分段自适应,数据密集区放大,稀疏区压缩
H007 bad质量显示 曲线:虚线;表格:;游标:
H008 数据缺失 曲线:断线;表格:
H009 模式切换 保持点位勾选状态和时间范围不变
H010 CSV导出 使用UTF-8 with BOM编码,与表格显示内容一致
H011 查询间隔建议 表格查询的 interval_minutes 建议不小于选中点位的 history_interval,否则可能大量无匹配数据

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