11
This commit is contained in:
+171
-212
@@ -1,7 +1,9 @@
|
||||
# 历史数据模块 — 开发规格说明
|
||||
|
||||
> 本文档属于《污水厂智能控制平台》的一部分,详细描述历史数据模块的功能、数据模型、API接口和业务逻辑,细度可达直接开发级别。
|
||||
> 本模块与 [数据管理模块](./spec-数据管理.md) 紧密关联,依赖其中的设备、采集点、TDengine 超级表等设计。
|
||||
> 本模块与 [数据管理模块](./spec-数据管理.md) 紧密关联,依赖其中公开的历史元数据领域接口和 TDengine 超级表设计。
|
||||
|
||||
> 跨文档契约发生冲突时,必须遵循《[一致性决策基线](一致性决策基线.md)》;该文件的已采纳决策优先于本文旧表述。
|
||||
|
||||
---
|
||||
|
||||
@@ -33,8 +35,7 @@
|
||||
|----------|------|------|
|
||||
| Web前端 | 返回 | 历史点位树、历史数据查询结果、CSV文件 |
|
||||
| TDengine | 查询 | 读取 collection_data 超级表下的子表数据 |
|
||||
| PostgreSQL | 查询 | 读取设备、采集点配置,构建树形结构 |
|
||||
| 数据管理模块 | 依赖 | 采集点配置、设备配置、分组信息 |
|
||||
| 数据管理模块 | 调用进程内只读领域接口 | 获取采集点、设备和分组元数据;不直接查询其 PostgreSQL 表或 Repository |
|
||||
|
||||
### 1.3 页面层级
|
||||
|
||||
@@ -57,23 +58,30 @@
|
||||
|
||||
### 2.1 当前数据来源:采集点历史数据
|
||||
|
||||
来自数据管理模块中**启用了 store_history=true** 的采集点,数据存储在 TDengine 的 `collection_data` 超级表中。
|
||||
历史树包含两类点位:
|
||||
|
||||
- **活跃点位**:设备和点位均启用、未删除,且当前 `store_history=true`;即使尚无历史记录也显示,并标记 `has_history_data=false`。
|
||||
- **归档点位**:设备或点位已禁用/逻辑删除,或当前 `store_history=false`,但 TDengine 中仍有保留期内记录;归档点位只读、可查询。
|
||||
|
||||
数据存储在 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)
|
||||
ts TIMESTAMP,
|
||||
value DOUBLE,
|
||||
quality INT, -- 0=good, 1=bad
|
||||
quality_reason VARCHAR(32),
|
||||
point_id VARCHAR(36),
|
||||
point_name VARCHAR(128)
|
||||
) TAGS (
|
||||
device_id VARCHAR(32),
|
||||
device_name VARCHAR(128),
|
||||
data_type VARCHAR(16)
|
||||
device_id VARCHAR(36),
|
||||
device_name VARCHAR(128),
|
||||
data_type VARCHAR(16)
|
||||
);
|
||||
```
|
||||
|
||||
`value` 可为 NULL。读取失败或断线时使用 `quality=bad`、`value=NULL`;超出有效范围时保留实际值并使用 `quality=bad`。
|
||||
|
||||
### 2.2 预留数据来源:非采集点位(内部数据)
|
||||
|
||||
未来的扩展数据(如AI模型推理的建议值、人工录入的补充数据等),统一归属为**内部数据**。
|
||||
@@ -86,7 +94,8 @@ CREATE STABLE IF NOT EXISTS computed_data (
|
||||
ts TIMESTAMP,
|
||||
value DOUBLE,
|
||||
quality INT, -- 0=good, 1=bad
|
||||
point_id VARCHAR(32),
|
||||
quality_reason VARCHAR(32),
|
||||
point_id VARCHAR(36),
|
||||
point_name VARCHAR(128),
|
||||
source VARCHAR(32) -- 数据来源: 'ai_model', 'manual_input', 'external' 等
|
||||
) TAGS (
|
||||
@@ -94,19 +103,23 @@ CREATE STABLE IF NOT EXISTS computed_data (
|
||||
);
|
||||
```
|
||||
|
||||
`point_id`、`device_id` 在 API、PostgreSQL 与 TDengine 中统一使用带连字符的 36 位标准 UUID 字符串。仅子表名使用 `p_<uuid32>` 格式:对已校验 UUID 规范化为小写、移除连字符并加 `p_` 前缀;历史模块不得使用请求中传入的表名。
|
||||
|
||||
**当前阶段只实现采集点历史数据展示,内部数据在树形结构中占位,不可勾选,显示"暂无数据"。**
|
||||
|
||||
### 2.3 历史数据点位判定规则
|
||||
|
||||
**一个采集点是否出现在历史数据树形结构中**,取决于:
|
||||
```text
|
||||
active ⇔ point.enabled AND NOT point.deleted
|
||||
AND device.enabled AND NOT device.deleted
|
||||
AND point.store_history
|
||||
|
||||
archived ⇔ NOT active AND TDengine 中仍存在保留期内记录
|
||||
|
||||
出现在树中 ⇔ active OR archived
|
||||
```
|
||||
采集点出现在历史树中 ⇔ collection_points.enabled = true
|
||||
AND collection_points.deleted = false
|
||||
AND collection_points.store_history = true
|
||||
AND 关联的 devices.enabled = true
|
||||
AND 关联的 devices.deleted = false
|
||||
```
|
||||
|
||||
历史模块调用数据管理模块的只读元数据接口取得包括逻辑删除记录在内的配置,再通过批量 TDengine 元数据查询判断 `has_history_data`。查询接口不得仅因点位当前禁用或逻辑删除而拒绝其归档历史。
|
||||
|
||||
---
|
||||
|
||||
@@ -114,61 +127,66 @@ CREATE STABLE IF NOT EXISTS computed_data (
|
||||
|
||||
### 3.1 树结构定义
|
||||
|
||||
```
|
||||
```text
|
||||
所有历史点位
|
||||
├── 分组A
|
||||
│ ├── ☐ 采集点1(最新值,质量戳)
|
||||
│ ├── ☐ 采集点2(最新值,质量戳)
|
||||
│ └── ...
|
||||
│ ├── ☐ 活跃点位1(最新值,质量戳)
|
||||
│ └── ☐ 归档点位2(归档标识)
|
||||
├── 分组B
|
||||
│ ├── ☐ 采集点3(最新值,质量戳)
|
||||
│ └── ...
|
||||
├── ...(其他分组)
|
||||
└── 内部数据(预留)
|
||||
└── (暂无数据)
|
||||
└── 暂无数据
|
||||
```
|
||||
|
||||
分组名称直接取自数据采集点配置中的 `group_name`,同一分组下的点位可来自不同设备。
|
||||
树保持“分组→点位”两层结构;活跃和归档点位可在同一分组中,通过 `lifecycle_status`、图标和样式区分。分组和点位按 `name` 的 Unicode 升序稳定排序。
|
||||
|
||||
### 3.2 构建规则
|
||||
|
||||
| 层级 | 数据来源 | 说明 |
|
||||
|------|----------|------|
|
||||
| 第一层:分组 | PostgreSQL `collection_points` 表的 `group_name` | 去重后作为树的顶层节点,只有包含可用历史数据点的分组才显示 |
|
||||
| 第二层:点位 | PostgreSQL `collection_points` | 满足 store_history=true 且 enabled 的采集点,按 group_name 归属到对应分组下 |
|
||||
| 独立节点:内部数据 | 硬编码占位 | 当前阶段显示"暂无数据",后续扩展 |
|
||||
| 分组 | `ListHistoryPointMetadata(IncludeArchived=true)` 的 `group_name` | 去重后生成稳定分组节点 |
|
||||
| 点位 | 数据管理元数据 + TDengine 批量存在性检查 | active 总是显示;archived 仅在有历史数据时显示 |
|
||||
| 内部数据 | 硬编码占位 | 当前不可勾选 |
|
||||
|
||||
分组 ID 使用 `group_` 加 `SHA-256(group_name)` 的前 16 个十六进制字符,不把任意分组名称直接作为 DOM/API 标识。
|
||||
|
||||
### 3.3 节点属性
|
||||
|
||||
每个采集点节点应包含以下信息(用于展示和查询):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "point-uuid",
|
||||
"name": "曝气池DO_01",
|
||||
"type": "collection", // 或 'computed'(预留)
|
||||
"type": "collection",
|
||||
"data_type": "REAL",
|
||||
"unit": "mg/L",
|
||||
"history_interval": 5,
|
||||
"device_id": "device-uuid",
|
||||
"device_name": "一期曝气柜PLC",
|
||||
"group_name": "曝气池",
|
||||
"lifecycle_status": "active",
|
||||
"has_history_data": true,
|
||||
"latest_value": {
|
||||
"value": 2.35,
|
||||
"quality": "good",
|
||||
"quality_reason": null,
|
||||
"ts": "2026-07-09T10:00:01+08:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 由于树结构按分组平铺,点位节点需额外携带 `device_id` 和 `device_name` 以便前端识别点位所属设备。
|
||||
- `lifecycle_status`:`active | archived`。
|
||||
- `latest_value` 来自数据管理模块实时缓存;无缓存时固定为 `null`,不得用 TDengine 最后一条记录冒充实时值。
|
||||
- 归档点位的 `latest_value` 通常为 null,但仍可勾选查询历史。
|
||||
|
||||
### 3.4 复选框行为
|
||||
|
||||
| 操作 | 行为 |
|
||||
|------|------|
|
||||
| 勾选点位 | 该点位数据加入曲线/表格 |
|
||||
| 取消勾选点位 | 该点位数据从曲线/表格中移除 |
|
||||
| 勾选上限 | 建议最多同时勾选20个点位(前端友好性考虑) |
|
||||
| 勾选状态 | 切换曲线/表格模式时,勾选状态保持不变 |
|
||||
| 勾选点位 | 加入当前曲线/表格查询 |
|
||||
| 取消勾选 | 从查询中移除 |
|
||||
| 勾选上限 | **强制最多20个**;前端阻止第21个,后端再次校验 |
|
||||
| 模式切换 | 保持勾选状态和时间范围 |
|
||||
| 归档点位 | 可勾选,只读查询 |
|
||||
|
||||
---
|
||||
|
||||
@@ -188,20 +206,21 @@ CREATE STABLE IF NOT EXISTS computed_data (
|
||||
|
||||
```sql
|
||||
-- 创建数据库时指定保留天数
|
||||
CREATE DATABASE IF NOT EXISTS aquacontrolai
|
||||
CREATE DATABASE IF NOT EXISTS ${TDENGINE_DATABASE}
|
||||
KEEP 365 -- 数据保留天数(对应配置值)
|
||||
DAYS 10 -- 每10天一个文件
|
||||
BLOCKS 100;
|
||||
|
||||
-- 修改保留天数(当用户在系统配置中修改时执行)
|
||||
ALTER DATABASE aquacontrolai KEEP 730;
|
||||
ALTER DATABASE ${TDENGINE_DATABASE} KEEP 730;
|
||||
```
|
||||
|
||||
### 4.3 注意
|
||||
|
||||
- 修改保留天数后,TDengine 会自动清理超出保留期的数据
|
||||
- 保留天数对 `collection_data` 和 `computed_data` 两个超级表同时生效
|
||||
- 建议在系统配置页面提供"立即清理过期数据"按钮,但通常不需要手动操作
|
||||
- 数据库名由 `TDENGINE_DATABASE` 配置;系统配置 Service 执行 ALTER DATABASE 并记录审计日志
|
||||
- 配置更新失败时不修改 PostgreSQL 中的已生效配置值;TDengine 自动执行过期清理,不提供伪同步的“立即清理”按钮
|
||||
|
||||
---
|
||||
|
||||
@@ -220,7 +239,13 @@ ALTER DATABASE aquacontrolai KEEP 730;
|
||||
|
||||
#### 5.2.1 GET /api/v1/history/tree — 获取历史点位树
|
||||
|
||||
无需参数,从 PostgreSQL 查询所有符合条件的历史点位,按分组→点位构建树。
|
||||
Query parameters:
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| include_archived | bool | 否 | true | 是否包含仍有历史数据的归档点位 |
|
||||
|
||||
历史模块调用 `ListHistoryPointMetadata(IncludeArchived=true)`,批量检查 TDengine 历史存在性并构建树;不得直接查询数据管理 PostgreSQL 表。
|
||||
|
||||
Response (200):
|
||||
|
||||
@@ -231,7 +256,7 @@ Response (200):
|
||||
"data": {
|
||||
"tree": [
|
||||
{
|
||||
"id": "group-aeration",
|
||||
"id": "group_a1b2c3d4e5f60708",
|
||||
"name": "曝气池",
|
||||
"type": "group",
|
||||
"children": [
|
||||
@@ -240,26 +265,33 @@ Response (200):
|
||||
"name": "曝气池DO_01",
|
||||
"type": "collection",
|
||||
"data_type": "REAL",
|
||||
"unit": "mg/L",
|
||||
"history_interval": 5,
|
||||
"device_id": "device-uuid-1",
|
||||
"device_name": "一期曝气柜PLC",
|
||||
"group_name": "曝气池",
|
||||
"lifecycle_status": "active",
|
||||
"has_history_data": true,
|
||||
"latest_value": {
|
||||
"value": 2.35,
|
||||
"quality": "good",
|
||||
"quality_reason": null,
|
||||
"ts": "2026-07-09T10:00:01+08:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "point-uuid-2",
|
||||
"name": "曝气池温度",
|
||||
"id": "point-uuid-archived",
|
||||
"name": "旧DO点位",
|
||||
"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"
|
||||
}
|
||||
"unit": "mg/L",
|
||||
"history_interval": 5,
|
||||
"device_id": "device-uuid-old",
|
||||
"device_name": "旧PLC",
|
||||
"group_name": "曝气池",
|
||||
"lifecycle_status": "archived",
|
||||
"has_history_data": true,
|
||||
"latest_value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -267,14 +299,7 @@ Response (200):
|
||||
"id": "internal-data",
|
||||
"name": "内部数据",
|
||||
"type": "reserved",
|
||||
"children": [
|
||||
{
|
||||
"id": "placeholder",
|
||||
"name": "暂无数据",
|
||||
"type": "placeholder",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
"children": [{"id":"placeholder","name":"暂无数据","type":"placeholder","disabled":true}]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -289,27 +314,25 @@ 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"
|
||||
"end_time": "2026-07-09T01:00:00+08:00",
|
||||
"max_samples": 2000
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| point_ids | string[] | 是 | 要查询的点位ID列表,最少1个,最多20个 |
|
||||
| start_time | string | 是 | 开始时间,ISO 8601格式 |
|
||||
| end_time | string | 是 | 结束时间,ISO 8601格式 |
|
||||
| point_ids | UUID[] | 是 | 1~20个,允许活跃或归档点位 |
|
||||
| start_time | string | 是 | ISO 8601 |
|
||||
| end_time | string | 是 | 必须晚于开始时间,跨度不超过31天 |
|
||||
| max_samples | int | 否 | 每序列100~10000,默认2000 |
|
||||
|
||||
后端逻辑:
|
||||
|
||||
```
|
||||
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. 聚合结果,返回
|
||||
```
|
||||
1. 调用 `GetHistoryPointMetadata(..., includeArchived=true)` 校验并批量取得当前名称、单位和类型;
|
||||
2. 由 UUID 派生并白名单校验 `p_<uuid32>` 子表名;
|
||||
3. 查询 `ts, value, quality, quality_reason`;
|
||||
4. 原始点数超过 `max_samples` 时执行自适应 min-max 降采样,保留首尾、极值、质量变化和 gap 边界;
|
||||
5. 返回每条序列的采样统计。
|
||||
|
||||
Response (200):
|
||||
|
||||
@@ -323,22 +346,16 @@ Response (200):
|
||||
"point_id": "point-uuid-1",
|
||||
"point_name": "曝气池DO_01",
|
||||
"data_type": "REAL",
|
||||
"unit": "mg/L",
|
||||
"sampled": false,
|
||||
"raw_count": 5,
|
||||
"sample_count": 5,
|
||||
"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" }
|
||||
{"ts":"2026-07-09T00:00:01+08:00","value":2.35,"quality":"good","quality_reason":null},
|
||||
{"ts":"2026-07-09T00:00:02+08:00","value":2.36,"quality":"good","quality_reason":null},
|
||||
{"ts":"2026-07-09T00:00:05+08:00","value":2.38,"quality":"bad","quality_reason":"out_of_range"},
|
||||
{"ts":"2026-07-09T00:00:08+08:00","value":null,"quality":"bad","quality_reason":"timeout"},
|
||||
{"ts":"2026-07-09T00:00:10+08:00","value":2.40,"quality":"good","quality_reason":null}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -361,35 +378,21 @@ Request body:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 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) |
|
||||
| point_ids | UUID[] | 是 | 1~20个 |
|
||||
| start_time | string | 是 | ISO 8601 |
|
||||
| end_time | string | 是 | 晚于开始时间,跨度不超过31天 |
|
||||
| interval_minutes | int | 是 | 1~1440任意整数 |
|
||||
|
||||
> **注意**:`interval_minutes` 是表格展示的对齐间隔,与采集点配置的 `history_interval`(存储间隔,见数据管理模块 §3.1.1)相互独立。如果 `interval_minutes` 小于选中点位中最大的 `history_interval`,大部分目标时间点将因无匹配数据而显示为 `—`。建议前端在表格模式下默认将 `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(显示—) |
|
||||
1. `window = interval_minutes / 2` 的精确时长,例如1分钟间隔对应30秒窗口;
|
||||
2. 查询范围扩大为 `[start_time-window, end_time+window]`;
|
||||
3. 目标时间为 `start_time + n*interval` 且 `<= end_time`,不强制追加未对齐的 end_time;
|
||||
4. 每个目标时间独立选择绝对距离最小的记录;距离相同时选择较早时间戳;
|
||||
5. 同一原始记录允许匹配相邻目标时间,响应通过 `matched_ts` 明示;
|
||||
6. 无匹配时返回固定 `none` 对象,数组长度始终与 `time_column` 相同。
|
||||
|
||||
Response (200):
|
||||
|
||||
@@ -401,37 +404,17 @@ 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"
|
||||
"2026-07-09T00:20: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": "曝气池温度",
|
||||
"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" }
|
||||
{"value":2.35,"quality":"good","quality_reason":null,"matched_ts":"2026-07-09T00:00:01+08:00"},
|
||||
{"value":2.40,"quality":"good","quality_reason":null,"matched_ts":"2026-07-09T00:10:02+08:00"},
|
||||
{"value":null,"quality":"none","quality_reason":null,"matched_ts":null}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -441,34 +424,34 @@ Response (200):
|
||||
|
||||
#### 5.2.4 POST /api/v1/history/export — 导出CSV
|
||||
|
||||
Request body 与 query-table 一致:
|
||||
请求体与 query-table 一致。成功响应直接返回 CSV 文件流,不使用 JSON envelope;失败时返回统一 JSON 错误。
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
- Content-Type:`text/csv; charset=utf-8-sig`
|
||||
- Content-Disposition:`attachment; filename="history_20260709T000000_20260709T010000_10m.csv"`
|
||||
- 文件名由服务端解析时间后重新格式化,不直接拼接请求字符串。
|
||||
- 最大输出 50,000 行;超出时返回 HTTP 413、`code=43006`。
|
||||
|
||||
Response:CSV文件(Content-Type: text/csv; charset=utf-8-sig)
|
||||
|
||||
CSV格式:
|
||||
报表 CSV 允许本地化动态标题:
|
||||
|
||||
```csv
|
||||
时间,曝气池DO_01,曝气池DO_01_质量,曝气池温度,曝气池温度_质量
|
||||
时间,曝气池DO_01[mg/L],曝气池DO_01[mg/L]_质量,曝气池温度[℃],曝气池温度[℃]_质量
|
||||
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)时,数据列显示为 `—`,质量列显示 `—`
|
||||
重复点位名称追加 UUID 前8位。bad 时数值列显示 `—`、质量列显示 `bad`;none 时两列均显示 `—`。
|
||||
|
||||
### 5.3 错误码
|
||||
|
||||
| code | HTTP | 场景 |
|
||||
|---:|---:|---|
|
||||
| 43001 | 400 | 参数格式或范围错误 |
|
||||
| 43002 | 400 | 点位数量不在1~20 |
|
||||
| 43003 | 400 | 时间范围无效或超过31天 |
|
||||
| 43004 | 404 | 点位元数据不存在或历史记录已过保留期 |
|
||||
| 43005 | 502 | TDengine 查询失败 |
|
||||
| 43006 | 413 | 导出行数超过限制 |
|
||||
|
||||
---
|
||||
|
||||
@@ -550,50 +533,24 @@ CSV格式:
|
||||
|
||||
**技术实现**:
|
||||
|
||||
1. 后端(或前端)对查询到的所有曲线数据进行全局分析,找出数据分布
|
||||
2. 将Y轴划分为若干段,每段内使用线性映射,段间不等距
|
||||
3. 段划分规则:
|
||||
- 找出所有曲线的数据值,按值聚类
|
||||
- 每个聚类形成一个"密度段",该段在Y轴上占据较大比例的高度
|
||||
- 聚类之间的空白区域形成一个"稀疏段",该段在Y轴上占据较小比例的高度
|
||||
4. 数据值 → 显示坐标的映射公式:
|
||||
分段自适应映射全部由前端统一图表组件完成;历史 API 只返回原始值,不返回映射后的坐标。
|
||||
|
||||
```
|
||||
对于每个数据值 v,计算其在Y轴上的显示位置 p:
|
||||
1. 组件对当前可绘制的非空数值分析分布并生成分段映射;
|
||||
2. 映射函数必须单调,刻度标签和 tooltip 始终显示原始值;
|
||||
3. 图表显著显示“非线性分段轴”标识和分段边界,避免把视觉距离误解为等比例数值差;
|
||||
4. 用户可切换到标准线性轴;导出数据始终使用原始值;
|
||||
5. 不同单位同时展示时,在图例和游标表格中显示单位,并给出“不同单位仅用于趋势对比”的提示。
|
||||
|
||||
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轴标题显示 `数值` |
|
||||
| 分段视觉提示 | 在Y轴背景上用浅色横条标记不同分段区域,帮助用户识别分段边界 |
|
||||
ECharts 使用封装在 `src/components/charts/` 的纯函数生成映射和 option;业务页面不得复制映射算法。
|
||||
|
||||
#### 6.3.3 折线规则
|
||||
|
||||
| 数据质量 | 显示方式 |
|
||||
|----------|----------|
|
||||
| 连续 good 数据 | 实线折线,正常连接 |
|
||||
| 连续 bad 数据 | 虚线连接(`--` 样式) |
|
||||
| good → bad 过渡 | 实线连接到 bad 点,bad 点之后变为虚线 |
|
||||
| 连续 bad 且 value 非空 | 虚线连接,并在 tooltip 显示 quality_reason |
|
||||
| good → bad 且 value 非空 | 实线连接到 bad 点,之后使用虚线 |
|
||||
| bad 且 value=null | 断线,不绘制数值点 |
|
||||
| 数据缺失(gap) | 断线,不连接 |
|
||||
|
||||
#### 6.3.4 游标功能
|
||||
@@ -629,7 +586,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
|
||||
- 游标跟随鼠标移动,实时更新
|
||||
- 鼠标离开图表区域时,游标消失
|
||||
- 游标所在时刻精确到最近的数据点,不插值
|
||||
- 游标表格中,质量戳为 bad 时,数值显示为 `—`
|
||||
- 游标表格中,bad 且 value 非空时显示数值并标红;bad 且 value=null 时显示 `—`
|
||||
|
||||
---
|
||||
|
||||
@@ -659,7 +616,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| 间隔时间 | 下拉选择:1分钟、2分钟、5分钟、10分钟、15分钟、20分钟、30分钟、60分钟 |
|
||||
| 间隔时间 | 数值输入,单位分钟,允许 1~1440 的任意整数;可提供 1、5、10、15、30、60 等快捷值,但快捷值不构成限制 |
|
||||
| 默认值 | 10分钟 |
|
||||
|
||||
### 7.3 表格数据规则
|
||||
@@ -667,7 +624,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 第一列 | 时间序列,从 start_time 开始,按 interval 递增 |
|
||||
| 数据列 | 每个勾选的点位对应一列,列标题为 `点位名称(单位)` |
|
||||
| 数据列 | 每个点位一列,标题为 `点位名称[单位]`;单位为空时只显示名称 |
|
||||
| 数据对齐 | 使用最近邻匹配(见 5.2.3 后端逻辑) |
|
||||
| bad 质量 | 数据单元格显示 `—`(全角破折号) |
|
||||
| 无匹配数据 | 数据单元格显示 `—`(全角破折号) |
|
||||
@@ -678,7 +635,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
|
||||
点击"导出CSV"按钮,触发 `POST /api/v1/history/export` 接口。
|
||||
|
||||
- 导出内容与当前表格显示内容一致(相同的时间范围、间隔、点位)
|
||||
- 文件名格式:`历史数据_{start_time}_{end_time}_{interval}min.csv`
|
||||
- 文件名由服务端生成:`history_YYYYMMDDTHHMMSS_YYYYMMDDTHHMMSS_{interval}m.csv`
|
||||
- 导出完成后浏览器自动下载
|
||||
|
||||
---
|
||||
@@ -717,7 +674,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
|
||||
- 曲线模式和表格模式通过 Tab 按钮切换
|
||||
- 切换时,**勾选的点位状态保持不变**
|
||||
- 切换时,**时间范围保持不变**
|
||||
- 切换时,不重复请求数据,按需加载(点击曲线Tab时请求曲线数据,点击表格Tab时请求表格数据)
|
||||
- 使用缓存键 `mode + point_ids + start_time + end_time + interval/max_samples`;缓存存在且参数未变化时不重复请求,否则按需请求
|
||||
|
||||
### 8.3 交互流程
|
||||
|
||||
@@ -737,8 +694,8 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
|
||||
└── 表格模式 → 选择间隔 → 点击查询 → POST /api/v1/history/query-table → 渲染表格
|
||||
│
|
||||
▼
|
||||
用户勾选/取消勾选点位 → 自动重新查询(保持时间范围不变)
|
||||
用户切换模式 → 自动重新查询(保持点位和时间范围不变)
|
||||
用户勾选/取消勾选点位 → 标记当前模式缓存失效,由用户点击查询或启用防抖自动查询
|
||||
用户切换模式 → 命中该模式缓存则直接展示;未命中时自动查询
|
||||
```
|
||||
|
||||
---
|
||||
@@ -747,19 +704,21 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
|
||||
|
||||
| 编号 | 规则 | 说明 |
|
||||
|------|------|------|
|
||||
| H001 | 历史数据来源 | 仅展示 store_history=true 且设备/点位均启用的采集点数据 |
|
||||
| H001 | 历史数据来源 | 展示活跃点位,以及仍有保留期内数据的归档点位 |
|
||||
| H002 | 树形结构 | 按分组→点位两层组织,分组名称取自采集点配置的 group_name,外加"内部数据"预留节点 |
|
||||
| H003 | 最大勾选数 | 同时勾选的点位不超过20个 |
|
||||
| H003 | 最大勾选数 | 前后端强制不超过20个 |
|
||||
| H004 | 数据保留 | 可配置1~730天,通过TDengine KEEP参数实现 |
|
||||
| H005 | 表格时间对齐 | 最近邻匹配,匹配窗口 = interval/2 |
|
||||
| H006 | 曲线Y轴 | 单Y轴分段自适应,数据密集区放大,稀疏区压缩 |
|
||||
| H007 | bad质量显示 | 曲线:虚线;表格:`—`;游标:`—` |
|
||||
| H005 | 表格时间对齐 | 精确半间隔窗口;扩展首尾查询范围;距离相同选较早记录 |
|
||||
| H006 | 曲线Y轴 | 前端实现可切换的非线性分段轴,API始终返回原始值 |
|
||||
| H007 | bad质量显示 | 有值bad画虚线;无值bad断线;表格bad显示— |
|
||||
| H008 | 数据缺失 | 曲线:断线;表格:`—` |
|
||||
| H009 | 模式切换 | 保持点位勾选状态和时间范围不变 |
|
||||
| H010 | CSV导出 | 使用UTF-8 with BOM编码,与表格显示内容一致 |
|
||||
| H011 | 查询间隔建议 | 表格查询的 `interval_minutes` 建议不小于选中点位的 `history_interval`,否则可能大量无匹配数据 |
|
||||
| H010 | CSV导出 | 报表型动态标题,安全服务端文件名,最多50,000行 |
|
||||
| H011 | 查询间隔建议 | 小于最大history_interval时仅提示,不禁止;查询最大31天 |
|
||||
| H012 | 曲线采样 | 每序列默认最多2000点,超限执行自适应min-max降采样 |
|
||||
| H013 | 缺失值协议 | 固定返回value=null、quality=none、matched_ts=null对象 |
|
||||
|
||||
---
|
||||
|
||||
> 本文档版本:v1.0
|
||||
> 最后更新:2026-07-09
|
||||
> 本文档版本:v1.1
|
||||
> 最后更新:2026-07-11
|
||||
|
||||
Reference in New Issue
Block a user