Files
hepu/docs/contract/6_J-00_接入接口规格_V1.8_OpenAPI.yaml
T
aomin a7605ef8b8 chore(repo): 导入 M-Z 业务底座 V1.9 基线并建立仓库规范
按周工交付的 SEC-Z-01 清理后源码建立 M-Z 主干,并按 Git 仓库惯例重排目录。

- aomin-gateway/  后端工程 87 文件,原样保留,路径与构建命令未变
- demo/           演示前端与设备模拟器 9 文件(原 02_前端与设备端),非正式三端
- docs/handover/  交付与安全清理材料 6 份,内容未改
- docs/contract/  J-00 规格、OpenAPI、内部五接口、状态机、Mock 样例
- docs/reference/ 指纹与校验清单,含 84 个源码文件的 sha256 清单

目录重排只动位置不动内容:源码树 84 文件哈希与交付一致,逐文件自校验通过;
12 条 Flyway 迁移与交付包内镜像逐字节一致。

未纳入:迁移镜像目录(与工程内一套重复,改留指纹)、target/、证据 raw、
应急恢复脚本、评审用虚构台账数据。

安全:SEC-Z-01 15 处明文已清零,导入前重新扫描无新增命中,本仓库无历史污染。
行尾按交付字节原样保存(.gitattributes 设 * -text),不做归一化以免整树摘要失效。
2026-11-13 17:32:37 +08:00

971 lines
38 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: 鹤仆云 · IoT 接入接口(internal)
version: 1.8.1
description: |
本文件是 `J-00 接入接口规格 V1.7` 的机器可执行版。
**两者关系(不要理解成两份契约):**
- **本文件管「长什么样」**:路径、方法、字段类型、必填、枚举、错误码——工具可执行、可校验、可生成代码。
- **J-00 管「为什么这样」**:业务语义、告警状态机、四条硬边界、幂等三档、断档与 0x04。这些写不进 YAML。
- 二者不一致时,以本文件为准,同时回改 J-00;但**业务语义的解释权仍在 J-00**。
**V1.8.1 变更(2026-11-12):**
- `CommandResult.claimGeneration` 进 `required`、取消 `nullable`;
`nodeId` 补必填语义(与 I-18 落库的 `claimed_node_id` 比对,不符按 `NODE_MISMATCH` 拒)。
- `RejectReason` 补命令侧四个原因码。
**V1.7.2 变更(2026-11-11):**
- `I-18` 响应新增 `claimGeneration`(领取代次),`I-05` 新增同名字段用于回带。
用途:一条命令租约到期被回收重派后,第一个节点的**迟到回执**会覆盖第二次的流转且不报错。
回带代次后服务端能用 `WHERE claim_generation = ?` 挡住它(更新 0 行、计 `duplicated`)。
- **兼容性**:`I-18` 必返;`I-05` 不回带也不会被拒,但等于没有代次校验。**V1.8 起 `I-05` 必填。**
**V1.7.1 变更(2026-11-10,需接入段确认后生效):**
- `Event.rawRef` 由**可选改必填**,并取消「墓碑帧为 `null`」的例外。
墓碑帧同样是独立的一条 I-01 事件(`eventType=MSG_UNDELIVERED`),有自己的原始字节,
摘要填**承载该 `undelivered[]` 的网关上行帧**,多对一共享同一 `rawRef` 是合法的。
墓碑行身份是 `sensorId + seq`,事件幂等键是 `eventId`,都不依赖 `rawRef`,去重逻辑不受影响。
V1.7.1 之前已入库的历史墓碑行保持 `null`,业务层兼容。
**V1.7 变更(对齐《总体架构与 S 线一期实施规范 V1.0 定稿版》):**
1. `protocolVersion` 不再接受 `tcp-1.10`(旧口径已废止)。
2. `seq` 不再是「仅 G 线」——铠湾序列号、bignbiot 帧号等分别映射;没有才传 `null`。
3. `rawRef` 允许 **MD5(32 位)或 SHA-256(64 位)**,但 **I-01 与 I-12 必须同一算法**。
4. `eventType` 枚举外的新值 → 映射 `UNKNOWN` 并填 `payload.rawEventCode`,不静默丢弃。
5. `resultCode` 改为**标准字典值**(`0/1/2/3/4`),厂家原码进新增的可选字段 `rawResultCode`。
6. 命令状态七态英文(`PENDING/CLAIMED/SENT/ACKED/FAILED/TIMEOUT/CANCELLED`),
**领取后是 `CLAIMED`,不是「已下发」**。
**五条不可协商的约束(J-00 §0.1 / §五):**
1. 全部接口由 **IoT 侧发起**,业务层一个出站请求都不发。
2. IoT 侧**不持有数据库账号**,不直连库。
3. IoT 侧**不判设备是否建档**——未建档设备照常上报,由业务层分流。
4. IoT 侧**不判业务策略**,只报亲眼看见的事实。
5. `deviceTime` 没有就传 `null`,**禁止用 `receivedAt` 顶替**。
**编号台账(J-00 §1.1):** 在役 `I-01`/`I-02`/`I-05`/`I-12`/`I-18`;二期预留 `I-17`。
废止永不复用:`I-03`、`I-04`、`I-06`~`I-11`、`I-13`~`I-16`。
servers:
- url: https://{host}/internal/iot/v1
description: 内网地址,由部署时确定。**不走公网、不做域名备案**。
variables:
host:
default: 127.0.0.1:8080
description: 业务层内网入口
tags:
- name: 上行
description: IoT → 业务层,推送设备数据
- name: 下行
description: IoT → 业务层,领取命令与回报结果
- name: 二期预留
description: 本期不实现
security:
- mtls: []
internalToken: []
paths:
/events/batch:
post:
tags: [上行]
operationId: ingestEvents
summary: I-01 设备事件批量上报
description: |
上报「能变成业务事件」的帧。心跳、鉴权帧、解析失败的未知帧**不从这里进**,走 I-12。
**限流**:单实例 200 条/批,1000 条/秒。
**幂等键**:`{sourceChannel}-{批内首条 eventId}-{条数}`。重推同一批必须算出同一个键。
**单条失败不整批回滚**:200 条里 1 条非法,其余 199 条照常入库。
parameters:
- $ref: '#/components/parameters/InternalToken'
- $ref: '#/components/parameters/Sign'
- $ref: '#/components/parameters/Timestamp'
- $ref: '#/components/parameters/Nonce'
- $ref: '#/components/parameters/IdempotencyKeyEvent'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EventBatchRequest'
responses:
'200':
description: 受理完成(含部分失败)。**HTTP 200 只表示受理,不表示处理完**。
content:
application/json:
schema:
$ref: '#/components/schemas/IngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/ServerError'
/device-status/batch:
post:
tags: [上行]
operationId: ingestDeviceStatus
summary: I-02 设备状态快照批量上报
description: |
并发上报「在线/电量/CSQ/最后报文时刻」。业务层按 `sensorId` **覆盖写**(谁最新谁覆盖),
**不做时间排序**,也**不参与告警判定**——只作界面展示与离线判断的原料。
**限流**:500 条/批。
**幂等键**:`{sourceChannel}-{批内首条 sensorId}-{reportedAt}-{条数}`。
⚠️ 注意:**I-02 报文里没有 `eventId`**,不能用它算幂等键。
parameters:
- $ref: '#/components/parameters/InternalToken'
- $ref: '#/components/parameters/Sign'
- $ref: '#/components/parameters/Timestamp'
- $ref: '#/components/parameters/Nonce'
- $ref: '#/components/parameters/IdempotencyKeyStatus'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StatusBatchRequest'
responses:
'200':
description: 受理完成
content:
application/json:
schema:
$ref: '#/components/schemas/IngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/ServerError'
/raw-frames:
post:
tags: [上行]
operationId: ingestRawFrames
summary: I-12 原始报文批量上报
description: |
**全集的口子**——所有上行帧都从这里进一次,其中「能变成业务事件」的那部分另由 I-01 再报一次。
同一帧走两条链路不是重复:一条存档,一条产生业务后果。
**限流**:200 条/批。
**幂等键**:`{sourceChannel}-{批内首条 rawRef}-{条数}`。
⚠️ 注意:**I-12 报文里没有 `eventId`**;`rawRef` 就是 `MD5(原始帧字节)`,重推同一帧必然算出同一个值。
parameters:
- $ref: '#/components/parameters/InternalToken'
- $ref: '#/components/parameters/Sign'
- $ref: '#/components/parameters/Timestamp'
- $ref: '#/components/parameters/Nonce'
- $ref: '#/components/parameters/IdempotencyKeyRaw'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RawFrameBatchRequest'
responses:
'200':
description: 受理完成
content:
application/json:
schema:
$ref: '#/components/schemas/IngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/ServerError'
/commands/pending:
get:
tags: [下行]
operationId: pullPendingCommands
summary: I-18 长轮询领取命令
description: |
⚠️ **本接口编号是 I-18**(原 `I-04` 已废止、永不复用)。老文档里的 `I-04` 不要再实现。
**长轮询**:`wait` 是最长挂起秒数,不是轮询间隔。有新命令立即返回,不等满。
服务端用 `DeferredResult` 挂起,**不占线程**。
**`gatewayIds` 只传当前在线的接入端点**——离线端点的命令因此一条都不会被拉走,
天然实现「等上线再发」。IoT 不需要知道这个 id 代表网关还是单品。
**租约**:拉走不等于拿走。业务层返回时置 **`CLAIMED`**、`leaseExpireAt = now + 60s`;
租约内不回报会自动回 `PENDING`,`reclaimCount + 1`。
⚠️ **领取这一步不能写「已下发」**——命令还在 IoT 手里没发出去。`CLAIMED`(领走)与
`SENT`(已发出)是两个状态,混成一个值,租约回收就判不出该不该重派。
**签名注意**:本接口是 GET,无 body —— `X-Sign` 里的 `sha256(body)` 取**空串的 hash**;
参与签名的 `path` **含 query**,`?nodeId=…&wait=30` 一并算进去。
parameters:
- $ref: '#/components/parameters/InternalToken'
- $ref: '#/components/parameters/Sign'
- $ref: '#/components/parameters/Timestamp'
- $ref: '#/components/parameters/Nonce'
- name: nodeId
in: query
required: true
description: IoT 实例标识。多实例时业务层按它记租约。
schema:
type: string
maxLength: 64
- name: gatewayIds
in: query
required: true
description: |
该实例**当前在线**的接入端点,逗号分隔,单次 ≤100。
**离线端点不要传**——传了等于让业务层以为能发。
schema:
type: string
example: OWN_GW_A,OWN_GW_B
- name: wait
in: query
required: false
description: 长轮询最长挂起秒数。0 = 立即返回。默认 30,上限 60。
schema:
type: integer
default: 30
minimum: 0
maximum: 60
- name: limit
in: query
required: false
description: 单次最多返回条数。默认 50,上限 100。
schema:
type: integer
default: 50
minimum: 1
maximum: 100
responses:
'200':
description: 返回命令列表。无命令时 `commands` 为空数组(长轮询到期的情况)。
content:
application/json:
schema:
$ref: '#/components/schemas/PendingResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/ServerError'
/commands/result:
post:
tags: [下行]
operationId: reportCommandResults
summary: I-05 命令结果批量回报
description: |
回报命令执行结果。**`resultCode` 是标准字典值(`0` 成功/`1` 不支持/`2` 参数非法/
`3` 执行失败/`4` 设备忙),不是厂家原码**——厂家原始返回码请放 `rawResultCode`。
把原码直透到标准字段,业务层与前端就得按每个厂家各写一套判断。
**幂等键**:`{nodeId}-{批内首条 commandId}-{条数}`。
**五个 `result` 值的含义(不要合并):**
- `ACKED` 设备有回应,看 `resultCode` 分流
- `SENT` 已发出,本协议无回执通道(S 线裸 TCP)——业务层置 `SENT` + 24h 超时兜底
- `TIMEOUT` 已发出,等不到回执
- `DEVICE_OFFLINE` **没有发出去**(连接不在)
- `FAILED` IoT 自身发送失败(编码/写通道异常)
parameters:
- $ref: '#/components/parameters/InternalToken'
- $ref: '#/components/parameters/Sign'
- $ref: '#/components/parameters/Timestamp'
- $ref: '#/components/parameters/Nonce'
- $ref: '#/components/parameters/IdempotencyKeyResult'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ResultBatchRequest'
responses:
'200':
description: 受理完成
content:
application/json:
schema:
$ref: '#/components/schemas/IngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/ServerError'
/commands/{commandId}/dump:
post:
tags: [二期预留]
operationId: uploadDiagDump
summary: 【二期预留,本期不实现】I-17 诊断包分片上传
description: |
**本期零调用,不要实现。** 一期 `diag_dump` 只留命令字,实际不下发。
等 `set_ota` 与诊断包真正启用时再补。
编号说明:原 `I-13` 在系统设计 V1.4 曾指「网关台账快照」,该功能取消后号一并作废,
诊断包另起 `I-17`,不沿用旧号。
parameters:
- name: commandId
in: path
required: true
schema:
type: string
maxLength: 64
responses:
'404':
description: |
本期不实现:**这条路由没有注册**。
请求由 Spring 默认错误处理返回 404,`body` 是框架的错误结构,
**不是本文档其他接口那种 `{code,msg,data}` 统一信封**,不要按信封解析。
留这一条只是占住 `I-17` 这个号,等二期真做时再补正式响应定义。
components:
securitySchemes:
mtls:
type: mutualTLS
description: 双向证书,90 天轮换。**没有它,其余四层都不必看**。
internalToken:
type: apiKey
in: header
name: X-Internal-Token
description: 固定长随机串,配置中心下发。
parameters:
InternalToken:
name: X-Internal-Token
in: header
required: true
schema:
type: string
Sign:
name: X-Sign
in: header
required: true
description: |
`HMAC-SHA256(token, method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body))`
- `path` **含 query**(GET 时尤其注意)
- GET 无 body 时,`sha256(body)` 取**空串的 hash** `e3b0c442…b855`,不要取字面量 `null`
schema:
type: string
Timestamp:
name: X-Timestamp
in: header
required: true
description: Unix 秒。与服务器偏差 > 300 秒直接拒。
schema:
type: integer
format: int64
Nonce:
name: X-Nonce
in: header
required: true
description: 随机串。Redis 留 600 秒做防重放。
schema:
type: string
maxLength: 64
IdempotencyKeyEvent:
name: X-Idempotency-Key
in: header
required: true
description: '`{sourceChannel}-{批内首条 eventId}-{条数}`'
schema:
type: string
example: OWN_GW-3F7A9C21_10231-2
IdempotencyKeyStatus:
name: X-Idempotency-Key
in: header
required: true
description: '`{sourceChannel}-{批内首条 sensorId}-{reportedAt}-{条数}`。**不要用 eventId,I-02 没有这个字段**'
schema:
type: string
example: HORN_CELL-868123456789012-2026-10-29T10:19:22+08:00-500
IdempotencyKeyRaw:
name: X-Idempotency-Key
in: header
required: true
description: '`{sourceChannel}-{批内首条 rawRef}-{条数}`。**不要用 eventId,I-12 没有这个字段**'
schema:
type: string
example: MIR_CELL-9f2c1e7b8a4d6f03c5e9a1b7d4f8c2e6-200
IdempotencyKeyResult:
name: X-Idempotency-Key
in: header
required: true
description: '`{nodeId}-{批内首条 commandId}-{条数}`'
schema:
type: string
example: iot-node-1-CMD-20261030-000123-50
responses:
BadRequest:
description: 请求体不符合本文件定义。**不重试**,字段问题重试也不会变。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: mTLS 失败、token 错、签名不匹配、时间戳超窗、nonce 重放。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
TooManyRequests:
description: 超出限流。**不重试**(重试还是被限),计入死信即可。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
ServerError:
description: 服务端异常。**这是唯一需要重试的**,按 J-00 §1.6 的五档退避。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
SourceChannel:
type: string
description: |
接入通道四值,**不是产品线**。同一户可以存在多个通道的设备。
它决定三件事:`dev_id` 生成规则、`ts_precision` 默认值、原文落哪张表。
⚠️ **它是业务来源,不承担具体物理通路。** 哪套协议、哪个监听口进来,由接入侧的
`linkType` 描述(落 `adapter_in_raw`,不进本信封),两者分列。
例:S 线两套 TCP 同属 `MIR_CELL`,靠 `linkType` 区分,不能靠这个字段猜协议。
enum: [OWN_GW, HORN_HOST, HORN_CELL, MIR_CELL]
TsSource:
type: integer
description: |
时源,**没有默认值,漏传直接拒收**(故意不设 default,让漏赋值当场暴露)。
`0` 设备原始 / `1` 厂家云 / `2` 平台接收 / `3` 网关打戳。
enum: [0, 1, 2, 3]
EventType:
type: string
description: |
29 个值,按家族成对出现。**少一个「解除」,告警就永远不闭环。**
枚举外的新值**映射 `UNKNOWN` 并必填 `payload.rawEventCode`**——不拒收、不静默丢弃。
enum:
# 成对:发生 / 解除
- DOOR_OPEN
- DOOR_CLOSE
- WATER_ACTIVITY
- WATER_ACTIVITY_STOP
- CURRENT_ACTIVITY
- CURRENT_ACTIVITY_STOP
- WATER_LEAK
- WATER_LEAK_CLEAR
- GAS_LEAK
- GAS_LEAK_CLEAR
- SMOKE
- SMOKE_CLEAR
- FALL
- FALL_CLEAR
- SOS
- SOS_CLEAR
- TAMPER
- TAMPER_CLEAR
- POWER_CUT
- POWER_RESTORE
# 单值
- VALVE_CLOSED
- TEMP_HUMIDITY
- DEVICE_ONLINE
- DEVICE_OFFLINE
- LOW_BATTERY
- HEARTBEAT
- PRE_ALARM_0X04
- MSG_UNDELIVERED
- UNKNOWN
EventPayload:
type: object
description: 事件明细。已知字段逐个列;**厂商新增字段原样保留,不丢**(见 `additionalProperties`)。
required: [deviceTypeRaw]
properties:
deviceTypeRaw:
type: integer
description: |
设备原始类型码(G 线 `type` 字节、S 线 `DT`、H 线子设备类型)。
**任何帧都带,含心跳与 UNKNOWN。** 不要在这里做归一化——归一化由业务层按设备类型表做。
rawEventCode:
type: integer
description: 设备原始事件码。**`eventType` 落到 `UNKNOWN` 时必填**,其余情况建议带。
batteryMv:
type: integer
description: 电池电压,**毫伏整数**。不换算成百分比(换算式会变,在业务层)。
rssi:
type: integer
description: 信号强度 dBm。
csq:
type: integer
minimum: 0
maximum: 99
description: 信号质量 0–31,`99` = 未知。
additionalProperties: true
Event:
type: object
required:
- eventId
- sensorId
- sourceChannel
- eventType
- receivedAt
- tsSource
- protocolVersion
- rawRef
- payload
properties:
eventId:
type: string
maxLength: 96
description: |
事件级幂等键,**≤96 字符,不得含平台时间戳**。
算法见 J-00 §2.4 三档:有 `seq` 用 `{sensorId}_{seq}`;有厂商报文 ID 用它;
两者都没有用 `{sensorId}_{sha256(原始帧字节)前16位}`。
**hash 要对原始字节算**,不能对反序列化后再序列化的 JSON 算。
example: 3F7A9C21_10231
sensorId:
type: string
maxLength: 64
description: |
设备物理标识,**必须与 `device.sensor_id` 完全一致(含厂商前缀)**。
H/S 线必带 `HORN:`/`MIR:` 前缀;G 线 8 位 hex 不加前缀。
**这是全篇最容易出错的一个字段**——不一致会导致全场设备判成未建档。
example: 3F7A9C21
sourceChannel:
$ref: '#/components/schemas/SourceChannel'
eventType:
$ref: '#/components/schemas/EventType'
deviceTime:
type: string
format: date-time
nullable: true
description: |
设备侧时间,**没有就传 `null`**。
⚠️ **禁止用 `receivedAt` 顶替**——填了等于伪造时源,界面会把平台收到的时刻当事情发生的时刻。
receivedAt:
type: string
format: date-time
description: 平台接收时刻,ISO-8601 带时区。
example: '2026-10-29T10:19:22+08:00'
tsSource:
$ref: '#/components/schemas/TsSource'
protocolVersion:
type: string
maxLength: 16
description: |
按**具体协议原件**填写,厂商改协议后要能回溯。
例:`gw-1.2`(契约 B)/`WTK-V3.0`(铠湾)/`bignbiot-3.0`/`nb-1.6`(NB&WIFI)。
**豪恩不得再写 `tcp-1.10`**——旧口径已废止。
example: gw-1.2
seq:
type: integer
format: int64
nullable: true
description: |
协议序号。**G 线**取 433 帧 `count`(2 字节);**铠湾 V3.0** 取帧内序列号;
**bignbiot** 取帧号(1 字节 1–255);其他协议取自带序号。
**没有才传 `null`,不得写成「S 线一律 null」**——S 线两套 TCP 都有序号。
payload:
$ref: '#/components/schemas/EventPayload'
rawRef:
type: string
pattern: '^([a-f0-9]{32}|[a-f0-9]{64})$'
description: |
原始报文摘要,回指 I-12 的 `rawRef`。
**MD5(32 位小写 hex)与 SHA-256(64 位小写 hex)都接受**,按长度识别。
**约束只有一条:I-01 与 I-12 用同一种算法**,否则回指链断开。
摘要只用于关联与幂等,不作鉴权。
**必填(V1.7.1 起)。墓碑帧也必填**:填承载该 `undelivered[]` 的网关上行帧摘要。
一条网关报文里的 N 个 `undelivered` → N 条墓碑行共享同一 `rawRef`,多对一合法。
墓碑行身份是 `sensorId + seq`、事件幂等键是 `eventId`,都**不是** `rawRef`,故不影响去重。
V1.7.1 之前已入库的历史墓碑行为 `null`,业务层保持兼容。
example: 9f2c1e7b8a4d6f03c5e9a1b7d4f8c2e6
EventBatchRequest:
type: object
required: [sourceChannel, events]
properties:
sourceChannel:
$ref: '#/components/schemas/SourceChannel'
events:
type: array
maxItems: 200
items:
$ref: '#/components/schemas/Event'
DeviceStatus:
type: object
required: [sensorId, sourceChannel, online, lastSeenAt, protocolVersion, reportedAt]
properties:
sensorId:
type: string
maxLength: 64
sourceChannel:
$ref: '#/components/schemas/SourceChannel'
online:
type: boolean
description: |
只表示「最近是否有报文」,**不表示设备好坏**。
是否「离线告警」由业务层按阈值判定,IoT 不判。
lastSeenAt:
type: string
format: date-time
description: 最后一次收到有效报文的时刻。
batteryMv:
type: integer
nullable: true
description: 毫伏整数。
csq:
type: integer
nullable: true
minimum: 0
maximum: 99
lastSeq:
type: integer
format: int64
nullable: true
description: 仅 G 线,最后收到的 433 序号。
lostCnt:
type: integer
nullable: true
minimum: 0
maximum: 255
description: 仅 G 线,窗口内未送达计数,`255` 表示饱和。
protocolVersion:
type: string
maxLength: 16
reportedAt:
type: string
format: date-time
description: 快照生成时刻。**幂等键要用它**。
StatusBatchRequest:
type: object
required: [sourceChannel, statuses]
properties:
sourceChannel:
$ref: '#/components/schemas/SourceChannel'
statuses:
type: array
maxItems: 500
items:
$ref: '#/components/schemas/DeviceStatus'
RawFrame:
type: object
required: [rawRef, sourceChannel, receivedAt, rawFrame, protocolVersion]
properties:
rawRef:
type: string
pattern: '^[a-f0-9]{32}$'
description: '`MD5(原始帧字节)`,32 位小写 hex。**它就是幂等键**,也是 I-01 里 `rawRef` 回指的目标。'
sensorId:
type: string
maxLength: 64
nullable: true
description: |
解析出设备标识时带上,规则同 I-01。**未建档设备也要带**——有值才追得回是哪个 SN。
实在解析不出可留空,原文照样落库。
sourceChannel:
$ref: '#/components/schemas/SourceChannel'
receivedAt:
type: string
format: date-time
description: 平台收到这一帧的时刻。**不是设备时间。**
rawFrame:
type: string
format: byte
description: |
**Base64,定死这一种表示。**
十六进制会把体积翻倍,而 200 条/批本来就是按字节数撑上限。
`rawRef` 对解码后的字节算——两种表示算出来一样,所以「写 hex 或 Base64 等于没定」。
写 hex 的实现方,MD5 永远对不上业务层。
frameBytes:
type: integer
nullable: true
description: 解码后的字节数。用于自检——**对不上说明 Base64 编码有问题**。
gatewayDevId:
type: string
maxLength: 64
nullable: true
description: 接入端点标识(网关 SN,或独立设备自身标识)。排障时回答「这帧从哪个口进来的」。
protocolVersion:
type: string
maxLength: 16
RawFrameBatchRequest:
type: object
required: [sourceChannel, frames]
properties:
sourceChannel:
$ref: '#/components/schemas/SourceChannel'
frames:
type: array
maxItems: 200
items:
$ref: '#/components/schemas/RawFrame'
DeliveryMode:
type: string
enum: [REALTIME, DEFERRED]
description: |
由业务层在创建命令时定,IoT 不判。
`REALTIME` 实时命令(布撤防、重启):设备离线必须当场失败,不能「等上线再发」。
`DEFERRED` 可延迟命令(配置下发):失败可重排。
CmdType:
type: string
description: 下行命令字 C1–C8。**IoT 只负责编码下发,不解释语义。**
enum: [set_mode, set_ack_policy, time_sync, set_report_cfg, del_sensor, reboot, diag_dump, set_ota]
Command:
type: object
required: [commandId, gatewayId, targetId, cmdType, deliveryMode, leaseExpireAt, expireAt, retryCount]
properties:
commandId:
type: string
maxLength: 64
description: 命令唯一标识。**IoT 与网关都要按它去重**——租约回收可能导致同一条被下发两次。
example: CMD-20261030-000123
gatewayId:
type: string
maxLength: 64
description: |
**「这条命令从哪个端点进去」,不是「网关 SN」。**
连网关的哨兵填网关 SN;独立设备填设备自身标识;H 线填厂家云节点标识。
IoT 拿它去自己的连接表里查,查不到报 `DEVICE_OFFLINE`。
targetId:
type: string
maxLength: 64
description: 目标设备标识。路由在业务层写指令时就算完了,IoT 不需要知道它与 `gatewayId` 的关系。
cmdType:
$ref: '#/components/schemas/CmdType'
deliveryMode:
$ref: '#/components/schemas/DeliveryMode'
params:
type: object
nullable: true
additionalProperties: true
leaseExpireAt:
type: string
format: date-time
description: 租约到期时刻(拉走 + 60s)。到期不回报会自动回「待下发」,`reclaimCount + 1`。
expireAt:
type: string
format: date-time
description: 命令最终有效期。过期即终态「已过期」。
retryCount:
type: integer
description: 设备侧已失败次数。**与 `reclaimCount` 分开记**——混成一个就分不清是设备坏了还是中间件坏了。
claimGeneration:
type: integer
minimum: 1
description: |
**领取代次(V1.7.2 新增):这条命令被领走第几次。** 每次 `PENDING → CLAIMED` 时 +1。
IoT 必须原样保存,并在 I-05 里回带。用途:一条命令租约到期被回收重派后,
第一个节点的迟到回执会覆盖第二次的流转且不报错——回带代次后服务端能把它挡住。
不回带也不会被拒(兼容未升级的接入端),但等于没有代次校验。
example: 1
PendingResponse:
type: object
required: [nodeId, commands, hasMore]
properties:
nodeId:
type: string
commands:
type: array
items:
$ref: '#/components/schemas/Command'
hasMore:
type: boolean
description: 还有更多(本次达到 `limit` 上限)。IoT 可立即再拉一次。
CommandResult:
type: object
required: [commandId, result, claimGeneration]
properties:
commandId:
type: string
maxLength: 64
result:
type: string
enum: [ACKED, SENT, TIMEOUT, DEVICE_OFFLINE, FAILED]
description: |
- `ACKED` 设备有回应(看 `resultCode`)
- `SENT` 已发出,本协议无回执通道
- `TIMEOUT` 已发出,等不到回执
- `DEVICE_OFFLINE` **没有发出去**
- `FAILED` IoT 自身发送失败
resultCode:
type: string
enum: ['0', '1', '2', '3', '4']
nullable: true
description: |
**标准字典值,由 IoT 翻译后填,不是厂家原码。**
`0` 成功/`1` 不支持/`2` 参数非法/`3` 执行失败/`4` 设备忙。
厂家原始返回码请放 `rawResultCode`——直透到标准字段,业务层与前端就得按每个厂家
各写一套判断,等于把适配器的活摊到全平台。
过渡期说明:服务端按字符串接收,**不因非标准值拒整批**(避免联调期因为一个码值
整批 400),非标准值会记日志并在 `rawResultCode` 为空时兜底采用。
rawResultCode:
type: string
maxLength: 32
nullable: true
description: |
厂家原始返回码,**原样字符串,不解释**。
V1.7 新增的**可选**字段:不填也能跑(`null` 时业务层只看 `resultCode`)。
resultMsg:
type: string
maxLength: 256
nullable: true
ackAt:
type: string
format: date-time
nullable: true
description: 收到回执的时刻。`SENT`/`DEVICE_OFFLINE`/`FAILED` 时为 `null`。
rawAck:
type: string
maxLength: 512
nullable: true
description: 回执帧原样(hex)。排障用。
claimGeneration:
type: integer
minimum: 1
description: |
领取时 I-18 给的 `claimGeneration`,**原样回带。V1.8 起 required,不可为空。**
服务端用它当 `WHERE claim_generation = ?` 的条件,挡住旧代次的迟到回执
(更新 0 行、计 `duplicated`)。**缺失直接拒**,原因 `CLAIM_GENERATION_REQUIRED`——
旧实现"缺了就拿库里当前值顶上",等于让代次校验自己失效,已取消。
与 `nodeId` 一起构成一次领取的唯一事实:
`claim_generation` 回答"第几轮",`nodeId` 回答"这一轮是谁"。
example: 1
ResultBatchRequest:
type: object
required: [nodeId, results]
properties:
nodeId:
type: string
maxLength: 64
description: |
**来源节点标识,必填。** 必须与 I-18 领取时落库的 `claimed_node_id` 一致,
否则整条回执按 `NODE_MISMATCH` 拒收;I-18 时未落节点的历史行按 `NODE_UNKNOWN` 拒收。
**作用边界**:它不是身份认证——值由 IoT 自报,同样可以伪造;
"调用方是谁"由内网来源白名单 + 签名保证。这里校验的是**领取事实的一致性**:
这条结果是不是领走这一轮的那个节点回的。
results:
type: array
maxItems: 50
items:
$ref: '#/components/schemas/CommandResult'
RejectReason:
type: string
enum: [RATE_LIMITED, SCHEMA_INVALID, UNKNOWN_CHANNEL, SENSOR_ID_INVALID,
COMMAND_NOT_FOUND, CLAIM_GENERATION_REQUIRED, NODE_MISMATCH, NODE_UNKNOWN]
description: |
前四个用于 I-01/I-02/I-12(事件侧),后四个用于 I-05(命令回执侧):
`COMMAND_NOT_FOUND` 命令号不存在;`CLAIM_GENERATION_REQUIRED` 缺 `claimGeneration`;
`NODE_MISMATCH` 回执节点与领取节点不符;`NODE_UNKNOWN` 历史在途行没有领取节点可核。
**不要按原因做分支。** 收到 `rejectedDetail` 一律**不重试**,落死信 + 计数即可。
唯一例外是 `NODE_UNKNOWN`:那条命令会在 60 秒租约到期后被回收重派,重领后节点列即有值。
需要重试的只有整批失败(网络、5xx)。
RejectedItem:
type: object
required: [reason]
properties:
eventId:
type: string
nullable: true
description: 事件类批次用 `eventId` 定位。
rawRef:
type: string
nullable: true
description: 原文类批次用 `rawRef` 定位。
sensorId:
type: string
nullable: true
description: 状态类批次用 `sensorId` 定位。
commandId:
type: string
nullable: true
description: 结果类批次用 `commandId` 定位。
reason:
$ref: '#/components/schemas/RejectReason'
IngestResponse:
type: object
required: [code, accepted, rejected, duplicated, unregistered]
description: |
**HTTP 200 只表示受理,不表示处理完。**
`unregistered` 是**计数字段,不是错误**——业务层已写进 `unregistered_device`,
接入侧不做任何动作:不重报、不换接口、不缓存。
properties:
code:
type: integer
description: 0 = 受理完成。非 0 见 `message`。
example: 0
accepted:
type: integer
description: 成功入库条数。
example: 198
rejected:
type: integer
description: 拒收条数。
example: 2
duplicated:
type: integer
description: 命中幂等跳过的条数(重推)。
example: 3
unregistered:
type: integer
description: 未建档设备条数。**不是错误。**
example: 5
rejectedDetail:
type: array
items:
$ref: '#/components/schemas/RejectedItem'
ErrorResponse:
type: object
required: [code, message]
properties:
code:
type: integer
description: |
错误码。**本文件暂不固定具体码值**——待与业务层错误码表统一后回填,
当前按 HTTP 状态码区段理解:400xx 请求非法、401xx 鉴权失败、429xx 限流。
message:
type: string
description: 人读的错误说明。**不要用它做分支判断**,用 `code`。