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),不做归一化以免整树摘要失效。
This commit is contained in:
commit
a7605ef8b8
121 files changed
+23629
No files matched your search
@@ -0,0 +1,138 @@
|
||||
# J-00 OpenAPI 机器可读版 · 使用说明
|
||||
|
||||
**文件**:`6_J-00_接入接口规格_V1.8_OpenAPI.yaml`(OpenAPI 3.0.3,v1.8.0)—— **与本说明放在同一目录**,可直接用编辑器或 Swagger Editor 打开
|
||||
**对应**:`6_J-00_接入接口规格_V1.8.md`(同一个契约的人读版)
|
||||
**编制**:周工 | 2026-11-02
|
||||
|
||||
> **为什么文件名这么长**:这是故意的。同一份契约有两个形态——`.md` 讲"为什么",`.yaml` 讲"长什么样"。两个文件共用 `6_J-00_接入接口规格_V1.8` 前缀,在文件夹里**永远相邻**,谁也漏不掉;仅仅一个叫 `internal-iot.yaml` 的文件,脱离上下文就没人知道它是 J-00 的机读版。
|
||||
|
||||
---
|
||||
|
||||
## 一、这是什么
|
||||
|
||||
一份**机器可读的接口契约**。内容和 J-00 V1.8 一一对应,只是换了一种能被工具直接消费的写法。
|
||||
|
||||
**两份文件的分工,不要理解成两份契约:**
|
||||
|
||||
| | 管什么 | 举例 |
|
||||
|---|---|---|
|
||||
| **本文件(YAML)** | **长什么样** | 路径、方法、字段类型、必填、枚举、长度上限、错误结构 |
|
||||
| **J-00 V1.8** | **为什么这样** | 告警状态机、四条硬边界、幂等三档算法、断档与 0x04、`tsSource` 为什么不能伪造 |
|
||||
|
||||
有些东西**写不进 YAML**,比如「`deviceTime` 没有就传 null,禁止用 `receivedAt` 顶替——填了等于伪造时源」。所以:**字段定义以本文件为准,业务语义的解释权仍在 J-00。**
|
||||
|
||||
两者不一致时,以本文件为准,同时回改 J-00,两边同版本号。
|
||||
|
||||
---
|
||||
|
||||
## 二、里面有什么
|
||||
|
||||
| 项 | 数量 | 说明 |
|
||||
|---|---:|---|
|
||||
| 接口 | **6** | 在役 5 个(I-01/02/05/12/18)+ 二期预留 1 个(I-17) |
|
||||
| schema | 20 | Event/DeviceStatus/RawFrame/Command/CommandResult 及其批次包装 |
|
||||
| 公共参数 | 8 | 鉴权头 4 个 + 幂等键 4 个(每接口一个) |
|
||||
| 公共响应 | 4 | 400/401/429/500 |
|
||||
|
||||
**五个在役接口:**
|
||||
|
||||
| 编号 | 方法 | 路径 | `operationId` |
|
||||
|---|---|---|---|
|
||||
| `I-01` | POST | `/events/batch` | `ingestEvents` |
|
||||
| `I-02` | POST | `/device-status/batch` | `ingestDeviceStatus` |
|
||||
| `I-12` | POST | `/raw-frames` | `ingestRawFrames` |
|
||||
| `I-18` | GET | `/commands/pending` | `pullPendingCommands` |
|
||||
| `I-05` | POST | `/commands/result` | `reportCommandResults` |
|
||||
| `I-17` | POST | `/commands/{commandId}/dump` | 二期预留,**本期固定 404,不要实现** |
|
||||
|
||||
**枚举已全部写死,且与 J-00 逐值一致:**
|
||||
|
||||
| 枚举 | 值数 | 内容 |
|
||||
|---|---:|---|
|
||||
| `eventType` | **29** | 10 对发生/解除 + 9 个单值 |
|
||||
| `sourceChannel` | 4 | `OWN_GW`/`HORN_HOST`/`HORN_CELL`/`MIR_CELL` |
|
||||
| `result` | 5 | `ACKED`/`SENT`/`TIMEOUT`/`DEVICE_OFFLINE`/`FAILED` |
|
||||
| `rejectReason` | 4 | `RATE_LIMITED`/`SCHEMA_INVALID`/`UNKNOWN_CHANNEL`/`SENSOR_ID_INVALID` |
|
||||
| `cmdType` | 8 | `set_mode`/`set_ack_policy`/`time_sync`/`set_report_cfg`/`del_sensor`/`reboot`/`diag_dump`/`set_ota` |
|
||||
|
||||
---
|
||||
|
||||
## 三、能拿它做什么(五件事)
|
||||
|
||||
> 下表里的 `$Y` 指本文件名。因为它带中文和版本号,直接敲命令很累,下面给了变量写法,复制即用。
|
||||
|
||||
| 用途 | 怎么用 | 省了什么 |
|
||||
|---|---|---|
|
||||
| **接口校验** | 用 `swagger-cli validate` 或编辑器插件跑一遍 | 字段漏写、枚举越界当场暴露,不用等联调 |
|
||||
| **生成服务端骨架** | `openapi-generator-cli generate -i "$Y" -g spring` | 五个 Controller 的入参出参自动生成,外包只填业务 |
|
||||
| **生成前端类型** | `-g typescript-fetch` 或 `typescript-axios` | 陈工不用照表格手敲 TS 类型 |
|
||||
| **起 Mock 服务** | `prism mock "$Y"` | 前端不用等后端就能联调 |
|
||||
| **生成测试用例** | `-g jmeter` 或 `schemathesis run` | 必填、枚举、边界自动出用例,比手写清单全 |
|
||||
|
||||
**推荐的命令(外包侧):**
|
||||
|
||||
```bash
|
||||
# 文件名太长,先存成变量,后面照抄即可
|
||||
Y="6_J-00_接入接口规格_V1.8_OpenAPI.yaml"
|
||||
|
||||
# 1. 语法与规范校验
|
||||
npx @redocly/cli lint "$Y"
|
||||
|
||||
# 2. 生成 Spring Boot 服务端接口骨架
|
||||
npx @openapitools/openapi-generator-cli generate \
|
||||
-i "$Y" -g spring \
|
||||
-o ./generated --additional-properties=interfaceOnly=true
|
||||
|
||||
# 3. 起 Mock 服务(前端联调用)
|
||||
npx @stoplight/prism-cli mock "$Y" -p 4010
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、与 J-00 V1.8 的逐项核对
|
||||
|
||||
已用脚本比对,以下全部一致:
|
||||
|
||||
| 项 | J-00 位置 | YAML 位置 |
|
||||
|---|---|---|
|
||||
| 五个接口的编号与路径 | §1.1 | `paths` |
|
||||
| `eventType` 29 值 | §1.5.1 | `schemas.EventType.enum` |
|
||||
| 事件对象 12 个字段与必填性 | §1.3 | `schemas.Event` |
|
||||
| I-02 十个字段 | §1.8 | `schemas.DeviceStatus` |
|
||||
| I-12 八个字段(含 `rawFrame` 定死 Base64) | §1.9.2 | `schemas.RawFrame` |
|
||||
| `result` 5 值 | §3.3 | `schemas.CommandResult.result` |
|
||||
| I-18 四个查询参数与上限 | §3.2 | `paths./commands/pending.parameters` |
|
||||
| 鉴权四层 + 幂等键算法 | §1.2 | `components.parameters`(每个接口的幂等键**分别写**,因为取法不同) |
|
||||
| 响应结构与 `unregistered` 非错误语义 | §1.7 | `schemas.IngestResponse` |
|
||||
|
||||
**幂等键按接口分开写,这是 V1.6 的重点修正:**
|
||||
|
||||
| 接口 | 幂等键 | 为什么不能统一 |
|
||||
|---|---|---|
|
||||
| I-01 | `{sourceChannel}-{首条 eventId}-{条数}` | 有 `eventId` |
|
||||
| **I-02** | `{sourceChannel}-{首条 sensorId}-{reportedAt}-{条数}` | **I-02 报文里没有 `eventId`** |
|
||||
| **I-12** | `{sourceChannel}-{首条 rawRef}-{条数}` | **I-12 报文里没有 `eventId`**,`rawRef` 就是 `MD5(原始帧字节)` |
|
||||
| I-05 | `{nodeId}-{首条 commandId}-{条数}` | 有 `commandId` |
|
||||
| I-18 | **不带** | GET 拉取,重复拉天然幂等,靠租约去重 |
|
||||
|
||||
---
|
||||
|
||||
## 五、已知未定项(不藏着)
|
||||
|
||||
| 项 | 现状 | 何时定 |
|
||||
|---|---|---|
|
||||
| **错误响应里的 `code` 具体码值** | 只写了 `{code, message}` 结构,**没固定码值** | 待与业务层错误码表统一后回填。当前按 HTTP 段理解:400xx 请求非法/401xx 鉴权失败/429xx 限流 |
|
||||
| **I-17 的请求体** | 只写了「本期 404」 | 二期启用诊断包时再补 |
|
||||
| **`params` 的结构** | `additionalProperties: true`,随 `cmdType` 变 | 八种命令字的参数结构**不在本文件里**,由业务层命令模块保证;外包按 `cmdType` 做编码即可 |
|
||||
|
||||
---
|
||||
|
||||
## 六、对陈工和外包各说一句
|
||||
|
||||
**给陈工**:这份 YAML 可以生成 TS 类型,`openapi-generator -g typescript-fetch`。你前端**不用照 J-00 的表格手敲字段**,也不用维护第二份接口定义——生成一次即可,接口变了重生成。
|
||||
|
||||
**给外包(谢工)**:五个接口全在这里。各字段的 `description` 是给你的实现提示,重点是三处最容易做错的:
|
||||
|
||||
1. **I-02 / I-12 的幂等键不是 `eventId`**(报文里没这个字段),按对应 `components.parameters` 的说明算。
|
||||
2. **I-18 是 GET,签名时 `sha256(body)` 取空串的 hash,`path` 含 query 一并算。**
|
||||
3. **`rejectedDetail` 里出现的条目一律不重试**,只有整批 5xx 才重试。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,970 @@
|
||||
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`。
|
||||
@@ -0,0 +1,156 @@
|
||||
# Mock 样例(可直接拿去打)
|
||||
|
||||
> 六组报文全部取自 2026-11-09 20:21 的**真实一次运行**,不是手编的示例。
|
||||
> 对应证据:`06_统一闭环题自测证据/05_SOS链路.json`
|
||||
|
||||
---
|
||||
|
||||
## 1. I-01 事件上报 · 请求
|
||||
|
||||
```http
|
||||
POST /internal/iot/v1/events/batch HTTP/1.1
|
||||
Content-Type: application/json
|
||||
X-Idempotency-Key: batch-20261109-2021-001
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceChannel": "OWN_GW",
|
||||
"events": [{
|
||||
"eventId": "EVT-SOS-3B9B2CDB9CD4",
|
||||
"sensorId": "3F7A9C23",
|
||||
"sourceChannel": "OWN_GW",
|
||||
"eventType": "SOS_PRESS",
|
||||
"deviceTime": null,
|
||||
"receivedAt": "2026-11-09T20:21:12+08:00",
|
||||
"tsSource": 3,
|
||||
"protocolVersion": "gw-1.2",
|
||||
"seq": 16872,
|
||||
"payload": {"deviceTypeRaw": 5, "rawEventCode": 1, "batteryMv": 3610},
|
||||
"rawRef": "e1abbc2d6515f118ac9e7e113c68d677"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
**三个刻意的点**
|
||||
|
||||
- `deviceTime: null` —— 433 帧没有时间字段,按 J-00 必须传 null,不许拿 `receivedAt` 顶替
|
||||
- `tsSource: 3` —— 网关打戳,界面不能显示成"发生时间"
|
||||
- `payload.deviceTypeRaw: 5` —— 任何帧都必须带,缺了整条拒收
|
||||
|
||||
## 2. I-01 响应
|
||||
|
||||
```json
|
||||
{"code":0,"msg":"ok","data":{
|
||||
"accepted":1, "rejected":0, "duplicated":0, "unregistered":0, "rejectedDetail":[]}}
|
||||
```
|
||||
|
||||
## 3. I-01 失败样例(四种)
|
||||
|
||||
```jsonc
|
||||
// ① 缺 deviceTypeRaw
|
||||
{"code":0,"data":{"accepted":0,"rejected":1,"rejectedDetail":[
|
||||
{"eventId":"EVT-BAD-01","reason":"SCHEMA_INVALID"}]}}
|
||||
|
||||
// ② SN 与通道不符(OWN_GW 传带前缀的 SN)
|
||||
{"code":0,"data":{"accepted":0,"rejected":1,"rejectedDetail":[
|
||||
{"eventId":"EVT-BAD-02","reason":"SENSOR_ID_INVALID"}]}}
|
||||
|
||||
// ③ tsSource 越界
|
||||
{"code":0,"data":{"accepted":0,"rejected":1,"rejectedDetail":[
|
||||
{"eventId":"EVT-BAD-03","reason":"SCHEMA_INVALID"}]}}
|
||||
|
||||
// ④ 批级通道非法 → 整批 400
|
||||
{"code":400,"msg":"sourceChannel 非法:H_MQTT"}
|
||||
```
|
||||
|
||||
## 4. I-02 设备状态快照
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceChannel": "OWN_GW",
|
||||
"items": [{
|
||||
"sensorId": "3F7A9C23",
|
||||
"sourceChannel": "OWN_GW",
|
||||
"batteryMv": 3610,
|
||||
"csq": 22,
|
||||
"online": true,
|
||||
"reportedAt": "2026-11-09T20:21:10+08:00"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. I-12 原始报文留档
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceChannel": "OWN_GW",
|
||||
"frames": [{
|
||||
"sensorId": "3F7A9C23",
|
||||
"rawRef": "e1abbc2d6515f118ac9e7e113c68d677",
|
||||
"frameHex": "A55A213F7A9C2350100001000000E8B4",
|
||||
"frameLen": 17,
|
||||
"receivedAt": "2026-11-09T20:21:12+08:00"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
> `rawRef` 与 I-01 必须同一种算法(MD5 32 位或 SHA-256 64 位),否则回指链断。
|
||||
|
||||
## 6. I-18 / I-05 下行命令
|
||||
|
||||
> 编号按 J-00 V1.8 §三接口表:领取是 **I-18**、回执是 **I-05**(原写 I-20/I-21,已作废)。
|
||||
|
||||
```jsonc
|
||||
// I-18 拉取(长轮询,wait 最长 30s)
|
||||
GET /internal/iot/v1/commands/pending?nodeId=iot-node-1&wait=30
|
||||
{"code":0,"data":{"nodeId":"iot-node-1","hasMore":false,"commands":[
|
||||
{"commandId":"CMD17942268075877","gatewayId":"GW-0001","targetId":"3F7A9C23",
|
||||
"cmdType":"time_sync","deliveryMode":"REALTIME",
|
||||
"params":{"ts":"2026-11-09T20:21:12+08:00"},
|
||||
"leaseExpireAt":"2026-11-09T20:22:12+08:00","expireAt":"2026-11-12T20:21:12+08:00",
|
||||
"retryCount":0}]}}
|
||||
|
||||
// I-05 回执(result 五值;resultCode 是标准码,不是厂家原码)
|
||||
POST /internal/iot/v1/commands/result
|
||||
{"nodeId":"iot-node-1","results":[
|
||||
{"commandId":"CMD17942268075877","result":"ACKED","resultCode":"0","resultMsg":"OK",
|
||||
"ackAt":"2026-11-09T20:21:14+08:00","rawResultCode":"00"}]}
|
||||
{"code":0,"msg":"ok"}
|
||||
```
|
||||
|
||||
## 7. 服务端推给家属端(SSE)
|
||||
|
||||
```
|
||||
event: alarm
|
||||
data:{"type":"alarm","alarmId":2796,"alarmNo":"AL20261109202112922",
|
||||
"householdId":9001,"level":"S1","tier":"T1","ruleKey":"SOS",
|
||||
"title":"紧急呼叫","content":"老人按下紧急按钮,请立即联系",
|
||||
"deviceName":"床头SOS","deviceSn":"3F7A9C23",
|
||||
"triggerAt":"2026-11-09T20:21:12","delayed":false}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 怎么用这些样例自测
|
||||
|
||||
```bash
|
||||
# 登录拿 token(账号由 DemoDataInitializer 在空库首次启动时生成)
|
||||
curl -X POST http://127.0.0.1:8080/api/v1/auth/wx/password-login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone":"13800000000","password":"123456"}'
|
||||
|
||||
# 打一条 SOS(把上面第 1 节的 body 存成 sos.json)
|
||||
curl -X POST http://127.0.0.1:8080/internal/iot/v1/events/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Idempotency-Key: batch-local-001" \
|
||||
-d @sos.json
|
||||
|
||||
# 查告警是否生成
|
||||
curl "http://127.0.0.1:8080/api/v1/alarms?size=3" -H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
预期:1 秒内家属端 SSE 收到 `event: alarm`,列表首条是刚才那条 SOS。
|
||||
|
||||
> **注意**:`sensorId` 必须已经入库(后台「设备入库」录入或跑 `03` 的虚构台账数据),
|
||||
> 否则会返回 `unregistered`,不建档也不告警 —— 这是 A 方案的刻意设计。
|
||||
@@ -0,0 +1,224 @@
|
||||
# 内部 IoT 五接口定义(实现侧)
|
||||
|
||||
> J-00 V1.8 定义了内部接口契约,本文是**我方实现的落地版**:路径、字段、幂等键、拒绝码,逐条对应代码。
|
||||
> 五个接口,三类上行(I-01 / I-02 / I-12)+ 两类下行(I-18 / I-05)。**业务层不向 IoT 侧发起任何调用**,只有 IoT 调业务层。
|
||||
>
|
||||
> **编号一次说清**:I-01、I-02、I-12、I-18、I-05 就是这五个接口,与 J-00 V1.8 §三接口表逐字一致。
|
||||
> 早期稿把下行两个写成 I-20/I-21,**该编号已作废**,本文与 `Mock样例.md`、`状态机.md` 均已统一。
|
||||
>
|
||||
> **与代码的一致性**:早期稿在这里登记过两条差距,两条都已修掉,本文与代码现在一致:
|
||||
> ① 命令状态值域 —— `V1.0.23__command_seven_states.sql` 已把存量中文六值刷成英文七态(含 `CLAIMED`),代码读写全用英文值;
|
||||
> ② `resultCode` —— `CommandService.standardCode()` 已把上报值收敛到标准码 `0`–`4`,厂家原码另存 `raw_result_code`,不再直透。
|
||||
|
||||
---
|
||||
|
||||
## 0. 公共约束(五个接口全适用)
|
||||
|
||||
| 项 | 约定 |
|
||||
|---|---|
|
||||
| 传输 | HTTP/1.1,`Content-Type: application/json`,**仅内网**(来源 IP 有代码校验,见文末附录) |
|
||||
| 鉴权 | `X-Internal-Token` + `X-Timestamp`(±300s)+ `X-Nonce`(Redis 600s 不复用)+ `X-Sign`(HMAC-SHA256) |
|
||||
| 签名原文 | `token\nmethod\npath\ntimestamp\nnonce\nsha256(body)` |
|
||||
| 幂等 | `X-Idempotency-Key`,同一 key 在 TTL 内返回首次结果 |
|
||||
| 限流 | 通道维度按批次、设备维度按条;超限 429 |
|
||||
| 响应包封 | `{"code":0,"msg":"ok","data":{...}}`,业务失败用 `code!=0` |
|
||||
| 校验顺序 | Token(最便宜)→ 时间戳 → nonce → 签名(最贵),前面挡掉的不付后面代价 |
|
||||
|
||||
> **当前配置** `GATEWAY_AUTH_ENABLED=false`(联调态,签名不校验)。机制完整实现于
|
||||
> `security/SignatureVerifier` + `security/NonceStore` + `interceptor/AuthInterceptor`,
|
||||
> 打开方式:环境变量 `GATEWAY_AUTH_ENABLED=true`,并同步配好 `GATEWAY_TOKEN`。
|
||||
> **必须与 IoT 侧同批切换**,单边打开会导致全部请求 401。
|
||||
|
||||
---
|
||||
|
||||
## I-01 事件批量上报
|
||||
|
||||
```
|
||||
POST /internal/iot/v1/events/batch
|
||||
X-Idempotency-Key: <批次键>
|
||||
```
|
||||
|
||||
**请求**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `sourceChannel` | enum | ✅ | `OWN_GW` / `HORN_HOST` / `HORN_CELL` / `MIR_CELL` |
|
||||
| `events[]` | array | ✅ | ≤200 条 |
|
||||
|
||||
**`events[]` 单条**(字段与 J-00 `Event` 逐项一致,详见 `04` 目录下字段差异表)
|
||||
|
||||
`eventId`(≤96,不含平台时间戳) / `sensorId`(≤64,含厂商前缀) / `sourceChannel` / `eventType` /
|
||||
`deviceTime`(可 null) / `receivedAt`(ISO-8601 带时区) / `tsSource`(0–3) / `protocolVersion` /
|
||||
`seq`(可 null) / `payload`(必须含 `deviceTypeRaw`) / `rawRef`(32 或 64 位小写 hex,可 null)
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{"code":0,"msg":"ok","data":{
|
||||
"accepted":1, "rejected":0, "duplicated":0, "unregistered":0, "rejectedDetail":[]}}
|
||||
```
|
||||
|
||||
四个计数各有含义,**不能只看 accepted**:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `accepted` | 本批收下的条数(含幂等命中,**不等于新建条数**) |
|
||||
| `duplicated` | 事件级幂等命中(同 `event_id` 已存在,未重复落库) |
|
||||
| `unregistered` | 设备未入库:**不建档**,写 `unregistered_device` 待后台确认 |
|
||||
| `rejected` | 条目级校验失败条数,明细在 `rejectedDetail` |
|
||||
|
||||
**拒绝码**
|
||||
|
||||
| 码 | 触发 | HTTP |
|
||||
|---|---|---|
|
||||
| `SCHEMA_INVALID` | 必填缺失 / 长度超限 / `tsSource` 越界 / `rawRef` 格式错 / 缺 `deviceTypeRaw` | 200 内逐条拒 |
|
||||
| `UNKNOWN_CHANNEL` | 通道不在 4 个枚举内(批级错 → 整批 400) | 400 |
|
||||
| `SENSOR_ID_INVALID` | SN 超 64 位,或前缀与通道不符(如 `OWN_GW` 传 `HORN:xxx`) | 200 内逐条拒 |
|
||||
|
||||
---
|
||||
|
||||
## I-02 设备状态快照
|
||||
|
||||
```
|
||||
POST /internal/iot/v1/device-status/batch
|
||||
```
|
||||
|
||||
上报电量、信号、在线状态。字段:`sensorId` / `sourceChannel` / `batteryMv` / `csq` / `online` / `reportedAt`。
|
||||
用于设备列表的「电量 / 信号 / 最近上报」,不产生告警。
|
||||
|
||||
---
|
||||
|
||||
## I-12 原始报文留档
|
||||
|
||||
```
|
||||
POST /internal/iot/v1/raw-frames
|
||||
```
|
||||
|
||||
把原始帧字节摘要与原文留档,供 I-01 的 `rawRef` 回指。
|
||||
`rawRef` 与 I-01 **必须同一种算法**(MD5 32 位或 SHA-256 64 位),否则回指链断开。
|
||||
`sensorId` 允许为空(实在解析不出时),原文照样落库。
|
||||
|
||||
---
|
||||
|
||||
## I-18 待下发命令拉取(长轮询)
|
||||
|
||||
```
|
||||
GET /internal/iot/v1/commands/pending?nodeId=<节点ID>&wait=30
|
||||
```
|
||||
|
||||
> **编号说明**:本节原写作 `I-20`,与 J-00 V1.7 §三接口表的 `I-18` 不一致,现已统一为 **I-18**(I-20/21 作废)。
|
||||
> 参数名也按 J-00 用 `nodeId`,不是 `gwId`。
|
||||
|
||||
网关/单品长轮询取下行命令,返回按创建时间与优先级排序。`wait` 最长 30s(用 `DeferredResult` 挂起,不占线程)。
|
||||
|
||||
```json
|
||||
{"code":0,"msg":"ok","data":{
|
||||
"nodeId":"iot-node-1",
|
||||
"commands":[{
|
||||
"commandId":"CMD-20261030-000123",
|
||||
"gatewayId":"OWN_GW_3F7A9C21",
|
||||
"targetId":"3F7A9C21",
|
||||
"cmdType":"set_mode",
|
||||
"deliveryMode":"REALTIME",
|
||||
"params":{"mode":"ARMED"},
|
||||
"leaseExpireAt":"2026-10-30T08:46:00+08:00",
|
||||
"expireAt":"2026-11-02T08:44:00+08:00",
|
||||
"retryCount":0
|
||||
}],
|
||||
"hasMore":false}}
|
||||
```
|
||||
|
||||
**拉走不等于拿走**:业务层返回命令时同时置 `CLAIMED`、`lease_expire_at = now + 60s`。
|
||||
60 秒没回报 → 回 `PENDING`,`reclaim_count + 1`(`retry_count` 不动)。
|
||||
|
||||
**命令类型 C1–C8**:`set_mode` / `set_ack_policy` / `time_sync` / `set_report_cfg` /
|
||||
`del_sensor` / `reboot` / `diag_dump` / `set_ota`。全部带 `seq`。
|
||||
|
||||
---
|
||||
|
||||
## I-05 命令执行结果回执
|
||||
|
||||
```
|
||||
POST /internal/iot/v1/commands/result
|
||||
```
|
||||
|
||||
> **编号说明**:本节原写作 `I-21`,现已统一为 J-00 V1.7 的 **I-05**。
|
||||
|
||||
```json
|
||||
{"code":0,"msg":"ok","data":{
|
||||
"nodeId":"iot-node-1",
|
||||
"results":[{
|
||||
"commandId":"CMD-20261030-000123",
|
||||
"result":"ACKED",
|
||||
"resultCode":"0",
|
||||
"resultMsg":"OK",
|
||||
"ackAt":"2026-10-30T08:45:03+08:00",
|
||||
"rawResultCode":"00",
|
||||
"rawAck":"020A1F3C…"
|
||||
}]}}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `commandId` | ✅ | I-18 返回的命令 ID |
|
||||
| `result` | ✅ | `ACKED` / `SENT` / `TIMEOUT` / `DEVICE_OFFLINE` / `FAILED`(**五值,见 J-00 §3.3**) |
|
||||
| `resultCode` | ✅ | **标准字典值**:`0` 成功/`1` 不支持/`2` 参数非法/`3` 执行失败/`4` 设备忙 |
|
||||
| `resultMsg` | | 可读说明 |
|
||||
| `ackAt` | | 设备侧回执时刻 |
|
||||
| `rawResultCode` | | **厂家原始码,原样字符串,不解释**(V1.7 新增可选) |
|
||||
| `rawAck` | | 回执原始字节(Base64,可选) |
|
||||
|
||||
**`result` 与 `resultCode` 的关系(写死,避免误读)**:
|
||||
|
||||
| `result` | 含义 | 业务层处置 |
|
||||
|---|---|---|
|
||||
| `ACKED` | **设备有回应**(收到回执帧),**不代表执行成功** | 看 `resultCode`:`0` → `ACKED`;非 `0` → `FAILED` |
|
||||
| `SENT` | 已发出,本协议无回执通道 | 置 `SENT`,`timeout_at = now+24h` |
|
||||
| `TIMEOUT` | 已发出,等不到回执 | 实时=失败;可延迟=重排 |
|
||||
| `DEVICE_OFFLINE` | **没有发出去**(连接不在) | 实时=失败;可延迟=重排 |
|
||||
| `FAILED` | IoT 自身发送失败 | 同上 |
|
||||
|
||||
> **`resultCode` 不得直透厂家原码**。厂家原码各不一样(铠湾 `08` 是水浸、bignbiot `08` 是白蚁,命令侧同样有这种坑),
|
||||
> 直透到标准列以后,业务层和前端就得按每个厂家各写一套判断,等于把适配器的活摊到全平台。厂家原码只能进 `rawResultCode`。
|
||||
|
||||
回执落 `device_command.status`(七态见 `状态机.md` §二),后台命令记录页可见。
|
||||
|
||||
---
|
||||
|
||||
## 与 J-00 的关系
|
||||
|
||||
| 项 | 结论 |
|
||||
|---|---|
|
||||
| 字段命名 | 11 个字段与 J-00 `Event` schema **完全一致**,0 处改名 |
|
||||
| 枚举 | `sourceChannel` 4 值、`tsSource` 4 值完全一致 |
|
||||
| 接口编号 | **I-01 / I-02 / I-12 / I-18 / I-05**,与 J-00 §三逐字一致 |
|
||||
| 事件类型 | J-00 29 个值全部接受;额外兼容 3 个别名(`SOS_PRESS` / `POWER_OFF` / `POWER_ON`),建议对外以 J-00 为准 |
|
||||
| 命令状态 | 契约七态(J-00 §3.4),**代码同值**;存量中文六值已由 `V1.0.23` 刷平,库里无残留 |
|
||||
| 命令结果码 | 契约 `resultCode` 标准码 `0`–`4`,**代码已按此收敛**(`standardCode()`),厂家原码落 `raw_result_code` 不参与判断 |
|
||||
| 回执来源节点 | I-05 顶层 `nodeId` **必填**(V1.8.1),与领取时落库的 `claimed_node_id` 比对:不符 `NODE_MISMATCH`、历史在途行 `NODE_UNKNOWN`,均不采纳。原子 `WHERE` = 节点 + 代次 + 来源状态(`V1.0.28`) |
|
||||
| 未实现 | `undelivered[]` 墓碑帧数组(表结构与写法已通,H/S 线不产生,仅 G 线用) |
|
||||
|
||||
字段级逐项对照见 `04_OpenAPI与内部IoT契约` 目录内说明,或 20261109 提交包中的《J-00 字段差异表》。
|
||||
|
||||
---
|
||||
|
||||
## 附:`/internal/**` 的两道闸(2026-11-11 补)
|
||||
|
||||
五个接口都挂 `/internal/**`,进业务逻辑之前有**两层**,顺序不能换:
|
||||
|
||||
| 顺序 | 闸 | 拦什么 | 开关 | 默认 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `InternalNetworkGuard` | 来源 IP 不在白名单 → 403 | `gateway.auth.network-guard-enabled` | **开** |
|
||||
| 2 | `AuthInterceptor` | Token / 时间戳 / nonce / 签名 | `gateway.auth.enabled` | 关(联调态) |
|
||||
|
||||
**为什么要分两个开关**:签名要和 IoT 侧同批切(单边打开全部 401),所以联调期只能关着。
|
||||
但签名关着的时候如果什么都没有,`/internal/**` 就是任何能连 8080 的人都能调——
|
||||
文档写的"仅内网"变成一句空话,谁都能伪造一条水浸事件。所以网络闸独立于签名开关,**默认就开着**。
|
||||
|
||||
白名单默认值:`127.0.0.0/8`、`::1/128`、`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`。
|
||||
覆盖本机自测、局域网演示、同机 Nginx 反代三种场景。要收紧就配 `gateway.auth.allowed-cidrs`,
|
||||
配了之后以配置为准(默认值不再参与)。
|
||||
|
||||
只认 `getRemoteAddr()`,**不认 `X-Forwarded-For`**——后者是请求头,调用方随便写。
|
||||
部署在 Nginx 后面时 `remoteAddr` 是 Nginx 那台机(本机或内网),仍在白名单内;
|
||||
真正的公网边界由 Nginx 做,这一层兜的是"绕过 Nginx 直连 8080"。
|
||||
@@ -0,0 +1,289 @@
|
||||
# 状态机:告警 与 下行命令
|
||||
|
||||
> 状态字段全部是 `varchar(16)` 枚举直存,不用数字码,便于人工核对。
|
||||
> 两条状态机是本模型最容易被评「含糊」的地方,因此把**谁能改、什么条件能改、改不了会怎样**写死在这里。
|
||||
>
|
||||
> **口径来源**:告警九态=GOV-001 §4.3「告警与命令」检查项;命令七态=`6_J-00_接入接口规格_V1.8.md` §3.4。
|
||||
> 本文只做展开说明,**不另立字段含义**;与 J-00 冲突时以 J-00 为准。
|
||||
|
||||
---
|
||||
|
||||
## 一、告警状态机(九态)
|
||||
|
||||
```
|
||||
┌────────── 新事件触发 ──────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────┐ 推送成功 ┌─────────┐ │
|
||||
│ NEW │ ──────────► │ PUSHED │ │
|
||||
└───────┘ └─────────┘ │
|
||||
│ │ │ │ │
|
||||
命中免打扰时段 │ │ └── 判定误报 ──► FALSE │
|
||||
▼ │ │ │
|
||||
┌────────┐ │ │
|
||||
│DELAYED │ 静默结束回 NEW │ │
|
||||
└────────┘ │ │
|
||||
│ │ │
|
||||
已知悉/确认 ────┴────────┬────────────────┘ │
|
||||
▼ │
|
||||
┌────────────┐ 超时未确认 │
|
||||
│ CONFIRMED │ ────────► ESCALATED │
|
||||
└────────────┘ │ │
|
||||
│ │ │
|
||||
处置完成 ──────────────┴─────────────────────┘ │
|
||||
▼ │
|
||||
┌──────────┐ 关闭 ┌────────┐ │
|
||||
│ HANDLED │ ──────► │ CLOSED │ │
|
||||
└──────────┘ └────────┘ │
|
||||
│ │
|
||||
同一设备同一规则再次触发,且本单无人处理 │
|
||||
▼ │
|
||||
┌────────────┐ │
|
||||
│ SUPERSEDED │ ◄─────────────────────┘
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
### 状态定义
|
||||
|
||||
| 状态 | 中文 | 含义 | 是否算「待处理」 |
|
||||
|---|---|---|---|
|
||||
| `NEW` | 新告警 | 刚生成,尚未推送或推送中 | ✅ |
|
||||
| `PUSHED` | 已推送 | 已送到家属端(SSE/短信/App),等人看 | ✅ |
|
||||
| `DELAYED` | 免打扰 | 落在收件人免打扰时段,不响铃不弹窗,**仍在列表里** | ✅ |
|
||||
| `CONFIRMED` | 已确认 | 有人接了这个单(`confirmed_by/confirmed_at` 有值),还没处置完 | ✅ |
|
||||
| `ESCALATED` | 已升级 | 超时未确认,已按规则升级(加急通知/通知下一位家属) | ✅ |
|
||||
| `HANDLED` | 已处置 | 家属点了「已处理」,但事还没完全关闭 | ❌(家属视角) |
|
||||
| `CLOSED` | 已关闭 | 处置完成并关闭 | ❌ |
|
||||
| `FALSE` | 误报 | 人工判定为误报 | ❌ |
|
||||
| `SUPERSEDED` | 已被替代 | 同一设备同一规则又触发了新的,本单被顶掉 | ❌ |
|
||||
|
||||
> **`HANDLED` 与 `CLOSED` 的区别**:前者是「我知道这件事了、我处理了」,后者是「这条记录可以结束了」。
|
||||
> 家属端把两者都当"不用再管",但后台统计「今日处置率」只认 `HANDLED` 及以上。
|
||||
>
|
||||
> **`PUSHED` 为什么必须独立**:`NEW` 是"系统生成了",`PUSHED` 是"人真的收到了"。
|
||||
> 合成一个值以后,「推送失败了没」查不出来——告警生成了但没送到人手上,恰恰是最危险的情况。
|
||||
|
||||
### 允许的状态迁移(白名单)
|
||||
|
||||
| 当前 | 允许去向 | 触发者 |
|
||||
|---|---|---|
|
||||
| `NEW` | `PUSHED` / `DELAYED` / `CONFIRMED` / `HANDLED` / `FALSE` / `SUPERSEDED` | 通知模块/免打扰判定/家属/人工 |
|
||||
| `PUSHED` | `CONFIRMED` / `HANDLED` / `FALSE` / `SUPERSEDED` | 家属/人工 |
|
||||
| `DELAYED` | `NEW`(静默结束唤醒)/ `CONFIRMED` / `HANDLED` / `SUPERSEDED` | 定时任务/家属 |
|
||||
| `CONFIRMED` | `HANDLED` / `ESCALATED` / `FALSE` | 家属/超时升级 |
|
||||
| `ESCALATED` | `CONFIRMED` / `HANDLED` / `FALSE` | 家属/人工 |
|
||||
| `HANDLED` | `CLOSED` | 家属/人工 |
|
||||
| `CLOSED` / `FALSE` / `SUPERSEDED` | **无(终态)** | — |
|
||||
|
||||
**白名单外的迁移一律 `400`,不是静默忽略。** 前端按钮由 `GET /alarms/{id}/actions` 返回,不靠猜。
|
||||
|
||||
### 自动替代规则(这套模型的重点)
|
||||
|
||||
同一 `dev_id + rule_key` 再次触发时,**不是往旧单上叠次数,而是开新单**,旧单按三种情况处理:
|
||||
|
||||
| 旧单当时的状态 | 处理 | 结果 |
|
||||
|---|---|---|
|
||||
| `NEW` / `PUSHED` / `DELAYED`(无人管) | 标为 `SUPERSEDED`,回填 `superseded_by=新单id`,释放 `active_key` | 家属看到最新一条,点开能看整组历史 |
|
||||
| `CONFIRMED` / `ESCALATED` / `HANDLED`(人处理过) | **保持原状**,只释放 `active_key`,不改状态 | 处理结果不被覆盖 |
|
||||
| `CLOSED` / `FALSE` / `SUPERSEDED`(终态) | 不动 | 历史记录保持 |
|
||||
|
||||
**执行顺序有讲究**:先释放 `active_key` → 再插新单 → 最后回填 `superseded_by`。
|
||||
顺序错了会撞唯一键(`active_key` 保证"同一设备同一规则同时只有一条活跃单")。
|
||||
|
||||
`active_key = dev_id + ':' + rule_key`,终结时置 NULL。
|
||||
|
||||
### 告警链(`GET /alarms/{id}/chain`)
|
||||
|
||||
沿 `superseded_by` 反向递归(MySQL 递归 CTE),返回整组:
|
||||
|
||||
```
|
||||
1 替代 2 替代 3 处理 4 替代 5 当前
|
||||
```
|
||||
|
||||
- 点 **5** → 看到 4、5(4 被替代,5 是当前)
|
||||
- 点 **3** → 看到 1、2、3(3 是被人处理过的,链在这里断)
|
||||
- 点**任一被替代的** → 只看到自己 + 一条提示「已被后面的告警替代,不能单独处理」
|
||||
|
||||
**被替代的单不可编辑**:`ack/confirm/handle/close/false` 一律 400。
|
||||
|
||||
### 恢复是「附加状态」,不是状态迁移
|
||||
|
||||
门关上、漏水已止、烟雾已散、设备来电 —— 这些**不新建告警**,只在原单上写 `cleared_at` 与 `clear_event_id`。
|
||||
|
||||
```
|
||||
设备状态:已恢复 · 2026-11-09 20:21:12(设备来电)
|
||||
```
|
||||
|
||||
状态标签仍是 `NEW`,旁边多一个绿色「已恢复」。
|
||||
**恢复事件严格按类型配对**:来电只恢复断电,绝不会把一条还没处理的 SOS 标成已恢复。
|
||||
|
||||
| 触发事件 | 只能恢复 |
|
||||
|---|---|
|
||||
| `DOOR_CLOSE` | `DOOR_OPEN` |
|
||||
| `WATER_LEAK_CLEAR` | `WATER_LEAK` |
|
||||
| `SMOKE_CLEAR` | `SMOKE` |
|
||||
| `SOS_CLEAR` | `SOS` |
|
||||
| `POWER_ON` / `POWER_RESTORE` | `POWER_OFF` / `POWER_CUT` |
|
||||
|
||||
**恢复只写附加字段,永远不把告警置为 `CLOSED`** —— 安全告警必须由人关闭,设备状态好转不等于这件事了了。
|
||||
|
||||
---
|
||||
|
||||
## 二、下行命令状态机(七态)
|
||||
|
||||
**英文枚举**:`PENDING / CLAIMED / SENT / ACKED / FAILED / TIMEOUT / CANCELLED`,由业务层维护。
|
||||
定义见 `6_J-00_接入接口规格_V1.8.md` §3.4,本节只做落地说明。
|
||||
|
||||
```
|
||||
┌──────── I-18 拉取 ────────┐
|
||||
▼ │
|
||||
PENDING ────> CLAIMED ──I-05 SENT──> SENT ──ACKED code=0──> ACKED(终态)
|
||||
▲ │ │
|
||||
│ ├─ 租约到期(60s) ────┼──> 回 PENDING(reclaim+1)
|
||||
│ │ │
|
||||
│ ├─ 可延迟 + OFFLINE/TIMEOUT/FAILED
|
||||
│ │ └──> 回 PENDING(retry+1,退避)
|
||||
│ │ │
|
||||
│ └─ 实时 + 任意失败 └─ timeout_at(24h) 到 ──> FAILED(终态)
|
||||
│ └──> FAILED(终态,当场返回用户)
|
||||
│
|
||||
├── retry 超 10 次 或 过 expire_at(72h)──> TIMEOUT(终态)
|
||||
└── 用户撤单/设备解绑 ──────────────────> CANCELLED(终态)
|
||||
```
|
||||
|
||||
| 状态 | 说明 | 进这个状态的时刻 |
|
||||
|---|---|---|
|
||||
| `PENDING` | 待领取 | 创建命令 |
|
||||
| **`CLAIMED`** | **已领取/租约中**(`lease_expire_at` 有效) | IoT 通过 **I-18** 领走 |
|
||||
| `SENT` | 已发出(`timeout_at` 有效) | **I-05** 回报 `SENT`(无回执协议) |
|
||||
| `ACKED` | **设备有回应**,且 `resultCode=0` | **I-05** 回报 `ACKED` + `resultCode=0` |
|
||||
| `FAILED` | 失败(终态) | 实时类任意失败;`SENT` 超 24h 无回执 |
|
||||
| `TIMEOUT` | 已过期(终态) | 重排 10 次 或 过 `expire_at`(创建+72h) |
|
||||
| `CANCELLED` | 已取消(终态) | 用户撤单/设备已解绑 |
|
||||
|
||||
> **`CLAIMED` 与 `SENT` 必须分开**:「IoT 领走了」和「已经发出去了」是两件事。
|
||||
> 混成一个值,租约回收就判不出该不该重派,也分不清是中间件失联还是设备没响应。
|
||||
|
||||
### ⚠ `ACKED` 不等于「执行成功」
|
||||
|
||||
这是最容易写错、也是评审明确列为否决情形的一条,在此写死:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `result`(I-05) | `ACKED` = **设备有回应**(收到回执帧)。**不代表设备按意图执行了。** |
|
||||
| `resultCode` | 标准字典值:`0` 成功/`1` 不支持/`2` 参数非法/`3` 执行失败/`4` 设备忙。**只有 `0` 才算设备声明成功** |
|
||||
| `rawResultCode` / `rawAck` | 厂家原始码与回执原始字节,原样保存,**不得透传到标准字段** |
|
||||
|
||||
所以:
|
||||
- `ACKED + resultCode≠0` → 落 **`FAILED`**,不是 `ACKED`;
|
||||
- 即使 `ACKED + resultCode=0`,也只说明**设备声明已执行**。业务效果要看后续状态快照或事件确认(例如 `set_mode` 之后要核对 `gateway.mode_cache` 是否一致)。
|
||||
- **库里绝不出现「已生效」这种把回执当结果的值。**
|
||||
|
||||
### 状态值迁移(库现值 → 目标七态)
|
||||
|
||||
`device_command.status` 现在是**中文六值**,`varchar(16)`;目标七态是英文。这是**值域迁移,不是加列**:
|
||||
|
||||
| 目标 | 库现值 | 迁移动作 |
|
||||
|---|---|---|
|
||||
| `PENDING` | 待下发 | 改值 |
|
||||
| `CLAIMED` | **无** | **新增枚举值**(列宽够,只扩值域) |
|
||||
| `SENT` | 已下发 | 改值 |
|
||||
| `ACKED` | 已生效 | 改值(**语义同时纠正**:从「已生效」改为「设备有回应」) |
|
||||
| `FAILED` | 失败 | 改值 |
|
||||
| `TIMEOUT` | 超时 | 改值 |
|
||||
| `CANCELLED` | 已取消 | 改值 |
|
||||
|
||||
> **迁移状态(2026-11-11 已闭环)**:V1.0.23 迁移脚本已把库里中文六值刷成英文七态,
|
||||
> 代码读写全部改为英文;`BizDeviceService.commands()` 保留一层"中文旧值 → 英文"的**读时映射**,
|
||||
> 只为兼容尚未升级的老库,跑过 V1.0.23 后这层就是空转。
|
||||
> 实测:`cmd_chain_test.py` 23 条全过,其中第 12 条专门验"命令记录里没有中文旧值残留"(20 条,中文 0 条)。
|
||||
|
||||
### 租约与两个计数器
|
||||
|
||||
| 计数器 | 记什么 | 用途 |
|
||||
|---|---|---|
|
||||
| `retry_count` | **设备侧**失败重排了几次 | 超 10 次 → `TIMEOUT` |
|
||||
| `reclaim_count` | **IoT 拿走却没回话**几次 | 超 5 次 → 说明该 `nodeId` 反复失联,告警转人工 |
|
||||
|
||||
混成一个,就分不清是设备坏了还是中间件坏了。
|
||||
|
||||
### 领取代次 `claim_generation`(V1.7.2 新增)
|
||||
|
||||
每次 `PENDING → CLAIMED` 时 +1,**I-18 返回、I-05 回带**。
|
||||
|
||||
| 步 | 发生什么 | 代次 |
|
||||
|---|---|---|
|
||||
| 1 | 创建 | `0`(尚未领取) |
|
||||
| 2 | 节点 A 领走 | `1` |
|
||||
| 3 | 租约 60 秒到期,没回话,回收成 `PENDING` | 仍是 `1`(回退不 +1,只记录 `reclaim_count`) |
|
||||
| 4 | 节点 B 领走 | `2` |
|
||||
| 5 | 节点 A 的回执**迟到**,带 `claimGeneration=1` | **更新 0 行 → 计 `duplicated`,状态不动** |
|
||||
| 6 | 节点 B 的回执,带 `2` | 更新 1 行 → 生效 |
|
||||
|
||||
没有代次的话第 5 步会把 B 的流转覆盖掉,而且不报错——只在报表里表现为"同一条命令走了两条路"。
|
||||
|
||||
> 合规边界:回执不带代次**不会被拒**……这条旧口径已作废。
|
||||
> **V1.8 起 I-05 的 `claimGeneration` 必填,缺了直接拒**(`CLAIM_GENERATION_REQUIRED`)。
|
||||
|
||||
### 领取节点 `claimed_node_id`(V1.8.1 新增,迁移 V1.0.28)
|
||||
|
||||
代次只回答"第几轮",**不回答"这一轮是谁"**。所以领取时必须把 `nodeId` 一起落库:
|
||||
|
||||
```
|
||||
UPDATE device_command
|
||||
SET status='CLAIMED',
|
||||
claim_generation = claim_generation + 1,
|
||||
claimed_node_id = ?, -- 这次来领的 IoT 节点
|
||||
claimed_at = NOW(3),
|
||||
lease_expire_at = ?
|
||||
WHERE id IN (...) AND status='PENDING'
|
||||
```
|
||||
|
||||
I-05 的原子更新条件因此变成**四个守卫**:
|
||||
|
||||
```sql
|
||||
WHERE id = ? -- 命令
|
||||
AND claim_generation = ? -- 第几轮
|
||||
AND claimed_node_id = ? -- 这一轮是谁(V1.8.1)
|
||||
AND status IN ('CLAIMED','SENT') -- 允许的来源状态
|
||||
```
|
||||
|
||||
| 回执情况 | 结果 |
|
||||
|---|---|
|
||||
| 节点对、代次对 | 更新 1 行,`accepted` |
|
||||
| 节点对、代次旧(同一节点上一轮) | 更新 0 行,计 `duplicated`(过期回执,不算错) |
|
||||
| 节点错 | `rejected` + `NODE_MISMATCH`,状态不动 |
|
||||
| 历史在途行(迁移前领的,节点为空) | `rejected` + `NODE_UNKNOWN`,不猜、不伪造;租约到期重派后即有值 |
|
||||
|
||||
> **作用边界**:`nodeId` 由 IoT 自报,**可以伪造,它不是身份认证**。
|
||||
> "调用方是谁"靠内网来源白名单 + 签名;这一列证明的是**领取事实一致**——
|
||||
> 这条结果是不是领走这一轮的那个节点回的。
|
||||
|
||||
---
|
||||
|
||||
## 三、两条状态机的共同原则
|
||||
|
||||
1. **状态只由事件驱动,不允许任意跳转**:没有"把 `CLOSED` 直接改回 `NEW`"这种口子。
|
||||
2. **改不了就明确拒绝**:白名单外的迁移一律 `400`,不是静默忽略。
|
||||
3. **动作白名单**:`GET /alarms/{id}/actions` 返回当前状态下允许的动作,前端据此渲染按钮,不靠猜。
|
||||
4. **审计留痕**:每个状态变更写 `audit_log`(后台操作)或 `alarm_action`(家属操作),**与状态变更同事务**。
|
||||
**审计写失败必须让整个动作回滚**,不能 `catch {}` 吞掉——否则会出现"告警关了但查不到谁关的",
|
||||
而且没有任何报错。非事务路径(后台建档等)改成记账 + ERROR 日志,计数进 `/health` 的 `auditFailures`。
|
||||
5. **回执 ≠ 结果**:`ACKED` 只代表"设备有回应",业务是否生效要看 `resultCode` 和后续核对。
|
||||
6. **恢复 ≠ 关闭**:设备状态好转只写 `cleared_at`,安全告警必须由人关闭。
|
||||
|
||||
---
|
||||
|
||||
## 四、与 J-00 V1.8 的对应关系
|
||||
|
||||
| 本文 | J-00 V1.8 位置 | 是否一致 |
|
||||
|---|---|---|
|
||||
| 告警九态 | GOV-001 §4.3 检查项 | ✅ 代码已实现九态 |
|
||||
| 命令七态 | §3.4「业务层的命令状态机」 | ✅ 契约一致;✅ 代码已迁移到英文七态(V1.0.23),23 条用例全过 |
|
||||
| 命令领取 `I-18` | §三 接口表 | ✅ 本文已统一为 I-18(原文档写作 I-20,已废止) |
|
||||
| 命令回执 `I-05` | §三 接口表 | ✅ 本文已统一为 I-05(原文档写作 I-21,已废止) |
|
||||
| `claimGeneration` | §3.2 / §3.3(V1.7.2) | ✅ I-18 返回、I-05 回带;旧代次回执被挡(用例 18) |
|
||||
| `result` 五值 | §3.3 | ✅ `ACKED/SENT/TIMEOUT/DEVICE_OFFLINE/FAILED` |
|
||||
| `resultCode` 标准码 | §3.3 | ✅ `0`–`4`,厂家原码另存 `rawResultCode` |
|
||||
|
||||
> 本文与 J-00 V1.8 冲突时,**以 J-00 为准**;新增枚举值须先改 J-00,再改本文与代码。
|
||||
Reference in new issue
Block a user