diff --git a/开发文档/code-standards.md b/开发文档/code-standards.md index 0ee68f9..3b41c89 100644 --- a/开发文档/code-standards.md +++ b/开发文档/code-standards.md @@ -474,6 +474,8 @@ CREATE UNIQUE INDEX idx_devices_name ON devices(name) WHERE deleted = FALSE; | 值 | 统一使用 `DOUBLE`,BOOL 类型存 0/1 | | 标签 | 元数据信息存为 TAGS,不做查询条件的有选择缓存 | +> **质量戳转换约定**:TDengine 存储 INT 值(0=good, 1=bad),后端 API 层在返回响应时统一转换为字符串格式。前端始终处理字符串格式的质量戳。`none` 状态仅在查询无匹配记录时由 API 层返回,不会出现在 TDengine 存储中。 + ### 6.3 SQL 书写规范 ```sql @@ -499,6 +501,20 @@ RETURNING id; - Go 后端使用连接池,并在应用启动时通过带超时的 `PingContext` 校验连接 - 数据库连接配置仅由 `.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. 日志与错误处理规范 diff --git a/开发文档/spec-历史数据.md b/开发文档/spec-历史数据.md index 4d102be..7c03e8c 100644 --- a/开发文档/spec-历史数据.md +++ b/开发文档/spec-历史数据.md @@ -151,9 +151,11 @@ CREATE STABLE IF NOT EXISTS computed_data ( "device_id": "device-uuid", "device_name": "一期曝气柜PLC", "group_name": "曝气池", - "latest_value": 2.35, - "latest_quality": "good", - "latest_ts": "2026-07-09T10:00:01+08:00" + "latest_value": { + "value": 2.35, + "quality": "good", + "ts": "2026-07-09T10:00:01+08:00" + } } ``` @@ -224,50 +226,58 @@ Response (200): ```json { - "tree": [ - { - "id": "group-aeration", - "name": "曝气池", - "type": "group", - "children": [ - { - "id": "point-uuid-1", - "name": "曝气池DO_01", - "type": "collection", - "data_type": "REAL", - "device_id": "device-uuid-1", - "device_name": "一期曝气柜PLC", - "latest_value": 2.35, - "latest_quality": "good", - "latest_ts": "2026-07-09T10:00:01+08:00" - }, - { - "id": "point-uuid-2", - "name": "曝气池温度", - "type": "collection", - "data_type": "REAL", - "device_id": "device-uuid-1", - "device_name": "一期曝气柜PLC", - "latest_value": 25.1, - "latest_quality": "good", - "latest_ts": "2026-07-09T10:00:01+08:00" - } - ] - }, - { - "id": "internal-data", - "name": "内部数据", - "type": "reserved", - "children": [ - { - "id": "placeholder", - "name": "暂无数据", - "type": "placeholder", - "disabled": true - } - ] - } - ] + "code": 0, + "message": "success", + "data": { + "tree": [ + { + "id": "group-aeration", + "name": "曝气池", + "type": "group", + "children": [ + { + "id": "point-uuid-1", + "name": "曝气池DO_01", + "type": "collection", + "data_type": "REAL", + "device_id": "device-uuid-1", + "device_name": "一期曝气柜PLC", + "latest_value": { + "value": 2.35, + "quality": "good", + "ts": "2026-07-09T10:00:01+08:00" + } + }, + { + "id": "point-uuid-2", + "name": "曝气池温度", + "type": "collection", + "data_type": "REAL", + "device_id": "device-uuid-1", + "device_name": "一期曝气柜PLC", + "latest_value": { + "value": 25.1, + "quality": "good", + "ts": "2026-07-09T10:00:01+08:00" + } + } + ] + }, + { + "id": "internal-data", + "name": "内部数据", + "type": "reserved", + "children": [ + { + "id": "placeholder", + "name": "暂无数据", + "type": "placeholder", + "disabled": true + } + ] + } + ] + } } ``` @@ -305,30 +315,34 @@ Response (200): ```json { - "series": [ - { - "point_id": "point-uuid-1", - "point_name": "曝气池DO_01", - "data_type": "REAL", - "data": [ - { "ts": "2026-07-09T00:00:01+08:00", "value": 2.35, "quality": "good" }, - { "ts": "2026-07-09T00:00:02+08:00", "value": 2.36, "quality": "good" }, - { "ts": "2026-07-09T00:00:05+08:00", "value": 2.38, "quality": "good" }, - { "ts": "2026-07-09T00:00:08+08:00", "value": 2.30, "quality": "bad" }, - { "ts": "2026-07-09T00:00:10+08:00", "value": 2.40, "quality": "good" } - ] - }, - { - "point_id": "point-uuid-2", - "point_name": "曝气池温度", - "data_type": "REAL", - "data": [ - { "ts": "2026-07-09T00:00:01+08:00", "value": 25.1, "quality": "good" }, - { "ts": "2026-07-09T00:00:05+08:00", "value": 25.2, "quality": "good" }, - { "ts": "2026-07-09T00:00:10+08:00", "value": 25.0, "quality": "good" } - ] - } - ] + "code": 0, + "message": "success", + "data": { + "series": [ + { + "point_id": "point-uuid-1", + "point_name": "曝气池DO_01", + "data_type": "REAL", + "data": [ + { "ts": "2026-07-09T00:00:01+08:00", "value": 2.35, "quality": "good" }, + { "ts": "2026-07-09T00:00:02+08:00", "value": 2.36, "quality": "good" }, + { "ts": "2026-07-09T00:00:05+08:00", "value": 2.38, "quality": "good" }, + { "ts": "2026-07-09T00:00:08+08:00", "value": 2.30, "quality": "bad" }, + { "ts": "2026-07-09T00:00:10+08:00", "value": 2.40, "quality": "good" } + ] + }, + { + "point_id": "point-uuid-2", + "point_name": "曝气池温度", + "data_type": "REAL", + "data": [ + { "ts": "2026-07-09T00:00:01+08:00", "value": 25.1, "quality": "good" }, + { "ts": "2026-07-09T00:00:05+08:00", "value": 25.2, "quality": "good" }, + { "ts": "2026-07-09T00:00:10+08:00", "value": 25.0, "quality": "good" } + ] + } + ] + } } ``` @@ -352,6 +366,8 @@ Request body: | end_time | string | 是 | 结束时间 | | interval_minutes | int | 是 | 间隔分钟数,范围1~1440,必须能被60整除(建议限制为1, 2, 5, 10, 15, 20, 30, 60) | +> **注意**:`interval_minutes` 是表格展示的对齐间隔,与采集点配置的 `history_interval`(存储间隔,见数据管理模块 §3.1.1)相互独立。如果 `interval_minutes` 小于选中点位中最大的 `history_interval`,大部分目标时间点将因无匹配数据而显示为 `—`。建议前端在表格模式下默认将 `interval_minutes` 的最小可选值限制为不小于所有已勾选点位中最大的 `history_interval`,或至少在用户选择过小间隔时给出提示。 + **后端逻辑(重点 — 最近邻匹配):** ``` @@ -379,43 +395,47 @@ Response (200): ```json { - "time_column": [ - "2026-07-09T00:00:00+08:00", - "2026-07-09T00:10:00+08:00", - "2026-07-09T00:20:00+08:00", - "2026-07-09T00:30:00+08:00", - "2026-07-09T00:40:00+08:00", - "2026-07-09T00:50:00+08:00", - "2026-07-09T01:00:00+08:00" - ], - "columns": [ - { - "point_id": "point-uuid-1", - "point_name": "曝气池DO_01", - "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" } - ] - } - ] + "code": 0, + "message": "success", + "data": { + "time_column": [ + "2026-07-09T00:00:00+08:00", + "2026-07-09T00:10:00+08:00", + "2026-07-09T00:20:00+08:00", + "2026-07-09T00:30:00+08:00", + "2026-07-09T00:40:00+08:00", + "2026-07-09T00:50:00+08:00", + "2026-07-09T01:00:00+08:00" + ], + "columns": [ + { + "point_id": "point-uuid-1", + "point_name": "曝气池DO_01", + "data": [ + { "value": 2.35, "quality": "good" }, + { "value": 2.40, "quality": "good" }, + null, + { "value": 2.38, "quality": "good" }, + { "value": 2.42, "quality": "good" }, + null, + { "value": 2.36, "quality": "good" } + ] + }, + { + "point_id": "point-uuid-2", + "point_name": "曝气池温度", + "data": [ + { "value": 25.1, "quality": "good" }, + { "value": 25.0, "quality": "good" }, + { "value": 25.3, "quality": "good" }, + null, + { "value": 25.1, "quality": "good" }, + { "value": 25.2, "quality": "good" }, + { "value": 25.0, "quality": "good" } + ] + } + ] + } } ``` @@ -737,6 +757,7 @@ p = sum( previous_segment_heights ) + (v - segment_start) / (segment_end - segme | H008 | 数据缺失 | 曲线:断线;表格:`—` | | H009 | 模式切换 | 保持点位勾选状态和时间范围不变 | | H010 | CSV导出 | 使用UTF-8 with BOM编码,与表格显示内容一致 | +| H011 | 查询间隔建议 | 表格查询的 `interval_minutes` 建议不小于选中点位的 `history_interval`,否则可能大量无匹配数据 | --- diff --git a/开发文档/spec-数据管理.md b/开发文档/spec-数据管理.md index b8ec2d1..191c985 100644 --- a/开发文档/spec-数据管理.md +++ b/开发文档/spec-数据管理.md @@ -202,20 +202,24 @@ Response (201): ```json { - "id": "uuid-string", - "name": "一期曝气柜PLC", - "protocol_type": "S7", - "enabled": true, - "host": "192.168.1.100", - "port": 102, - "connect_timeout": 5, - "reconnect_interval": 10, - "protocol_config": { - "rack": 0, - "slot": 1 - }, - "created_at": "2026-07-09T10:00:00+08:00", - "updated_at": "2026-07-09T10:00:00+08:00" + "code": 0, + "message": "success", + "data": { + "id": "uuid-string", + "name": "一期曝气柜PLC", + "protocol_type": "S7", + "enabled": true, + "host": "192.168.1.100", + "port": 102, + "connect_timeout": 5, + "reconnect_interval": 10, + "protocol_config": { + "rack": 0, + "slot": 1 + }, + "created_at": "2026-07-09T10:00:00+08:00", + "updated_at": "2026-07-09T10:00:00+08:00" + } } ``` @@ -292,25 +296,29 @@ Response (200): ```json { - "total": 50, - "page": 1, - "page_size": 20, - "items": [ - { - "id": "uuid", - "name": "一期曝气柜PLC", - "protocol_type": "S7", - "enabled": true, - "host": "192.168.1.100", - "port": 102, - "connect_timeout": 5, - "reconnect_interval": 10, - "protocol_config": { "rack": 0, "slot": 1 }, - "connection_status": "connected", - "created_at": "...", - "updated_at": "..." - } - ] + "code": 0, + "message": "success", + "data": { + "total": 50, + "page": 1, + "page_size": 20, + "items": [ + { + "id": "uuid", + "name": "一期曝气柜PLC", + "protocol_type": "S7", + "enabled": true, + "host": "192.168.1.100", + "port": 102, + "connect_timeout": 5, + "reconnect_interval": 10, + "protocol_config": { "rack": 0, "slot": 1 }, + "connection_status": "connected", + "created_at": "...", + "updated_at": "..." + } + ] + } } ``` @@ -520,32 +528,36 @@ Response (200): ```json { - "total": 200, - "page": 1, - "page_size": 20, - "items": [ - { - "id": "uuid", - "name": "曝气池DO_01", - "group_name": "曝气池", - "device_id": "uuid", - "device_name": "一期曝气柜PLC", - "protocol_type": "S7", - "address": "DB2.10", - "data_type": "REAL", - "collect_interval": 1, - "store_history": true, - "history_interval": 1, - "enabled": true, - "latest_value": { - "value": 2.35, - "quality": "good", - "ts": "2026-07-09T10:00:01+08:00" - }, - "created_at": "...", - "updated_at": "..." - } - ] + "code": 0, + "message": "success", + "data": { + "total": 200, + "page": 1, + "page_size": 20, + "items": [ + { + "id": "uuid", + "name": "曝气池DO_01", + "group_name": "曝气池", + "device_id": "uuid", + "device_name": "一期曝气柜PLC", + "protocol_type": "S7", + "address": "DB2.10", + "data_type": "REAL", + "collect_interval": 1, + "store_history": true, + "history_interval": 1, + "enabled": true, + "latest_value": { + "value": 2.35, + "quality": "good", + "ts": "2026-07-09T10:00:01+08:00" + }, + "created_at": "...", + "updated_at": "..." + } + ] + } } ``` @@ -555,20 +567,24 @@ Response (200): ```json { - "groups": [ - { - "name": "default", - "count": 10 - }, - { - "name": "曝气池", - "count": 25 - }, - { - "name": "加药间", - "count": 15 - } - ] + "code": 0, + "message": "success", + "data": { + "groups": [ + { + "name": "default", + "count": 10 + }, + { + "name": "曝气池", + "count": 25 + }, + { + "name": "加药间", + "count": 15 + } + ] + } } ``` @@ -684,28 +700,32 @@ Response (200): ```json { - "total": 500, - "page": 1, - "page_size": 20, - "items": [ - { - "id": "uuid", - "point_id": "uuid", - "point_name": "加药泵频率", - "device_id": "uuid", - "device_name": "一期加药间PLC", - "address": "DB2.10", - "data_type": "REAL", - "source": "manual", - "target_value": "20.5", - "readback_value": "20.5", - "result": "success", - "error_message": null, - "operator": "张三", - "reason": "AI推荐加药量调整", - "created_at": "2026-07-09T10:00:00+08:00" - } - ] + "code": 0, + "message": "success", + "data": { + "total": 500, + "page": 1, + "page_size": 20, + "items": [ + { + "id": "uuid", + "point_id": "uuid", + "point_name": "加药泵频率", + "device_id": "uuid", + "device_name": "一期加药间PLC", + "address": "DB2.10", + "data_type": "REAL", + "source": "manual", + "target_value": "20.5", + "readback_value": "20.5", + "result": "success", + "error_message": null, + "operator": "张三", + "reason": "AI推荐加药量调整", + "created_at": "2026-07-09T10:00:00+08:00" + } + ] + } } ``` @@ -769,12 +789,16 @@ Response (200): ```json { - "id": "uuid", - "point_name": "加药泵频率", - "value": 20.5, - "result": "success", - "readback_value": 20.5, - "ts": "2026-07-09T10:00:00+08:00" + "code": 0, + "message": "success", + "data": { + "id": "uuid", + "point_name": "加药泵频率", + "value": 20.5, + "result": "success", + "readback_value": 20.5, + "ts": "2026-07-09T10:00:00+08:00" + } } ``` @@ -782,12 +806,33 @@ Response (200): ```json { - "id": "uuid", - "point_name": "加药泵频率", - "value": 20.5, - "result": "failed", - "error": "回读值不匹配: 期望20.5, 实际18.3", - "ts": "2026-07-09T10:00:00+08:00" + "code": 0, + "message": "success", + "data": { + "id": "uuid", + "point_name": "加药泵频率", + "value": 20.5, + "result": "failed", + "error": "回读值不匹配: 期望20.5, 实际18.3", + "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 质量戳判定规则 -| 条件 | 质量戳 | -|------|--------| -| 协议读取成功,返回有效值 | `good` | -| 协议读取成功,但值为 null 或解析异常 | `bad` | -| 协议读取失败(超时/无响应/异常) | `bad` | -| 设备连接断开 | `bad` | -| 设备连接断开后,断线期间所有采集点都标记为 `bad` | `bad` | +| 条件 | 质量戳(API层) | 质量戳(存储层 INT) | +|------|----------------|---------------------| +| 协议读取成功,返回有效值 | `good` | `0` | +| 协议读取成功,但值为 null 或解析异常 | `bad` | `1` | +| 协议读取失败(超时/无响应/异常) | `bad` | `1` | +| 设备连接断开 | `bad` | `1` | +| 设备连接断开后,断线期间所有采集点都标记为 `bad` | `bad` | `1` | +| 查询时间范围内无对应数据记录 | `none` | 不存在(无记录) | + +> **质量戳转换约定**:TDengine 存储 INT 值(0=good, 1=bad),后端 API 层在返回响应时统一转换为字符串格式。前端始终处理字符串格式的质量戳。`none` 状态仅在查询无匹配记录时由 API 层返回,不会出现在 TDengine 存储中。 ### 5.5 断线重连机制 diff --git a/开发文档/三文档一致性与冲突审查报告.md b/开发文档/三文档一致性与冲突审查报告.md new file mode 100644 index 0000000..54d9a49 --- /dev/null +++ b/开发文档/三文档一致性与冲突审查报告.md @@ -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-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_`。 + +--- + +#### 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_`; +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_`。 +- 历史树返回 `history_interval`、`unit`、`archived`、`has_history_data`。 +- 表格间隔采用明确枚举;缺失值统一 `{value:null, quality:"none"}`。 +- 历史查询最大 31 天并强制降采样/最大点数。 +- 写入操作者从认证上下文生成,失败使用非零业务码。 +- JSON API 使用统一 envelope;CSV 文件流为明确例外。 +- 配置 CSV 使用固定 snake_case;历史报表 CSV 允许本地化动态表头。