按周工交付的 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),不做归一化以免整树摘要失效。
225 lines
10 KiB
Markdown
225 lines
10 KiB
Markdown
# 内部 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"。
|