Files
hepu/docs/contract/内部五接口定义.md
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

10 KiB
Raw Permalink Blame History

内部 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 是水浸、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"。