Files
2026-07-13 14:31:49 +08:00

51 KiB
Raw Permalink Blame History

数据管理模块 — 开发规格说明

本文档属于《污水厂智能控制平台》的一部分,详细描述数据管理模块的功能、数据模型、API接口和业务逻辑,细度可达直接开发级别。

跨文档契约发生冲突时,必须遵循《一致性决策基线》;该文件的已采纳决策优先于本文旧表述。


目录


1. 概述

1.1 模块定位

本模块是平台的数据基础层,负责:

  • 管理PLC设备及其连接配置
  • 定义数据采集点位(从PLC读取数据)
  • 定义数据写入点位(向PLC写入控制指令)
  • 运行时执行数据采集任务,将数据写入TDengine
  • 运行时执行控制指令下发任务

1.2 模块边界

交互对象 方向 内容
Web前端 双向 设备/点位CRUD、实时数据展示、控制指令下发
TDengine 单向写入 采集到的时序数据
PostgreSQL 双向 设备配置、点位配置、操作日志
PLC设备 双向 读取寄存器、写入寄存器

1.3 核心概念关系

设备 (Device)                  ← 一个物理PLC或Modbus设备
 ├── 采集点 (CollectionPoint)     ← 从该设备读取的测点(N个)
 └── 写入点 (WritePoint)       ← 向该设备写入的测点(M个)

采集点 ≠ 写入点,两者分开管理,独立配置。

2. 设备管理

2.1 数据模型

2.1.1 数据库表:devices

CREATE TABLE devices (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name                VARCHAR(128) NOT NULL,
    protocol_type       VARCHAR(32) NOT NULL,
    enabled             BOOLEAN NOT NULL DEFAULT TRUE,
    deleted             BOOLEAN NOT NULL DEFAULT FALSE,
    
    -- 网络连接参数
    host                VARCHAR(256) NOT NULL,
    port                INTEGER NOT NULL,
    connect_timeout     INTEGER NOT NULL DEFAULT 5,       -- 连接超时,单位秒
    reconnect_interval  INTEGER NOT NULL DEFAULT 10,       -- 断线重连间隔,单位秒
    
    -- 协议级配置(JSON格式,由协议驱动自行解析)
    protocol_config     JSONB NOT NULL DEFAULT '{}',
    
    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    created_by          VARCHAR(64),
    updated_by          VARCHAR(64)
);

-- 名称唯一(逻辑删除的记录不参与唯一约束)
CREATE UNIQUE INDEX idx_devices_name ON devices(name) WHERE deleted = FALSE;

2.1.2 协议类型枚举

协议类型标识 说明 当前状态
S7 西门子S7协议 采集、写入和现场联调已支持
MODBUS_TCP Modbus TCP协议 已支持运行时连接、采集和写入
(后续扩展) 新增协议通过插件机制添加 待扩展

2.1.3 各协议 protocol_config 定义

S7协议

{
    "rack": 0,
    "slot": 1
}
字段 类型 必填 说明
rack int PLC机架号,默认0
slot int PLC插槽号,默认1

Modbus TCP协议

{
    "unit_id": 1,
    "float32_order": "ABCD"
}
字段 类型 必填 说明
unit_id int Modbus从站地址,范围1~247
float32_order string REAL 占用两个寄存器时的四字节排列:ABCDBADCCDABDCBA;默认 ABCD

float32_order 直接描述从两个寄存器读取到的四个字节如何重排为 IEEE-754 32 位浮点数,不再叠加第二个 word_order 字段,避免字节交换与字交换职责重叠。INT 类型按标准 16 位寄存器顺序解析。

2.1.4 设备的业务状态

持久化状态(数据库)

状态 说明
enabled=true 设备启用,采集引擎会为该设备建立连接并执行采集任务
enabled=false 设备禁用,采集引擎跳过该设备,已有连接断开
deleted=true 逻辑删除;不再出现在数据管理活动列表或运行时任务中,但保留期内历史数据仍可在历史模块只读查询

运行时连接状态(采集引擎内存维护,不落库)

连接状态 枚举值 说明
连接成功 connected 设备已启用(enabled=true)且与PLC连接成功,可正常采集
连接失败 disconnected 设备已启用(enabled=true)但连接断开或连接失败(初次连接或重连均失败)
连接禁用 disabled 设备未启用(enabled=false),不进行连接。采集引擎不维护该设备的连接状态

说明:

  • connection_status 是运行时状态,由采集引擎维护在内存中,不存储到数据库
  • last_online_atlast_offline_at 也是运行时内存字段,仅在状态实际变化时更新,用于设备列表展示最近在线/离线时间
  • 设备逻辑删除(deleted=true)时,在接口响应中表现为 disabled
  • 采集引擎未运行时,所有 enabled=true 的设备均返回 disconnected

2.2 RESTful API

2.2.1 接口列表

方法 路径 说明
GET /api/v1/devices 获取设备列表(支持分页、搜索、按协议筛选)
POST /api/v1/devices 新增设备
GET /api/v1/devices/{id} 获取单个设备详情
PUT /api/v1/devices/{id} 修改设备
DELETE /api/v1/devices/{id} 逻辑删除设备
POST /api/v1/devices/export 导出设备列表为CSV
POST /api/v1/devices/import 从CSV文件导入设备
GET /api/v1/devices/protocols 获取当前支持的所有协议类型列表

2.2.2 接口详细定义

POST /api/v1/devices — 新增设备

Request body

{
    "name": "一期曝气柜PLC",
    "protocol_type": "S7",
    "host": "192.168.1.100",
    "port": 102,
    "connect_timeout": 5,
    "reconnect_interval": 10,
    "protocol_config": {
        "rack": 0,
        "slot": 1
    }
}

Response (201)

{
    "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"
    }
}

校验规则:

  • name:必填,1~128字符,不可重复(逻辑删除的记录不参与唯一性校验)
  • protocol_type:必填,必须是当前系统支持的协议类型
  • host:必填,合法的IP地址或域名
  • port:必填,1~65535
  • connect_timeout:可选,默认5,范围1~60
  • reconnect_interval:可选,默认10,范围1~3600
  • protocol_config:必填,根据protocol_type不同,校验规则不同

PUT /api/v1/devices/{id} — 修改设备

PUT 使用完整替换语义,以下字段全部必填:

{
    "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
    }
}
  • 校验规则与新增一致,额外要求 enabled 为布尔值。
  • 修改连接参数或协议配置后,Service 保存成功即调用连接管理器使该设备旧连接失效;下一次采集或写入会按新参数重新建立连接。
  • enabled=false 时运行时状态立即进入 disabledenabled=true 后先返回 disconnected,连接成功后变为 connected
  • 采集调度器每秒重新加载活动采集点,点位配置变更通常在下一个调度周期生效。

DELETE /api/v1/devices/{id} — 逻辑删除

  • 将该设备的 deleted 字段设为 true
  • 如果设备正在运行中,采集引擎应立即停止该设备的所有采集任务并断开连接
  • 同时该设备下所有采集点和写入点也一并逻辑删除;已有 TDengine 历史数据不删除,作为归档数据继续只读查询

POST /api/v1/devices/export — 导出设备列表为CSV

Request body

{
    "ids": ["uuid1", "uuid2"]
}

ids 可选;省略时导出全部符合当前权限和逻辑删除规则的设备。

ResponseCSV文件(Content-Type: text/csv; charset=utf-8-sig

CSV格式定义:

name,protocol_type,host,port,connect_timeout,reconnect_interval,protocol_config,enabled
一期曝气柜PLC,S7,192.168.1.100,102,5,10,"{""rack"":0,""slot"":1}",TRUE
二期加药间PLC,MODBUS_TCP,192.168.2.50,502,5,10,"{""unit_id"":1,""float32_order"":""ABCD""}",TRUE

注意:

  • CSV使用UTF-8 with BOM编码
  • 首行为标题行
  • 布尔值导出为 TRUE/FALSE
  • JSON字段导出为转义后的JSON字符串

POST /api/v1/devices/import — 从CSV导入设备

  • Content-Type: multipart/form-data,文件字段名:file
  • 导入逻辑:
    • 以name为唯一标识:如果CSV中的设备名称已存在,则更新该设备;不存在则新增
    • 导入完成后返回处理结果:成功数、失败数、失败原因列表
    • 校验规则与新增接口一致,校验失败的行跳过并记录原因

GET /api/v1/devices — 获取设备列表

Query parameters

参数 类型 必填 说明
page int 页码,默认1
page_size int 每页数量,默认20,最大100
keyword string 搜索关键词(匹配name
protocol_type string 按协议筛选
enabled bool 按启用状态筛选

Response (200)

{
    "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",
                "last_online_at": "2026-07-13T09:30:00+08:00",
                "last_offline_at": null,
                "created_at": "...",
                "updated_at": "..."
            }
        ]
    }
}

connection_status 字段说明:

  • 由设备 Service 通过 DeviceRuntimeStatusProvider 获取运行时状态后组装;Handler 不直接访问采集引擎
  • 枚举值:connecteddisconnecteddisabled
  • enabled=false 时始终为 disabled,无需查询引擎运行时状态
  • enabled=true 时,如果采集引擎未运行或查询不到该设备状态,统一返回 disconnected

GET /api/v1/devices/{id} 返回与列表项相同的完整设备对象;不存在或已逻辑删除时返回 HTTP 404、code=40004

GET /api/v1/devices/protocols 返回注册表中的协议工厂元数据:

{
    "code": 0,
    "message": "success",
    "data": {
        "items": [
            {"protocol_type":"S7","default_port":102,"config_schema":{"type":"object"}},
            {"protocol_type":"MODBUS_TCP","default_port":502,"config_schema":{"type":"object"}}
        ]
    }
}

设备导入结果使用统一结构:

{
    "code": 0,
    "message": "success",
    "data": {
        "total": 10,
        "created": 6,
        "updated": 3,
        "failed": 1,
        "errors": [{"row":8,"field":"host","message":"无效的IP地址或域名"}]
    }
}

设备 CSV 成功时直接返回文件流,不使用 JSON envelope;失败时返回统一 JSON 错误。


3. 数据采集点管理

3.1 数据模型

3.1.1 数据库表:collection_points

CREATE TABLE collection_points (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name                VARCHAR(128) NOT NULL,
    group_name          VARCHAR(64) NOT NULL DEFAULT 'default',
    device_id           UUID NOT NULL REFERENCES devices(id),
    enabled             BOOLEAN NOT NULL DEFAULT TRUE,
    deleted             BOOLEAN NOT NULL DEFAULT FALSE,

    address             VARCHAR(256) NOT NULL,
    data_type           VARCHAR(16) NOT NULL,  -- 'BOOL', 'INT', 'REAL'
    unit                VARCHAR(32),

    collect_interval    INTEGER NOT NULL DEFAULT 1 CHECK (collect_interval >= 1),
    store_history       BOOLEAN NOT NULL DEFAULT TRUE,
    history_interval    INTEGER NOT NULL DEFAULT 1 CHECK (history_interval BETWEEN 1 AND 1440),
    history_started_at  TIMESTAMP WITH TIME ZONE,

    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    created_by          VARCHAR(64),
    updated_by          VARCHAR(64)
);

CREATE UNIQUE INDEX idx_collection_points_name
    ON collection_points(name) WHERE deleted = FALSE;
CREATE INDEX idx_collection_points_device_id
    ON collection_points(device_id) WHERE deleted = FALSE;
CREATE INDEX idx_collection_points_group_name
    ON collection_points(group_name) WHERE deleted = FALSE;

有效最小值/最大值已从采集点配置中移除,采集质量不再执行业务有效范围判断;协议读写成功即为 good,失败才标记 bad

history_started_at 在点位首次准备写入 TDengine 前设置。当前实现允许编辑采集点的 device_iddata_type;历史子表中的 device_namepoint_namedata_type 仍是写入时快照,历史页面展示以 PostgreSQL 当前元数据为准。若业务需要严格保留历史语义,后续应通过新建点位或迁移流程处理。

3.1.1.1 数据库表:collection_groups

CREATE TABLE collection_groups (
    name       VARCHAR(64) PRIMARY KEY,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

collection_groups 是采集点和写入点共享的扁平分组字典。启动迁移会把历史 group_name 同步进该表,并确保 default 分组存在。

3.1.1.2 历史模块只读元数据接口

设备与采集点配置由数据管理模块拥有。历史模块必须调用公开的进程内只读领域接口,不得直接查询 devicescollection_points 表或调用数据管理 Repository。

接口能力:

type HistoryPointMetadataOptions struct {
    IncludeArchived bool
}

ListHistoryPointMetadata(ctx context.Context, opts HistoryPointMetadataOptions) ([]HistoryPointMetadata, error)
GetHistoryPointMetadata(ctx context.Context, pointIDs []uuid.UUID, includeArchived bool) ([]HistoryPointMetadata, error)

稳定 DTO 至少包含:

  • 点位 ID、名称、分组、单位、设备 ID/名称、数据类型;
  • enableddeleted、设备启用/删除状态、store_historyhistory_interval
  • history_started_at
  • 可空 latest_value,结构为 {value, quality, quality_reason, ts}

IncludeArchived=true 时必须返回逻辑删除或禁用但配置记录仍保留的点位。历史模块再结合 TDengine 是否存在记录确定 has_history_datalifecycle_status

3.1.2 各协议的地址格式

S7协议地址格式

地址由协议类型决定解析方式,统一存储在 address 字段中:

地址类型 格式 示例 说明
DB DB{db_number}.{byte_offset}.{bit} DB2.10.0 数据块,DB号2,字节偏移10,第0位
M M{byte_offset}.{bit} M4.0 中间寄存器,字节偏移4,第0位
I I{byte_offset}.{bit} I1.0 输入映像区,字节偏移1,第0位
Q Q{byte_offset}.{bit} Q3.0 输出映像区,字节偏移3,第0位

对于 INT 和 REAL 类型,address 不需要 .bit 部分:

数据类型 示例地址 说明
BOOL DB2.10.0 读DB2的第10字节的第0位
INT DB2.10 读DB2的第10字节开始的2字节(16位整数)
REAL DB2.10 读DB2的第10字节开始的4字节(32位浮点数)
REAL DB2.1186.0 兼容现场 DB 数值地址末尾 .0 的工程记法,实际按字节偏移 1186 连续读取 4 字节
REAL MD540 M 区双字地址,按字节偏移 540 连续读写 4 字节
INT MW540 M 区字地址,按字节偏移 540 连续读写 2 字节

解析规则:

  • 地址格式:{type_prefix}{numbers},其中 numbers 以 . 分隔
  • 对于 BOOL 类型:必须有3段(或2段,M/I/Q类型),如 DB2.10.0M4.0
  • 对于 INT/REAL 类型:BOOL类型的地址通常去掉最后的 .bit 部分,如 DB2.10M4
  • 为兼容现场工程记法,DB 数值地址允许末尾 .0,驱动解析时忽略该尾段;其他非 BOOL 地址不得包含 bit
  • M/Q 区数值地址允许 MW/QW 表示 INTMD/QD 表示 REAL;I 区只读,写入点不得使用 I 区地址
  • S7 REAL 读取按 IEEE-754 大端解析,并统一四舍五入到小数点后最多 3 位

Modbus TCP协议地址格式

地址范围 功能码 对应Modbus寄存器类型 示例
00001~09999 01/05/15 线圈(Coil),可读可写,位类型 00001
10001~19999 02 离散输入(Discrete Input),只读,位类型 10001
30001~39999 04 输入寄存器(Input Register),只读,16位 30001
40001~49999 03/06/16 保持寄存器(Holding Register),可读可写,16位 40001

地址与功能码的映射规则:

地址范围 读取功能码 写入功能码 数据类型限制
00001~09999 01 05(写单个)/15(写多个) 仅BOOL
10001~19999 02 不支持 仅BOOL
30001~39999 04 不支持 INT/REAL
40001~49999 03 06(写单个)/16(写多个) INT/REAL

地址与寄存器地址的换算:

  • 地址 00001 → 协议中的寄存器地址 0
  • 地址 40001 → 协议中的寄存器地址 0
  • 地址 40011 → 协议中的寄存器地址 10
  • 公式:register_address = modbus_address - (address_prefix * 10000 + 1)

对于 BOOL 类型,每个地址对应一个位(线圈/离散输入)。 对于 INT 类型,每个地址对应1个寄存器(16位)。 对于 REAL 类型,每个地址对应2个连续寄存器(32位),读取时从指定地址连续读2个寄存器,按 device 中配置的 float32_order 解析。

3.1.3 分组机制

分组是扁平结构,group_name 字段即分组名称。

  • 默认分组名:default
  • 分组名支持任意字符串(建议不超过64字符)
  • 一个采集点或写入点只能属于一个分组
  • 分组名称保存在 collection_groups 表;采集点和写入点通过各自的 group_name 字段引用
  • 新建分组直接插入 collection_groups
  • 改名分组时,同步更新该分组下未逻辑删除的采集点和写入点
  • 删除非 default 分组时,先将该分组下未逻辑删除的采集点和写入点迁移到 default,再删除分组;不逻辑删除任何点位
  • default 分组不可删除
  • 查询时支持按分组名筛选

3.1.4 实时数据

实时数据不是存储在数据库中的字段,而是采集引擎运行时维护在内存中的最新值。

API 返回采集点时应附带最新采集值;没有缓存时 latest_value 固定为 null

{
    "id": "uuid",
    "name": "曝气池DO_01",
    "latest_value": {
        "value": 2.35,
        "quality": "good",
        "quality_reason": null,
        "ts": "2026-07-09T10:00:01+08:00"
    }
}

3.2 RESTful API

3.2.1 接口列表

方法 路径 说明
GET /api/v1/collection-points 获取采集点列表(支持分页、搜索、按设备/分组/数据类型筛选)
POST /api/v1/collection-points 新增采集点
GET /api/v1/collection-points/{id} 获取单个采集点详情(含实时数据)
PUT /api/v1/collection-points/{id} 修改采集点
DELETE /api/v1/collection-points/{id} 逻辑删除采集点
POST /api/v1/collection-points/export 导出采集点列表为CSV
POST /api/v1/collection-points/import 从CSV文件导入采集点
GET /api/v1/collection-points/groups 获取所有分组列表

3.2.2 接口详细定义

POST /api/v1/collection-points — 新增采集点

Request body

{
    "name": "曝气池DO_01",
    "group_name": "曝气池",
    "device_id": "uuid-of-device",
    "address": "DB2.10",
    "data_type": "REAL",
    "unit": "mg/L",
    "collect_interval": 1,
    "store_history": true,
    "history_interval": 1
}

校验规则:

  • name:必填,全局唯一,1~128字符
  • group_name:可选,默认"default"1~64字符
  • device_id:必填,必须引用一个已存在的设备(且deleted=false
  • address:必填,1~256字符,根据设备协议类型校验格式
  • data_type:必填,枚举值:BOOLINTREAL
  • unit:可选,0~32字符;用于历史表格和图表展示
  • collect_interval:必填,最小值1(秒)
  • store_history:可选,默认true
  • history_interval:当 store_history=true 时必填,必须为 1~1440 的整数(分钟);store_history=false 时可省略并使用默认值 1,若提供仍须符合该范围
  • address 和 data_type 的兼容性校验:
    • S7协议:BOOL类型必须包含bit位(如 DB2.10.0);INT/REAL 类型通常不包含 bit,仅 DB 数值地址兼容末尾 .0
    • Modbus协议:00001/10001 地址只能使用BOOL类型,30001/40001 地址只能使用INT/REAL类型

PUT /api/v1/collection-points/{id} — 修改采集点

PUT 使用完整替换语义,请求字段与新增一致,并额外要求 enabled

{
    "name": "曝气池DO_01",
    "group_name": "曝气池",
    "device_id": "uuid-of-device",
    "enabled": true,
    "address": "DB2.10",
    "data_type": "REAL",
    "unit": "mg/L",
    "collect_interval": 1,
    "store_history": true,
    "history_interval": 5
}
  • 当前实现允许修改 device_iddata_type、名称、分组、地址、单位、采集周期和历史间隔;修改后历史已有数据仍保留在原子表中,展示名称和单位以当前 PostgreSQL 元数据为准。
  • 当前采集调度器每秒重新加载活动采集点;点位新增、修改、删除和启停通常在下一个调度周期生效。

DELETE /api/v1/collection-points/{id} — 逻辑删除

  • 将该采集点的 deleted 字段设为 true
  • 采集引擎应立即停止该点的采集任务

GET /api/v1/collection-points — 获取采集点列表

Query parameters

参数 类型 必填 说明
page int 默认1
page_size int 默认20,最大100
keyword string 搜索name、group_name
device_id string 按设备筛选
group_name string 按分组筛选
data_type string 按数据类型筛选
enabled bool 按启用状态筛选

Response (200)

{
    "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",
                "unit": "mg/L",
                "collect_interval": 1,
                "store_history": true,
                "history_interval": 1,
                "enabled": true,
                "latest_value": {
                    "value": 2.35,
                    "quality": "good",
                    "quality_reason": null,
                    "ts": "2026-07-09T10:00:01+08:00"
                },
                "created_at": "...",
                "updated_at": "..."
            }
        ]
    }
}

GET /api/v1/collection-points/groups — 获取分组列表

Response (200)

{
    "code": 0,
    "message": "success",
    "data": {
        "groups": [
            {
                "name": "default",
                "count": 10,
                "collection_count": 10,
                "write_count": 2
            },
            {
                "name": "曝气池",
                "count": 25,
                "collection_count": 25,
                "write_count": 4
            },
            {
                "name": "加药间",
                "count": 15,
                "collection_count": 15,
                "write_count": 6
            }
        ]
    }
}

POST /api/v1/collection-points/export — 导出CSV

CSV格式定义:

name,group_name,device_name,address,data_type,unit,collect_interval,store_history,history_interval,enabled
曝气池DO_01,曝气池,一期曝气柜PLC,DB2.10,REAL,mg/L,1,TRUE,1,TRUE
进水pH,进水仪表,进水仪表柜PLC,DB1.0,REAL,pH,5,TRUE,5,TRUE
风机运行状态,曝气池,一期曝气柜PLC,M4.0,BOOL,,1,TRUE,1,TRUE

注意:

  • device_name 列关联设备名称,导入时根据设备名称查找对应的 device_id
  • 如果导入时 device_name 找不到对应的设备,则该行导入失败
  • 其他规则与设备CSV导入一致

POST /api/v1/collection-points/import — 从CSV导入

  • 导入逻辑:以 name 为唯一标识,存在则更新,不存在则新增。
  • 先校验 device_name,再按对应协议校验地址和数据类型。
  • 响应沿用设备导入的 {total,created,updated,failed,errors} 结构。

GET /api/v1/collection-points/{id} 返回列表项的完整对象,并包含 history_started_at;无实时缓存时 latest_value=null

采集点 CSV 成功时直接返回文件流,不使用 JSON envelope;失败时返回统一 JSON 错误。


4. 数据写入点管理

4.1 数据模型

4.1.1 数据库表:write_points

CREATE TABLE write_points (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name                VARCHAR(128) NOT NULL,
    group_name          VARCHAR(64) NOT NULL DEFAULT 'default',
    device_id           UUID NOT NULL REFERENCES devices(id),
    write_enabled       BOOLEAN NOT NULL DEFAULT FALSE,
    deleted             BOOLEAN NOT NULL DEFAULT FALSE,

    address             VARCHAR(256) NOT NULL,
    data_type           VARCHAR(16) NOT NULL,  -- 'BOOL', 'INT', 'REAL'
    unit                VARCHAR(32),

    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    created_by          VARCHAR(64),
    updated_by          VARCHAR(64)
);

CREATE UNIQUE INDEX idx_write_points_name
    ON write_points(name) WHERE deleted = FALSE;
CREATE INDEX idx_write_points_device_id
    ON write_points(device_id) WHERE deleted = FALSE;
CREATE INDEX idx_write_points_group_name
    ON write_points(group_name) WHERE deleted = FALSE;

4.1.2 与采集点的区别

维度 采集点 写入点
方向 从PLC读取 向PLC写入
存储历史 支持 不支持,只记录操作日志
周期 周期采集 API 指令触发
写入开关 write_enabled
写入来源 当前阶段仅人工写入,服务端固定为 manual
回读 写入后必须回读验证

4.1.3 地址与回读规则

  • 地址格式与采集点一致,但只允许可写区域:S7 的 DB/M/QModbus 的 Coil/保持寄存器。
  • BOOL、INT 回读必须严格相等。
  • REAL 使用固定协议精度 abs(readback-target) <= 0.0001,该容差不作为写入点配置或 API 字段暴露。
  • 同一设备的采集、写入和回读通过设备连接命令队列串行执行,避免协议客户端并发冲突。

4.2 RESTful API

4.2.1 接口列表

方法 路径 说明
GET /api/v1/write-points 获取写入点列表
POST /api/v1/write-points 新增写入点
GET /api/v1/write-points/{id} 获取单个写入点详情
PUT /api/v1/write-points/{id} 完整修改写入点
DELETE /api/v1/write-points/{id} 逻辑删除
POST /api/v1/write-points/export 导出配置 CSV
POST /api/v1/write-points/import 从配置 CSV 导入
POST /api/v1/write-points/{id}/write 执行人工写入
GET /api/v1/write-logs 查询写入操作日志

4.2.2 写入点 CRUD

新增请求:

{
    "name": "加药泵频率",
    "group_name": "加药间",
    "device_id": "uuid-of-device",
    "write_enabled": false,
    "address": "DB2.10",
    "data_type": "REAL",
    "unit": "Hz"
}
  • POST/PUT 均只使用 write_enabled 作为写入权限开关,不再提供独立 enabled 字段。
  • 名称全局唯一;设备必须存在且未删除;地址必须可写并与数据类型兼容。
  • GET 列表支持 pagepage_sizekeyworddevice_idgroup_namedata_typewrite_enabled
  • DELETE 设置 deleted=true 并立即拒绝新的写入请求。

成功响应中的写入点对象包含上述字段及 iddevice_nameprotocol_typereadback_valuecreated_atupdated_atreadback_value 来自该写入点最近一条写入日志,无记录时为 null

配置 CSV

name,group_name,device_name,address,data_type,unit,write_enabled
加药泵频率,加药间,一期加药间PLC,DB2.10,REAL,Hz,FALSE

导入以 name 为唯一标识,存在则更新;失败结果返回行号、字段和稳定错误原因。写入点 CSV 成功时直接返回文件流,不使用 JSON envelope;失败时返回统一 JSON 错误。

4.2.3 执行写入

POST /api/v1/write-points/{id}/write

{
    "value": 20.5,
    "reason": "工艺调整"
}

校验:

  • BOOL 只接受 JSON booleanINT 只接受整数;REAL 接受 JSON number。
  • reason 可选,最多 500 字符。
  • 请求体不得包含 sourceoperator;服务端固定记录 source=manualoperator=null

执行流程:

  1. 校验点位、设备和 write_enabled
  2. 通过设备级连接队列写入;
  3. 回读并按数据类型验证;不一致时重试一次;
  4. 无论成功、失败或超时都写入 write_logs
  5. 返回稳定业务结果,原始协议错误只写服务端日志。

成功:HTTP 200、code=0

{
    "code": 0,
    "message": "success",
    "data": {
        "write_log_id": "uuid",
        "point_name": "加药泵频率",
        "data_type": "REAL",
        "value": 20.5,
        "readback_value": 20.50001,
        "result": "success",
        "ts": "2026-07-09T10:00:00+08:00"
    }
}

失败:HTTP 502、code=51001;超时:HTTP 504、code=51002。失败响应 data 至少包含 write_log_idresult 和稳定 error_message

4.2.4 写入日志

查询参数:

参数 类型 必填 说明
page int 默认1,最大 page_size=100
point_id UUID 按写入点筛选
device_id UUID 按设备筛选
result string `success
start_time / end_time string ISO 8601,最大跨度31天
keyword string 搜索 point_name

API 根据 data_type 把数据库 TEXT 转换为 JSON boolean/number

{
    "id": "uuid",
    "point_id": "uuid",
    "point_name": "加药泵频率",
    "device_id": "uuid",
    "device_name": "一期加药间PLC",
    "address": "DB2.10",
    "data_type": "REAL",
    "unit": "Hz",
    "source": "manual",
    "target_value": 20.5,
    "readback_value": 20.50001,
    "result": "success",
    "error_message": null,
    "operator": null,
    "reason": "工艺调整",
    "created_at": "2026-07-09T10:00:00+08:00"
}
CREATE TABLE write_logs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    point_id        UUID NOT NULL REFERENCES write_points(id),
    point_name      VARCHAR(128) NOT NULL,
    device_id       UUID NOT NULL REFERENCES devices(id),
    device_name     VARCHAR(128) NOT NULL,
    address         VARCHAR(256) NOT NULL,
    data_type       VARCHAR(16) NOT NULL,
    unit            VARCHAR(32),

    source          VARCHAR(16) NOT NULL DEFAULT 'manual' CHECK (source = 'manual'),
    target_value    TEXT NOT NULL,
    readback_value  TEXT,
    result          VARCHAR(16) NOT NULL CHECK (result IN ('success', 'failed', 'timeout')),
    error_message   TEXT,
    operator        VARCHAR(64),
    reason          VARCHAR(500),
    created_at      TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_write_logs_point_id ON write_logs(point_id);
CREATE INDEX idx_write_logs_device_id ON write_logs(device_id);
CREATE INDEX idx_write_logs_created_at ON write_logs(created_at DESC);

5. 采集引擎(运行时)

5.1 架构概述

采集引擎是嵌入 Web 服务进程的后台常驻组件,负责执行实际的数据采集任务。

运行模型采用单进程嵌入式cmd/server/main.go 是唯一的生产可执行入口。服务启动时初始化采集引擎并启动其后台协程;服务关闭时先停止接收新请求,再停止采集引擎并释放设备连接。采集引擎不作为独立的 cmd/collector 进程运行,因此 API 服务可通过进程内依赖访问其运行时状态。

┌─────────────────────────────────────────────────────┐
│                  采集引擎(Collector Engine          │
│                                                       │
│  ┌──────────────┐  ┌──────────────┐                   │
│  │ 设备连接管理器  │  │ 采集任务调度器  │                 │
│  │ (Connection   │  │ (Scheduler)  │                   │
│  │  Manager)     │  └──────┬───────┘                   │
│  └──────┬───────┘         │                            │
│         │                 │ 有界 worker pool 中的采集任务     │
│         │         ┌───────▼────────┐                   │
│         │         │  采集点执行单元   │                  │
│         │         │  (Point Runner) │                  │
│         │         └───────┬────────┘                   │
│         │                 │                            │
│         │         ┌───────▼────────┐                   │
│         │         │  数据后处理      │                   │
│         │         │  - 质量戳判定    │                   │
│         │         │  - 更新内存缓存  │                   │
│         │         │  - 写入TDengine  │                   │
│         │         └────────────────┘                   │
└─────────────────────────────────────────────────────┘

5.2 启动流程

1. Web 服务完成依赖初始化后启动采集引擎
2. 启动固定数量 worker,并启动每秒一次的调度循环
3. 调度循环每秒从 PostgreSQL 加载所有未删除的采集点及其设备元数据
4. 跳过 `enabled=false` 的采集点;按每个点位的 `collect_interval` 判断是否到期
5. 到期点位投递到有界 worker pool;队列满时记录告警并跳过该次调度
6. worker 执行点位任务时通过连接管理器获取设备连接;连接不存在时按设备配置懒创建
7. 同一设备连接内部串行执行协议读写

5.3 采集任务执行流程

每个点位到期时:
1. 调度器生成任务并投递到有界 worker pool;队列满时记录告警,不无限创建 goroutine。
2. 获取该设备的独立 ProtocolConnection,并进入设备级串行命令队列。
3. 读取和解析:
   - 成功 → value=实际值, quality=good
   - 超时/断线/读取失败/解析失败 → value=null, quality=bad,并填写稳定 quality_reason。
4. 更新内存 latest_values{value, quality, quality_reason, ts}。
5. store_history=true 且到达 history_interval 时写入 TDengine。
6. 首次准备写入历史数据前设置 history_started_at。

5.4 质量戳判定规则

条件 value quality quality_reason TDengine quality
读取、解析成功 实际值 good null 0
协议超时 null bad timeout 1
设备断开 null bad disconnected 1
读取异常 null bad read_error 1
解析异常 null bad parse_error 1
查询目标时间无记录 null none null 不落库

API 始终返回字符串质量戳。none 只表示查询没有匹配记录;不得用 bad 代替缺失,也不得用 0 或上次值填充失败读取。

5.5 断线重连机制

1. 采集任务获取连接或读取失败时,连接管理器将设备标记为 `disconnected`,并记录最近离线时间。
2. 失败点位本次最新值写入内存缓存:`value=null, quality=bad``quality_reason` 按失败阶段记录。
3. 后续点位到期时再次通过连接管理器获取连接;连接不存在或已失效时重新按设备配置建立连接。
4. 任一采集读取成功后调用 `MarkConnected` 恢复设备状态为 `connected`,并仅在状态变化时更新最近在线时间。
5. `reconnect_interval` 作为设备配置保留字段;当前实现没有独立重连计时器,实际重试节奏由点位采集周期和任务调度决定。
6. 设备禁用或逻辑删除后不再执行采集任务。

5.6 动态配置更新

当用户通过API修改设备或采集点配置时,采集引擎需要动态响应:

用户操作 采集引擎响应
新增设备 立即建立连接,启动采集任务
修改设备参数(IP/端口等) 断开旧连接,使用新参数重新连接
删除设备 断开连接,停止所有采集任务
启用/禁用设备 禁用则断开;启用则连接并启动
新增采集点 立即启动该点的采集任务
修改采集点参数(地址/周期等) 停止旧任务,按新参数启动
删除采集点 停止该点的采集任务
启用/禁用采集点 禁用则停止,启用则启动

实现方式:当前实现不使用独立配置事件总线。采集调度器每秒重新读取活动采集点,因此点位配置通常在下一个调度周期生效;设备配置保存成功后立即使旧连接失效,下一次采集或写入按新配置重新建连。

5.7 TDengine 数据写入

5.7.1 超级表定义

CREATE STABLE IF NOT EXISTS collection_data (
    ts              TIMESTAMP,
    value           DOUBLE,           -- 可空;BOOL 使用 0/1
    quality         INT,              -- 0=good, 1=bad
    quality_reason  VARCHAR(32),      -- 可空稳定原因代码
    point_id        VARCHAR(36),
    point_name      VARCHAR(128)
) TAGS (
    device_id       VARCHAR(36),
    device_name     VARCHAR(128),
    data_type       VARCHAR(16)
);

point_iddevice_id 使用 36 位标准 UUID。point_namedevice_namedata_type 是写入/建表时快照;历史 API 的当前显示名称、单位和生命周期状态以 PostgreSQL 元数据为准。

5.7.2 子表创建策略

每个采集点对应一个子表。子表名由已校验 UUID 的规范化小写形式派生:固定前缀 p_ 加上去除连字符后的 32 位十六进制字符串,即 p_<uuid32>。不得接受或使用请求中直接传入的表名。

-- 假设采集点ID为: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
-- 子表名: p_a1b2c3d4e5f64a7b8c9d0e1f2a3b4c5d
CREATE TABLE IF NOT EXISTS p_a1b2c3d4e5f64a7b8c9d0e1f2a3b4c5d
USING collection_data TAGS (
    'd2e3f4a5-b6c7-4d8e-9f01-a2b3c4d5e6f7', '一期曝气柜PLC', 'REAL'
);

5.7.3 写入策略

  • 首次准备历史写入前设置 history_started_at,然后使用 INSERT INTO ... USING ... TAGS 创建/写入子表
  • 当前实现到达 history_interval 后立即写入 TDengine;后续如引入批量队列,必须保证服务关闭前尽力刷新
  • store_history=false 时停止新增历史记录,但已存在的数据仍可在历史模块中查询
  • 写入频率由 history_interval 控制
  • 动态子表名只由已验证 UUID 派生并通过 ^p_[0-9a-f]{32}$ 校验;时间和值参数仍使用参数绑定

5.8 运行时状态查询接口

采集引擎实现只读 DeviceRuntimeStatusProvider,由设备 Service 注入使用;Handler 不直接访问引擎。

type DeviceRuntimeStatusProvider interface {
    GetDeviceStatus(ctx context.Context, deviceID uuid.UUID) ConnectionStatus
    GetDeviceStatuses(ctx context.Context, deviceIDs []uuid.UUID) map[uuid.UUID]ConnectionStatus
}

状态枚举:connected | disconnected | disabled。设备 Service 根据持久化 enabled/deleted 与运行时状态统一计算响应:禁用或删除始终为 disabled;启用但引擎无状态时为 disconnected

采集引擎和最新值缓存均为进程内组件,通过公开接口注入相应 Service;不得由 Handler 获取单例或读取内部 map。


6. 写入引擎(运行时)

6.1 架构概述

写入引擎负责接收人工写入请求、执行写入操作并返回结果。当前阶段未接入身份认证或自动写入;服务端将每次请求记录为 source=manualoperator=null,表示发生了未认证的人工写入请求,而非已识别具体操作者。

写入请求
    │
    ▼
┌─────────────────────┐
│  请求校验             │
│  - 写入点是否存在      │
│  - write_enabled=true │
│  - 值类型校验          │
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  指令执行             │
│  - 获取设备连接        │
│  - 调用协议驱动写入     │
│  - 回读验证            │
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  结果记录             │
│  - 写入 write_logs   │
│  - 返回结果给调用方    │
└─────────────────────┘

6.2 写入安全策略

  1. write_enabled 开关:写入点必须显式开启 write_enabled=true 才能写入,防止误操作
  2. 回读验证BOOL/INT 严格相等;REAL 满足固定协议精度 abs(actual-target) <= 0.0001 才算成功
  3. 操作记录:所有写入操作记录到 write_logs,包含日志 ID、时间、点位、目标值、回读值、结果、失败原因和原因;当前阶段不记录具体操作者
  4. 访问边界:当前阶段未实施身份认证,写入 API 仅应部署在受信任的内部网络;接入身份认证后再补充操作者归属与权限控制

7. 协议插件化架构

7.1 接口定义

协议注册表保存无状态工厂;每个设备创建独立连接实例。

type DataType string

const (
    DataTypeBool DataType = "BOOL"
    DataTypeInt  DataType = "INT"
    DataTypeReal DataType = "REAL"
)

type DeviceConnectionConfig struct {
    DeviceID       uuid.UUID
    Host           string
    Port           int
    ConnectTimeout time.Duration
    ProtocolConfig map[string]any
}

type ProtocolDriverFactory interface {
    ProtocolType() string
    ValidateConfig(config map[string]any) error
    ValidateAddress(address string, dataType DataType, writable bool) error
    ConfigSchema() map[string]any
    NewConnection(ctx context.Context, cfg DeviceConnectionConfig) (ProtocolConnection, error)
}

type ProtocolConnection interface {
    Read(ctx context.Context, address string, dataType DataType) (float64, error)
    Write(ctx context.Context, address string, dataType DataType, value float64) error
    Close() error
}

约束:

  • Factory 必须无连接状态、可并发调用。
  • 每个设备持有一个独立 ProtocolConnection
  • Connection 默认不要求并发安全;ConnectionManager 必须按设备串行调度 Read/Write。
  • 所有调用接收 context.Context,超时和取消由上层控制。
  • Close 必须幂等。

7.2 注册机制

type ProtocolRegistry struct {
    mu        sync.RWMutex
    factories map[string]ProtocolDriverFactory
}

func (r *ProtocolRegistry) Register(factory ProtocolDriverFactory) error {
    protocolType := factory.ProtocolType()
    r.mu.Lock()
    defer r.mu.Unlock()
    if _, exists := r.factories[protocolType]; exists {
        return fmt.Errorf("协议已注册: %s", protocolType)
    }
    r.factories[protocolType] = factory
    return nil
}

func (r *ProtocolRegistry) Get(protocolType string) (ProtocolDriverFactory, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    factory, ok := r.factories[protocolType]
    if !ok {
        return nil, ErrUnsupportedProtocol
    }
    return factory, nil
}

注册错误必须在应用启动阶段暴露并终止启动,不使用隐藏失败的包级可变全局变量。

7.3 扩展新协议

  1. 实现 ProtocolDriverFactory 和设备级 ProtocolConnection
  2. 为配置、地址、读写、超时、取消和并发隔离编写测试;
  3. 在应用装配阶段显式注册;
  4. 前端通过 ConfigSchema() 渲染协议配置表单;
  5. 更新协议列表和配置导入校验。

7.4 当前协议

7.4.1 S7

属性 说明
ProtocolType S7
默认端口 102
连接粒度 每设备一个连接实例
地址解析 见 3.1.2
配置 rackslot
运行时状态 已接入 gos7,现场已完成 DB REAL 采集、MD REAL 写入和回读验证

7.4.2 Modbus TCP

属性 说明
ProtocolType MODBUS_TCP
默认端口 502
连接粒度 每设备一个连接实例
地址解析 见 3.1.2
配置 unit_idfloat32_order,其中 float32_order ∈ {ABCD,BADC,CDAB,DCBA}
运行时状态 已实现 Modbus TCP 客户端;支持线圈、离散输入、输入寄存器、保持寄存器读取,支持线圈、保持寄存器写入,并支持 ABCD/BADC/CDAB/DCBA REAL 字节序

附录A:前端页面结构参考

A.1 页面布局

┌──────────────────────────────────────────┐
│  ┌──────────┐  ┌───────────────────────┐ │
│  │  ▽ 数据管理 │  │                        │ │
│  │    设备管理 │  │      内容区域           │ │
│  │    数据采集 │  │                        │ │
│  │    数据写入 │  │                        │ │
│  └──────────┘  └───────────────────────┘ │
└──────────────────────────────────────────┘

左侧导航栏说明:

  • 导航栏为树形结构,主菜单可展开/收起
  • 当前展开的主菜单:数据管理
  • 展开后显示三个子菜单项:设备管理数据采集数据写入
  • 点击子菜单项,右侧内容区域切换对应页面

A.2 子页面:设备管理

  • 设备列表表格(ID | 名称 | 协议类型 | IP | 端口 | 状态 | 操作)
  • 新增/编辑设备弹窗(动态表单,协议类型切换时protocol_config区域联动变化)
  • 删除确认(二次确认,提示"同时会删除该设备下所有采集点和写入点")
  • 启用/禁用开关
  • 导入/导出CSV按钮

A.3 子页面:数据采集

  • 分组筛选器(横向标签或下拉菜单)
  • 采集点列表表格(名称 | 分组 | 所属设备 | 地址 | 数据类型 | 采集周期 | 存储历史 | 最新值 | 质量戳 | 操作)
  • 新增/编辑采集点弹窗(设备选择后,地址格式提示跟随设备协议变化)
  • 拖拽修改分组(或通过编辑弹窗修改)
  • 导入/导出CSV按钮

A.4 子页面:数据写入

  • 写入点列表表格(名称 | 分组 | 所属设备 | 地址 | 数据类型 | 单位 | 回读容差 | 允许写入 | 操作)
  • 新增/编辑写入点弹窗(配置是否允许写入)
  • 执行写入操作弹窗:输入值和可选原因,点击确认后执行并显示结果
  • 写入操作日志查看(展示未认证人工写入的时间、点位、值、结果和原因)
  • 导入/导出CSV按钮

附录B:关键业务规则一览

编号 规则 说明
R001 设备名称全局唯一 逻辑删除的记录不参与唯一性校验
R002 采集点名称全局唯一 同上
R003 写入点名称全局唯一 同上
R004 逻辑删除级联 删除设备时,该设备下所有采集点和写入点也逻辑删除
R005 采集地址格式校验 根据设备协议类型校验地址格式,创建设备时即确定协议
R006 采集周期最小1秒 不可低于1秒
R007 历史存储间隔范围 1~1440 分钟;当store_history=true时必填
R008 人工写入记录 服务端固定记录 source=manual、operator=null;当前阶段不支持自动写入
R009 写入回读验证 BOOL/INT严格相等;REAL按固定0.0001协议精度判断,不一致重试1次
R010 写入点地址类型限制 必须使用可写地址类型
R011 质量戳自动判定 失败时value=null且bad;读取成功即good;无匹配记录为none
R012 断线恢复 后续采集任务按采集周期重新建连;任一读取成功后恢复在线状态
R013 配置变更动态生效 点位配置由每秒调度加载生效;设备配置保存后旧连接立即失效并在下次使用时重连
R014 历史启动标记 首次准备写入历史前设置history_started_at;当前实现不阻断后续device_id或data_type编辑
R015 协议连接隔离 注册Factory,每个设备独立Connection,同设备读写串行

本文档版本:v1.3 最后更新:2026-07-13