11
This commit is contained in:
+108
-53
@@ -22,6 +22,8 @@
|
||||
|
||||
## 1. 通用原则
|
||||
|
||||
> 跨文档契约发生冲突时,必须遵循《[一致性决策基线](一致性决策基线.md)》;该文件的已采纳决策优先于本文旧表述。
|
||||
|
||||
### 1.1 核心原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
@@ -30,7 +32,7 @@
|
||||
| **一致性** | 同一概念在代码、数据库、API、前端中保持命名一致(如 `device_id` 在所有层统一) |
|
||||
| **最小惊讶** | 函数的名称和签名应让调用者无需查看实现就能大致猜到行为 |
|
||||
| **防御式编程** | 不信任外部输入,对所有外部输入做校验,但内部调用尽量减少冗余校验 |
|
||||
| **模块自治** | 每个模块有清晰的边界,模块间通过 API 通信,不直接访问其他模块的内部数据 |
|
||||
| **模块自治** | 每个模块有清晰的边界。模块间通过公开的 Service/领域接口通信;同一进程内不得直接访问其他模块的 Repository、ORM 模型或数据库表 |
|
||||
|
||||
### 1.2 命名风格对照表
|
||||
|
||||
@@ -70,9 +72,7 @@
|
||||
```
|
||||
water-plant-control/
|
||||
├── cmd/ # 可执行程序入口
|
||||
│ ├── server/ # Web 服务端入口
|
||||
│ │ └── main.go
|
||||
│ └── collector/ # 采集引擎入口
|
||||
│ └── server/ # Web 服务端入口(嵌入采集引擎)
|
||||
│ └── main.go
|
||||
├── internal/ # 私有应用代码(不对外暴露)
|
||||
│ ├── api/ # HTTP handler 层
|
||||
@@ -105,7 +105,7 @@ water-plant-control/
|
||||
│ │ └── writer/ # 写入引擎
|
||||
│ │ └── writer.go
|
||||
│ ├── protocol/ # 协议插件化
|
||||
│ │ ├── driver.go # ProtocolDriver 接口定义
|
||||
│ │ ├── driver.go # ProtocolDriverFactory / ProtocolConnection 接口定义
|
||||
│ │ ├── registry.go # 注册表
|
||||
│ │ ├── s7/ # S7 协议实现
|
||||
│ │ └── modbus/ # Modbus TCP 协议实现
|
||||
@@ -133,6 +133,8 @@ water-plant-control/
|
||||
└── Makefile # 构建脚本
|
||||
```
|
||||
|
||||
运行模型采用**单进程嵌入式**:`cmd/server/main.go` 是唯一的生产可执行入口,负责初始化依赖、创建并启动采集引擎、启动 Web API,并在服务关闭时按序停止采集引擎。`internal/engine/collector` 是进程内运行时组件,不提供独立的 `cmd/collector` 进程。
|
||||
|
||||
### 2.2 模块目录与命名约束
|
||||
|
||||
新增模块时,后端在 `internal/` 下各层同步添加子包,前端在 `web/src/views/` 下添加页面目录。
|
||||
@@ -188,7 +190,8 @@ Repository (数据访问、ORM 查询)
|
||||
func (h *DeviceHandler) Create(c *gin.Context) {
|
||||
var req CreateDeviceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "无效的请求参数", err.Error())
|
||||
logger.Warn("请求参数绑定失败", "error", err)
|
||||
response.BadRequest(c, "无效的请求参数", nil)
|
||||
return
|
||||
}
|
||||
device, err := h.svc.Create(c.Request.Context(), &req)
|
||||
@@ -221,10 +224,20 @@ func (s *DeviceService) Create(ctx context.Context, req *CreateDeviceRequest) (*
|
||||
|
||||
#### 3.2.3 Repository 层规范
|
||||
|
||||
- 使用参数化查询,禁止字符串拼接 SQL
|
||||
- 所有用户值使用参数化查询,禁止拼接用户输入
|
||||
- 显式指定查询字段,禁止 `SELECT *`
|
||||
- 复杂查询使用 QueryBuilder,避免字符串拼接
|
||||
- 复杂查询使用 QueryBuilder
|
||||
- 简单 CRUD 操作统一封装 BaseRepo
|
||||
- 动态表名仅允许用于 TDengine 子表:必须由已验证 UUID 派生为 `p_<uuid32>`、通过 `^p_[0-9a-f]{32}$` 白名单并使用安全标识符引用;请求不得直接传入表名
|
||||
|
||||
#### 3.2.4 模块间调用规范
|
||||
|
||||
本项目采用模块化单体:模块间调用使用进程内公开的 Service/领域接口,不通过 HTTP 调用本应用自身。
|
||||
|
||||
- 数据访问权归属数据所有模块;只有该模块的 Service 可以调用其 Repository。
|
||||
- 调用模块依赖数据所有模块公开的只读接口,不得导入、调用或复用其 Repository、ORM 模型及表名。
|
||||
- 公开接口返回稳定的领域 DTO,不暴露数据库实体;接口的实现与依赖装配由 `cmd/server/main.go` 负责。
|
||||
- 需要未来拆分为独立服务时,保持接口语义不变,再以 HTTP/gRPC 适配器替换进程内实现。
|
||||
|
||||
### 3.3 错误处理
|
||||
|
||||
@@ -250,7 +263,9 @@ if err != nil {
|
||||
- 采集引擎涉及设备连接池、共享内存状态,必须使用 `sync.Mutex` 或 `sync.RWMutex` 保护
|
||||
- 避免裸 `sync.Map`,优先使用 `map + sync.RWMutex`
|
||||
- 协程启动必须可控:使用 `context.Context` + `sync.WaitGroup` 管理生命周期
|
||||
- **禁止无限制启动 goroutine**,必须通过缓冲 channel 或协程池限制并发数
|
||||
- **禁止无限制启动 goroutine**,采集调度必须使用有界 worker pool;每个点位是调度任务,不默认占用一个长期 goroutine
|
||||
- 协议注册表只保存无状态工厂;每个设备持有独立连接实例
|
||||
- 同一设备连接默认串行读写,采集与人工写入通过设备级互斥或单线程命令队列协调
|
||||
|
||||
### 3.5 单元测试
|
||||
|
||||
@@ -359,7 +374,7 @@ export function fetchDeviceList(params: Record<string, unknown>) {
|
||||
|
||||
### 5.2 响应格式
|
||||
|
||||
所有 API 响应使用统一格式:
|
||||
JSON API 使用统一格式:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -371,10 +386,14 @@ export function fetchDeviceList(params: Record<string, unknown>) {
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `code` | 是 | 0 表示成功,非 0 表示业务错误码 |
|
||||
| `message` | 是 | 人类可读的描述信息 |
|
||||
| `code` | 是 | 0 表示成功,非 0 表示模块业务错误码 |
|
||||
| `message` | 是 | 稳定、可展示的人类可读描述;不得暴露原始驱动或数据库错误 |
|
||||
| `data` | 否 | 响应数据,可为 null |
|
||||
|
||||
**流式响应例外**:CSV/其他文件下载、SSE 和 WebSocket 成功时直接返回对应的文件或数据流,不使用 JSON 包装;发生错误时仍返回统一 JSON 错误。
|
||||
|
||||
同步执行设备操作时,只有实际执行与回读均成功才可返回 HTTP 200 和 `code=0`。设备执行失败或超时必须返回非 2xx HTTP 状态和非零业务码;如已生成操作日志,`data` 返回 `write_log_id`。
|
||||
|
||||
### 5.3 分页响应格式
|
||||
|
||||
```json
|
||||
@@ -392,20 +411,26 @@ export function fetchDeviceList(params: Record<string, unknown>) {
|
||||
|
||||
### 5.4 错误码规范
|
||||
|
||||
| 错误码范围 | 类别 | 说明 |
|
||||
|-----------|------|------|
|
||||
| 0 | 成功 | 请求正常处理 |
|
||||
| 400xx | 参数错误 | 请求参数校验失败 |
|
||||
| 401xx | 鉴权错误 | 未登录或 token 过期 |
|
||||
| 403xx | 权限错误 | 无操作权限 |
|
||||
| 404xx | 未找到 | 请求的资源不存在 |
|
||||
| 409xx | 冲突 | 唯一性冲突等 |
|
||||
| 500xx | 服务端错误 | 内部错误 |
|
||||
HTTP 状态码表达错误类别;响应体 `code` 表达模块内具体原因。不得根据业务码前缀推断 HTTP 状态。
|
||||
|
||||
各模块错误码分配:
|
||||
| HTTP 状态 | 类别 | 典型场景 |
|
||||
|---|---|---|
|
||||
| 400 | 请求参数错误 | 格式、范围、必填项或时间范围不合法 |
|
||||
| 401 | 未认证 | token 缺失或失效 |
|
||||
| 403 | 无权限 | 已认证但无权执行操作 |
|
||||
| 404 | 资源不存在 | 设备、点位或历史元数据不存在 |
|
||||
| 409 | 业务冲突 | 名称冲突、不可变字段修改、状态冲突 |
|
||||
| 413 | 请求或导出结果过大 | 超出行数或文件限制 |
|
||||
| 422 | 业务校验失败 | 值类型或设备状态不满足执行条件 |
|
||||
| 502 | 下游设备/数据库执行失败 | PLC 写入失败、TDengine 查询失败 |
|
||||
| 504 | 下游超时 | PLC 或数据库超时 |
|
||||
| 500 | 未预期内部错误 | 未分类异常 |
|
||||
|
||||
业务码按模块分配:
|
||||
|
||||
| 模块 | 范围 |
|
||||
|------|------|
|
||||
| 通用、认证与权限 | 10001~10999 |
|
||||
| 设备管理 | 40001~40999 |
|
||||
| 采集点管理 | 41001~41999 |
|
||||
| 写入点管理 | 42001~42999 |
|
||||
@@ -413,6 +438,8 @@ export function fetchDeviceList(params: Record<string, unknown>) {
|
||||
| 采集引擎 | 50001~50999 |
|
||||
| 写入引擎 | 51001~51999 |
|
||||
|
||||
每个模块必须维护错误码清单;同一业务原因不得复用多个错误码。
|
||||
|
||||
### 5.5 路径参数命名规范
|
||||
|
||||
```
|
||||
@@ -467,14 +494,16 @@ CREATE UNIQUE INDEX idx_devices_name ON devices(name) WHERE deleted = FALSE;
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 表名 | 超级表用 `snake_case`:`collection_data`, `computed_data` |
|
||||
| 子表名 | 使用采集点 ID 去连字符后的 32 位小写字符串 |
|
||||
| 超级表名 | 使用 `snake_case`:`collection_data`, `computed_data` |
|
||||
| 子表名 | 固定为 `p_<uuid32>`,必须匹配 `^p_[0-9a-f]{32}$` |
|
||||
| UUID 字段/TAG | 使用带连字符的 36 位标准 UUID |
|
||||
| 时间戳 | 统一使用 `TIMESTAMP`,精度毫秒 |
|
||||
| 质量戳 | `INT` 类型:`0=good`, `1=bad` |
|
||||
| 值 | 统一使用 `DOUBLE`,BOOL 类型存 0/1 |
|
||||
| 标签 | 元数据信息存为 TAGS,不做查询条件的有选择缓存 |
|
||||
| 质量戳 | `INT`:`0=good`, `1=bad`;`none` 不落库 |
|
||||
| 值 | 使用可空 `DOUBLE`,BOOL 存 0/1;读取失败可存 NULL |
|
||||
| 质量原因 | 可空 `VARCHAR(32)` 稳定代码,例如 `timeout`, `parse_error`, `out_of_range` |
|
||||
| 标签 | `device_name`、`data_type` 等为创建子表时的快照,不作为当前配置的权威来源 |
|
||||
|
||||
> **质量戳转换约定**:TDengine 存储 INT 值(0=good, 1=bad),后端 API 层在返回响应时统一转换为字符串格式。前端始终处理字符串格式的质量戳。`none` 状态仅在查询无匹配记录时由 API 层返回,不会出现在 TDengine 存储中。
|
||||
> API 质量枚举统一为 `good | bad | none`。无匹配记录返回固定对象 `{value:null, quality:"none", quality_reason:null, matched_ts:null}`。
|
||||
|
||||
### 6.3 SQL 书写规范
|
||||
|
||||
@@ -496,24 +525,40 @@ RETURNING id;
|
||||
|
||||
### 6.4 数据库连接规范
|
||||
|
||||
- 数据库连接参数通过环境变量注入,禁止硬编码在代码中
|
||||
- 连接 DSN 按 `postgres://USER:PASS@HOST:PORT/DB?sslmode=require` 格式组装
|
||||
- Go 后端使用连接池,并在应用启动时通过带超时的 `PingContext` 校验连接
|
||||
- 数据库连接配置仅由 `.env.example` 提供占位符模板,`.env` 文件必须加入 `.gitignore`
|
||||
- PostgreSQL 和 TDengine 的主机、端口、用户、密码和数据库名均通过环境变量注入,禁止硬编码
|
||||
- PostgreSQL DSN 按 `postgres://USER:PASS@HOST:PORT/DB?sslmode=require` 格式组装
|
||||
- TDengine 数据库名使用 `TDENGINE_DATABASE`,默认值可为 `aquacontrolai`
|
||||
- Go 后端使用连接池,并在应用启动时通过带超时的健康检查校验连接
|
||||
- `.env.example` 只提供占位符;`.env` 必须加入 `.gitignore`
|
||||
|
||||
### 6.5 CSV 导入导出格式规范
|
||||
|
||||
CSV 分为两类:
|
||||
|
||||
#### 6.5.1 配置型 CSV
|
||||
|
||||
用于设备、采集点和写入点导入导出,必须可再次导入。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 编码 | 统一使用 `UTF-8 with BOM`(`charset=utf-8-sig`) |
|
||||
| 首行 | 标题行,字段名使用 `snake_case` |
|
||||
| 布尔值 | 导出为 `TRUE` / `FALSE`(大写) |
|
||||
| 编码 | `UTF-8 with BOM`(`charset=utf-8-sig`) |
|
||||
| 标题 | 固定字段,使用 `snake_case` |
|
||||
| 布尔值 | `TRUE` / `FALSE` |
|
||||
| 数字 | 整数无小数位,浮点数按实际精度输出 |
|
||||
| JSON 字段 | 导出为转义后的 JSON 字符串 |
|
||||
| 空值 | 空字段(无匹配数据)统一使用 `—`(全角破折号) |
|
||||
| 日期时间 | 格式 `YYYY-MM-DD HH:mm:ss`(24小时制) |
|
||||
| 文件扩展名 | `.csv` |
|
||||
| 导入校验 | 与对应 API 的校验规则一致,校验失败的行跳过并记录失败原因 |
|
||||
| JSON 字段 | RFC 4180 转义后的紧凑 JSON 字符串 |
|
||||
| 空值 | 空字段;导入时按字段规则解释 |
|
||||
| 日期时间 | `YYYY-MM-DD HH:mm:ss` |
|
||||
| 导入校验 | 与对应 API 一致,失败行跳过并返回行号与稳定原因 |
|
||||
|
||||
#### 6.5.2 报表型 CSV
|
||||
|
||||
用于历史数据等面向用户的下载,不要求再次导入。
|
||||
|
||||
- 允许本地化标题和动态点位列。
|
||||
- 重复点位名称追加 UUID 前 8 位,保证列名唯一。
|
||||
- 缺失或不展示的值使用 `—`,质量列使用 `good | bad | —`。
|
||||
- 文件名由服务端使用已解析参数重新格式化,只允许 ASCII 字母、数字、下划线、短横线和点。
|
||||
- 文件扩展名统一为 `.csv`。
|
||||
|
||||
---
|
||||
|
||||
@@ -551,7 +596,8 @@ logger.Info("设备连接成功",
|
||||
| 采集点/写入点创建/修改/删除 | INFO |
|
||||
| 写入操作 | INFO |
|
||||
| 采集引擎启停 | INFO |
|
||||
| 设备连接/断开 | WARN |
|
||||
| 设备连接成功 | INFO |
|
||||
| 设备断开或连接失败 | WARN |
|
||||
| 重连失败 | WARN |
|
||||
| 协议驱动异常 | ERROR |
|
||||
|
||||
@@ -562,12 +608,20 @@ logger.Info("设备连接成功",
|
||||
### 8.1 SQL 注入防护
|
||||
|
||||
```go
|
||||
// ✅ 正确:使用参数化查询,并显式声明返回字段
|
||||
// ✅ 用户值使用参数化查询
|
||||
db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name)
|
||||
|
||||
// ✗ 禁止:字符串拼接 SQL
|
||||
```
|
||||
|
||||
TDengine 动态子表名按以下流程处理:
|
||||
|
||||
1. 解析并验证标准 UUID;
|
||||
2. 转为小写、移除连字符并加 `p_`;
|
||||
3. 校验 `^p_[0-9a-f]{32}$`;
|
||||
4. 使用驱动的安全标识符引用能力构造 SQL;
|
||||
5. 时间和值条件继续使用参数绑定。
|
||||
|
||||
禁止把请求参数、点位名称、分组名称或任意未验证字符串拼入 SQL。
|
||||
|
||||
### 8.2 输入校验
|
||||
|
||||
| 场景 | 校验规则 |
|
||||
@@ -575,15 +629,16 @@ db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name)
|
||||
| IP 地址 | 使用 `net.ParseIP()` 或正则校验合法格式 |
|
||||
| 端口号 | 范围 1~65535 |
|
||||
| 时间范围 | 结束时间必须晚于开始时间,最大跨度不超过 31 天 |
|
||||
| 曲线结果 | `max_samples` 范围 100~10000,默认 2000 |
|
||||
| 分页 | page >= 1, page_size <= 100 |
|
||||
| 文件名 | 导出的 CSV 文件名不含用户输入(防止路径穿越) |
|
||||
| 文件名 | 不直接拼接原始用户输入;解析后由服务端重新格式化为安全文件名 |
|
||||
|
||||
### 8.3 写入安全
|
||||
|
||||
- **写入开关校验**:在 Service 层校验 `write_enabled=true`
|
||||
- **写入来源校验**:校验 `write_source` 与请求 `source` 的匹配关系
|
||||
- **回读验证**:写入后必须执行回读确认
|
||||
- **写入权限**:人工写入需登录鉴权;程序自动写入需 API Token
|
||||
- **人工写入记录**:当前阶段写入接口仅支持人工写入;Service 固定记录 `source=manual`、`operator=null`,请求体不得传入这两个字段
|
||||
- **访问边界**:未实施身份认证前,写入 API 只允许部署在受信任的内部网络;接入身份认证后再补充权限与操作者归属
|
||||
|
||||
### 8.4 配置安全
|
||||
|
||||
@@ -615,10 +670,10 @@ db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name)
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| 模块边界是否清晰 | 是否有 Handler 直接调用其他模块的 Repo |
|
||||
| 模块边界是否清晰 | 是否有跨模块直接调用 Repository、ORM 模型或访问其他模块的数据表 |
|
||||
| 分层是否遵守 | Handler → Service → Repository 单向依赖 |
|
||||
| 是否引入循环依赖 | 包间依赖不能成环 |
|
||||
| 扩展点是否留好 | 协议驱动是否遵循 ProtocolDriver 接口 |
|
||||
| 扩展点是否留好 | 协议驱动是否使用 Factory/Connection 模型,且每设备连接隔离 |
|
||||
|
||||
#### 功能层面
|
||||
|
||||
@@ -634,14 +689,14 @@ db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name)
|
||||
|--------|------|
|
||||
| N+1 查询 | 列表查询时是否 batch 加载关联数据 |
|
||||
| 索引 | 新增查询条件是否添加了对应的数据库索引 |
|
||||
| 分页 | 列表接口是否都有分页,且 page_size 有上限约束 |
|
||||
| 分页/采样 | 列表是否分页;历史曲线是否有 max_samples 和降采样 |
|
||||
| 连接池 | 数据库连接是否使用了连接池 |
|
||||
|
||||
#### 安全层面
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| SQL 注入 | 全库搜索字符串拼接的 SQL 语句 |
|
||||
| SQL 注入 | 用户值是否参数化;动态子表名是否只由 UUID 派生并通过白名单 |
|
||||
| 参数校验 | 所有用户输入是否都通过了校验 |
|
||||
| 敏感信息泄露 | 错误信息是否直接返回给前端 |
|
||||
| 路径穿越 | 文件操作的路径是否使用用户输入构造 |
|
||||
@@ -703,7 +758,7 @@ type Device struct {
|
||||
|
||||
---
|
||||
|
||||
> 本文档版本:v1.1
|
||||
> 本文档版本:v1.2
|
||||
> 最后更新:2026-07-11
|
||||
> 适用范围:污水厂智能控制系统全部后端(Go)、前端(Vue 3)代码
|
||||
>
|
||||
@@ -711,4 +766,4 @@ type Device struct {
|
||||
> 1. 开发者开发新功能前阅读本文档,确保代码风格一致
|
||||
> 2. 提交 MR/PR 前依据 [第 9 章 代码审核清单](#9-代码审核清单) 逐条自查
|
||||
> 3. Code Review 时以本文档作为审核标准,不符合规范的要求修改后重新提审
|
||||
> 4. 本文档随项目推进持续更新,新增模块时补充对应章节
|
||||
> 4. 本文档随项目推进持续更新,新增模块时补充对应章节
|
||||
|
||||
+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
|
||||
|
||||
+425
-379
File diff suppressed because it is too large
Load Diff
@@ -1,981 +0,0 @@
|
||||
# 三文档一致性与冲突审查报告
|
||||
|
||||
## 1. 审查对象
|
||||
|
||||
- `spec-数据管理.md`:v1.1,最后更新 2026-07-09
|
||||
- `spec-历史数据.md`:v1.0,最后更新 2026-07-09
|
||||
- `code-standards.md`:v1.1,最后更新 2026-07-11
|
||||
|
||||
本报告按本地文件实际行号定位。严重级别定义:
|
||||
|
||||
- **阻断**:按现有文档无法得到唯一、可运行或安全的实现,应在开发前解决。
|
||||
- **高**:会造成接口不兼容、数据错误、安全问题或大范围返工。
|
||||
- **中**:实现可以继续,但不同团队很可能产生不同解释。
|
||||
- **低**:主要影响命名、示例准确性或长期维护。
|
||||
|
||||
## 2. 总体结论
|
||||
|
||||
共发现 **38 项**需要协调的问题,其中:
|
||||
|
||||
- 阻断:4 项
|
||||
- 高:12 项
|
||||
- 中:18 项
|
||||
- 低:4 项
|
||||
|
||||
最需要优先解决的主题是:
|
||||
|
||||
1. 服务进程模型与运行时内存状态的访问方式不一致。
|
||||
2. 历史模块直接读取数据管理模块数据库,违反模块自治要求。
|
||||
3. TDengine 中 UUID 字段长度与 API/PG UUID 表达不兼容。
|
||||
4. 表格查询间隔规则自相矛盾,且缺少实现所需的 `history_interval`。
|
||||
5. 写入接口允许客户端自报操作者,和鉴权规范冲突。
|
||||
6. CSV、统一响应包、错误码、质量戳存在多套互不兼容的约定。
|
||||
7. 历史数据对禁用/删除点位不可访问,与“展示所有已存储历史数据”冲突。
|
||||
|
||||
---
|
||||
|
||||
## 3. 冲突明细
|
||||
|
||||
### A. 架构与模块边界
|
||||
|
||||
#### C-01|阻断|历史模块直接访问数据管理内部数据,违反模块自治
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L30-L37:历史模块直接查询 PostgreSQL 设备、采集点配置。
|
||||
- `spec-历史数据.md` L133-L139、L221-L223:直接读取 `collection_points` 构建树。
|
||||
- `code-standards.md` L29-L33:模块间通过 API 通信,不直接访问其他模块内部数据。
|
||||
- `code-standards.md` L614-L620:代码审查要求检查跨模块 Repo 调用。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
历史规格把 `collection_points`、`devices` 当作历史模块可直接查询的数据源;代码规范则要求模块之间只能通过 API 通信。
|
||||
|
||||
**影响**
|
||||
|
||||
- 历史模块会与数据管理表结构强耦合。
|
||||
- 数据管理字段或逻辑删除规则变化会直接破坏历史模块。
|
||||
- 无法明确历史 Service 应调用数据管理 API、共享领域服务,还是共享 Repository。
|
||||
|
||||
**建议**
|
||||
|
||||
二选一并写入架构决策:
|
||||
|
||||
1. **模块化单体方案**:允许 Service 通过共享只读领域接口访问配置数据,并删除“模块间只能通过 API”的绝对要求。
|
||||
2. **服务化方案**:历史模块通过数据管理内部 API/客户端获取点位元数据,禁止直接读表。
|
||||
|
||||
---
|
||||
|
||||
#### C-02|阻断|`server`/`collector` 分进程结构与“直接共享内存单例”冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `code-standards.md` L71-L76:存在独立的 `cmd/server` 和 `cmd/collector` 可执行程序。
|
||||
- `spec-数据管理.md` L439-L443:最新值维护在采集引擎内存。
|
||||
- `spec-数据管理.md` L1024-L1051:采集引擎单例运行在服务端进程中,Handler 直接获得实例引用。
|
||||
- `spec-数据管理.md` L325-L329:设备 API 从采集引擎运行时状态组装 `connection_status`。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
目录规范表明 Web 服务和采集器是两个独立进程;数据管理规格却要求 API Handler 直接引用采集引擎内存对象。独立进程不能直接共享 Go 内存。
|
||||
|
||||
**影响**
|
||||
|
||||
`latest_value`、`connection_status` 等接口无法按文档实现,除非实际部署结构与目录规范不同。
|
||||
|
||||
**建议**
|
||||
|
||||
明确唯一运行模型:
|
||||
|
||||
- 若采集器嵌入 Web 服务:删除独立 `cmd/collector`,或说明它仅用于离线/独立部署。
|
||||
- 若保持双进程:引入 Redis、MQTT、NATS、数据库状态表或内部 RPC;Web 服务不得直接引用 CollectorManager。
|
||||
|
||||
---
|
||||
|
||||
#### C-03|高|Handler 直接访问采集引擎,违反三层架构
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L325-L329、L1024-L1051:API Handler 直接查询引擎实例并组装状态。
|
||||
- `code-standards.md` L165-L180:Handler → Service → Repository 单向调用。
|
||||
- `code-standards.md` L184-L203:Handler 仅做参数校验和响应转换。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
运行时状态查询属于业务聚合逻辑,应由 Service 完成;规格明确要求 Handler 直接访问 Engine。
|
||||
|
||||
**影响**
|
||||
|
||||
Handler 与运行时实现耦合,单元测试和未来进程拆分困难。
|
||||
|
||||
**建议**
|
||||
|
||||
定义 `RuntimeStatusProvider` 接口,由 Service 注入;Handler 只调用 Service。
|
||||
|
||||
---
|
||||
|
||||
#### C-04|高|历史“最新值”的数据源和语义未统一
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L439-L454:`latest_value` 明确定义为采集引擎内存中的实时值。
|
||||
- `spec-历史数据.md` L141-L159、L221-L273:历史树也返回同名 `latest_value`。
|
||||
- `spec-历史数据.md` L32-L37:历史模块声明的数据源只有 PostgreSQL 和 TDengine。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
历史模块没有说明如何取得实时内存值。若从 TDengine 读取,它受 `history_interval` 影响,不再是数据管理接口定义的“实时最新值”;若从采集引擎读取,又违反当前模块边界和分进程结构。
|
||||
|
||||
**影响**
|
||||
|
||||
同名字段在两个接口中可能代表不同时间新鲜度。
|
||||
|
||||
**建议**
|
||||
|
||||
明确命名和来源:
|
||||
|
||||
- 实时缓存值:`realtime_value`,由运行时状态服务提供。
|
||||
- TDengine 最新存储值:`latest_stored_value`,并返回 `stored_at`/延迟说明。
|
||||
|
||||
---
|
||||
|
||||
### B. 历史数据可见性与业务规则
|
||||
|
||||
#### C-05|高|“展示所有已存储历史数据”与禁用/删除过滤冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L23-L28:模块负责展示所有已存储历史时序数据。
|
||||
- `spec-历史数据.md` L99-L109:树仅显示点位和设备均启用、未删除的点。
|
||||
- `spec-数据管理.md` L142-L150:逻辑删除时数据保留。
|
||||
- `spec-数据管理.md` L243-L247:删除设备会逻辑删除其点位,但 TDengine 历史数据未删除。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
禁用或逻辑删除后,历史数据仍保留,但点位从树中消失,用户无法再查询这些已存储数据。
|
||||
|
||||
**影响**
|
||||
|
||||
历史追溯、事故审计和删除前数据查看不可实现。
|
||||
|
||||
**建议**
|
||||
|
||||
历史树应区分:
|
||||
|
||||
- 活跃点位;
|
||||
- 已禁用点位;
|
||||
- 已删除/归档点位。
|
||||
|
||||
至少提供“包含归档点位”筛选,并禁止采集但允许只读历史查询。
|
||||
|
||||
---
|
||||
|
||||
#### C-06|中|树声明“可用历史数据点”,实际只检查配置,不检查是否存在历史数据
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L133-L139:只显示包含“可用历史数据点”的分组。
|
||||
- `spec-历史数据.md` L99-L109:判定条件仅为 enabled/deleted/store_history。
|
||||
- `spec-数据管理.md` L1017-L1022:子表和数据只有实际写入时才产生。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
刚启用 `store_history`、尚未首次写入的点位也会进入树,但并不存在历史数据。
|
||||
|
||||
**影响**
|
||||
|
||||
“可用”含义不一致,用户勾选后可能得到空结果。
|
||||
|
||||
**建议**
|
||||
|
||||
将术语改为“已配置历史存储的点位”,或额外检查 TDengine 子表/首条数据是否存在,并返回 `has_history_data`。
|
||||
|
||||
---
|
||||
|
||||
#### C-07|中|树过滤规则与查询接口校验规则不一致
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L99-L109:树过滤 enabled/deleted/store_history。
|
||||
- `spec-历史数据.md` L302-L311、L371-L383:查询仅根据 `point_ids` 映射子表,没有说明重复校验业务状态。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
客户端可绕过树,直接传入禁用、删除或 `store_history=false` 的旧点位 ID。
|
||||
|
||||
**影响**
|
||||
|
||||
UI 和 API 的业务边界不一致;可能访问本应隐藏的数据。
|
||||
|
||||
**建议**
|
||||
|
||||
明确查询权限策略:是否允许归档历史查询。Service 必须根据该策略重新校验点位,不信任前端树。
|
||||
|
||||
---
|
||||
|
||||
### C. 数据模型与数据格式
|
||||
|
||||
#### C-08|阻断|TDengine UUID 字段长度与 PostgreSQL/API UUID 不兼容
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L58-L63、L337-L346:主键为 PostgreSQL `UUID`。
|
||||
- `spec-历史数据.md` L64-L74:`point_id`、`device_id` 为 `VARCHAR(32)`。
|
||||
- `spec-数据管理.md` L989-L1001:同样使用 `VARCHAR(32)`。
|
||||
- `spec-数据管理.md` L1006-L1014:仅“子表名”明确去除 UUID 连字符。
|
||||
- API 示例普遍使用带连字符的 UUID/UUID 语义。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
标准 UUID 字符串带连字符时长度为 36;`VARCHAR(32)` 只能保存去连字符格式。文档只规定子表名去连字符,没有规定普通字段和 TAG 也去连字符。
|
||||
|
||||
**影响**
|
||||
|
||||
插入失败、截断、查询无法匹配 PostgreSQL ID,属于数据完整性阻断问题。
|
||||
|
||||
**建议**
|
||||
|
||||
统一一种方案:
|
||||
|
||||
- 推荐 TDengine `point_id`、`device_id` 改为 `VARCHAR(36)`,API/PG/TD 全部使用标准 UUID 字符串;
|
||||
- 子表名使用固定安全前缀加 32 位无连字符形式,例如 `p_<uuid32>`。
|
||||
|
||||
---
|
||||
|
||||
#### C-09|高|可变名称/设备/数据类型与 TDengine 冗余字段、TAGS 更新规则缺失
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L235-L241、L504-L506:允许修改设备和采集点配置。
|
||||
- `spec-数据管理.md` L991-L1001:TDengine 保存 `point_name`、`device_name`、`data_type`。
|
||||
- `spec-数据管理.md` L1004-L1014:子表按点位 ID 创建,TAGS 在创建时写入。
|
||||
- `spec-历史数据.md` L141-L159、L314-L344:历史接口返回名称和数据类型。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
点位改名、换设备、改数据类型后,PostgreSQL 当前配置与 TDengine 历史行/TAGS 可能不同;文档没有规定更新 TAG、保留历史名称还是使用当前名称。
|
||||
|
||||
**影响**
|
||||
|
||||
同一条曲线可能显示错误设备名或数据类型;历史审计失去“当时配置”。
|
||||
|
||||
**建议**
|
||||
|
||||
明确元数据版本策略:
|
||||
|
||||
- 历史响应默认使用当前配置,但另存 `recorded_point_name`;
|
||||
- 或配置变更生成新点位 ID/新子表;
|
||||
- 若允许更新 TAG,写明 TDengine 更新流程及失败补偿。
|
||||
|
||||
---
|
||||
|
||||
#### C-10|高|表格要求显示“单位”,但数据模型和 API 没有单位字段
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L665-L674:列标题为 `点位名称(单位)`。
|
||||
- `spec-数据管理.md` L337-L363:`collection_points` 无 `unit`。
|
||||
- `spec-历史数据.md` L141-L159、L314-L439:树、曲线和表格响应均无 `unit`。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
前端要求无法由任何已定义数据源实现。
|
||||
|
||||
**影响**
|
||||
|
||||
不同前端可能硬编码单位、忽略单位或自行推断,产生错误展示。
|
||||
|
||||
**建议**
|
||||
|
||||
在 `collection_points` 增加 `unit VARCHAR(...)`,并在树、曲线、表格 API 元数据中返回;或删除单位要求。
|
||||
|
||||
---
|
||||
|
||||
#### C-11|中|树节点属性定义与实际 API 响应不一致
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L141-L159:点位节点要求包含 `group_name`。
|
||||
- `spec-历史数据.md` L225-L273:`GET /history/tree` 的点位对象没有 `group_name`。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
同一文档对同一对象给出两种结构。
|
||||
|
||||
**影响**
|
||||
|
||||
前后端类型定义不一致。
|
||||
|
||||
**建议**
|
||||
|
||||
补齐 `group_name`,或从节点属性要求中删除并说明可由父节点推导。
|
||||
|
||||
---
|
||||
|
||||
#### C-12|中|质量缺失状态 `none` 与历史 API 的 `null` 表达冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L943-L952:无记录时 API 质量为 `none`,前端始终处理字符串质量戳。
|
||||
- `code-standards.md` L466-L477:重复同一约定。
|
||||
- `spec-历史数据.md` L378-L383、L394-L439:未匹配数据直接返回数组元素 `null`。
|
||||
- `spec-历史数据.md` L467-L471:CSV 中用 `—`。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
缺失数据到底是:
|
||||
|
||||
- `{ "value": null, "quality": "none" }`;
|
||||
- 整个元素为 `null`;
|
||||
- 或完全没有点;
|
||||
|
||||
文档没有统一。
|
||||
|
||||
**影响**
|
||||
|
||||
前端类型、图表 gap 处理、CSV 转换会出现分支差异。
|
||||
|
||||
**建议**
|
||||
|
||||
统一为显式对象,例如:
|
||||
|
||||
```json
|
||||
{ "value": null, "quality": "none", "matched_ts": null }
|
||||
```
|
||||
|
||||
曲线原始序列可继续用“无记录即无点”,但表格对齐结果应保持固定对象结构。
|
||||
|
||||
---
|
||||
|
||||
#### C-13|中|坏质量数据是否必须有数值未定义
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L917-L935、L943-L949:断线、读取失败、null、解析异常均为 bad。
|
||||
- `spec-历史数据.md` L327-L331:bad 示例仍有数值。
|
||||
- `spec-历史数据.md` L590-L597:连续 bad 数据要求画虚线。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
读取失败或 null 时通常没有可绘制数值;历史曲线规则却假设 bad 点有数值并可连接。
|
||||
|
||||
**影响**
|
||||
|
||||
实现者可能使用上次值、0、null 或丢点,曲线结果完全不同。
|
||||
|
||||
**建议**
|
||||
|
||||
定义 bad 子类型或值策略:
|
||||
|
||||
- `bad_with_value`:有可疑数值,可虚线绘制;
|
||||
- `bad_no_value`:值为 null,形成断线;
|
||||
- 禁止用 0 或上次值隐式补齐,除非响应显式标注。
|
||||
|
||||
---
|
||||
|
||||
### D. 历史查询接口与算法
|
||||
|
||||
#### C-14|阻断|`interval_minutes` 约束自相矛盾
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L362-L367:范围 1~1440,“必须能被60整除”,同时建议 1、2、5、10、15、20、30、60。
|
||||
- `spec-历史数据.md` L658-L663:前端选项仅上述 8 个值。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
中文“必须能被 60 整除”通常表示 `interval % 60 == 0`,则 1、2、5、10 等均不满足。文档实际可能想表达“必须是 60 的约数”。
|
||||
|
||||
**影响**
|
||||
|
||||
后端校验会产生两种完全不同的实现。
|
||||
|
||||
**建议**
|
||||
|
||||
直接定义枚举,避免自然语言:
|
||||
|
||||
```text
|
||||
interval_minutes ∈ {1, 2, 5, 10, 15, 20, 30, 60}
|
||||
```
|
||||
|
||||
若确需支持到 1440,应给出完整规则和前端选项。
|
||||
|
||||
---
|
||||
|
||||
#### C-15|高|前端需要 `history_interval`,树接口却不返回
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L369:前端应根据已选点位最大的 `history_interval` 限制或提示。
|
||||
- `spec-历史数据.md` L141-L159、L225-L273:树节点不含 `history_interval`。
|
||||
- `spec-数据管理.md` L355-L357:该字段只存在于采集点配置。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
历史页面没有已定义接口可获得每个勾选点位的 `history_interval`。
|
||||
|
||||
**影响**
|
||||
|
||||
前端只能忽略要求,或逐点调用数据管理详情接口,造成跨模块依赖和 N+1 请求。
|
||||
|
||||
**建议**
|
||||
|
||||
在历史树节点中增加 `history_interval`,或增加批量元数据接口。
|
||||
|
||||
---
|
||||
|
||||
#### C-16|高|`history_interval` 无上限,与历史表格可选范围不兼容
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L355-L357、L491-L499:仅规定最小值 1 分钟,没有上限。
|
||||
- `spec-历史数据.md` L367:表格 API 最大 1440 分钟。
|
||||
- `spec-历史数据.md` L662:前端最大选项仅 60 分钟。
|
||||
- `spec-历史数据.md` L369:建议表格间隔不小于最大 `history_interval`。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
若点位 `history_interval=120`,前端没有可选值满足建议;若大于 1440,连 API 都无法满足。
|
||||
|
||||
**影响**
|
||||
|
||||
合法的数据管理配置会产生无法正确展示的历史点位。
|
||||
|
||||
**建议**
|
||||
|
||||
统一约束。推荐:
|
||||
|
||||
- `history_interval` 枚举与历史表格可选间隔共享;
|
||||
- 或前端动态生成可选值并允许到 1440;
|
||||
- 数据管理 API 增加最大值校验。
|
||||
|
||||
---
|
||||
|
||||
#### C-17|高|代码规范要求最大 31 天,历史接口未定义该限制
|
||||
|
||||
**位置**
|
||||
|
||||
- `code-standards.md` L571-L579:时间范围最大跨度不超过 31 天。
|
||||
- `spec-历史数据.md` L284-L300、L349-L367:只要求起止时间,未规定顺序和最大跨度。
|
||||
- `spec-历史数据.md` L498-L508:自定义时间范围未规定上限。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
规格可被理解为允许任意时间跨度,代码规范要求拒绝超过 31 天。
|
||||
|
||||
**影响**
|
||||
|
||||
前后端校验不一致;大查询可能导致内存和网络压力。
|
||||
|
||||
**建议**
|
||||
|
||||
在三个历史接口中明确:
|
||||
|
||||
- `end_time > start_time`;
|
||||
- 最大 31 天;
|
||||
- 超限错误码;
|
||||
- 导出是否允许更长范围以及异步策略。
|
||||
|
||||
---
|
||||
|
||||
#### C-18|高|动态子表 SQL 与“禁止字符串拼接 SQL”冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L302-L311:`FROM {subtable_name}` 动态替换表名。
|
||||
- `code-standards.md` L222-L226、L562-L569:必须参数化,禁止字符串拼接 SQL。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
表名通常不能作为普通参数绑定;历史规格又要求动态表名。
|
||||
|
||||
**影响**
|
||||
|
||||
开发者可能直接拼接用户可影响的标识,形成 SQL 注入风险,或违反代码审查规则。
|
||||
|
||||
**建议**
|
||||
|
||||
为动态标识符增加明确例外和安全实现:
|
||||
|
||||
1. 只接受数据库查询得到的 UUID;
|
||||
2. 服务端派生固定格式 `p_<uuid32>`;
|
||||
3. 使用严格正则白名单;
|
||||
4. 值条件仍使用参数绑定;
|
||||
5. 禁止直接使用请求中的表名。
|
||||
|
||||
---
|
||||
|
||||
#### C-19|中|最近邻窗口与原始查询边界不一致
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L375:只查询 `start_time ~ end_time`。
|
||||
- `spec-历史数据.md` L378-L382:每个目标点在 `± interval/2` 窗口内寻找最近值。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
第一个目标点可能需要 `start_time` 之前的数据,最后一个目标点可能需要 `end_time` 之后的数据;当前查询范围把这些候选值排除。
|
||||
|
||||
**影响**
|
||||
|
||||
边界行会出现不必要的 `null`,与“最近邻窗口”定义不符。
|
||||
|
||||
**建议**
|
||||
|
||||
原始查询范围扩展为:
|
||||
|
||||
```text
|
||||
[start_time - window, end_time + window]
|
||||
```
|
||||
|
||||
最终输出仍只保留目标时间序列。
|
||||
|
||||
---
|
||||
|
||||
#### C-20|中|最近邻算法缺少并列、复用和非整除终点规则
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L373-L383。
|
||||
|
||||
**缺失内容**
|
||||
|
||||
- 前后两个点距离相同,选前还是选后;
|
||||
- 同一原始点能否匹配两个目标时间;
|
||||
- `end_time-start_time` 不是 interval 整数倍时是否追加 `end_time`;
|
||||
- bad 与 good 距离相同时是否优先 good。
|
||||
|
||||
**影响**
|
||||
|
||||
不同后端实现会返回不同表格和 CSV。
|
||||
|
||||
**建议**
|
||||
|
||||
补充确定性规则,并建立算法测试样例。
|
||||
|
||||
---
|
||||
|
||||
#### C-21|高|曲线原始查询缺少降采样/结果上限,难以满足性能规范
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L298-L311:最多 20 点,但返回范围内全部原始数据。
|
||||
- `spec-数据管理.md` L355:采集周期最小 1 秒。
|
||||
- `code-standards.md` L631-L638:要求审查分页、索引和连接池等性能问题。
|
||||
- `code-standards.md` L577:最大跨度仍可达 31 天。
|
||||
|
||||
**问题**
|
||||
|
||||
单个 1 秒点位 31 天约 267 万条;20 点可能超过 5300 万条。API 没有 `max_points`、降采样或服务端聚合。
|
||||
|
||||
**影响**
|
||||
|
||||
内存、TDengine 查询、JSON 序列化、浏览器 ECharts 都可能失控。
|
||||
|
||||
**建议**
|
||||
|
||||
根据像素宽度/最大点数服务端降采样,或要求客户端传 `max_samples`;原始明细导出采用流式响应。
|
||||
|
||||
---
|
||||
|
||||
#### C-22|中|Y 轴“后端转换”方案与 API 响应结构不匹配
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L553-L577:推荐后端把原始值转换为显示坐标。
|
||||
- `spec-历史数据.md` L314-L344:曲线 API 只返回原始 `value`,没有 `display_value` 或映射元数据。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
推荐方案没有数据合同支持,前端无法同时绘制映射值和显示原始 tooltip。
|
||||
|
||||
**建议**
|
||||
|
||||
确定唯一职责:
|
||||
|
||||
- 推荐纯前端映射;或
|
||||
- API 返回 `display_value`、原始 `value` 和分段映射定义。
|
||||
|
||||
---
|
||||
|
||||
### E. REST API、错误和安全
|
||||
|
||||
#### C-23|高|所有 API 统一 JSON 包装与 CSV 文件响应冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `code-standards.md` L360-L376:“所有 API 响应”统一为 `{code,message,data}`。
|
||||
- `spec-数据管理.md` L249-L273、L591-L605:导出直接返回 CSV。
|
||||
- `spec-历史数据.md` L442-L471:导出直接返回 CSV。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
文件下载不可能同时是原始 CSV 和 JSON 包装。
|
||||
|
||||
**建议**
|
||||
|
||||
在代码规范中增加明确例外:文件流、SSE、WebSocket 不使用统一 JSON 包装;错误响应仍返回统一 JSON。
|
||||
|
||||
---
|
||||
|
||||
#### C-24|高|写入失败/超时仍返回 `code=0`、`message=success`
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L805-L836:`result=failed/timeout`,但 envelope 仍为成功。
|
||||
- `code-standards.md` L372-L376、L393-L403:`code=0` 表示成功,非 0 表示业务错误。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
同一响应同时表示“成功”和“写入失败”。
|
||||
|
||||
**影响**
|
||||
|
||||
通用请求层可能把失败当作成功;告警和重试逻辑不可靠。
|
||||
|
||||
**建议**
|
||||
|
||||
区分两类情况:
|
||||
|
||||
- 请求成功且设备写入成功:`code=0`。
|
||||
- 请求已处理但设备写入失败:使用明确业务错误码,HTTP 409/422/502/504 按原因选择,同时 `data` 可附审计记录 ID。
|
||||
|
||||
---
|
||||
|
||||
#### C-25|高|写入请求允许客户端指定 operator,与鉴权规范冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L736-L767:客户端提交 `source`、`operator`。
|
||||
- `spec-数据管理.md` L1092-L1098:写入需要登录权限。
|
||||
- `code-standards.md` L581-L586:人工写入需登录,自动写入需 API Token。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
已鉴权身份应由服务端从登录会话或 Token 推导;允许请求体任意填写 `operator` 会导致身份伪造。`source` 同样不应完全信任客户端声明。
|
||||
|
||||
**影响**
|
||||
|
||||
写入审计日志不可可信,存在严重审计与安全风险。
|
||||
|
||||
**建议**
|
||||
|
||||
- 从认证上下文生成 `operator` 和调用方类型;
|
||||
- 请求体删除 `operator`,必要时仅保留 `operator_display_name` 作为非权威备注;
|
||||
- manual/auto 使用不同凭证或不同内部路由。
|
||||
|
||||
---
|
||||
|
||||
#### C-26|中|写入日志错误字段命名不一致
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L719-L723:日志列表返回 `error_message`。
|
||||
- `spec-数据管理.md` L805-L836:写入结果返回 `error`。
|
||||
- `spec-数据管理.md` L851-L855:数据库字段为 `error_message`。
|
||||
- `code-standards.md` L29-L30:同一概念跨层命名一致。
|
||||
|
||||
**影响**
|
||||
|
||||
前端需要两套字段,Go DTO 和模型映射易混乱。
|
||||
|
||||
**建议**
|
||||
|
||||
统一为 `error_message`,或明确 `error` 是标准错误对象而不是字符串。
|
||||
|
||||
---
|
||||
|
||||
#### C-27|中|写入值在不同 API 中类型不一致
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L719-L720:日志列表的 `target_value`、`readback_value` 是字符串。
|
||||
- `spec-数据管理.md` L790-L800:写入响应 `value`、`readback_value` 是数字。
|
||||
- `spec-数据管理.md` L851-L853:数据库统一存 TEXT。
|
||||
|
||||
**影响**
|
||||
|
||||
同一业务值在接口间不能复用类型,BOOL/INT/REAL 转换规则不清楚。
|
||||
|
||||
**建议**
|
||||
|
||||
日志 API 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"data_type": "REAL",
|
||||
"target_value": 20.5,
|
||||
"readback_value": 20.5,
|
||||
"raw_target_value": "20.5"
|
||||
}
|
||||
```
|
||||
|
||||
或统一使用可辨别联合类型。
|
||||
|
||||
---
|
||||
|
||||
#### C-28|中|日志查询筛选值遗漏 `timeout`
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L693-L695:`result` 仅说明 success/failed。
|
||||
- `spec-数据管理.md` L822-L835:存在 timeout。
|
||||
- `spec-数据管理.md` L854:数据库枚举包含 timeout。
|
||||
|
||||
**建议**
|
||||
|
||||
筛选枚举统一为 `success | failed | timeout`。
|
||||
|
||||
---
|
||||
|
||||
#### C-29|高|错误码分类和模块分段无法同时成立
|
||||
|
||||
**位置**
|
||||
|
||||
- `code-standards.md` L395-L403:400xx=参数、401xx=鉴权、403xx=权限、404xx=未找到、409xx=冲突。
|
||||
- `code-standards.md` L405-L414:采集点=41001~41999,写入点=42001~42999,历史=43001~43999。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
例如历史数据参数错误按类型应是 400xx,但按模块必须是 430xx;历史数据未找到按类型应是 404xx,但又必须是 430xx。
|
||||
|
||||
**影响**
|
||||
|
||||
无法设计一致的错误码,前端不能按前缀分类。
|
||||
|
||||
**建议**
|
||||
|
||||
采用一种二维编码方式,例如:
|
||||
|
||||
- HTTP 状态表达错误类别;
|
||||
- 业务码按模块分段;
|
||||
- 或业务码格式 `模块两位 + 类别两位 + 序号`,并给出算法。
|
||||
|
||||
---
|
||||
|
||||
#### C-30|中|代码规范示例直接返回 `err.Error()`,与安全审查项冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `code-standards.md` L184-L199:绑定失败时把 `err.Error()` 返回前端。
|
||||
- `code-standards.md` L640-L647:禁止直接向前端泄露错误信息。
|
||||
|
||||
**影响**
|
||||
|
||||
开发者会复制示例,可能暴露内部字段、解析器或库错误。
|
||||
|
||||
**建议**
|
||||
|
||||
对外仅返回稳定校验消息;原始错误写结构化日志。
|
||||
|
||||
---
|
||||
|
||||
### F. CSV 与文件规范
|
||||
|
||||
#### C-31|高|历史 CSV 表头不符合统一 `snake_case`
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L457-L470:表头为中文“时间”、动态点位名和“_质量”。
|
||||
- `code-standards.md` L504-L515:CSV 标题字段名统一使用 `snake_case`。
|
||||
- `spec-数据管理.md` L263-L267、L595-L600:配置 CSV 使用 snake_case。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
历史展示型 CSV 和配置导入型 CSV 实际需求不同,但代码规范没有区分。
|
||||
|
||||
**建议**
|
||||
|
||||
在规范中分为:
|
||||
|
||||
1. **机器可导入配置 CSV**:固定 snake_case。
|
||||
2. **用户展示/报表 CSV**:允许本地化和动态列名,但需定义转义和重复点名处理。
|
||||
|
||||
---
|
||||
|
||||
#### C-32|高|历史 CSV 文件名使用用户输入,违反路径安全规范
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L676-L682:文件名包含 `{start_time}`、`{end_time}`、`{interval}`。
|
||||
- `code-standards.md` L571-L579:导出文件名不得包含用户输入。
|
||||
|
||||
**影响**
|
||||
|
||||
除路径穿越外,ISO 时间中的冒号、加号在部分系统不适合作为文件名。
|
||||
|
||||
**建议**
|
||||
|
||||
服务端解析并重新格式化为安全值,例如:
|
||||
|
||||
```text
|
||||
history_20260709T000000_20260709T010000_10m.csv
|
||||
```
|
||||
|
||||
只使用数字、ASCII 字母、下划线和短横线。
|
||||
|
||||
---
|
||||
|
||||
### G. 前端交互和接口契约
|
||||
|
||||
#### C-33|中|模式切换“不重复请求”与“自动重新查询”冲突
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L715-L720:切换模式“不重复请求数据,按需加载”。
|
||||
- `spec-历史数据.md` L740-L741:切换模式自动重新查询。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
无法判断是每次切换都请求、首次进入某模式请求一次,还是使用缓存。
|
||||
|
||||
**建议**
|
||||
|
||||
明确缓存键:`mode + point_ids + start_time + end_time + interval`。只有缓存不存在或参数变化时请求。
|
||||
|
||||
---
|
||||
|
||||
#### C-34|中|20 点上限同时被描述为建议和强制
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-历史数据.md` L164-L171:建议最多 20。
|
||||
- `spec-历史数据.md` L296-L300、L362-L367、L748-L753:API/附录要求最多 20。
|
||||
|
||||
**建议**
|
||||
|
||||
统一为强制规则;前端在第 21 个点时阻止并提示,后端仍校验。
|
||||
|
||||
---
|
||||
|
||||
#### C-35|中|设备 PUT 声称与新增同结构,但新增结构没有 `enabled`
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L182-L198:新增请求体无 `enabled`。
|
||||
- `spec-数据管理.md` L235-L241:PUT 与新增相同,同时说明可修改 `enabled`。
|
||||
|
||||
**影响**
|
||||
|
||||
无法按请求合同执行启用/禁用。
|
||||
|
||||
**建议**
|
||||
|
||||
使用独立 `UpdateDeviceRequest`,明确字段可选性;或增加专用动作接口 `/devices/{id}/enable`、`/disable`。
|
||||
|
||||
---
|
||||
|
||||
#### C-36|中|“立即生效”与 5 秒轮询实现不一致
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L243-L247、L504-L511、L970-L981:删除/变更后立即停止或动态更新。
|
||||
- `spec-数据管理.md` L983:可每 5 秒轮询数据库。
|
||||
|
||||
**冲突内容**
|
||||
|
||||
轮询方案最多延迟约 5 秒,不是“立即”。
|
||||
|
||||
**建议**
|
||||
|
||||
定义 SLA,例如 5 秒内生效;若必须立即,使用事务后事件、LISTEN/NOTIFY 或消息总线。
|
||||
|
||||
---
|
||||
|
||||
### H. 命名、代码风格和内部标准
|
||||
|
||||
#### C-37|中|索引命名不符合 `idx_{表名}_{字段名}`
|
||||
|
||||
**位置**
|
||||
|
||||
- `code-standards.md` L432-L442:索引命名要求包含实际字段名。
|
||||
- `spec-数据管理.md` L368-L372:`idx_collection_points_device/group`,实际字段为 `device_id/group_name`。
|
||||
- `spec-数据管理.md` L643-L645:`idx_write_points_device/group`。
|
||||
- `spec-数据管理.md` L863-L866:`idx_write_logs_point/device/time`,实际字段为 `point_id/device_id/created_at`。
|
||||
|
||||
**建议**
|
||||
|
||||
改为:
|
||||
|
||||
- `idx_collection_points_device_id`
|
||||
- `idx_collection_points_group_name`
|
||||
- `idx_write_logs_created_at`
|
||||
- 其他同理
|
||||
|
||||
或放宽规范,明确允许语义化简称。
|
||||
|
||||
---
|
||||
|
||||
#### C-38|低|核心类型命名 `CollectPoint` 与 `CollectionPoint` 不一致
|
||||
|
||||
**位置**
|
||||
|
||||
- `spec-数据管理.md` L39-L46:概念图使用 `CollectPoint`。
|
||||
- `code-standards.md` L664-L669:Go 模型使用 `CollectionPoint`。
|
||||
- API/table/package 也使用 `collection`。
|
||||
|
||||
**建议**
|
||||
|
||||
统一为 `CollectionPoint`,避免出现第三种命名。
|
||||
|
||||
---
|
||||
|
||||
## 4. 其他需要补充但尚不足以判定为直接冲突的事项
|
||||
|
||||
以下问题属于规格缺失或高风险歧义,建议一并处理:
|
||||
|
||||
1. `spec-数据管理.md` L926-L929 使用“值在合理范围内”判定 good,但数据模型没有合理范围、量程或校验表达式字段。
|
||||
2. Modbus `byte_order` 与 `word_order` 的职责存在重叠;`CDAB` 已涉及字交换,同时又提供 `word_order=BA`,组合语义需要重新定义。
|
||||
3. 历史查询同一原始点是否可匹配多个表格时间点未规定。
|
||||
4. 历史树、分组、点位的排序规则未规定。
|
||||
5. 最新值不存在、采集器未运行或缓存过期时,`latest_value` 是 `null`、省略还是返回 `quality=none` 未规定。
|
||||
6. 导出大文件是否流式传输、是否设置 Content-Disposition、超时和最大行数未规定。
|
||||
7. `history_retention_days` 只有参数说明,没有系统配置存储模型、API、权限和变更失败补偿。
|
||||
8. `CREATE/ALTER DATABASE aquacontrolai` 使用固定数据库名,而代码规范要求连接参数环境化;需明确数据库名是否也是环境配置。
|
||||
9. 数据管理导出请求示例含 `// 可选` 注释,不是合法 JSON。
|
||||
10. 代码规范日志示例用 INFO 记录“连接成功”,但日志埋点表把“设备连接/断开”统一定为 WARN;应拆分成功 INFO、断开/失败 WARN。
|
||||
|
||||
---
|
||||
|
||||
## 5. 建议的协调优先级
|
||||
|
||||
### 第一批:开发前必须定稿
|
||||
|
||||
1. C-02 服务进程模型和运行时状态传输。
|
||||
2. C-01 模块边界和历史元数据访问方式。
|
||||
3. C-08 UUID 存储格式。
|
||||
4. C-14/C-15/C-16 表格间隔与 `history_interval` 合同。
|
||||
5. C-25 写入身份来源与鉴权。
|
||||
6. C-24/C-29 错误语义和错误码体系。
|
||||
7. C-05 归档历史数据可见性。
|
||||
|
||||
### 第二批:接口冻结前完成
|
||||
|
||||
1. C-10 单位字段。
|
||||
2. C-12 质量缺失表示。
|
||||
3. C-17/C-21 时间范围、降采样、结果上限。
|
||||
4. C-23 文件响应例外。
|
||||
5. C-31/C-32 CSV 两类规范和安全文件名。
|
||||
6. C-09 TDengine 元数据变更策略。
|
||||
|
||||
### 第三批:编码规范和文档清理
|
||||
|
||||
1. C-33 至 C-38。
|
||||
2. 补齐排序、空值、超时、流式导出和配置变更 SLA。
|
||||
3. 为关键算法增加契约测试样例。
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐的统一决策摘要
|
||||
|
||||
建议最终统一为以下基线:
|
||||
|
||||
- Web API 与 Collector 保持双进程,通过内部状态服务或消息/缓存同步状态。
|
||||
- 历史模块通过只读领域接口取得点位元数据,不直接依赖数据管理 Repo。
|
||||
- UUID 在 API、PG、TDengine 字段中统一使用 36 字符标准形式;仅表名使用 `p_<uuid32>`。
|
||||
- 历史树返回 `history_interval`、`unit`、`archived`、`has_history_data`。
|
||||
- 表格间隔采用明确枚举;缺失值统一 `{value:null, quality:"none"}`。
|
||||
- 历史查询最大 31 天并强制降采样/最大点数。
|
||||
- 写入操作者从认证上下文生成,失败使用非零业务码。
|
||||
- JSON API 使用统一 envelope;CSV 文件流为明确例外。
|
||||
- 配置 CSV 使用固定 snake_case;历史报表 CSV 允许本地化动态表头。
|
||||
Reference in New Issue
Block a user