chore(repo): 导入 M-Z 业务底座 V1.9 基线并建立仓库规范
按周工交付的 SEC-Z-01 清理后源码建立 M-Z 主干,并按 Git 仓库惯例重排目录。 - aomin-gateway/ 后端工程 87 文件,原样保留,路径与构建命令未变 - demo/ 演示前端与设备模拟器 9 文件(原 02_前端与设备端),非正式三端 - docs/handover/ 交付与安全清理材料 6 份,内容未改 - docs/contract/ J-00 规格、OpenAPI、内部五接口、状态机、Mock 样例 - docs/reference/ 指纹与校验清单,含 84 个源码文件的 sha256 清单 目录重排只动位置不动内容:源码树 84 文件哈希与交付一致,逐文件自校验通过; 12 条 Flyway 迁移与交付包内镜像逐字节一致。 未纳入:迁移镜像目录(与工程内一套重复,改留指纹)、target/、证据 raw、 应急恢复脚本、评审用虚构台账数据。 安全:SEC-Z-01 15 处明文已清零,导入前重新扫描无新增命中,本仓库无历史污染。 行尾按交付字节原样保存(.gitattributes 设 * -text),不做归一化以免整树摘要失效。
This commit is contained in:
commit
a7605ef8b8
121 files changed
+23629
No files matched your search
@@ -0,0 +1,289 @@
|
||||
# 状态机:告警 与 下行命令
|
||||
|
||||
> 状态字段全部是 `varchar(16)` 枚举直存,不用数字码,便于人工核对。
|
||||
> 两条状态机是本模型最容易被评「含糊」的地方,因此把**谁能改、什么条件能改、改不了会怎样**写死在这里。
|
||||
>
|
||||
> **口径来源**:告警九态=GOV-001 §4.3「告警与命令」检查项;命令七态=`6_J-00_接入接口规格_V1.8.md` §3.4。
|
||||
> 本文只做展开说明,**不另立字段含义**;与 J-00 冲突时以 J-00 为准。
|
||||
|
||||
---
|
||||
|
||||
## 一、告警状态机(九态)
|
||||
|
||||
```
|
||||
┌────────── 新事件触发 ──────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────┐ 推送成功 ┌─────────┐ │
|
||||
│ NEW │ ──────────► │ PUSHED │ │
|
||||
└───────┘ └─────────┘ │
|
||||
│ │ │ │ │
|
||||
命中免打扰时段 │ │ └── 判定误报 ──► FALSE │
|
||||
▼ │ │ │
|
||||
┌────────┐ │ │
|
||||
│DELAYED │ 静默结束回 NEW │ │
|
||||
└────────┘ │ │
|
||||
│ │ │
|
||||
已知悉/确认 ────┴────────┬────────────────┘ │
|
||||
▼ │
|
||||
┌────────────┐ 超时未确认 │
|
||||
│ CONFIRMED │ ────────► ESCALATED │
|
||||
└────────────┘ │ │
|
||||
│ │ │
|
||||
处置完成 ──────────────┴─────────────────────┘ │
|
||||
▼ │
|
||||
┌──────────┐ 关闭 ┌────────┐ │
|
||||
│ HANDLED │ ──────► │ CLOSED │ │
|
||||
└──────────┘ └────────┘ │
|
||||
│ │
|
||||
同一设备同一规则再次触发,且本单无人处理 │
|
||||
▼ │
|
||||
┌────────────┐ │
|
||||
│ SUPERSEDED │ ◄─────────────────────┘
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
### 状态定义
|
||||
|
||||
| 状态 | 中文 | 含义 | 是否算「待处理」 |
|
||||
|---|---|---|---|
|
||||
| `NEW` | 新告警 | 刚生成,尚未推送或推送中 | ✅ |
|
||||
| `PUSHED` | 已推送 | 已送到家属端(SSE/短信/App),等人看 | ✅ |
|
||||
| `DELAYED` | 免打扰 | 落在收件人免打扰时段,不响铃不弹窗,**仍在列表里** | ✅ |
|
||||
| `CONFIRMED` | 已确认 | 有人接了这个单(`confirmed_by/confirmed_at` 有值),还没处置完 | ✅ |
|
||||
| `ESCALATED` | 已升级 | 超时未确认,已按规则升级(加急通知/通知下一位家属) | ✅ |
|
||||
| `HANDLED` | 已处置 | 家属点了「已处理」,但事还没完全关闭 | ❌(家属视角) |
|
||||
| `CLOSED` | 已关闭 | 处置完成并关闭 | ❌ |
|
||||
| `FALSE` | 误报 | 人工判定为误报 | ❌ |
|
||||
| `SUPERSEDED` | 已被替代 | 同一设备同一规则又触发了新的,本单被顶掉 | ❌ |
|
||||
|
||||
> **`HANDLED` 与 `CLOSED` 的区别**:前者是「我知道这件事了、我处理了」,后者是「这条记录可以结束了」。
|
||||
> 家属端把两者都当"不用再管",但后台统计「今日处置率」只认 `HANDLED` 及以上。
|
||||
>
|
||||
> **`PUSHED` 为什么必须独立**:`NEW` 是"系统生成了",`PUSHED` 是"人真的收到了"。
|
||||
> 合成一个值以后,「推送失败了没」查不出来——告警生成了但没送到人手上,恰恰是最危险的情况。
|
||||
|
||||
### 允许的状态迁移(白名单)
|
||||
|
||||
| 当前 | 允许去向 | 触发者 |
|
||||
|---|---|---|
|
||||
| `NEW` | `PUSHED` / `DELAYED` / `CONFIRMED` / `HANDLED` / `FALSE` / `SUPERSEDED` | 通知模块/免打扰判定/家属/人工 |
|
||||
| `PUSHED` | `CONFIRMED` / `HANDLED` / `FALSE` / `SUPERSEDED` | 家属/人工 |
|
||||
| `DELAYED` | `NEW`(静默结束唤醒)/ `CONFIRMED` / `HANDLED` / `SUPERSEDED` | 定时任务/家属 |
|
||||
| `CONFIRMED` | `HANDLED` / `ESCALATED` / `FALSE` | 家属/超时升级 |
|
||||
| `ESCALATED` | `CONFIRMED` / `HANDLED` / `FALSE` | 家属/人工 |
|
||||
| `HANDLED` | `CLOSED` | 家属/人工 |
|
||||
| `CLOSED` / `FALSE` / `SUPERSEDED` | **无(终态)** | — |
|
||||
|
||||
**白名单外的迁移一律 `400`,不是静默忽略。** 前端按钮由 `GET /alarms/{id}/actions` 返回,不靠猜。
|
||||
|
||||
### 自动替代规则(这套模型的重点)
|
||||
|
||||
同一 `dev_id + rule_key` 再次触发时,**不是往旧单上叠次数,而是开新单**,旧单按三种情况处理:
|
||||
|
||||
| 旧单当时的状态 | 处理 | 结果 |
|
||||
|---|---|---|
|
||||
| `NEW` / `PUSHED` / `DELAYED`(无人管) | 标为 `SUPERSEDED`,回填 `superseded_by=新单id`,释放 `active_key` | 家属看到最新一条,点开能看整组历史 |
|
||||
| `CONFIRMED` / `ESCALATED` / `HANDLED`(人处理过) | **保持原状**,只释放 `active_key`,不改状态 | 处理结果不被覆盖 |
|
||||
| `CLOSED` / `FALSE` / `SUPERSEDED`(终态) | 不动 | 历史记录保持 |
|
||||
|
||||
**执行顺序有讲究**:先释放 `active_key` → 再插新单 → 最后回填 `superseded_by`。
|
||||
顺序错了会撞唯一键(`active_key` 保证"同一设备同一规则同时只有一条活跃单")。
|
||||
|
||||
`active_key = dev_id + ':' + rule_key`,终结时置 NULL。
|
||||
|
||||
### 告警链(`GET /alarms/{id}/chain`)
|
||||
|
||||
沿 `superseded_by` 反向递归(MySQL 递归 CTE),返回整组:
|
||||
|
||||
```
|
||||
1 替代 2 替代 3 处理 4 替代 5 当前
|
||||
```
|
||||
|
||||
- 点 **5** → 看到 4、5(4 被替代,5 是当前)
|
||||
- 点 **3** → 看到 1、2、3(3 是被人处理过的,链在这里断)
|
||||
- 点**任一被替代的** → 只看到自己 + 一条提示「已被后面的告警替代,不能单独处理」
|
||||
|
||||
**被替代的单不可编辑**:`ack/confirm/handle/close/false` 一律 400。
|
||||
|
||||
### 恢复是「附加状态」,不是状态迁移
|
||||
|
||||
门关上、漏水已止、烟雾已散、设备来电 —— 这些**不新建告警**,只在原单上写 `cleared_at` 与 `clear_event_id`。
|
||||
|
||||
```
|
||||
设备状态:已恢复 · 2026-11-09 20:21:12(设备来电)
|
||||
```
|
||||
|
||||
状态标签仍是 `NEW`,旁边多一个绿色「已恢复」。
|
||||
**恢复事件严格按类型配对**:来电只恢复断电,绝不会把一条还没处理的 SOS 标成已恢复。
|
||||
|
||||
| 触发事件 | 只能恢复 |
|
||||
|---|---|
|
||||
| `DOOR_CLOSE` | `DOOR_OPEN` |
|
||||
| `WATER_LEAK_CLEAR` | `WATER_LEAK` |
|
||||
| `SMOKE_CLEAR` | `SMOKE` |
|
||||
| `SOS_CLEAR` | `SOS` |
|
||||
| `POWER_ON` / `POWER_RESTORE` | `POWER_OFF` / `POWER_CUT` |
|
||||
|
||||
**恢复只写附加字段,永远不把告警置为 `CLOSED`** —— 安全告警必须由人关闭,设备状态好转不等于这件事了了。
|
||||
|
||||
---
|
||||
|
||||
## 二、下行命令状态机(七态)
|
||||
|
||||
**英文枚举**:`PENDING / CLAIMED / SENT / ACKED / FAILED / TIMEOUT / CANCELLED`,由业务层维护。
|
||||
定义见 `6_J-00_接入接口规格_V1.8.md` §3.4,本节只做落地说明。
|
||||
|
||||
```
|
||||
┌──────── I-18 拉取 ────────┐
|
||||
▼ │
|
||||
PENDING ────> CLAIMED ──I-05 SENT──> SENT ──ACKED code=0──> ACKED(终态)
|
||||
▲ │ │
|
||||
│ ├─ 租约到期(60s) ────┼──> 回 PENDING(reclaim+1)
|
||||
│ │ │
|
||||
│ ├─ 可延迟 + OFFLINE/TIMEOUT/FAILED
|
||||
│ │ └──> 回 PENDING(retry+1,退避)
|
||||
│ │ │
|
||||
│ └─ 实时 + 任意失败 └─ timeout_at(24h) 到 ──> FAILED(终态)
|
||||
│ └──> FAILED(终态,当场返回用户)
|
||||
│
|
||||
├── retry 超 10 次 或 过 expire_at(72h)──> TIMEOUT(终态)
|
||||
└── 用户撤单/设备解绑 ──────────────────> CANCELLED(终态)
|
||||
```
|
||||
|
||||
| 状态 | 说明 | 进这个状态的时刻 |
|
||||
|---|---|---|
|
||||
| `PENDING` | 待领取 | 创建命令 |
|
||||
| **`CLAIMED`** | **已领取/租约中**(`lease_expire_at` 有效) | IoT 通过 **I-18** 领走 |
|
||||
| `SENT` | 已发出(`timeout_at` 有效) | **I-05** 回报 `SENT`(无回执协议) |
|
||||
| `ACKED` | **设备有回应**,且 `resultCode=0` | **I-05** 回报 `ACKED` + `resultCode=0` |
|
||||
| `FAILED` | 失败(终态) | 实时类任意失败;`SENT` 超 24h 无回执 |
|
||||
| `TIMEOUT` | 已过期(终态) | 重排 10 次 或 过 `expire_at`(创建+72h) |
|
||||
| `CANCELLED` | 已取消(终态) | 用户撤单/设备已解绑 |
|
||||
|
||||
> **`CLAIMED` 与 `SENT` 必须分开**:「IoT 领走了」和「已经发出去了」是两件事。
|
||||
> 混成一个值,租约回收就判不出该不该重派,也分不清是中间件失联还是设备没响应。
|
||||
|
||||
### ⚠ `ACKED` 不等于「执行成功」
|
||||
|
||||
这是最容易写错、也是评审明确列为否决情形的一条,在此写死:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `result`(I-05) | `ACKED` = **设备有回应**(收到回执帧)。**不代表设备按意图执行了。** |
|
||||
| `resultCode` | 标准字典值:`0` 成功/`1` 不支持/`2` 参数非法/`3` 执行失败/`4` 设备忙。**只有 `0` 才算设备声明成功** |
|
||||
| `rawResultCode` / `rawAck` | 厂家原始码与回执原始字节,原样保存,**不得透传到标准字段** |
|
||||
|
||||
所以:
|
||||
- `ACKED + resultCode≠0` → 落 **`FAILED`**,不是 `ACKED`;
|
||||
- 即使 `ACKED + resultCode=0`,也只说明**设备声明已执行**。业务效果要看后续状态快照或事件确认(例如 `set_mode` 之后要核对 `gateway.mode_cache` 是否一致)。
|
||||
- **库里绝不出现「已生效」这种把回执当结果的值。**
|
||||
|
||||
### 状态值迁移(库现值 → 目标七态)
|
||||
|
||||
`device_command.status` 现在是**中文六值**,`varchar(16)`;目标七态是英文。这是**值域迁移,不是加列**:
|
||||
|
||||
| 目标 | 库现值 | 迁移动作 |
|
||||
|---|---|---|
|
||||
| `PENDING` | 待下发 | 改值 |
|
||||
| `CLAIMED` | **无** | **新增枚举值**(列宽够,只扩值域) |
|
||||
| `SENT` | 已下发 | 改值 |
|
||||
| `ACKED` | 已生效 | 改值(**语义同时纠正**:从「已生效」改为「设备有回应」) |
|
||||
| `FAILED` | 失败 | 改值 |
|
||||
| `TIMEOUT` | 超时 | 改值 |
|
||||
| `CANCELLED` | 已取消 | 改值 |
|
||||
|
||||
> **迁移状态(2026-11-11 已闭环)**:V1.0.23 迁移脚本已把库里中文六值刷成英文七态,
|
||||
> 代码读写全部改为英文;`BizDeviceService.commands()` 保留一层"中文旧值 → 英文"的**读时映射**,
|
||||
> 只为兼容尚未升级的老库,跑过 V1.0.23 后这层就是空转。
|
||||
> 实测:`cmd_chain_test.py` 23 条全过,其中第 12 条专门验"命令记录里没有中文旧值残留"(20 条,中文 0 条)。
|
||||
|
||||
### 租约与两个计数器
|
||||
|
||||
| 计数器 | 记什么 | 用途 |
|
||||
|---|---|---|
|
||||
| `retry_count` | **设备侧**失败重排了几次 | 超 10 次 → `TIMEOUT` |
|
||||
| `reclaim_count` | **IoT 拿走却没回话**几次 | 超 5 次 → 说明该 `nodeId` 反复失联,告警转人工 |
|
||||
|
||||
混成一个,就分不清是设备坏了还是中间件坏了。
|
||||
|
||||
### 领取代次 `claim_generation`(V1.7.2 新增)
|
||||
|
||||
每次 `PENDING → CLAIMED` 时 +1,**I-18 返回、I-05 回带**。
|
||||
|
||||
| 步 | 发生什么 | 代次 |
|
||||
|---|---|---|
|
||||
| 1 | 创建 | `0`(尚未领取) |
|
||||
| 2 | 节点 A 领走 | `1` |
|
||||
| 3 | 租约 60 秒到期,没回话,回收成 `PENDING` | 仍是 `1`(回退不 +1,只记录 `reclaim_count`) |
|
||||
| 4 | 节点 B 领走 | `2` |
|
||||
| 5 | 节点 A 的回执**迟到**,带 `claimGeneration=1` | **更新 0 行 → 计 `duplicated`,状态不动** |
|
||||
| 6 | 节点 B 的回执,带 `2` | 更新 1 行 → 生效 |
|
||||
|
||||
没有代次的话第 5 步会把 B 的流转覆盖掉,而且不报错——只在报表里表现为"同一条命令走了两条路"。
|
||||
|
||||
> 合规边界:回执不带代次**不会被拒**……这条旧口径已作废。
|
||||
> **V1.8 起 I-05 的 `claimGeneration` 必填,缺了直接拒**(`CLAIM_GENERATION_REQUIRED`)。
|
||||
|
||||
### 领取节点 `claimed_node_id`(V1.8.1 新增,迁移 V1.0.28)
|
||||
|
||||
代次只回答"第几轮",**不回答"这一轮是谁"**。所以领取时必须把 `nodeId` 一起落库:
|
||||
|
||||
```
|
||||
UPDATE device_command
|
||||
SET status='CLAIMED',
|
||||
claim_generation = claim_generation + 1,
|
||||
claimed_node_id = ?, -- 这次来领的 IoT 节点
|
||||
claimed_at = NOW(3),
|
||||
lease_expire_at = ?
|
||||
WHERE id IN (...) AND status='PENDING'
|
||||
```
|
||||
|
||||
I-05 的原子更新条件因此变成**四个守卫**:
|
||||
|
||||
```sql
|
||||
WHERE id = ? -- 命令
|
||||
AND claim_generation = ? -- 第几轮
|
||||
AND claimed_node_id = ? -- 这一轮是谁(V1.8.1)
|
||||
AND status IN ('CLAIMED','SENT') -- 允许的来源状态
|
||||
```
|
||||
|
||||
| 回执情况 | 结果 |
|
||||
|---|---|
|
||||
| 节点对、代次对 | 更新 1 行,`accepted` |
|
||||
| 节点对、代次旧(同一节点上一轮) | 更新 0 行,计 `duplicated`(过期回执,不算错) |
|
||||
| 节点错 | `rejected` + `NODE_MISMATCH`,状态不动 |
|
||||
| 历史在途行(迁移前领的,节点为空) | `rejected` + `NODE_UNKNOWN`,不猜、不伪造;租约到期重派后即有值 |
|
||||
|
||||
> **作用边界**:`nodeId` 由 IoT 自报,**可以伪造,它不是身份认证**。
|
||||
> "调用方是谁"靠内网来源白名单 + 签名;这一列证明的是**领取事实一致**——
|
||||
> 这条结果是不是领走这一轮的那个节点回的。
|
||||
|
||||
---
|
||||
|
||||
## 三、两条状态机的共同原则
|
||||
|
||||
1. **状态只由事件驱动,不允许任意跳转**:没有"把 `CLOSED` 直接改回 `NEW`"这种口子。
|
||||
2. **改不了就明确拒绝**:白名单外的迁移一律 `400`,不是静默忽略。
|
||||
3. **动作白名单**:`GET /alarms/{id}/actions` 返回当前状态下允许的动作,前端据此渲染按钮,不靠猜。
|
||||
4. **审计留痕**:每个状态变更写 `audit_log`(后台操作)或 `alarm_action`(家属操作),**与状态变更同事务**。
|
||||
**审计写失败必须让整个动作回滚**,不能 `catch {}` 吞掉——否则会出现"告警关了但查不到谁关的",
|
||||
而且没有任何报错。非事务路径(后台建档等)改成记账 + ERROR 日志,计数进 `/health` 的 `auditFailures`。
|
||||
5. **回执 ≠ 结果**:`ACKED` 只代表"设备有回应",业务是否生效要看 `resultCode` 和后续核对。
|
||||
6. **恢复 ≠ 关闭**:设备状态好转只写 `cleared_at`,安全告警必须由人关闭。
|
||||
|
||||
---
|
||||
|
||||
## 四、与 J-00 V1.8 的对应关系
|
||||
|
||||
| 本文 | J-00 V1.8 位置 | 是否一致 |
|
||||
|---|---|---|
|
||||
| 告警九态 | GOV-001 §4.3 检查项 | ✅ 代码已实现九态 |
|
||||
| 命令七态 | §3.4「业务层的命令状态机」 | ✅ 契约一致;✅ 代码已迁移到英文七态(V1.0.23),23 条用例全过 |
|
||||
| 命令领取 `I-18` | §三 接口表 | ✅ 本文已统一为 I-18(原文档写作 I-20,已废止) |
|
||||
| 命令回执 `I-05` | §三 接口表 | ✅ 本文已统一为 I-05(原文档写作 I-21,已废止) |
|
||||
| `claimGeneration` | §3.2 / §3.3(V1.7.2) | ✅ I-18 返回、I-05 回带;旧代次回执被挡(用例 18) |
|
||||
| `result` 五值 | §3.3 | ✅ `ACKED/SENT/TIMEOUT/DEVICE_OFFLINE/FAILED` |
|
||||
| `resultCode` 标准码 | §3.3 | ✅ `0`–`4`,厂家原码另存 `rawResultCode` |
|
||||
|
||||
> 本文与 J-00 V1.8 冲突时,**以 J-00 为准**;新增枚举值须先改 J-00,再改本文与代码。
|
||||
Reference in new issue
Block a user