按周工交付的 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),不做归一化以免整树摘要失效。
10 KiB
内部 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)
响应
{"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 挂起,不占线程)。
{"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。
{"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是水浸、bignbiot08是白蚁,命令侧同样有这种坑), 直透到标准列以后,业务层和前端就得按每个厂家各写一套判断,等于把适配器的活摊到全平台。厂家原码只能进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"。