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:
aomin committed 2026-11-13 17:32:37 +08:00
commit a7605ef8b8
121 files changed
+23629

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
# 文档索引
本目录只放**交付与契约类**文档,不放产品需求与项目计划(那些在项目文档库,不在代码主干)。
三个子目录按性质划分,判定标准是"这份文档过期时,谁能证明它是对的"。
## 一、handover/交付与安全清理
周工交付的 `SEC-Z-01 清理后` 材料,**原样保留,内容未改**。这是"这批源码从哪来、清理了什么、怎么证明"的凭据。
| 文件 | 内容 | 什么时候看 |
|---|---|---|
| `00_包内容与使用说明.md` | 包里有什么、没什么、怎么用 | 第一次接手 |
| `01_SEC-Z-01_最小安全清理差异清单.md` | 15 处明文逐处改成了什么 | 复核清理是否彻底 |
| `02_SEC-Z-01_敏感信息扫描报告.md` | 扫描范围、结果、复算方法、盲区、轮换结论 | 怀疑有凭据泄露时 |
| `03_Z-02_清理后哈希与构建说明.md` | 清理前后指纹、构建步骤、首提交顺序 | 核对版本标识、复现构建 |
| `04_155条证据适用边界.md` | 155 条重跑结果,**以及它不能证明什么** | 有人拿 155 条说"全过了"时 |
| `gitignore_建议.txt` | 交付方给的忽略清单建议 | 调整 `.gitignore` 时对照 |
> `00` 第二节称"没给演示前端",但实际交付包含 8 个演示文件,已整理到 `demo/`。该说明为早于补包的旧稿,以实际文件为准。
## 二、contract/接口契约
**唯一的机读契约来源。** 本工程未引入 springdoc,没有运行时 OpenAPI 生成,所以这里的 md 与 yaml 是手工维护的——改了代码不改这里,就是契约漂移,且没有任何工具会报错。
| 文件 | 内容 |
|---|---|
| `6_J-00_接入接口规格_V1.8.md` | 接入接口规格正文(J-00 V1.8.1) |
| `6_J-00_接入接口规格_V1.8_OpenAPI.yaml` | 同上的机读形式 |
| `6_J-00_OpenAPI使用说明_V1.8.md` | yaml 怎么用、字段怎么读 |
| `内部五接口定义.md` | I-01/I-02/I-05/I-12/I-18 字段与语义 |
| `状态机.md` | 命令与告警的状态、动作、迁移条件 |
| `Mock样例.md` | 可直接喂给接入侧的请求样例 |
契约版本号升级时,文件名、正文、yaml 的 `info.version` 三处必须一致;旧版本文件保留不删。
## 三、reference/指纹与校验
| 文件 | 用途 |
|---|---|
| `原始交付包指纹.md` | 说明各清单的作用范围,并给出 12 条迁移脚本的哈希 |
| `SHA256SUMS_源码树.txt` | 84 个源码文件的逐文件哈希,**仓库根执行 `sha256sum -c` 可直接校验** |
| `SHA256SUMS_原始交付包.txt` | 原交付包的文件哈希清单(**只对原包有效**) |
| `SHA256SUMS_前端与设备端.txt` | 原包内演示前端子清单(**只对原包有效**) |
前两个清单在目录重排后**已不能直接校验**本仓库,保留是为了证明"交付时的文件与现在仓库里的是同一份"。怎么核对见 `原始交付包指纹.md`。
## 四、路径说明
`handover/` 里的文档写于交付当时,文中提到的目录名是**原交付包**的结构。对照本仓库:
| 文中写法 | 本仓库位置 |
|---|---|
| `aomin-gateway/` | `aomin-gateway/`(同名,未变) |
| `01_后端源码/` | `aomin-gateway/`(交付时的旧称) |
| `02_前端与设备端/` | `demo/` |
| `03_数据库与Flyway迁移/` | **未纳入**,见下 |
| `04_OpenAPI与内部IoT契约/` | `docs/contract/` |
### 迁移镜像为什么没纳入
原包的 `03_数据库与Flyway迁移/` 与 `aomin-gateway/src/main/resources/db/migration/` 是同一套 12 条 SQL 的两次出现,用途是"应用与镜像同源核对"。
同一套文件在仓库里出现两次,迟早会只改一处。所以仓库只保留工程内一套(Flyway 只认它),核对能力用哈希代替:12 条脚本的 SHA-256 记在 `reference/原始交付包指纹.md`,交付时已核为**逐字节一致**。
## 五、新增文档往哪放
| 新增内容 | 放这里 |
|---|---|
| 接口字段、状态机、协议变更 | `contract/` |
| 交付清单、安全清理、复验记录 | `handover/` |
| 哈希、版本指纹、校验脚本输出 | `reference/` |
| 都不属于(需求、计划、会议记录) | 不放本仓库 |
改版不覆盖旧件:同主题新建文件、版本号 +1,旧件保留。
@@ -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`。
+156
View File
@@ -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 方案的刻意设计。
+224
View File
@@ -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"。
+289
View File
@@ -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,再改本文与代码。
@@ -0,0 +1,79 @@
# 包内容与使用说明
> 本包是 **M-Z 业务底座的受控源码**(SEC-Z-01 清理后),只覆盖中段业务层,不含其他段的东西。
> 用途:陈工按 AM-S1-GOV-020 §6.2(1) 接收此源码,建立唯一主干。
---
## 一、包里有什么(108 个条目)
| 目录 / 文件 | 数量 | 是什么 |
|---|---|---|
| `aomin-gateway/` | 87 | **M-Z 业务层完整 Maven 工程**:`pom.xml`、`src/main`(84 个文件:controller / service / dao / biz / engine / 迁移脚本 / 配置)、`README.md`、`start-gateway.bat` |
| `03_数据库与Flyway迁移/` | 13 | **迁移镜像**:12 条 SQL(V1.0.0~V1.0.28)+ 迁移说明。与 `aomin-gateway/src/main/resources/db/migration` **逐字节一致**,供"应用与镜像同源"核对 |
| `04_OpenAPI与内部IoT契约/` | 6 | J-00 接入接口规格(md + OpenAPI yaml)、内部五接口定义、状态机、Mock 样例、使用说明 |
| `01_SEC-Z-01_最小安全清理差异清单.md` | 1 | 15 处明文逐处改了什么 |
| `02_SEC-Z-01_敏感信息扫描报告.md` | 1 | 扫描范围、结果、复算方法、盲区、轮换结论 |
| `03_Z-02_清理后哈希与构建说明.md` | 1 | 清理前后指纹、构建步骤、入库顺序(**先清后推**) |
| `04_155条证据适用边界.md` | 1 | 155 条重跑结果,以及**它不能证明什么** |
| `gitignore_建议.txt` | 1 | 建主干时直接用的 `.gitignore` |
| `SHA256SUMS_包内.txt` | 1 | 包内文件哈希,解压后在包根执行 `sha256sum -c` 即可校验 |
---
## 二、包里**没有**什么,为什么
| 没给 | 原因 |
|---|---|
| `02_前端与设备端源码`(8 文件,演示前端) | 三端由陈工按 §6.2(3) 统一接入。这是评审用演示前端,给了反而分不清哪套是主干。**需要对照可以单独要** |
| `06_统一闭环题自测证据/raw`(12 份结果 JSON、11 份日志) | 体积大、每轮重跑全变。证据按受控目录交付,不进代码主干;要复跑见 `04` 里的命令 |
| `99_应急恢复(非正式路径)` | 明确是应急手段,不是主干迁移序列。给进去容易被当成常规路径 |
| `评审用虚构台账数据.sql` | 演示数据,不属于主干 |
| `09_代码与版本/SHA256SUMS.txt`(131 项全包清单) | 那是整个评审包的清单,路径以评审包为根;本包用 `SHA256SUMS_包内.txt`,路径与包内一致,可直接校验 |
| 明文口令 | 已按 SEC-Z-01 清空,**一个字符都没有** |
---
## 三、怎么用(三步)
```bash
# 1) 校验(在解压后的包根)
sha256sum -c SHA256SUMS_包内.txt
# 2) 编译(JDK 21;离线仓库按你们环境改)
cd aomin-gateway
mvn -o -q -s <settings.xml> -DskipTests clean package
# 产物 target/aomin-gateway-0.0.1-SNAPSHOT.jar,约 43.5 MB
# 3) 启动(必须带 DB_PASS,否则启动中止、退出码 1)
set DB_PASS=<口令>
java -jar target/aomin-gateway-0.0.1-SNAPSHOT.jar --server.port=8080 --server.address=0.0.0.0
curl http://127.0.0.1:8080/health
# 预期 db=connected、tables=87、flyway=1.0.28、status=UP
```
---
## 四、建主干前必读的两条
**① 顺序不能反:先清后推。**
当前目录**不是 git 仓库**,是干净起点。`git init` 必须在 15 处清理完成之后——一旦推上去,即使删除文件,第一条提交里仍有明文,只能重写全部历史。详见 `03_Z-02_清理后哈希与构建说明.md` §4.2。
**② 迁移只有一套序列。**
`aomin-gateway/src/main/resources/db/migration` 与 `03_数据库与Flyway迁移/` 是同一套 12 条的两次出现,**不是两套**。主干只保留一套默认序列;若两者路径必须二选一,以应用内那份为准,镜像目录作对照。
---
## 五、已知边界(不让接收方误判)
| 项 | 状态 |
|---|---|
| A-Z-01/02/03 | **未关闭**。作者自测通过,仍需接收方复验签认;本包不构成关闭 |
| 并发 PULL | 未覆盖(`v20` 是串行 10 条),由谢工按完整矩阵补测 |
| 真机 | 未覆盖,等黄工真机窗口 |
| 前端三端 | 不在本包范围 |
| 口令轮换 | **未轮换**,登记为待确认(见 `02` §4) |
---
*对应 GOV-020:Z-01(差异+扫描+轮换结论)、Z-02(哈希+构建说明+155 条边界)。*
@@ -0,0 +1,126 @@
# SEC-Z-01 最小安全清理差异清单(Z-01)
| 项 | 内容 |
|---|---|
| 依据 | AM-S1-GOV-020 §4.2「第一优先级:完成 SEC-Z-01 最小安全清理」 |
| 执行人 | 周工 |
| 执行时间 | 2026-11-13 11:20–11:45 |
| 范围 | **15 处明文** + **1 处启动守卫**(为满足"缺参明确失败") |
| 是否升版 | **否**,V1.9 内做最小安全清理,未升 V1.10,无无关重构 |
| 口令复述 | **本件不含原口令任何字符**,差异表中一律写作「(原值已脱敏)」 |
---
## 一、清理前后总览
| 项 | 清理前 | 清理后 |
|---|---|---|
| 含明文口令的文件 | **15 个** | **0** |
| 明文出现次数 | 15 处(每文件 1 处) | 0 |
| 配置层(yml) | 带非空默认值 | 无默认值,缺参启动中止 |
| 脚本层(py) | 带非空默认值 | 无默认值,缺参直接退出 |
| 文档层(md) | 写出明文示例 | 改为占位符 |
| 历史包 | 6 个 zip 内含旧凭据 | 已移入受控归档目录,标注禁止外发 |
---
## 二、逐处差异(15 处)
### A 类:后端配置(1 处)
| # | 文件 | 位置 | 改前 | 改后 |
|---|---|---|---|---|
| 1 | `01_后端源码/src/main/resources/application.yml` | `spring.datasource.password` | `${DB_PASS:<原值已脱敏>}` | `${DB_PASS}` |
同时把上方两行自相矛盾的注释("包里不再有第二处明文"——实际上还有 14 处)改成实际口径。
### B 类:Python 证据脚本(12 处)
12 个脚本写法相同,改法相同。以一处示意:
| 改前 | 改后 |
|---|---|
| `DB_PASS = os.environ.get("AOMIN_DB_PASS", "<原值已脱敏>")` | `DB_PASS = os.environ.get("AOMIN_DB_PASS")`<br>加 4 行守卫:为空则 `raise SystemExit(...)`,打印设置方法 |
| # | 文件 |
|---|---|
| 2 | `06_统一闭环题自测证据/alarm_retry_test.py` |
| 3 | `06_统一闭环题自测证据/closed_loop_test.py` |
| 4 | `06_统一闭环题自测证据/cmd_chain_test.py` |
| 5 | `06_统一闭环题自测证据/run_evidence.py` |
| 6 | `06_统一闭环题自测证据/scope_test.py` |
| 7 | `06_统一闭环题自测证据/v15_fix_test.py` |
| 8 | `06_统一闭环题自测证据/v16_fix_test.py` |
| 9 | `06_统一闭环题自测证据/v17_merge_test.py` |
| 10 | `06_统一闭环题自测证据/v18_fix_test.py` |
| 11 | `06_统一闭环题自测证据/v19_gov008_test.py` |
| 12 | `06_统一闭环题自测证据/v20_node_test.py` |
| 13 | `06_统一闭环题自测证据/stamp_evidence.py`(特殊:原为内联调用 `"-p" + os.environ.get(...)`,改为在模块顶部定义 `DB_PASS` 并加同样的守卫,调用处改引用常量) |
### C 类:说明文档(2 处)
| # | 文件 | 位置 | 改前 | 改后 |
|---|---|---|---|---|
| 14 | `05_启动构建与测试说明/启动构建与测试说明.md` | §5 启动 | `export DB_PASS=<原值已脱敏>` | `export DB_PASS=<口令>`,注释改为"必需:配置里已无默认值,未设置时启动直接失败" |
| 15 | `09_代码与版本/README.md` | 环境变量表 | 默认值列写出明文 | `root` / **必需,无默认值**;说明未设置时脚本直接退出 |
---
## 三、新增 1 处启动守卫(为什么必须有)
**这是本次唯一的代码改动,不是重构,是为满足 §4.2「缺少必需口令时明确失败」。**
### 问题:只删默认值,并没有"明确失败"
先按最省事的方式改(`password: ${DB_PASS}` 不设默认值)实测了一次,结果不是报"缺少 DB_PASS",而是:
```
Access denied for user 'root'@'localhost' (using password: YES)
```
原因:Spring Boot 的占位符解析在“解析不到”时是**忽略**而不是报错,于是拿空口令去连库。表现出来像"口令错了",实际是"根本没配"——这正是不允许的静默回退。
### 处理:在 `main` 里提前拦住
| 文件 | 改动 |
|---|---|
| `src/main/java/cn/aomin/gateway/GatewayApplication.java` | `main()` 首行调用 `requireEnv("DB_PASS", "数据库口令")`;新增私有方法 `requireEnv`,为空则打印提示并 `System.exit(1)` |
改后实测输出(退出码 **1**):
```
[启动中止] 缺少必需的环境变量 DB_PASS(数据库口令)。
SEC-Z-01:程序不再内置任何默认口令,也不会回退到包内明文。
设置方法:Windows set DB_PASS=<口令> Linux/macOS export DB_PASS=<口令>
```
**只校验"有没有",不校验"对不对",也不打印任何取值。**
---
## 四、构建产物与缓存清理
| 对象 | 处理 |
|---|---|
| `target/classes/application.yml` | 旧副本含明文;`mvn clean package` 后已由新文件覆盖,实测 jar 内配置 **0 命中** |
| `__pycache__` / `*.pyc` | 已删除(编译产物里会残留字符串常量) |
| 应用日志 `G:/aomin-dev/logs` | 实测 **0 命中**,保留(不含凭据,且是运行证据) |
| `target/` 其余内容 | 43 MB 构建产物,已随 `clean` 重建;**不入代码仓库**(见 `03` 的 `.gitignore`) |
---
## 五、历史包处置
`_历史包/` 下 6 个 zip 都含旧凭据(解开后逐成员扫描,命中 13~15 处)。直接按字节搜 zip 查不出来,因为内容被 deflate 压缩过。
| 处理 | 说明 |
|---|---|
| 目录改名 | `_历史包/` → **`_历史包_含旧凭据_禁止外发/`** |
| 登记 | 同目录 `_清单_含旧凭据.md` 记录每个 zip 的字节数、SHA-256、含旧口令文件数 |
| 约束 | **禁止外发、禁止入代码库、禁止上传公共 AI**;仅作内部留档 |
> 这是"轮换结论"要覆盖的一部分:口令一旦进过包,就不能假设它只存在于一个地方。
---
*后续:清理后哈希、构建说明、155 条证据边界见同目录 `03_…` 与 `04_…`。*
@@ -0,0 +1,113 @@
# SEC-Z-01 敏感信息扫描报告(Z-01 附件)
| 项 | 内容 |
|---|---|
| 扫描时间 | 2026-11-13 11:45 |
| 扫描人 | 周工 |
| 结论 | 包内、源码树、构建产物、日志 **全部 0 命中**;历史归档包 6 个含旧凭据,已隔离 |
| 模式 | 原口令字面值(大小写不敏感)+ URL 编码形式 `%40` |
---
## 一、扫描范围与结果
| # | 位置 | 类型 | 命中 |
|---|---|---|---|
| 1 | `候选模型评审包/`(全部文件,含 md / yml / py / json / xml) | 文本 | **0** |
| 2 | `候选模型评审包/`(全部文件,**按字节**,含 pyc 等二进制) | 二进制 | **0** |
| 3 | `01_后端源码/src` + `pom.xml`(工作目录 `G:/aomin-dev/app/aomin-gateway`) | 文本 | **0** |
| 4 | `aomin-gateway-0.0.1-SNAPSHOT.jar` → `BOOT-INF/classes/application.yml` | 二进制 | **0** |
| 5 | `target/`(全部,43 MB,构建产物) | 二进制 | **0** |
| 6 | `G:/aomin-dev/logs`(全部,6.1 MB) | 二进制 | **0** |
| 7 | `_历史包_含旧凭据_禁止外发/`(6 个 zip,**解压后逐成员**) | 文本 | **13~15 处/包,已隔离** |
### 第 7 项明细(历史包)
| 文件 | 含旧口令文件数 |
|---|---|
| 周工_V1.9定点修复包_20261112_上午版.zip | 15 |
| 周工_V1.9定点修复包_20261112_第二版.zip | 15 |
| 周工_候选模型评审去敏精简包_V1.6_20261111.zip | 13 |
| 周工_候选模型评审去敏精简包_V1.8_20261111.zip | 15 |
| 周工_候选模型评审去敏精简包_V1.9.1_20261112.zip | 15 |
| 周工_候选模型评审去敏精简包_V1.9_20261111.zip | 13 |
**处置**:整个目录改名为 `_历史包_含旧凭据_禁止外发`,同目录 `_清单_含旧凭据.md` 登记每个包的字节数与 SHA-256。禁止外发、禁止入代码库、禁止上传公共 AI。
> 这一条要特别说明:**直接对 zip 做字节搜索是查不出来的**,因为成员内容被 deflate 压缩过,第一遍按字节扫全部返回"否",是错的。必须解开逐成员扫。
---
## 二、复算方法(陈工可原样执行)
```python
import os, zipfile
SECRET = b"<原口令>" # 自行填入,不要写进任何文件
hits = []
for root in [r"<包根>", r"<源码根>/src", r"<源码根>/target", r"<日志目录>"]:
for dp, dn, fn in os.walk(root):
for f in fn:
p = os.path.join(dp, f)
try:
if os.path.getsize(p) > 30 * 1024 * 1024:
continue
if SECRET in open(p, "rb").read():
hits.append(p)
except Exception:
pass
# 压缩包必须解开再扫
for zp in [r"<_历史包>/*.zip"]:
import glob
for p in glob.glob(zp):
with zipfile.ZipFile(p) as z:
for nm in z.namelist():
try:
if SECRET in z.read(nm):
hits.append(p + "!" + nm)
except Exception:
pass
print("命中:", len(hits))
for h in hits:
print(" ", h)
```
判读规则:
- **文本命中和字节命中分开看**。只 grep 文本会漏掉 pyc、jar、zip;
- **zip / jar 必须解压后扫**,压缩态下的字节搜索没有意义;
- 只扫"当前工作副本",不扫 git 历史——**本次交付时尚无 git 仓库**(见下节)。
---
## 三、盲区声明(扫描覆盖不到什么)
| 盲区 | 说明 |
|---|---|
| Git 历史 | 当前 `G:/aomin-dev/app/aomin-gateway` **不是 git 仓库**,包内也没有 `.git`。所以不存在历史污染;这也是必须在首次 `git init` **之前**完成清理的原因 |
| 数据库内部 | 本扫描只查文件。数据库用户表里的口令哈希不属于"明文口令",另行按鉴权设计处理 |
| 其他凭据 | 本次只针对 SEC-Z-01 指出的那一处数据库口令。若后续新增 token、AK/SK,需另做一次扫描 |
| 聊天记录与截图 | 不在本次范围。已在回执中说明"不得在回复、截图、差异表或本通知中复述原口令" |
| 已外发的旧包 | 6 个历史包若曾通过群聊或邮件发出,**接收方副本无法回收**——按 GOV-020 §4.2,这是"检查历史包、受控配置和访问记录"要求的部分,如实列出,不隐瞒 |
---
## 四、轮换结论(要求 Z-01 必须给的一句)
**结论:不轮换,但登记为"待确认项",由仇总/陈工判断是否升级。**
理由(如实写,不美化):
1. 该口令是 **评审机本地 MySQL 演示库** 的 `root` 口令(库名 `aomin_dev`),不是生产凭据,也不是任何对外服务的入口;
2. 该库仅监听 `127.0.0.1:3306`,未对公网开放;
3. **但**:它已经进过 6 个历史包,若这些包曾被外发(群聊、邮件、他人机器),则等于该口令已离开本机;
4. 因此本件的表述是:**本地场景不需要轮换;一旦确认历史包曾外发,立即轮换并重跑 155 条**(重跑约 13 分钟,成本可控)。
> 不写"已轮换"——因为确实没换,换了就要重跑全部证据,那是另一件事,不能混在这次清理里糊过去。
---
*扫描与复算脚本、结果均可在本机重现;如需基线比对,见 `03_Z-02_清理后哈希与构建说明.md`。*
@@ -0,0 +1,175 @@
# Z-02 清理后源码树哈希、包哈希与构建说明
| 项 | 内容 |
|---|---|
| 依据 | AM-S1-GOV-020 §4.4「Z-02」 |
| 交付人 | 周工 |
| 时间 | 2026-11-13 |
| 结论 | 清理后指纹已重算;155 条在本基线**真实重跑并全过**,不是沿用旧结果 |
---
## 一、清理前后指纹对比
| 指纹 | 清理前(V1.9 第二版) | 清理后(本件) | 变化原因 |
|---|---|---|---|
| 源码树摘要(`01_后端源码/src`,84 文件) | `60FA6938…`(上批) | **`5E9F894B0D1071969A968F2A9B1451279ADF39D423148186886BFD064398F286`** | 改了 1 个 java(启动守卫)+ yml 注释与默认值 |
| 可执行 jar | `43,516,447 B` / `652E1602…` | **`43,517,256 B`** / **`1F38F527605F09AA45E9E54528AFB11230469E1B197FA001C06E8725FD102192`** | 重新构建,含新配置与新类 |
| 契约版本 | `1.8.1` | `1.8.1` | **未变** |
| 迁移版本 | `1.0.28`(12 条) | `1.0.28`(12 条) | **未变**(本次不动 DDL) |
| 明文口令 | 15 处 | **0 处** | 本件清理目标 |
> 源码树摘要算法:把 `01_后端源码/src` 下每个文件的「相对路径 + 该文件 SHA-256」按路径排序后拼接,再整体算一次 SHA-256。改一个字节即变,等效于"这一棵树"的指纹(不是 Git commit)。
---
## 二、本次改动文件逐个哈希(16 个)
| # | 文件 | 字节 | SHA-256(改后) |
|---|---|---|---|
| 1 | `01_后端源码/src/main/resources/application.yml` | 9,636 | `3912006F4C6432965DAF2F590D229509363A1FEAFF9D7FB60DF6F93E5D6C16C9` |
| 2 | `01_后端源码/src/main/java/cn/aomin/gateway/GatewayApplication.java` | 2,639 | `2C315FCBABD70D5282AE33CA00DCEAFB352D14253C6B3D33D9C2A3FC717DC1B6` |
| 3 | `05_启动构建与测试说明/启动构建与测试说明.md` | 16,116 | `D4AAF430598FD1DB121697B5519F0206D7CFA5552C6C400292CABF18730A8BC1` |
| 4 | `09_代码与版本/README.md` | 5,845 | `50F128904869F1F56676DA06A42794C77106154C946D91AEA95BF24A02CFA365` |
| 5 | `06_统一闭环题自测证据/alarm_retry_test.py` | 6,708 | `24F062EAE16BF449170FA5D7A9404364F16950262108C9247C378DC2C9966E2A` |
| 6 | `06_统一闭环题自测证据/closed_loop_test.py` | 9,579 | `F17E59D6B9F7B63CE7818108D4D574EEF60EB16FEB0E8E708EA91F15B5054C8E` |
| 7 | `06_统一闭环题自测证据/cmd_chain_test.py` | 17,290 | `1C4FD7CF30D7A4ADBA4D61683449B8954D2FA630E5FEB079C97E9BCC3958035B` |
| 8 | `06_统一闭环题自测证据/run_evidence.py` | 21,551 | `478D672726193874CD9544EAEDB92D6FA887A2BAAB7D211F753290204822CA37` |
| 9 | `06_统一闭环题自测证据/scope_test.py` | 11,005 | `ACE8150FBC566D5AF796D4FADA14CC9927E4C8D6114DB919C883F307309E0651` |
| 10 | `06_统一闭环题自测证据/stamp_evidence.py` | 10,670 | `DA33B7B431279FE5064420AD6C8BA7F48CED6E4B7EF79BC7AE0B3A0171D63035` |
| 11 | `06_统一闭环题自测证据/v15_fix_test.py` | 11,575 | `C0C073AE6495717AF902F94552D9E40A69DCC67A8D840ECC3C130F322F6518EA` |
| 12 | `06_统一闭环题自测证据/v16_fix_test.py` | 16,120 | `08C425D9620E414B56E2FEF59B9C632054F18BBA9EEBA92F640FB447CC3F51E7` |
| 13 | `06_统一闭环题自测证据/v17_merge_test.py` | 11,748 | `A2BCBED23683554B1E661BD85304F056B55F2149272A7CBC72D7EE92546F7603` |
| 14 | `06_统一闭环题自测证据/v18_fix_test.py` | 7,375 | `48EC29ACFA0F9E9EAE28971973B1095FBA77B7CF42C8027FBAC615D66C7197A4` |
| 15 | `06_统一闭环题自测证据/v19_gov008_test.py` | 15,881 | `8599CBCF41E81E670598310D003FC03BAAD21CB0B0130EADAAB7A15A8858A4DC` |
| 16 | `06_统一闭环题自测证据/v20_node_test.py` | 10,843 | `8DDBF8996B989D446D667D454DB222F2EAA15D0918F28C0033E4DAF53931088C` |
> 除以上 16 个,包内其余文件的哈希都在 `09_代码与版本/SHA256SUMS.txt` 里,本件已重新生成(原 131 项 → 现 131 项,内容更新)。
---
## 三、构建说明(可复现,约 21 秒)
### 环境
| 项 | 要求 |
|---|---|
| JDK | 21.0.2(`G:/aomin-dev/jdk-21.0.2`) |
| Maven | 3.x,离线仓库 `G:/aomin-dev/downloads/settings.xml` |
| 依赖 | 已预置在本地仓库,`-o` 离线模式可用 |
### 构建命令
```bash
cd <源码根>/aomin-gateway
mvn -o -q -s <settings.xml> -DskipTests clean package
# 产物:target/aomin-gateway-0.0.1-SNAPSHOT.jar
```
实测:**21 秒**,`exit=0`,产物 `43,517,256 B`。
### 启动(**必须带 `DB_PASS`**)
```bash
# Windows
set DB_PASS=<口令>
# Linux / macOS
export DB_PASS=<口令>
java -jar target/aomin-gateway-0.0.1-SNAPSHOT.jar --server.port=8080 --server.address=0.0.0.0
```
**缺少 `DB_PASS` 会立即中止**(退出码 1),不会带着空口令去连库:
```
[启动中止] 缺少必需的环境变量 DB_PASS(数据库口令)。
SEC-Z-01:程序不再内置任何默认口令,也不会回退到包内明文。
设置方法:Windows set DB_PASS=<口令> Linux/macOS export DB_PASS=<口令>
```
### 启动后自检
```bash
curl http://127.0.0.1:8080/health
```
本机实测返回:
```json
{"code":0,"msg":"ok","data":{"app":"aomin-gateway","db":"connected","tables":87,
"schema":"aomin_dev","redis":"connected","flyway":"1.0.28","alarmBacklog":0,
"alarmStale":0,"alarmDeadLetter":0,"auditFailures":0,"warnings":[],"status":"UP"}}
```
要点:`db: connected`、`tables: 87`、`flyway: 1.0.28`、`status: UP`。
---
## 四、入库建议(给陈工建主干时用)
### 4.1 `.gitignore`
见同目录 `gitignore_建议.txt`,核心是三条:
- `target/`、`*.jar` —— 构建产物不入库;
- `.env`、`*.local.yml`、`application-local.yml` —— 本地配置可能含口令;
- `06_统一闭环题自测证据/raw|out|screenshots/` —— 证据原始数据体积大、每轮变化,按受控目录单独交付,不进代码历史。
### 4.2 首次提交顺序(**顺序不能反**)
```
① 确认 15 处已清、扫描 0 命中
② git init
③ 放入 .gitignore
④ git add -A && git status ← 人工看一眼,确认没把 target/ 和 .env 加进来
⑤ git commit -m "初始提交:M-Z 业务底座 V1.9 基线(SEC-Z-01 清理后)"
⑥ git remote add origin <陈工给的地址> && git push -u origin main
```
**为什么必须先清后推**:git 记录每一版。今天删掉明文、明天再提交一次,第一条提交里照样有。要彻底清只能重写全部历史(`filter-repo`),成本远高于现在清 15 处。
**好消息**:当前 `G:/aomin-dev/app/aomin-gateway` **不是 git 仓库**,包内也没有 `.git`,不存在历史污染,属于干净起点。
### 4.3 交付目录建议
```
主干/
├── src/main/java/… 业务代码(M-Z)
├── src/main/resources/
│ ├── application.yml 已无默认口令
│ └── db/migration/ Flyway 12 条,最新 1.0.28
├── pom.xml
└── .gitignore
```
证据材料(`06_…/raw`、`06_…/out`、截图)**不进主干**,走受控目录交付。
---
## 五、版本标识一致性(四项对齐)
| 标识 | 值 |
|---|---|
| 模型版本 | V1.9(已冻结,本次仅为 SEC-Z-01 最小安全清理,**未升 V1.10**) |
| 源码树摘要 | `5E9F894B0D1071969A968F2A9B1451279ADF39D423148186886BFD064398F286` |
| 可执行 jar | `1F38F527605F09AA45E9E54528AFB11230469E1B197FA001C06E8725FD102192`(43,517,256 B) |
| 契约版本 | J-00 `1.8.1` |
| 迁移版本 | Flyway `1.0.28`,12 条 |
| 证据批次 | `R20261113-112443`,11 组 / 155 条 / 失败 0 |
---
## 六、交付包自检(打出包后实测)
| 项 | 结果 |
|---|---|
| 条目数 | 93(源码 87 + 4 份说明 + `gitignore_建议.txt` + `SHA256SUMS_源码契约迁移脚本.txt`) |
| 解压后 `aomin-gateway/src` | 84 个文件,与包内 `01_后端源码/src` **逐文件哈希完全一致** |
| 解压后含明文口令 | **0** |
| 独立编译 | 解压到空目录后 `mvn -o -q -s <settings> -DskipTests compile` → **exit 0**,`GatewayApplication.class` 正常生成 |
> 包自身的 SHA-256 只在交付渠道给出,**不写进包内**(写进去包一改哈希就变,形成死循环)。
---
*配套材料:`01` 差异清单、`02` 扫描报告、`04` 155 条证据适用边界、`gitignore_建议.txt`。*
@@ -0,0 +1,109 @@
# 155 条作者证据的适用边界(Z-02 附件)
| 项 | 内容 |
|---|---|
| 依据 | AM-S1-GOV-020 §4.4「Z-02:……现有 155 条作者证据的适用边界;若作为清理后当前结果,必须真实重跑」 |
| 处理结论 | **已真实重跑**,不是沿用清理前结果 |
| 批次号 | `R20261113-112443` |
| 结果 | **155 条 / 通过 155 / 未过 0**,11 组脚本 **失败 0 组** |
---
## 一、这 155 条是什么
| 证据文件 | 条数 | 覆盖内容 | 耗时 |
|---|---|---|---|
| `05_SOS链路.json` | —(非用例) | 单次闭环的原始请求/响应记录,不作计数 | 8.9s(与下一项同组) |
| `06_命令链.json` | 23 | 命令七态、回执、幂等、状态机边界 | 58.6s |
| `06_异常与权限.json` | 28 | 异常分支与对象级权限 | 同上组 |
| `07_告警失败不吞事件.json` | 5 | 故障注入:规则异常 / 库异常 / 重试 / 死信 | 84.6s |
| `08_闭环后半段.json` | 13 | 通知、家属确认、处置关闭 | 22.0s |
| `09_数据范围.json` | 16 | 数据权限与可见范围 | 19.9s |
| `10_摘要与状态防护.json` | 15 | rawRef 摘要、状态不可回退 | 34.0s |
| `11_超窗与死信.json` | 15 | 时间源分流、退避、死信、审计回滚 | 286.2s |
| `12_合并与替代.json` | 6 | 连续触发的合并与 superseded | 77.7s |
| `13_GOV008整改.json` | 16 | GOV-008 定点整改项 | 105.8s |
| `13_分区与一致性.json` | 8 | 分区维护、计数一致性 | 18.4s |
| `14_命令节点闭环.json` | 10 | 领取节点、代次、租约、错节点拒收 | 42.7s |
| **合计** | **155** | — | 约 13 分钟 |
---
## 二、为什么必须重跑(不是"为了好看")
清理动了三处会影响执行的东西:
1. **`application.yml` 的 `password`** 由「带默认值」改成「无默认值 + 缺参中止」——启动条件变了;
2. **12 个证据脚本** 的口令来源由「环境变量或内置默认」改成「只认环境变量」——脚本启动条件变了;
3. **`GatewayApplication`** 增加启动守卫——可执行产物变了。
换句话说,**清理前的 155 条是在"另一套启动条件"下跑出来的**,直接拿来当"清理后的当前结果"属于沿用旧结果。所以按 §4.4 那句"必须真实重跑"执行。
重跑后源码树摘要、jar 摘要、批次号全部更新,见 `03`。
---
## 三、这批证据能证明什么
| 能证明 | 依据 |
|---|---|
| 155 条在**这一棵源码树**(`5E9F894B…`)、**这一个 jar**(`1F38F527…`)、契约 `1.8.1`、迁移 `1.0.28` 上真实执行过 | 每份结果 JSON 的 `_meta` + `raw/_execution_manifest.json` |
| 每组是哪个脚本、什么参数、几点开始几点结束、耗时多少、退出码几 | `raw/_run_logs/*.log`(11 份完整 stdout+stderr) |
| 失败项不会被藏起来 | 清单外的结果自动标「历史结果补充索引」,**不计入通过数**;本次该数为 **0** |
| 清理没有破坏原有功能 | 与清理前同为 155/155,无新增失败 |
---
## 四、这批证据**不能**证明什么(必须写明,否则就是夸大)
| 不能证明 | 说明 |
|---|---|
| **不是接收方复验** | 这是**作者自测**。按 §9.2 第 4 条,收到文件 ≠ 接收通过;A 类关闭要谢工、陈工签认,本件不构成关闭 |
| **不含并发** | `v20_node_test.py` 是串行 10 条,**没有 Thread / 线程池 / 并发 PULL**。并发领取、节点与租约不匹配、重复/乱序/迟到回执,由谢工按完整矩阵补测 |
| **不含真机** | 全部是接口级与故障注入。真实设备的上报、断连、无恢复帧场景,要等黄工真机窗口 |
| **不代表生产性能** | 单机、本地 MySQL、单实例;没有压测、没有长稳 |
| **不覆盖前端与小程序** | 只覆盖 M-Z 业务层与内部契约 |
| **不是重复性统计** | 每项跑一次。偶发失败只能靠重跑发现,本批失败 0 不代表永不失败 |
| **不覆盖 A-Z-02「停机超窗」的完整场景** | 有故障注入与死信验证,但"停服 60 分钟以上再起、事件被处理"需在共同环境由谢工/陈工复验 |
---
## 五、怎么复跑(陈工/谢工可按原样执行)
```bash
# 1) 先起服务(必须带口令)
set DB_PASS=<口令>
java -jar aomin-gateway-0.0.1-SNAPSHOT.jar --server.port=8080 --server.address=0.0.0.0
# 2) 跑全部 11 组(约 13 分钟)
cd 06_统一闭环题自测证据
python run_all_evidence.py # 产出 raw/_execution_manifest.json + raw/_run_logs/*.log
python stamp_evidence.py # 给每份结果写 _meta.execution
python count_evidence.py # 对账,exit 0 = 155 条一致
```
前置环境变量(缺一个都会失败或跳项):
| 变量 | 用途 | 缺失后果 |
|---|---|---|
| `DB_PASS` | 应用启动 | **启动中止**(退出码 1) |
| `AOMIN_DB_PASS` | 12 个证据脚本 | **脚本退出**,提示设置方法 |
| `AOMIN_MYSQL` | mysql 客户端路径 | 找不到命令,相关断言失败 |
| `AOMIN_LOG` | 应用日志路径(`v17` 读横幅合并日志) | 误报"跳过 0 次" |
| `NO_PROXY` | 本机回环不走代理 | 报 `10061`,与用例无关 |
---
## 六、复验入口(对应 GOV-011 / GOV-016 的 A 类)
| 关注点 | 脚本 | 建议核对 |
|---|---|---|
| A-Z-01 命令节点与代次 | `v20_node_test.py`(10 条)+ `cmd_chain_test.py`(23 条) | 领取后 `claimed_node_id` 落库、错节点 `NODE_MISMATCH`、历史空节点 `NODE_UNKNOWN`、旧节点+新代次仍被拒 |
| A-Z-02 超窗补偿 | `v16_fix_test.py`(15 条)+ `alarm_retry_test.py`(5 条) | 无时间窗过滤、`receive_time` 库端打戳、退避与死信、人工回放 |
| A-Z-03 审计可靠性 | `v16_fix_test.py` 的审计回滚段 | 审计写失败 → 业务整条回滚;`audit_fail_log` 可查可补录;重启后不清零 |
复验请填:**结论(通过/失败/未执行/不适用)+ 证据位置 + 日期**。作者不单方面宣布关闭。
---
*关联:`01` 差异清单、`02` 扫描报告、`03` 哈希与构建说明。*
+51
View File
@@ -0,0 +1,51 @@
# ─────────────────────────────────────────────
# 代码主干 .gitignore(M-Z 业务底座)
# 原则:源码、迁移、契约、测试脚本进仓库;
# 构建产物、本地配置、日志、证据原始数据不进。
# ─────────────────────────────────────────────
# 构建产物
target/
build/
out/
*.class
*.jar
*.war
# 本地配置(可能含口令,绝不入库)
.env
.env.*
*.local.yml
application-local.yml
application-*-local.yml
application-secret.yml
# 日志与临时
logs/
*.log
*.tmp
*.bak
*.swp
# IDE
.idea/
*.iml
.vscode/
.settings/
.classpath
.project
# Python
__pycache__/
*.pyc
*.pyo
.pytest_cache/
# 证据原始数据(体积大、每轮变化;证据按受控目录单独交付,不进代码历史)
06_统一闭环题自测证据/raw/
06_统一闭环题自测证据/out/
06_统一闭环题自测证据/screenshots/
# 系统
.DS_Store
Thumbs.db
@@ -0,0 +1,12 @@
# 包内文件 SHA-256 清单(路径相对包根)
# 校验:在本包根目录执行 sha256sum -c SHA256SUMS_包内.txt
848de973be78a75f47f1e7d78242a9d2862ffa94699c5405b06a4e4408123572 README.md
55dfa815e9c2801a37d2dcff0ab095c975402ab66ef7ffe33a13af54bebbf994 sim/sim_device.py
72eaa3997aab635fea28b79480922c7e8162e650602665d32c1f13eff68e489f web/admin/index.html
98cee85935be9a55af85cd194aaa105ae9d4d3b931ef6faea5b7423314aec439 web/common.js
6166979b12559a690747f0190b2f8882ecd6086e0696527b700b0341891e062c web/family/index.html
e0200f12ac905c295ec760439756b330639a4e922ab19918d76d65319a0809a1 web/family/map-baidu.html
a68d78beb94fff53d91eb474081c4c907e3dcfb769609d6bf9212773bd7e06c0 web/family/map-offline.html
2a13542045932ecd4e8b2a5e64cd6a14215d49e3da22975c6db147994bb5650a web/family/map-picker.html
6328a6aa17f9a67bd55d96bf944bbd60a0fcb27c2b9dcd3a4a8366b7a01edbb9 web/index.html
@@ -0,0 +1,116 @@
# 包内文件 SHA-256 清单(路径相对包根)
# 校验:在本包根目录执行 sha256sum -c SHA256SUMS_包内.txt
# 说明:03_数据库与Flyway迁移 与 aomin-gateway/src/main/resources/db/migration 为同一套迁移的两次出现,不是两套。
71d8471d07c4c067f375de16bbc5920820ea30a1bf05a60898c5ee71043c0267 00_包内容与使用说明.md
65dff0ffaba7ab185e7b4b7f3bd2c0a6782ad9faeab3669cbc6aeb1ede74dd05 01_SEC-Z-01_最小安全清理差异清单.md
2f88825dbd03c828ce19ac7736138a46d1ba9badee018087dde60ab012096cdb 02_SEC-Z-01_敏感信息扫描报告.md
f01f5a40a1423b0cdb69a0ff56227652da58bfc87aa9fd3860ca5ada5168978b 03_Z-02_清理后哈希与构建说明.md
07e5263e3d755535a5fadaa18b8529d6244af975daa06faef9b3e908bcf0c7cc 03_数据库与Flyway迁移/V1.0.0__baseline_schema.sql
8be5a9816f1f661d0a54c2a23c56079b76df88f47bd153609a94c659e42e7299 03_数据库与Flyway迁移/V1.0.18__raw_link_columns.sql
45e64f88796d46267b8f4f67f76d9ea5ec3ad34fbbe62736c4768216cbaf23a2 03_数据库与Flyway迁移/V1.0.19__household_geo.sql
cde7f6df804cdc1a827b0a109815c0af3a52d7efa4f35a170f262d8ce429418d 03_数据库与Flyway迁移/V1.0.20__notify_read_at.sql
7751f42076e39874a85f147cf1ce5c45653161207cb06e6042d60f7888d90710 03_数据库与Flyway迁移/V1.0.21__alarm_chain_seq.sql
3306548d3c02f6750cfb93c535e5c9e98062710ed6d4d099f69e0fc7a7b8edc1 03_数据库与Flyway迁移/V1.0.22__alarm_source_event.sql
9c941b1675d5a82daef81d7f297d03b206a028deb9923b78cd92f473a9ca8272 03_数据库与Flyway迁移/V1.0.23__command_seven_states.sql
ec0b7e0f23f74f52bd373fc0fb7d84bacdda9be62c19e9b6163bf669da442af2 03_数据库与Flyway迁移/V1.0.24__region_scope.sql
e480a143b8a079971c285d63bf51252eecc2034054459f83e2104c877fa51d40 03_数据库与Flyway迁移/V1.0.25__raw_ref_64_and_gap.sql
61e8c5f2167de4588e8aa337c9d45dfa12015680d6b12d1d456b3f159770c29e 03_数据库与Flyway迁移/V1.0.26__alarm_retry_deadletter.sql
778141cf45e9b3f62afc9bdbb3f7079794daedfba9f0a20caf84fe90eb3503dd 03_数据库与Flyway迁移/V1.0.27__receive_time_local_and_audit_fail.sql
2d6e90a3e6a2db9771b7820df4dfc7e58c211627ac02271612e2511764e83286 03_数据库与Flyway迁移/V1.0.28__command_claim_node.sql
94fc66069dabce35327e4ab2af724f7269bae6ced4f9c4de2e6681ca3498b36c 03_数据库与Flyway迁移/数据库与迁移说明.md
981c79f28b85d56a225c524e297af1cf16d63acdbf763f520a17fa65454feeb6 04_155条证据适用边界.md
1cc027d867b7538becd0e566666d772c09f092db2b81d6363abc643a03ec4821 04_OpenAPI与内部IoT契约/6_J-00_OpenAPI使用说明_V1.8.md
80b3ff9468fb71ed47b5378658e3354a0ac2f1338a253edf45f07343d0ffba1f 04_OpenAPI与内部IoT契约/6_J-00_接入接口规格_V1.8.md
99a659435512909e0b48ede82114325ba562958f65ff0d49b3772b672645fab7 04_OpenAPI与内部IoT契约/6_J-00_接入接口规格_V1.8_OpenAPI.yaml
6d16a685bb765b3aa1ad54039686c8f5936de22bb83eaba276cd5be022382877 04_OpenAPI与内部IoT契约/Mock样例.md
89c492c591e4c591351b5f3ddabe24de5566c9e19f2dbb26312dedb3e81a8484 04_OpenAPI与内部IoT契约/内部五接口定义.md
5272bb1d4d31bc7677cf12d44aafde4e2ec8e4eeb1bb09fe5a444da49cac6a5e 04_OpenAPI与内部IoT契约/状态机.md
1ddf2498987f80336b568efff85d0964264a06435d5c84e76fce115bcf7c83dd aomin-gateway/README.md
dbc64823853fef475d8735475a62f92e95df841e8a82fc8ab13de573b0438270 aomin-gateway/pom.xml
2c315fcbabd70d5282ae33ca00dceafb352d14253c6b3d33d9c2a3fc717dc1b6 aomin-gateway/src/main/java/cn/aomin/gateway/GatewayApplication.java
8fa464b5a4dc0c246b5585bbaabd7ad4fea5271d524eb6e9d4b7b91faf72c011 aomin-gateway/src/main/java/cn/aomin/gateway/biz/DemoDataInitializer.java
116a3b56bbb3b2b051ed239a5a4b82111c9c010b0a620efdbfe59a2528fcdc35 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/AdminController.java
a7f7f7f3129a23d09d40f5589bb65c3cb7d933a8ceba8a929c20501b05b45432 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/AlarmController.java
58fadae221f2763f2f51e267ddefdf8478661a15eb1583686205119c6bb9e434 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/AuthController.java
d27df1ed1695ffcc75b9c2e00fa1111088694ab4ddfc04af9d29aad8b9d2b0cb aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/DeviceController.java
95b432a2c7a84b0f834c21b721e4131a7c71edb30f42cbb49dc014182e0201f3 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/FamilyController.java
cb04548d6386555e7df9070e19ebe0b007c2e825646c685ecfa80e78b6204d7c aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/NotifyController.java
2c40eaf77abd7a73d19022c85faf8dc319ccff7948fbd8553b595c4653e3c324 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/NotifyStreamController.java
84829224a845f88986683c4c87dd37b9b4dae4b27405e558b78b5e8fe34bbdbf aomin-gateway/src/main/java/cn/aomin/gateway/biz/dao/BizDao.java
f44f3cc9af74c6639241633aa30039e1baa22fad40487c05c5b3a8f172ba93bf aomin-gateway/src/main/java/cn/aomin/gateway/biz/dto/Dtos.java
e936b9acf37ef2f4d61840665329fc503df72d646480cc8b5a6fd438d1cd574b aomin-gateway/src/main/java/cn/aomin/gateway/biz/engine/AlarmEngine.java
2de186fdede0cd58e2d2f54d08616436a760557b9e9f53a13518d69cf7359954 aomin-gateway/src/main/java/cn/aomin/gateway/biz/engine/AlarmTrigger.java
26e417d5d41adf35d50d0917f2e0a17ac6f7d688bc18e159c4080ea3ba63f872 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/AdminService.java
30b0f7d7ce7a753e24b78e252743e9524d7e11f5ec846655e72b7964f702bc33 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/AlarmService.java
d296c463aa6416cc7a4df87c0e821f3989b4cc9bd5b9e3d06e06f5e3fc549894 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/AuthService.java
4bcf644491cd6d0b924fd2f42e07b6583ad9499d06a8beed7d28bc9c89fc768e aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/BizDeviceService.java
84fb58da94f2a9395e372746b850ccf6f03b8f3c71bd1be1dc2400898a62989b aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/FamilyService.java
f7f6bd7e73741b2bbc774e8e05f91e344f00ba893727209fdfb22cf908201448 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/NotifyService.java
18ba93498aad6ffab1680e6cb780eb934dbd85f29625b2dd688beb362894d970 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/PiiService.java
18191289464412e7b1408662421b83291f1a95378a868c5f9892489964652a5b aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/SmsGateway.java
5d4d8c17959734e7c969c681369850be76fee08f0cc50910d5344f2832f9c65e aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/AdminScope.java
00378b6a55265fb2adce15480faa80a20e85bfc7268efd074467427f607df560 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/AuditGuard.java
88b74c167b8e7b3f594d82a501072eb2dc736e07ee52e9e5ba87e05f3a5d03e2 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/AuthContext.java
e77c7b1cad576b5ee23ee05acb27f62af6a5d1cb80fbc955ce6c25db62a668cb aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/BizAuthInterceptor.java
fc12b2e26e2e39012479df1f4a22a2a4605ffe67d1793edbe01baef0aa2ac7b5 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/Crypto.java
65994b8ccfba75de35fe6a35f1632cf7d3a3a5e3d9412018facd6c2ecd66126e aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/PageResult.java
e255bc089e83bd35fdce6d5d5521bd0f05c78c84919775459fb0a2f4ba26f88a aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/PartitionMaintainer.java
ecc3e56a10b9e02a828a9c54314b9724f107f11506a354dd842c783b2ab69be8 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/SseHub.java
46e4c0987cfa35f2067ccb1951dbd9f04321dfb01d22f9392242521d8d35e1d7 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/SuppressGate.java
b068a6413895de569ec33d9b967c5aca13818258abc5747adf16d5b25adcf31f aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/TokenService.java
5d3b73cb52245ea2fcfe301fe6d794350d7f0bebd32851627eef5c3a8507ea42 aomin-gateway/src/main/java/cn/aomin/gateway/common/ApiResponse.java
6ef484310f27e7b951a490d7ae32786fd27e9d1fe14e38487b0451c62fbaf4a3 aomin-gateway/src/main/java/cn/aomin/gateway/common/BizException.java
3aa57442b289ed4f7a27120fbb69153dc670a94efbcccf361860e7e02a857613 aomin-gateway/src/main/java/cn/aomin/gateway/common/GlobalExceptionHandler.java
3fdb0e845a3b7d1b5bc025b5ef784b0b5635a514df522761973afc7c4a6226ed aomin-gateway/src/main/java/cn/aomin/gateway/config/GatewayProperties.java
615c60e33233c424764d4a1e4082375ca2244a836aa04619d7706033280efa76 aomin-gateway/src/main/java/cn/aomin/gateway/config/RedisConfig.java
879e6341ee07c5b933c5f3cda0dcf04196eea89189feacbdb048af5f723b2cfd aomin-gateway/src/main/java/cn/aomin/gateway/config/WebConfig.java
745e893fc2606ff4c4b694a9b9ca1a4f51e14076878adfb19e2a5863eca8923e aomin-gateway/src/main/java/cn/aomin/gateway/controller/CommandController.java
bf40814adbc2e5b708ac8bd521f4f4e28b58f18230b234ba539d0c72cc7584a9 aomin-gateway/src/main/java/cn/aomin/gateway/controller/HealthController.java
087b82f0f4b49bfbfadfab31b9354c5355e28acdca2d1354afd887432a01bf90 aomin-gateway/src/main/java/cn/aomin/gateway/controller/IngestController.java
bc872eef7be69cbab1b3b014efaf3c519aae4a37d9e071932b91f52673c2c94f aomin-gateway/src/main/java/cn/aomin/gateway/dao/CommandDao.java
9f495136f8a13420ce0746783873d437a875fd15d422e55816080c30f6d32dd0 aomin-gateway/src/main/java/cn/aomin/gateway/dao/DeviceDao.java
d9410152ba5940bb2b8c75d45466d94effdc0e3417ecfdbc4e913007a9778438 aomin-gateway/src/main/java/cn/aomin/gateway/dao/IotEventDao.java
e8140a24b1d0f037bdae70dc6e49b4481b9ce8ce60b24f9e46df13232bd9efcf aomin-gateway/src/main/java/cn/aomin/gateway/dao/RawFrameDao.java
61fdbf0f9bca0e7d86e9f0cd37bece1c90412de0107faf21df8beee0a3c9be80 aomin-gateway/src/main/java/cn/aomin/gateway/dao/RawRefGapDao.java
d81e3276b697cde3c024bcc64f5c8420f91cdeadc290247750cd0e2be883877e aomin-gateway/src/main/java/cn/aomin/gateway/dao/TechCacheDao.java
17e6215e0b9868369bfe28d623c9a18f619114df79590cddfc8b57590281e1cf aomin-gateway/src/main/java/cn/aomin/gateway/dao/UnregisteredDao.java
c1b4fd06e88dd1085396455396e3d68bfabfbacbfc59a71bed993270c3824a5e aomin-gateway/src/main/java/cn/aomin/gateway/domain/Channel.java
dc142f281fab1a31ae0c6040baef02e10ef2359d97809b95ceae5bd189ea45c2 aomin-gateway/src/main/java/cn/aomin/gateway/domain/DeviceRow.java
bccd9cfba21c63449734a1d1585ab02c0dd36257e5b238d409ddee6a1a4491f5 aomin-gateway/src/main/java/cn/aomin/gateway/domain/TimeResult.java
c3a4d760ab8596e58eeb8ca9ad08e55066c99b4867a24a003d8f63fab814b465 aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/CommandResultRequest.java
17b9924b78175c83ef20c2f390b04106ffeb6bbf1770fddcb061b7cf62f01a72 aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/EventBatchRequest.java
fad99f362b82adb7a4f06eace7583711295ffae4aa6d12e4b20965b0acc77f6b aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/IngestResponse.java
07ff87965099f00c9ba0f8adcf1a4649e7359d71d380e6b7350687cd35d1a3ab aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/PendingCommandResponse.java
ec23b4048a1a505e712c68f4e8bc941c3cb9d83168db18487930f8fdd457c4fd aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/RawFrameBatchRequest.java
954eb9c474eba1a75fa10df8310616a6892d80ff2f9ed5efbbc70890f7a9e391 aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/StatusBatchRequest.java
ab1222707fdc4e8f5ed059fae8135cb676af0053987a29bf992eb915d1c7be99 aomin-gateway/src/main/java/cn/aomin/gateway/interceptor/AuthInterceptor.java
b99a9bb97500b5b2e1096a3049375cd2348b8b478f1700878968410946c1fc2b aomin-gateway/src/main/java/cn/aomin/gateway/interceptor/InternalNetworkGuard.java
368726ecbd86aec46a0c145ab47eb79eedddad3e7e3e16c35e2eade681aa67b7 aomin-gateway/src/main/java/cn/aomin/gateway/security/CachedBodyFilter.java
6efcd0422225e4d2835225847ded8c4382efa638307d866b8252e11882f71f3f aomin-gateway/src/main/java/cn/aomin/gateway/security/IdempotencyStore.java
474c3aea6bee182ed96fbac6eec478a1d3bc025e2923736a07bf45b59d0ce61b aomin-gateway/src/main/java/cn/aomin/gateway/security/IdempotentExecutor.java
8452ee990a69d223ae4d16d5b1dea152823d10fb2b59169190965ef8e9c9a33f aomin-gateway/src/main/java/cn/aomin/gateway/security/NonceStore.java
8cc16695275e6a69615b50c4a8d88a3e78302d528c8f4f795da1c276099c95de aomin-gateway/src/main/java/cn/aomin/gateway/security/RateLimiter.java
96e0c685a04299f854b7586383eee28c31007def56f692f426bedb258ab4554b aomin-gateway/src/main/java/cn/aomin/gateway/security/SignatureVerifier.java
c38d62b372c8aac7f4f9bb27d8c3d9652539bd441d68f35b4b2c718dec8b9a87 aomin-gateway/src/main/java/cn/aomin/gateway/service/CommandService.java
bfbdec7c215746a09bebfb8b2594d025b1db4c931bbff11c90a2b6c2689d16b0 aomin-gateway/src/main/java/cn/aomin/gateway/service/IngestService.java
66d22c25a1b2871c5802b686a2c47836264b15d3cde89d723b037865ca2ff380 aomin-gateway/src/main/java/cn/aomin/gateway/service/RawFrameService.java
2f9abbae9b5c1c227b2b7816845e2145f9169ac2098f64e0017076c55e56beb0 aomin-gateway/src/main/java/cn/aomin/gateway/service/StatusService.java
77387e73bb73e7c687da953a83851d9654c204171aa874254a72fe521f6178e7 aomin-gateway/src/main/java/cn/aomin/gateway/support/EventTimeResolver.java
e77267248d3aa7ecc9eab37ca039902c82e9f3879936ad742035c2993b8d25fd aomin-gateway/src/main/java/cn/aomin/gateway/support/EventValidator.java
f4172bca391f5b4ebb90bf74a11ae2c998ef763c2b2908cd9c8b13bc56f86150 aomin-gateway/src/main/java/cn/aomin/gateway/support/LinkTypeResolver.java
3912006f4c6432965daf2f590d229509363a1feaff9d7fb60df6f93e5d6c16c9 aomin-gateway/src/main/resources/application.yml
07e5263e3d755535a5fadaa18b8529d6244af975daa06faef9b3e908bcf0c7cc aomin-gateway/src/main/resources/db/migration/V1.0.0__baseline_schema.sql
8be5a9816f1f661d0a54c2a23c56079b76df88f47bd153609a94c659e42e7299 aomin-gateway/src/main/resources/db/migration/V1.0.18__raw_link_columns.sql
45e64f88796d46267b8f4f67f76d9ea5ec3ad34fbbe62736c4768216cbaf23a2 aomin-gateway/src/main/resources/db/migration/V1.0.19__household_geo.sql
cde7f6df804cdc1a827b0a109815c0af3a52d7efa4f35a170f262d8ce429418d aomin-gateway/src/main/resources/db/migration/V1.0.20__notify_read_at.sql
7751f42076e39874a85f147cf1ce5c45653161207cb06e6042d60f7888d90710 aomin-gateway/src/main/resources/db/migration/V1.0.21__alarm_chain_seq.sql
3306548d3c02f6750cfb93c535e5c9e98062710ed6d4d099f69e0fc7a7b8edc1 aomin-gateway/src/main/resources/db/migration/V1.0.22__alarm_source_event.sql
9c941b1675d5a82daef81d7f297d03b206a028deb9923b78cd92f473a9ca8272 aomin-gateway/src/main/resources/db/migration/V1.0.23__command_seven_states.sql
ec0b7e0f23f74f52bd373fc0fb7d84bacdda9be62c19e9b6163bf669da442af2 aomin-gateway/src/main/resources/db/migration/V1.0.24__region_scope.sql
e480a143b8a079971c285d63bf51252eecc2034054459f83e2104c877fa51d40 aomin-gateway/src/main/resources/db/migration/V1.0.25__raw_ref_64_and_gap.sql
61e8c5f2167de4588e8aa337c9d45dfa12015680d6b12d1d456b3f159770c29e aomin-gateway/src/main/resources/db/migration/V1.0.26__alarm_retry_deadletter.sql
778141cf45e9b3f62afc9bdbb3f7079794daedfba9f0a20caf84fe90eb3503dd aomin-gateway/src/main/resources/db/migration/V1.0.27__receive_time_local_and_audit_fail.sql
2d6e90a3e6a2db9771b7820df4dfc7e58c211627ac02271612e2511764e83286 aomin-gateway/src/main/resources/db/migration/V1.0.28__command_claim_node.sql
8f2bf8c6287bdf169546c664c2af98b8153412247c5c2e65b68570bfec412f54 aomin-gateway/start-gateway.bat
84ce77a2d36ce08f0807d9c5476af5aa9490daee70c5f9f233f7bf115a52ac89 gitignore_建议.txt
+84
View File
@@ -0,0 +1,84 @@
2c315fcbabd70d5282ae33ca00dceafb352d14253c6b3d33d9c2a3fc717dc1b6 aomin-gateway/src/main/java/cn/aomin/gateway/GatewayApplication.java
8fa464b5a4dc0c246b5585bbaabd7ad4fea5271d524eb6e9d4b7b91faf72c011 aomin-gateway/src/main/java/cn/aomin/gateway/biz/DemoDataInitializer.java
116a3b56bbb3b2b051ed239a5a4b82111c9c010b0a620efdbfe59a2528fcdc35 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/AdminController.java
a7f7f7f3129a23d09d40f5589bb65c3cb7d933a8ceba8a929c20501b05b45432 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/AlarmController.java
58fadae221f2763f2f51e267ddefdf8478661a15eb1583686205119c6bb9e434 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/AuthController.java
d27df1ed1695ffcc75b9c2e00fa1111088694ab4ddfc04af9d29aad8b9d2b0cb aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/DeviceController.java
95b432a2c7a84b0f834c21b721e4131a7c71edb30f42cbb49dc014182e0201f3 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/FamilyController.java
cb04548d6386555e7df9070e19ebe0b007c2e825646c685ecfa80e78b6204d7c aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/NotifyController.java
2c40eaf77abd7a73d19022c85faf8dc319ccff7948fbd8553b595c4653e3c324 aomin-gateway/src/main/java/cn/aomin/gateway/biz/controller/NotifyStreamController.java
84829224a845f88986683c4c87dd37b9b4dae4b27405e558b78b5e8fe34bbdbf aomin-gateway/src/main/java/cn/aomin/gateway/biz/dao/BizDao.java
f44f3cc9af74c6639241633aa30039e1baa22fad40487c05c5b3a8f172ba93bf aomin-gateway/src/main/java/cn/aomin/gateway/biz/dto/Dtos.java
e936b9acf37ef2f4d61840665329fc503df72d646480cc8b5a6fd438d1cd574b aomin-gateway/src/main/java/cn/aomin/gateway/biz/engine/AlarmEngine.java
2de186fdede0cd58e2d2f54d08616436a760557b9e9f53a13518d69cf7359954 aomin-gateway/src/main/java/cn/aomin/gateway/biz/engine/AlarmTrigger.java
26e417d5d41adf35d50d0917f2e0a17ac6f7d688bc18e159c4080ea3ba63f872 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/AdminService.java
30b0f7d7ce7a753e24b78e252743e9524d7e11f5ec846655e72b7964f702bc33 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/AlarmService.java
d296c463aa6416cc7a4df87c0e821f3989b4cc9bd5b9e3d06e06f5e3fc549894 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/AuthService.java
4bcf644491cd6d0b924fd2f42e07b6583ad9499d06a8beed7d28bc9c89fc768e aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/BizDeviceService.java
84fb58da94f2a9395e372746b850ccf6f03b8f3c71bd1be1dc2400898a62989b aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/FamilyService.java
f7f6bd7e73741b2bbc774e8e05f91e344f00ba893727209fdfb22cf908201448 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/NotifyService.java
18ba93498aad6ffab1680e6cb780eb934dbd85f29625b2dd688beb362894d970 aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/PiiService.java
18191289464412e7b1408662421b83291f1a95378a868c5f9892489964652a5b aomin-gateway/src/main/java/cn/aomin/gateway/biz/service/SmsGateway.java
5d4d8c17959734e7c969c681369850be76fee08f0cc50910d5344f2832f9c65e aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/AdminScope.java
00378b6a55265fb2adce15480faa80a20e85bfc7268efd074467427f607df560 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/AuditGuard.java
88b74c167b8e7b3f594d82a501072eb2dc736e07ee52e9e5ba87e05f3a5d03e2 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/AuthContext.java
e77c7b1cad576b5ee23ee05acb27f62af6a5d1cb80fbc955ce6c25db62a668cb aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/BizAuthInterceptor.java
fc12b2e26e2e39012479df1f4a22a2a4605ffe67d1793edbe01baef0aa2ac7b5 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/Crypto.java
65994b8ccfba75de35fe6a35f1632cf7d3a3a5e3d9412018facd6c2ecd66126e aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/PageResult.java
e255bc089e83bd35fdce6d5d5521bd0f05c78c84919775459fb0a2f4ba26f88a aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/PartitionMaintainer.java
ecc3e56a10b9e02a828a9c54314b9724f107f11506a354dd842c783b2ab69be8 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/SseHub.java
46e4c0987cfa35f2067ccb1951dbd9f04321dfb01d22f9392242521d8d35e1d7 aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/SuppressGate.java
b068a6413895de569ec33d9b967c5aca13818258abc5747adf16d5b25adcf31f aomin-gateway/src/main/java/cn/aomin/gateway/biz/support/TokenService.java
5d3b73cb52245ea2fcfe301fe6d794350d7f0bebd32851627eef5c3a8507ea42 aomin-gateway/src/main/java/cn/aomin/gateway/common/ApiResponse.java
6ef484310f27e7b951a490d7ae32786fd27e9d1fe14e38487b0451c62fbaf4a3 aomin-gateway/src/main/java/cn/aomin/gateway/common/BizException.java
3aa57442b289ed4f7a27120fbb69153dc670a94efbcccf361860e7e02a857613 aomin-gateway/src/main/java/cn/aomin/gateway/common/GlobalExceptionHandler.java
3fdb0e845a3b7d1b5bc025b5ef784b0b5635a514df522761973afc7c4a6226ed aomin-gateway/src/main/java/cn/aomin/gateway/config/GatewayProperties.java
615c60e33233c424764d4a1e4082375ca2244a836aa04619d7706033280efa76 aomin-gateway/src/main/java/cn/aomin/gateway/config/RedisConfig.java
879e6341ee07c5b933c5f3cda0dcf04196eea89189feacbdb048af5f723b2cfd aomin-gateway/src/main/java/cn/aomin/gateway/config/WebConfig.java
745e893fc2606ff4c4b694a9b9ca1a4f51e14076878adfb19e2a5863eca8923e aomin-gateway/src/main/java/cn/aomin/gateway/controller/CommandController.java
bf40814adbc2e5b708ac8bd521f4f4e28b58f18230b234ba539d0c72cc7584a9 aomin-gateway/src/main/java/cn/aomin/gateway/controller/HealthController.java
087b82f0f4b49bfbfadfab31b9354c5355e28acdca2d1354afd887432a01bf90 aomin-gateway/src/main/java/cn/aomin/gateway/controller/IngestController.java
bc872eef7be69cbab1b3b014efaf3c519aae4a37d9e071932b91f52673c2c94f aomin-gateway/src/main/java/cn/aomin/gateway/dao/CommandDao.java
9f495136f8a13420ce0746783873d437a875fd15d422e55816080c30f6d32dd0 aomin-gateway/src/main/java/cn/aomin/gateway/dao/DeviceDao.java
d9410152ba5940bb2b8c75d45466d94effdc0e3417ecfdbc4e913007a9778438 aomin-gateway/src/main/java/cn/aomin/gateway/dao/IotEventDao.java
e8140a24b1d0f037bdae70dc6e49b4481b9ce8ce60b24f9e46df13232bd9efcf aomin-gateway/src/main/java/cn/aomin/gateway/dao/RawFrameDao.java
61fdbf0f9bca0e7d86e9f0cd37bece1c90412de0107faf21df8beee0a3c9be80 aomin-gateway/src/main/java/cn/aomin/gateway/dao/RawRefGapDao.java
d81e3276b697cde3c024bcc64f5c8420f91cdeadc290247750cd0e2be883877e aomin-gateway/src/main/java/cn/aomin/gateway/dao/TechCacheDao.java
17e6215e0b9868369bfe28d623c9a18f619114df79590cddfc8b57590281e1cf aomin-gateway/src/main/java/cn/aomin/gateway/dao/UnregisteredDao.java
c1b4fd06e88dd1085396455396e3d68bfabfbacbfc59a71bed993270c3824a5e aomin-gateway/src/main/java/cn/aomin/gateway/domain/Channel.java
dc142f281fab1a31ae0c6040baef02e10ef2359d97809b95ceae5bd189ea45c2 aomin-gateway/src/main/java/cn/aomin/gateway/domain/DeviceRow.java
bccd9cfba21c63449734a1d1585ab02c0dd36257e5b238d409ddee6a1a4491f5 aomin-gateway/src/main/java/cn/aomin/gateway/domain/TimeResult.java
c3a4d760ab8596e58eeb8ca9ad08e55066c99b4867a24a003d8f63fab814b465 aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/CommandResultRequest.java
17b9924b78175c83ef20c2f390b04106ffeb6bbf1770fddcb061b7cf62f01a72 aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/EventBatchRequest.java
fad99f362b82adb7a4f06eace7583711295ffae4aa6d12e4b20965b0acc77f6b aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/IngestResponse.java
07ff87965099f00c9ba0f8adcf1a4649e7359d71d380e6b7350687cd35d1a3ab aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/PendingCommandResponse.java
ec23b4048a1a505e712c68f4e8bc941c3cb9d83168db18487930f8fdd457c4fd aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/RawFrameBatchRequest.java
954eb9c474eba1a75fa10df8310616a6892d80ff2f9ed5efbbc70890f7a9e391 aomin-gateway/src/main/java/cn/aomin/gateway/domain/dto/StatusBatchRequest.java
ab1222707fdc4e8f5ed059fae8135cb676af0053987a29bf992eb915d1c7be99 aomin-gateway/src/main/java/cn/aomin/gateway/interceptor/AuthInterceptor.java
b99a9bb97500b5b2e1096a3049375cd2348b8b478f1700878968410946c1fc2b aomin-gateway/src/main/java/cn/aomin/gateway/interceptor/InternalNetworkGuard.java
368726ecbd86aec46a0c145ab47eb79eedddad3e7e3e16c35e2eade681aa67b7 aomin-gateway/src/main/java/cn/aomin/gateway/security/CachedBodyFilter.java
6efcd0422225e4d2835225847ded8c4382efa638307d866b8252e11882f71f3f aomin-gateway/src/main/java/cn/aomin/gateway/security/IdempotencyStore.java
474c3aea6bee182ed96fbac6eec478a1d3bc025e2923736a07bf45b59d0ce61b aomin-gateway/src/main/java/cn/aomin/gateway/security/IdempotentExecutor.java
8452ee990a69d223ae4d16d5b1dea152823d10fb2b59169190965ef8e9c9a33f aomin-gateway/src/main/java/cn/aomin/gateway/security/NonceStore.java
8cc16695275e6a69615b50c4a8d88a3e78302d528c8f4f795da1c276099c95de aomin-gateway/src/main/java/cn/aomin/gateway/security/RateLimiter.java
96e0c685a04299f854b7586383eee28c31007def56f692f426bedb258ab4554b aomin-gateway/src/main/java/cn/aomin/gateway/security/SignatureVerifier.java
c38d62b372c8aac7f4f9bb27d8c3d9652539bd441d68f35b4b2c718dec8b9a87 aomin-gateway/src/main/java/cn/aomin/gateway/service/CommandService.java
bfbdec7c215746a09bebfb8b2594d025b1db4c931bbff11c90a2b6c2689d16b0 aomin-gateway/src/main/java/cn/aomin/gateway/service/IngestService.java
66d22c25a1b2871c5802b686a2c47836264b15d3cde89d723b037865ca2ff380 aomin-gateway/src/main/java/cn/aomin/gateway/service/RawFrameService.java
2f9abbae9b5c1c227b2b7816845e2145f9169ac2098f64e0017076c55e56beb0 aomin-gateway/src/main/java/cn/aomin/gateway/service/StatusService.java
77387e73bb73e7c687da953a83851d9654c204171aa874254a72fe521f6178e7 aomin-gateway/src/main/java/cn/aomin/gateway/support/EventTimeResolver.java
e77267248d3aa7ecc9eab37ca039902c82e9f3879936ad742035c2993b8d25fd aomin-gateway/src/main/java/cn/aomin/gateway/support/EventValidator.java
f4172bca391f5b4ebb90bf74a11ae2c998ef763c2b2908cd9c8b13bc56f86150 aomin-gateway/src/main/java/cn/aomin/gateway/support/LinkTypeResolver.java
3912006f4c6432965daf2f590d229509363a1feaff9d7fb60df6f93e5d6c16c9 aomin-gateway/src/main/resources/application.yml
07e5263e3d755535a5fadaa18b8529d6244af975daa06faef9b3e908bcf0c7cc aomin-gateway/src/main/resources/db/migration/V1.0.0__baseline_schema.sql
8be5a9816f1f661d0a54c2a23c56079b76df88f47bd153609a94c659e42e7299 aomin-gateway/src/main/resources/db/migration/V1.0.18__raw_link_columns.sql
45e64f88796d46267b8f4f67f76d9ea5ec3ad34fbbe62736c4768216cbaf23a2 aomin-gateway/src/main/resources/db/migration/V1.0.19__household_geo.sql
cde7f6df804cdc1a827b0a109815c0af3a52d7efa4f35a170f262d8ce429418d aomin-gateway/src/main/resources/db/migration/V1.0.20__notify_read_at.sql
7751f42076e39874a85f147cf1ce5c45653161207cb06e6042d60f7888d90710 aomin-gateway/src/main/resources/db/migration/V1.0.21__alarm_chain_seq.sql
3306548d3c02f6750cfb93c535e5c9e98062710ed6d4d099f69e0fc7a7b8edc1 aomin-gateway/src/main/resources/db/migration/V1.0.22__alarm_source_event.sql
9c941b1675d5a82daef81d7f297d03b206a028deb9923b78cd92f473a9ca8272 aomin-gateway/src/main/resources/db/migration/V1.0.23__command_seven_states.sql
ec0b7e0f23f74f52bd373fc0fb7d84bacdda9be62c19e9b6163bf669da442af2 aomin-gateway/src/main/resources/db/migration/V1.0.24__region_scope.sql
e480a143b8a079971c285d63bf51252eecc2034054459f83e2104c877fa51d40 aomin-gateway/src/main/resources/db/migration/V1.0.25__raw_ref_64_and_gap.sql
61e8c5f2167de4588e8aa337c9d45dfa12015680d6b12d1d456b3f159770c29e aomin-gateway/src/main/resources/db/migration/V1.0.26__alarm_retry_deadletter.sql
778141cf45e9b3f62afc9bdbb3f7079794daedfba9f0a20caf84fe90eb3503dd aomin-gateway/src/main/resources/db/migration/V1.0.27__receive_time_local_and_audit_fail.sql
2d6e90a3e6a2db9771b7820df4dfc7e58c211627ac02271612e2511764e83286 aomin-gateway/src/main/resources/db/migration/V1.0.28__command_claim_node.sql
+89
View File
@@ -0,0 +1,89 @@
# 原始交付包指纹
本文件记录周工交付的 `SEC-Z-01 清理后` 源码包的指纹,用途只有一个:**证明仓库里这份代码与交付确认过的那份是同一份**。
## 一、两个 SHA256SUMS 的作用范围(重要)
`docs/reference/` 下的两个清单是交付当时生成的,路径以**原交付包根**为基准。
| 清单 | 覆盖 | 现在还能直接校验吗 |
|---|---|---|
| `SHA256SUMS_原始交付包.txt` | 原包全部 108 个条目 | **不能**。仓库目录已重排(`02_前端与设备端/` → `demo/` 等),路径对不上 |
| `SHA256SUMS_前端与设备端.txt` | 原包内演示前端 8 个文件 | **不能**,同上 |
保留它们不是为了"跑一下 `sha256sum -c`",而是为了在需要追溯时能回答:**交付当天每个文件的哈希是多少**。路径变了,文件内容没变——逐文件比对哈希即可证明。
## 二、版本标识(与 `handover/03` 第五节一致)
| 标识 | 值 |
|---|---|
| 模型版本 | V1.9(已冻结;本次仅 SEC-Z-01 最小安全清理,未升 V1.10) |
| 内部契约 | J-00 1.8.1 |
| 迁移序列 | Flyway 1.0.28,12 条 |
| 源码树摘要 | `5E9F894B0D1071969A968F2A9B1451279ADF39D423148186886BFD064398F286` |
| 可执行 jar | `1F38F527605F09AA45E9E54528AFB11230469E1B197FA001C06E8725FD102192`(43,517,256 字节) |
| 证据批次 | `R20261113-112443`,11 组 / 155 条 / 失败 0 |
**源码树摘要**的描述(`handover/03` 第一节)是:把 `src` 下每个文件的「相对路径 + 该文件 SHA-256」按路径排序后拼接,再整体算一次 SHA-256。它等效于"这一棵树"的指纹,**不是** Git commit 哈希。
但这段描述**不足以复现该值**——分隔符、哈希大小写、路径是否带前缀都没写。本仓库按 60 余种组合(分隔符 `""`/`\n`/`\r\n`/空格/`|`/`:`/`\t`,大小写两种,路径前缀 `""`/`src/`/`01_后端源码/src/`,拼接顺序两种,末尾换行两种)逐一复算,**均未得到 `5E9F894B…`**。
结论:这个值只能作为"交付方声称的标识"引用,不能作为可校验凭据。可校验的替代物是 `SHA256SUMS_源码树.txt`(84 行,`sha256sum -c` 直接可用),见第四节。
## 三、12 条迁移脚本指纹
原包的 `03_数据库与Flyway迁移/` 与工程内 `aomin-gateway/src/main/resources/db/migration/` 是同一套文件的两次出现。仓库只保留工程内一套,交付时已核为**逐字节一致**。下表可直接用于复核。
| # | 迁移脚本 | 字节 | SHA-256 |
|---|---|---|---|
| 1 | `V1.0.0__baseline_schema.sql` | 101,859 | `07E5263E3D755535A5FADAA18B8529D6244AF975DAA06FAEF9B3E908BCF0C7CC` |
| 2 | `V1.0.18__raw_link_columns.sql` | 5,134 | `8BE5A9816F1F661D0A54C2A23C56079B76DF88F47BD153609A94C659E42E7299` |
| 3 | `V1.0.19__household_geo.sql` | 1,211 | `45E64F88796D46267B8F4F67F76D9EA5EC3AD34FBBE62736C4768216CBAF23A2` |
| 4 | `V1.0.20__notify_read_at.sql` | 1,140 | `CDE7F6DF804CDC1A827B0A109815C0AF3A52D7EFA4F35A170F262D8CE429418D` |
| 5 | `V1.0.21__alarm_chain_seq.sql` | 1,803 | `7751F42076E39874A85F147CF1CE5C45653161207CB06E6042D60F7888D90710` |
| 6 | `V1.0.22__alarm_source_event.sql` | 656 | `3306548D3C02F6750CFB93C535E5C9E98062710ED6D4D099F69E0FC7A7B8EDC1` |
| 7 | `V1.0.23__command_seven_states.sql` | 2,678 | `9C941B1675D5A82DAEF81D7F297D03B206A028DEB9923B78CD92F473A9CA8272` |
| 8 | `V1.0.24__region_scope.sql` | 2,099 | `EC0B7E0F23F74F52BD373FC0FB7D84BACDDA9BE62C19E9B6163BF669DA442AF2` |
| 9 | `V1.0.25__raw_ref_64_and_gap.sql` | 3,951 | `E480A143B8A079971C285D63BF51252EECC2034054459F83E2104C877FA51D40` |
| 10 | `V1.0.26__alarm_retry_deadletter.sql` | 3,793 | `61E8C5F2167DE4588E8AA337C9D45DFA12015680D6B12D1D456B3F159770C29E` |
| 11 | `V1.0.27__receive_time_local_and_audit_fail.sql` | 2,452 | `778141CF45E9B3F62AFC9BDBB3F7079794DAEDFBA9F0A20CAF84FE90EB3503DD` |
| 12 | `V1.0.28__command_claim_node.sql` | 1,989 | `2D6E90A3E6A2DB9771B7820DF4DFC7E58C211627AC02271612E2511764E83286` |
**这 12 条的哈希只用于确认"与交付时相同"。** 迁移脚本一旦被修改,即使只改注释,Flyway 也会报校验和不符;那时要做的是 `flyway repair` 加会签记录,不是回头改这张表。
## 四、怎么复核
### 单条脚本
```bash
cd aomin-gateway/src/main/resources/db/migration
sha256sum V1.0.28__command_claim_node.sql
# 与上表第 12 行比对,应完全相同
```
### 全部 12 条
```bash
cd aomin-gateway/src/main/resources/db/migration
sha256sum V1.0.*.sql | sort -k2
```
### 源码树(84 个文件)
```bash
# 在仓库根执行
sha256sum -c docs/reference/SHA256SUMS_源码树.txt
# 84 行全部 "OK" 即为一致
```
`SHA256SUMS_源码树.txt` 覆盖 `aomin-gateway/src` 下全部 84 个文件,格式是标准 `sha256sum` 清单(哈希 + 相对仓库根的路径),生成于本次导入、自校验 84/84 通过。
这就是"逐字节一致"的可校验形式:交付包的 `01_后端源码/src` 与本仓库 `aomin-gateway/src` 只差一层目录名,逐个文件比对即可。行尾差异(CRLF/LF)会直接改变哈希,所以比对不上时先查行尾——`.gitattributes` 已设为 `* -text`,不会自动转换。
## 五、这张表什么时候需要更新
| 情况 | 处理 |
|---|---|
| 新增迁移脚本 `V1.0.29` | 追加一行;不修改已有 12 行 |
| 契约版本升级(J-00 1.8.1 → 更高) | 更新第二节的契约版本与 `03` 的对应项 |
| 代码发生实质变更 | 源码树摘要与 jar 哈希随之失效,**应记入 `CHANGELOG.md` 而不是改这张表**——这张表描述的是交付那一刻 |