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

762 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 历史数据模块 — 开发规格说明
> 本文档属于《污水厂智能控制平台》的一部分,详细描述历史数据模块的功能、数据模型、API接口和业务逻辑,细度可达直接开发级别。
> 本模块与 [数据管理模块](./spec-数据管理.md) 紧密关联,依赖其中的设备、采集点、TDengine 超级表等设计。
---
## 目录
- [1. 概述](#1-概述)
- [2. 数据来源](#2-数据来源)
- [3. 历史点位树形结构](#3-历史点位树形结构)
- [4. 数据保留策略](#4-数据保留策略)
- [5. RESTful API](#5-restful-api)
- [6. 曲线模式(前端)](#6-曲线模式前端)
- [7. 表格模式(前端)](#7-表格模式前端)
- [8. 前端页面结构](#8-前端页面结构)
---
## 1. 概述
### 1.1 模块定位
历史数据模块负责展示平台中所有已存储的历史时序数据,提供两种查看方式:
- **曲线模式**:多条数据在同一时间轴上的变化趋势对比,支持游标查看
- **表格模式**:按固定时间间隔展示数据,支持导出CSV
### 1.2 模块边界
| 交互对象 | 方向 | 内容 |
|----------|------|------|
| Web前端 | 返回 | 历史点位树、历史数据查询结果、CSV文件 |
| TDengine | 查询 | 读取 collection_data 超级表下的子表数据 |
| PostgreSQL | 查询 | 读取设备、采集点配置,构建树形结构 |
| 数据管理模块 | 依赖 | 采集点配置、设备配置、分组信息 |
### 1.3 页面层级
历史数据与数据管理在同一层级,位于左侧导航栏:
```
┌──────────┐
│ ▽ 数据管理 │
│ 设备管理 │
│ 数据采集 │
│ 数据写入 │
│ │
│ ○ 历史数据 │ ← 同层级,无子菜单
│ │
│ ○ 实时监控 │
│ ○ 智能控制 │
│ ○ 系统配置 │
└──────────┘
```
---
## 2. 数据来源
### 2.1 当前数据来源:采集点历史数据
来自数据管理模块中**启用了 store_history=true** 的采集点,数据存储在 TDengine 的 `collection_data` 超级表中。
```sql
-- 已在数据管理模块中定义的超级表
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` 超级表
```sql
-- 预留:非采集点位的时序数据
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 节点属性
每个采集点节点应包含以下信息(用于展示和查询):
```json
{
"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` 参数控制:
```sql
-- 创建数据库时指定保留天数
CREATE DATABASE IF NOT EXISTS water_plant
KEEP 365 -- 数据保留天数(对应配置值)
DAYS 10 -- 每10天一个文件
BLOCKS 100;
-- 修改保留天数(当用户在系统配置中修改时执行)
ALTER DATABASE water_plant KEEP 730;
```
### 4.3 注意
- 修改保留天数后,TDengine 会自动清理超出保留期的数据
- 保留天数对 `collection_data``computed_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)
```json
{
"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
```json
{
"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)
```json
{
"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
```json
{
"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)
```json
{
"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 一致:
```json
{
"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格式:
```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.0~3.0 mg/L,一条温度在 20~30℃。如果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轴高度)
```
5. 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