This commit is contained in:
2026-07-11 18:04:51 +08:00
parent 3193d5e739
commit aab73d6f0c
4 changed files with 1288 additions and 222 deletions
+16
View File
@@ -474,6 +474,8 @@ CREATE UNIQUE INDEX idx_devices_name ON devices(name) WHERE deleted = FALSE;
| 值 | 统一使用 `DOUBLE`BOOL 类型存 0/1 | | 值 | 统一使用 `DOUBLE`BOOL 类型存 0/1 |
| 标签 | 元数据信息存为 TAGS,不做查询条件的有选择缓存 | | 标签 | 元数据信息存为 TAGS,不做查询条件的有选择缓存 |
> **质量戳转换约定**TDengine 存储 INT 值(0=good, 1=bad),后端 API 层在返回响应时统一转换为字符串格式。前端始终处理字符串格式的质量戳。`none` 状态仅在查询无匹配记录时由 API 层返回,不会出现在 TDengine 存储中。
### 6.3 SQL 书写规范 ### 6.3 SQL 书写规范
```sql ```sql
@@ -499,6 +501,20 @@ RETURNING id;
- Go 后端使用连接池,并在应用启动时通过带超时的 `PingContext` 校验连接 - Go 后端使用连接池,并在应用启动时通过带超时的 `PingContext` 校验连接
- 数据库连接配置仅由 `.env.example` 提供占位符模板,`.env` 文件必须加入 `.gitignore` - 数据库连接配置仅由 `.env.example` 提供占位符模板,`.env` 文件必须加入 `.gitignore`
### 6.5 CSV 导入导出格式规范
| 规则 | 说明 |
|------|------|
| 编码 | 统一使用 `UTF-8 with BOM``charset=utf-8-sig` |
| 首行 | 标题行,字段名使用 `snake_case` |
| 布尔值 | 导出为 `TRUE` / `FALSE`(大写) |
| 数字 | 整数无小数位,浮点数按实际精度输出 |
| JSON 字段 | 导出为转义后的 JSON 字符串 |
| 空值 | 空字段(无匹配数据)统一使用 `—`(全角破折号) |
| 日期时间 | 格式 `YYYY-MM-DD HH:mm:ss`24小时制) |
| 文件扩展名 | `.csv` |
| 导入校验 | 与对应 API 的校验规则一致,校验失败的行跳过并记录失败原因 |
--- ---
## 7. 日志与错误处理规范 ## 7. 日志与错误处理规范
+30 -9
View File
@@ -151,9 +151,11 @@ CREATE STABLE IF NOT EXISTS computed_data (
"device_id": "device-uuid", "device_id": "device-uuid",
"device_name": "一期曝气柜PLC", "device_name": "一期曝气柜PLC",
"group_name": "曝气池", "group_name": "曝气池",
"latest_value": 2.35, "latest_value": {
"latest_quality": "good", "value": 2.35,
"latest_ts": "2026-07-09T10:00:01+08:00" "quality": "good",
"ts": "2026-07-09T10:00:01+08:00"
}
} }
``` ```
@@ -224,6 +226,9 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"tree": [ "tree": [
{ {
"id": "group-aeration", "id": "group-aeration",
@@ -237,9 +242,11 @@ Response (200)
"data_type": "REAL", "data_type": "REAL",
"device_id": "device-uuid-1", "device_id": "device-uuid-1",
"device_name": "一期曝气柜PLC", "device_name": "一期曝气柜PLC",
"latest_value": 2.35, "latest_value": {
"latest_quality": "good", "value": 2.35,
"latest_ts": "2026-07-09T10:00:01+08:00" "quality": "good",
"ts": "2026-07-09T10:00:01+08:00"
}
}, },
{ {
"id": "point-uuid-2", "id": "point-uuid-2",
@@ -248,9 +255,11 @@ Response (200)
"data_type": "REAL", "data_type": "REAL",
"device_id": "device-uuid-1", "device_id": "device-uuid-1",
"device_name": "一期曝气柜PLC", "device_name": "一期曝气柜PLC",
"latest_value": 25.1, "latest_value": {
"latest_quality": "good", "value": 25.1,
"latest_ts": "2026-07-09T10:00:01+08:00" "quality": "good",
"ts": "2026-07-09T10:00:01+08:00"
}
} }
] ]
}, },
@@ -268,6 +277,7 @@ Response (200)
] ]
} }
] ]
}
} }
``` ```
@@ -305,6 +315,9 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"series": [ "series": [
{ {
"point_id": "point-uuid-1", "point_id": "point-uuid-1",
@@ -329,6 +342,7 @@ Response (200)
] ]
} }
] ]
}
} }
``` ```
@@ -352,6 +366,8 @@ Request body
| end_time | string | 是 | 结束时间 | | end_time | string | 是 | 结束时间 |
| interval_minutes | int | 是 | 间隔分钟数,范围1~1440,必须能被60整除(建议限制为1, 2, 5, 10, 15, 20, 30, 60 | | interval_minutes | int | 是 | 间隔分钟数,范围1~1440,必须能被60整除(建议限制为1, 2, 5, 10, 15, 20, 30, 60 |
> **注意**`interval_minutes` 是表格展示的对齐间隔,与采集点配置的 `history_interval`(存储间隔,见数据管理模块 §3.1.1)相互独立。如果 `interval_minutes` 小于选中点位中最大的 `history_interval`,大部分目标时间点将因无匹配数据而显示为 `—`。建议前端在表格模式下默认将 `interval_minutes` 的最小可选值限制为不小于所有已勾选点位中最大的 `history_interval`,或至少在用户选择过小间隔时给出提示。
**后端逻辑(重点 — 最近邻匹配):** **后端逻辑(重点 — 最近邻匹配):**
``` ```
@@ -379,6 +395,9 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"time_column": [ "time_column": [
"2026-07-09T00:00:00+08:00", "2026-07-09T00:00:00+08:00",
"2026-07-09T00:10:00+08:00", "2026-07-09T00:10:00+08:00",
@@ -416,6 +435,7 @@ Response (200)
] ]
} }
] ]
}
} }
``` ```
@@ -737,6 +757,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme
| H008 | 数据缺失 | 曲线:断线;表格:`—` | | H008 | 数据缺失 | 曲线:断线;表格:`—` |
| H009 | 模式切换 | 保持点位勾选状态和时间范围不变 | | H009 | 模式切换 | 保持点位勾选状态和时间范围不变 |
| H010 | CSV导出 | 使用UTF-8 with BOM编码,与表格显示内容一致 | | H010 | CSV导出 | 使用UTF-8 with BOM编码,与表格显示内容一致 |
| H011 | 查询间隔建议 | 表格查询的 `interval_minutes` 建议不小于选中点位的 `history_interval`,否则可能大量无匹配数据 |
--- ---
+55 -7
View File
@@ -202,6 +202,9 @@ Response (201)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"id": "uuid-string", "id": "uuid-string",
"name": "一期曝气柜PLC", "name": "一期曝气柜PLC",
"protocol_type": "S7", "protocol_type": "S7",
@@ -216,6 +219,7 @@ Response (201)
}, },
"created_at": "2026-07-09T10:00:00+08:00", "created_at": "2026-07-09T10:00:00+08:00",
"updated_at": "2026-07-09T10:00:00+08:00" "updated_at": "2026-07-09T10:00:00+08:00"
}
} }
``` ```
@@ -292,6 +296,9 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"total": 50, "total": 50,
"page": 1, "page": 1,
"page_size": 20, "page_size": 20,
@@ -311,6 +318,7 @@ Response (200)
"updated_at": "..." "updated_at": "..."
} }
] ]
}
} }
``` ```
@@ -520,6 +528,9 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"total": 200, "total": 200,
"page": 1, "page": 1,
"page_size": 20, "page_size": 20,
@@ -546,6 +557,7 @@ Response (200)
"updated_at": "..." "updated_at": "..."
} }
] ]
}
} }
``` ```
@@ -555,6 +567,9 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"groups": [ "groups": [
{ {
"name": "default", "name": "default",
@@ -569,6 +584,7 @@ Response (200)
"count": 15 "count": 15
} }
] ]
}
} }
``` ```
@@ -684,6 +700,9 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"total": 500, "total": 500,
"page": 1, "page": 1,
"page_size": 20, "page_size": 20,
@@ -706,6 +725,7 @@ Response (200)
"created_at": "2026-07-09T10:00:00+08:00" "created_at": "2026-07-09T10:00:00+08:00"
} }
] ]
}
} }
``` ```
@@ -769,12 +789,16 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"id": "uuid", "id": "uuid",
"point_name": "加药泵频率", "point_name": "加药泵频率",
"value": 20.5, "value": 20.5,
"result": "success", "result": "success",
"readback_value": 20.5, "readback_value": 20.5,
"ts": "2026-07-09T10:00:00+08:00" "ts": "2026-07-09T10:00:00+08:00"
}
} }
``` ```
@@ -782,12 +806,33 @@ Response (200)
```json ```json
{ {
"code": 0,
"message": "success",
"data": {
"id": "uuid", "id": "uuid",
"point_name": "加药泵频率", "point_name": "加药泵频率",
"value": 20.5, "value": 20.5,
"result": "failed", "result": "failed",
"error": "回读值不匹配: 期望20.5, 实际18.3", "error": "回读值不匹配: 期望20.5, 实际18.3",
"ts": "2026-07-09T10:00:00+08:00" "ts": "2026-07-09T10:00:00+08:00"
}
}
```
写入超时:
```json
{
"code": 0,
"message": "success",
"data": {
"id": "uuid",
"point_name": "加药泵频率",
"value": 20.5,
"result": "timeout",
"error": "写入操作超时: 设备无响应超过30秒",
"ts": "2026-07-09T10:00:00+08:00"
}
} }
``` ```
@@ -895,13 +940,16 @@ CREATE INDEX idx_write_logs_time ON write_logs(created_at DESC);
### 5.4 质量戳判定规则 ### 5.4 质量戳判定规则
| 条件 | 质量戳 | | 条件 | 质量戳(API层) | 质量戳(存储层 INT) |
|------|--------| |------|----------------|---------------------|
| 协议读取成功,返回有效值 | `good` | | 协议读取成功,返回有效值 | `good` | `0` |
| 协议读取成功,但值为 null 或解析异常 | `bad` | | 协议读取成功,但值为 null 或解析异常 | `bad` | `1` |
| 协议读取失败(超时/无响应/异常) | `bad` | | 协议读取失败(超时/无响应/异常) | `bad` | `1` |
| 设备连接断开 | `bad` | | 设备连接断开 | `bad` | `1` |
| 设备连接断开后,断线期间所有采集点都标记为 `bad` | `bad` | | 设备连接断开后,断线期间所有采集点都标记为 `bad` | `bad` | `1` |
| 查询时间范围内无对应数据记录 | `none` | 不存在(无记录) |
> **质量戳转换约定**TDengine 存储 INT 值(0=good, 1=bad),后端 API 层在返回响应时统一转换为字符串格式。前端始终处理字符串格式的质量戳。`none` 状态仅在查询无匹配记录时由 API 层返回,不会出现在 TDengine 存储中。
### 5.5 断线重连机制 ### 5.5 断线重连机制
@@ -0,0 +1,981 @@
# 三文档一致性与冲突审查报告
## 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-L1051API Handler 直接查询引擎实例并组装状态。
- `code-standards.md` L165-L180Handler → Service → Repository 单向调用。
- `code-standards.md` L184-L203Handler 仅做参数校验和响应转换。
**冲突内容**
运行时状态查询属于业务聚合逻辑,应由 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-L1001TDengine 保存 `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-L471CSV 中用 `—`
**冲突内容**
缺失数据到底是:
- `{ "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-L331bad 示例仍有数值。
- `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-L403400xx=参数、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-L515CSV 标题字段名统一使用 `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-L753API/附录要求最多 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-L669Go 模型使用 `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 使用统一 envelopeCSV 文件流为明确例外。
- 配置 CSV 使用固定 snake_case;历史报表 CSV 允许本地化动态表头。