|
|
@@ -0,0 +1,1903 @@
|
|
|
+# 智能轮灌系统 技术需求与技术方案文档
|
|
|
+
|
|
|
+**版本:** v1.6
|
|
|
+**日期:** 2026-02-26
|
|
|
+**状态:** 草稿
|
|
|
+**适用范围:** 后端开发团队、架构评审
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 系统概述
|
|
|
+
|
|
|
+本系统为智能农业场景下的自动化轮灌控制平台,支持对多个灌区(灌溉组)按照预设规则进行有序、安全的轮流灌溉。系统具备以下核心能力:
|
|
|
+
|
|
|
+- **多种触发方式**:支持定时调度、传感器联动、手动触发等多种触发模式,同一任务可同时关联多个触发来源,满足不同农业场景需求。
|
|
|
+- **设备协同控制**:统一管理水泵(变频恒压/普通)、电磁阀、球阀(支持角度+压力控制)、施肥机等农业设备。
|
|
|
+- **安全执行保障**:内置先开后关、设备互锁、ACK确认、重试、安全关闭等多重安全机制。
|
|
|
+- **高可靠性架构**:基于 RocketMQ 延迟消息 + 责任链模式实现异步流程编排,配合看门狗机制保障任务不丢失。
|
|
|
+- **全程记录与报警**:每次执行保存完整节点日志,异常时多通道报警(短信 + 电话 + 移动端推送)。
|
|
|
+- **配置与执行解耦**:用户只表达灌溉"意图"(选设备、配时长),系统自动按安全规则生成精确执行计划;配置顺序与执行顺序分离,支持运行时动态跳过灌区与故障接续执行。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 业务需求
|
|
|
+
|
|
|
+### 2.1 触发方式
|
|
|
+
|
|
|
+轮灌任务支持以下触发方式,各触发方式可共存,同一任务允许同时关联多个触发来源(如既配定时又配联动)。无论哪个触发来源先触发,均创建同一套执行计划;任务正在运行时,其他触发来源的信号将被拒绝(见 §6.6 并发触发防护)。
|
|
|
+
|
|
|
+#### 2.1.1 定时轮灌
|
|
|
+
|
|
|
+**模式 A:按星期 + 时间重复**
|
|
|
+
|
|
|
+- 用户选择星期(可多选,如:周一、周三、周五)和具体时刻(如 02:00)。
|
|
|
+- 系统自动将配置转换为 Cron 表达式存储。
|
|
|
+- 适用场景:固定周期的常规灌溉计划。
|
|
|
+
|
|
|
+**模式 B:启动时间 + 执行次数 + 执行周期**
|
|
|
+
|
|
|
+- 用户配置起始时间、执行总次数(如 10 次)、执行间隔周期(如每 3 天)。
|
|
|
+- 系统使用简单调度器(SimpleScheduleBuilder)管理,记录已执行次数,达到上限后自动停用任务。
|
|
|
+- 适用场景:临时性或有限次数的灌溉计划。
|
|
|
+
|
|
|
+#### 2.1.2 联动控制(传感器触发)
|
|
|
+
|
|
|
+- 监听单个传感器实时数据,当数据满足指定条件时,自动触发轮灌流程。
|
|
|
+- 条件支持:`大于(>)` / `大于等于(>=)` / `小于(<)` / `小于等于(<=)` / `等于(==)` + 阈值。
|
|
|
+- **冷却时间机制**:成功触发一次后,在设定的冷却时间窗口内(如 N 分钟),即使传感器数据再次满足条件,也不重复触发,防止短时间内重复启动。
|
|
|
+
|
|
|
+**联动规则配置字段:**
|
|
|
+
|
|
|
+| 字段 | 说明 |
|
|
|
+| ------------- | -------------------------------- |
|
|
|
+| 传感器设备 ID | 监听的具体传感器设备 |
|
|
|
+| 比较符 | `>` / `>=` / `<` / `<=` / `==` |
|
|
|
+| 阈值 | 触发条件的数值边界 |
|
|
|
+| 冷却时间 | 单位:分钟,触发后的静默时间窗口 |
|
|
|
+| 关联任务 ID | 满足条件时触发的轮灌任务 |
|
|
|
+
|
|
|
+#### 2.1.3 手动触发
|
|
|
+
|
|
|
+用户通过 APP 或后台手动点击“立即执行”,任务即时启动,无需等待定时或传感器触发。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2.2 设备配置
|
|
|
+
|
|
|
+每个轮灌任务可以独立配置以下设备:
|
|
|
+
|
|
|
+#### 2.2.1 压力控制配置
|
|
|
+
|
|
|
+灌溉系统的压力控制分为水泵压力和球阀压力两个维度,两者可以同时存在、独立生效:
|
|
|
+
|
|
|
+**维度一:水泵压力控制**
|
|
|
+
|
|
|
+通过变频恒压水泵控制管路压力,系统下发目标压力值给水泵,水泵自动调频维持恒压。任务创建时选择水泵压力模式:
|
|
|
+
|
|
|
+| 模式 | 说明 | 配置位置 |
|
|
|
+| ------------ | -------------------------------------------------------------- | ------------------------------ |
|
|
|
+| **无** | 水泵作为普通水泵启停,不下发压力指令 | 无额外配置 |
|
|
|
+| **统一压力** | 所有灌区使用同一压力值,整个任务只下发一次 `SET_PUMP_PRESSURE` | 任务级配置 `targetPressureKpa` |
|
|
|
+| **分区压力** | 每个灌区可设置不同的水泵目标压力,灌区切换时自动下发新压力值 | 灌溉组级配置 `zonePressureKpa` |
|
|
|
+
|
|
|
+**维度二:球阀压力控制**
|
|
|
+
|
|
|
+通过球阀自身的压力控制功能调节管路压力,每个球阀在灌溉组中配置目标压力,系统在开启球阀时同时下发角度和压力指令。球阀压力控制独立于水泵压力控制,两者可同时工作,系统根据不同的配置综合控制水泵和球阀,确保整个灌溉系统的稳定性和安全性。
|
|
|
+
|
|
|
+| 配置位置 | 说明 |
|
|
|
+| ------------------------ | --------------------------------------------------------------------------- |
|
|
|
+| 灌溉组设备列表中每个球阀 | 配置 `targetPressureKpa`,系统在 `OPEN_GROUP` 时同时下发角度+压力指令给球阀 |
|
|
|
+
|
|
|
+> **共存示例**:水泵设为统一压力 300kPa,同时灌区内的球阀各自配置了目标压力,系统会在启动时下发 `SET_PUMP_PRESSURE(300)` 给水泵,同时在 `OPEN_GROUP` 时将各球阀的目标压力随开启指令一起下发。
|
|
|
+
|
|
|
+**水泵配置参数:**
|
|
|
+
|
|
|
+| 参数 | 说明 |
|
|
|
+| -------------- | ----------------------------------------------------------------------- |
|
|
|
+| 水泵压力模式 | `NONE`(普通启停)/ `PUMP_UNIFIED`(统一压力)/ `PUMP_ZONE`(分区压力) |
|
|
|
+| 统一目标压力值 | 仅 `PUMP_UNIFIED` 模式需配置,单位:kPa |
|
|
|
+
|
|
|
+#### 2.2.2 施肥机配置
|
|
|
+
|
|
|
+施肥机由以下子设备控制,每个子设备独立接收指令和回传 ACK:
|
|
|
+
|
|
|
+| 子设备 | 说明 |
|
|
|
+| -------- | ------------------------------------------ |
|
|
|
+| 施肥泵 | 将肥料从储肥桶注入灌溉管路的计量泵 |
|
|
|
+| 搅拌电机 | 对储肥桶中的肥料持续搅拌,确保肥料均匀溶解 |
|
|
|
+
|
|
|
+> **注意**:施肥机的主水泵与灌溉系统的水泵是**同一个水泵**,都是灌溉清水的,施肥泵用于抽取肥料,搅拌电机用于搅拌肥料。此设计节省设备成本并简化系统操作。
|
|
|
+
|
|
|
+施肥的配置参数、控制模式、时序规则及异常处理见 §2.7,技术方案见 §5.1.4。
|
|
|
+
|
|
|
+#### 2.2.3 任务级安全参数
|
|
|
+
|
|
|
+| 参数 | 说明 | 默认值 |
|
|
|
+| ----------------------- | ------------------------------------------------------------------------------------------------------------------ | ------ |
|
|
|
+| `switch_stable_seconds` | 灌区切换时,开启下一灌区成功后到关闭上一灌区之前的稳压等待时间(秒)。用于平衡管路水压,防止压力突变损坏设备或爆管 | 5 秒 |
|
|
|
+
|
|
|
+> **设计原则**:`switch_stable_seconds` 由计划生成引擎在每个 `OPEN_GROUP(ZoneI)` 与 `CLOSE_GROUP(ZoneI-1)` 之间自动插入一个 `ZONE_SWITCH_WAIT` 节点,用户仅需配置秒数,无需关心插入位置。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2.3 灌溉组与任务灌区配置
|
|
|
+
|
|
|
+#### 2.3.1 灌溉组(灌区预配置模板)
|
|
|
+
|
|
|
+灌溉组是灌区设备的预配置模板,用于统一管理一个灌区内的设备组合及其控制参数。灌溉组独立于任务存在,可被多个任务复用。
|
|
|
+
|
|
|
+**灌溉组配置参数:**
|
|
|
+
|
|
|
+| 参数 | 说明 |
|
|
|
+| ---------- | -------------------------------------------------------------------------------------------------- |
|
|
|
+| 灌区组名称 | 用于标识灌区组,方便管理和报警展示 |
|
|
|
+| 分区压力 | 本灌区的水泵目标压力(kPa);仅在任务压力模式为 `PUMP_ZONE` 时生效,灌区切换时系统自动下发新压力值 |
|
|
|
+| 设备列表 | 本灌区包含的设备及其控制参数(见下表) |
|
|
|
+
|
|
|
+**设备列表中每个设备的参数:**
|
|
|
+
|
|
|
+| 设备类型 | 参数 | 说明 |
|
|
|
+| -------- | ---------------- | ----------------------------------------------------------------------------------------- |
|
|
|
+| 电磁阀 | 设备ID | 开(ON)/ 关(OFF)控制 |
|
|
|
+| 球阀 | 设备ID、目标角度 | 角度范围 0~100%,用于流量调节 |
|
|
|
+| 球阀 | 目标压力 | 本球阀的目标压力(kPa);若球阀支持压力控制则配置,系统在开启球阀时同时下发角度和压力指令 |
|
|
|
+
|
|
|
+一个灌溉组内可同时包含多个电磁阀和球阀,所有设备同步下发指令,全部 ACK 成功方视为开启/关闭成功。
|
|
|
+
|
|
|
+> **配置说明**:灌溉组中的分区压力仅在水泵压力模式为 `PUMP_ZONE` 时生效;球阀目标压力只要球阀配置了即生效(与水泵压力可共存)。用户在灌溉组中统一配置好设备和压力参数后,创建任务时只需选择灌溉组并配置每个灌区的灌溉时长即可。
|
|
|
+
|
|
|
+#### 2.3.2 任务级灌区配置
|
|
|
+
|
|
|
+创建任务时,用户从已有灌溉组中选择灌区,并为每个灌区配置灌溉时长:
|
|
|
+
|
|
|
+| 参数 | 说明 |
|
|
|
+| -------- | ---------------------------------------------------- |
|
|
|
+| 灌溉组 | 从已配置的灌溉组中选择,选择顺序即灌溉意图顺序 |
|
|
|
+| 灌溉时长 | 本灌区的灌溉持续时间(分钟),每个灌区可设置不同时长 |
|
|
|
+
|
|
|
+**施肥配置:**
|
|
|
+
|
|
|
+是否施肥由任务是否选择了施肥机设备来决定,灌溉组中不需要额外的“是否施肥”字段。当任务选择了施肥机后,需配置施肥计划参数(任务级,所有灌区共用):
|
|
|
+
|
|
|
+| 参数 | 说明 |
|
|
|
+| ------------ | -------------------------------------------------------------------------- |
|
|
|
+| 施肥延迟时间 | (分钟)灌区开始灌溉后,等待多少分钟再启动搅拌,相对于灌溉开始时间计算 |
|
|
|
+| 搅拌提前时长 | (分钟)搅拌电机比施肥泵提前多少分钟启动,一直搅拌到施肥结束 |
|
|
|
+| 施肥时长 | (分钟)施肥泵持续运行时长;与施肥量二选一 |
|
|
|
+| 施肥量 | (升)施肥目标容积,下发定量指令给施肥机,施肥机自动停止;与施肥时长二选一 |
|
|
|
+
|
|
|
+> **设计说明**:灌溉时长属于任务级配置而非灌溉组模板配置,因为同一灌溉组在不同任务中可能需要不同的灌溉时长。施肥参数为任务级统一配置,所有启用施肥的灌区共用同一套施肥参数,系统根据每个灌区的灌溉时长自动判断施肥时间窗口是否充足。用户在表单中配置的灌溉组列表及顺序代表灌溉意图,实际执行顺序由系统根据安全规则自动编排。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2.4 安全执行规则
|
|
|
+
|
|
|
+安全执行规则是本系统的核心约束,所有执行计划必须严格遵守以下规则:
|
|
|
+
|
|
|
+| 规则编号 | 规则描述 |
|
|
|
+| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
+| S-01 | 水泵必须在**第一个灌区开启成功之后**才能启动,严禁在任何灌区开启前运行水泵 |
|
|
|
+| S-02 | 灌区切换时,必须**先开启下一个灌区成功**,再等待稳压时间(`switch_stable_seconds`),最后关闭上一个灌区(先开→稳压→后关),确保管路中始终有水流路径且压力平稳 |
|
|
|
+| S-03 | 正常结束时,固定执行顺序:先关水泵 → 最后关闭末个灌区电磁阀/球阀 |
|
|
|
+| S-04 | **开路优先原则**(先开后关,全场景适用):任何涉及设备通断的操作,必须先建立新通路/开启新设备,再断开旧通路/关闭旧设备,防止管路瞬间封闭导致水锤效应或爆管 |
|
|
|
+| S-05 | **故障接续启动序列**:任务从 PAUSED 恢复执行时,若水泵已在安全关闭中停止,必须按"先开灌区 → 再启水泵"的顺序重建启动序列,严禁直接继续执行 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2.5 执行时序示例
|
|
|
+
|
|
|
+**示例 1:3 个灌区,水泵普通启停 + 球阀配置了目标压力**
|
|
|
+
|
|
|
+```
|
|
|
+步骤 操作
|
|
|
+---- ──────────────────────────
|
|
|
+ 1 开启灌区1(电磁阀/球阀,球阀同时下发角度+压力)
|
|
|
+ 2 开启水泵(普通启停,不下发压力)
|
|
|
+ 3 等待 3 分钟(灌溉)
|
|
|
+ 4 开启灌区2
|
|
|
+ 5 等待 5 秒(稳压,switch_stable_seconds) ← S-02/S-04 稳压等待
|
|
|
+ 6 关闭灌区1
|
|
|
+ 7 等待 3 分钟(灌溉)
|
|
|
+ 8 开启灌区3
|
|
|
+ 9 等待 5 秒(稳压)
|
|
|
+10 关闭灌区2
|
|
|
+11 等待 3 分钟(灌溉)
|
|
|
+12 关闭水泵
|
|
|
+13 关闭灌区3
|
|
|
+```
|
|
|
+
|
|
|
+**示例 2:故障接续执行(泵已停,恢复后重建启动序列)**
|
|
|
+
|
|
|
+```
|
|
|
+步骤 操作
|
|
|
+---- ──────────────────────────
|
|
|
+(故障前已完成)
|
|
|
+ ✅ 开启灌区1 → 开启水泵 → 等待3分钟
|
|
|
+
|
|
|
+(故障发生,安全关闭)
|
|
|
+ ✅ 关闭水泵 → 关闭灌区1
|
|
|
+
|
|
|
+(用户修复完成,调用 Resume API)
|
|
|
+(系统检测到泵已停止,插入重建序列)
|
|
|
+
|
|
|
+ 1 开启灌区1(重新开启,遵循 S-05) ← 先开灌区
|
|
|
+ 2 设置水泵目标压力值 ← PUMP_UNIFIED/PUMP_ZONE 模式时插入
|
|
|
+ 3 开启水泵 ← 再开水泵(S-05)
|
|
|
+ 4 继续执行剩余灌溉(从中断处接续)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2.6 执行记录与报警
|
|
|
+
|
|
|
+#### 2.6.1 执行记录
|
|
|
+
|
|
|
+每次任务执行需保存完整日志,包括:
|
|
|
+
|
|
|
+- 每个节点的开始时间、结束时间、执行结果(成功/失败)。
|
|
|
+- 每个设备的 ACK 状态、重试次数、失败原因。
|
|
|
+- 整体任务状态(运行中 / 成功 / 失败 / 手动终止)。
|
|
|
+
|
|
|
+#### 2.6.2 报警通知
|
|
|
+
|
|
|
+当任务发生异常(设备失败超过重试次数、超时、安全关闭等)时,触发多通道报警:
|
|
|
+
|
|
|
+| 通道 | 说明 |
|
|
|
+| ---------- | -------------------------------------------- |
|
|
|
+| 短信 | 向绑定手机号发送报警短信 |
|
|
|
+| 电话 | 向绑定手机号拨打语音报警电话,适用于紧急告警 |
|
|
|
+| 移动端推送 | 通过 APP 消息推送通知移动端用户 |
|
|
|
+
|
|
|
+**报警内容包括:**
|
|
|
+
|
|
|
+- 失败节点名称与原因
|
|
|
+- 成功/失败设备列表
|
|
|
+- 安全关闭执行状态(是否完成、哪些设备关闭失败需人工处理)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2.7 施肥控制
|
|
|
+
|
|
|
+施肥机作为轮灌系统的组成部分,在灌溉过程中实现自动化施肥。是否施肥由任务是否选择了施肥机设备来决定,施肥参数在任务级统一配置(见 §2.3.2),所有灌区共用同一套施肥参数。施肥在灌溉时间窗口内并行执行,与主灌溉流程互不阻塞。设备组成见 §2.2.2。
|
|
|
+
|
|
|
+> **注意**:施肥机的主水泵与灌溉的水泵是同一个水泵,施肥机仅包含施肥泵和搅拌电机两个独立子设备。
|
|
|
+
|
|
|
+#### 2.7.1 施肥参数
|
|
|
+
|
|
|
+施肥参数在创建任务时统一配置(见 §2.3.2),所有灌区共用同一套参数:
|
|
|
+
|
|
|
+| 参数 | 类型 | 说明 |
|
|
|
+| ------------ | ---- | -------------------------------------------------------------------------------- |
|
|
|
+| 施肥延迟时间 | 分钟 | 灌区开始灌溉后,等待多少分钟再启动搅拌电机(相对于本灌区灌溉开始时刻) |
|
|
|
+| 搅拌提前时长 | 分钟 | 搅拌电机比施肥泵提前多少分钟启动;搅拌电机从启动后一直运行至施肥结束 |
|
|
|
+| 施肥时长 | 分钟 | 施肥泵持续运行时长;与施肥量二选一,两者不可同时配置 |
|
|
|
+| 施肥量 | 升 | 施肥目标容积,下发定量指令给施肥机,施肥机达到目标量后自动停止;与施肥时长二选一 |
|
|
|
+
|
|
|
+> **约束**:施肥延迟时间 + 搅拌提前时长 + 施肥时长 ≤ 灌区总灌溉时长。若超出,系统在保存时给出警告。施肥停止后灌溉继续进行至灌区时长结束,无需单独配置肥后清水时间。
|
|
|
+
|
|
|
+#### 2.7.2 施肥时序
|
|
|
+
|
|
|
+以单个灌区为例,各设备的启停时序如下(灌区总时长 30 分钟,施肥延迟 3 分钟,搅拌提前 2 分钟,施肥时长 5 分钟):
|
|
|
+
|
|
|
+```
|
|
|
+灌区开始时刻(T=0)
|
|
|
+ ├─ T+0: 水泵启动 + 灌区球阀/电磁阀开启(随灌溉开始)
|
|
|
+ ├─ T+3: 搅拌电机启动(施肥延迟时间 = 3min)
|
|
|
+ ├─ T+5: 施肥泵启动(T+3 + 搅拌提前 2min = T+5)
|
|
|
+ ├─ T+10: 施肥泵停止 + 搅拌电机停止(T+5 + 施肥时长 5min = T+10)
|
|
|
+ ├─ T+10 ~ T+30: 继续纯水灌溉(施肥已结束,灌溉继续)
|
|
|
+ └─ T+30: 灌区结束(关水泵、关阀门)
|
|
|
+```
|
|
|
+
|
|
|
+> **说明**:施肥停止后灌溉自然继续至灌区时长结束,无需单独的“肥后清水时间”参数。
|
|
|
+
|
|
|
+#### 2.7.3 施肥控制模式
|
|
|
+
|
|
|
+**时间模式**(配置施肥时长):
|
|
|
+
|
|
|
+- 施肥泵运行固定时长后停止。
|
|
|
+- 施肥泵停止时同时停止搅拌电机。
|
|
|
+- 流量计作为辅助监控(可选),不控制停止时机。
|
|
|
+
|
|
|
+**容量模式**(配置施肥量):
|
|
|
+
|
|
|
+- 系统下发特定容量的施肥任务指令给施肥机,施肥机通过流量计监控,达到目标量后自动停止施肥泵。
|
|
|
+- 容量模式下搅拌电机的停止由施肥机定量控制指令自行控制。
|
|
|
+- 灌区切换时,系统额外发送关闭施肥机的指令,确保安全性和准确性。
|
|
|
+- 若灌区灌溉结束时仍未达到目标量:停止施肥泵和搅拌电机,**不影响后续灌区**,不将剩余施肥量转移至下一灌区。
|
|
|
+
|
|
|
+> **搅拌停止规则**:时间模式下,施肥泵停止时同步停止搅拌电机;容量模式下,搅拌电机由施肥机定量指令自行控制停止。但无论哪种模式,灌区结束时必须确保搅拌电机已关闭。
|
|
|
+
|
|
|
+#### 2.7.4 施肥与灌溉任务的集成方式
|
|
|
+
|
|
|
+用户在创建任务时选择施肥机设备即启用施肥,施肥参数为任务级统一配置(见 §2.3.2),所有灌区共用同一套施肥参数。执行计划生成时:
|
|
|
+
|
|
|
+1. 灌溉水泵即施肥主水泵,随灌区开启而启动,无需单独控制。
|
|
|
+2. 搅拌电机和施肥泵通过**延迟子事件**在灌区灌溉等待期间触发,不占用主节点链路。
|
|
|
+3. 施肥结束后,灌溉继续进行至灌区时长结束。
|
|
|
+
|
|
|
+> **实现方式**:采用独立的施肥子任务(FertTask)模型,在 `WAIT(IRRIGATE)` 节点执行时通过延迟 MQ 消息触发各子设备启停,与主链路并行运行、互不阻塞。技术方案见 §5.1.4。
|
|
|
+
|
|
|
+#### 2.7.5 施肥异常处理
|
|
|
+
|
|
|
+| 异常类型 | 处理方式 |
|
|
|
+| -------------------- | ----------------------------------------------------------------- |
|
|
|
+| 施肥泵启动失败 | 停止搅拌电机,向主任务报告警告,灌溉继续 |
|
|
|
+| 搅拌电机启动失败 | 停止施肥泵(防止未搅匀施肥),向主任务报告警告,灌溉继续 |
|
|
|
+| 施肥过程整体异常 | 记录异常原因,本灌区施肥放弃,灌溉继续执行(不触发任务级 PAUSED) |
|
|
|
+| 灌区结束时施肥未完成 | 强制关闭施肥泵和搅拌电机,确保安全 |
|
|
|
+
|
|
|
+> **设计原则**:施肥异常不中断灌溉主任务,二者相互独立,施肥失败仅触发告警,不影响灌区轮灌流程。灌溉水泵即施肥主水泵,水泵状态由灌溉主流程统一控制。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 整体技术架构
|
|
|
+
|
|
|
+### 3.1 架构分层
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ 触发层 │
|
|
|
+│ ┌──────────────────────┐ ┌───────────────────────────────┐ │
|
|
|
+│ │ Quartz 定时调度 │ │ 传感器联动规则引擎 │ │
|
|
|
+│ │ (模式A: Cron表达式) │ │ (实时数据 → 规则比对) │ │
|
|
|
+│ │ (模式B: Simple调度) │ │ (冷却时间: Redis TTL) │ │
|
|
|
+│ └──────────┬───────────┘ └────────────────┬──────────────┘ │
|
|
|
+└──────────────┼─────────────────────────────────┼─────────────────┘
|
|
|
+ │ 触发信号(发MQ消息) │
|
|
|
+ ▼ ▼
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ 流程编排层 │
|
|
|
+│ │
|
|
|
+│ ┌─────────────────────────────────────────────────────────┐ │
|
|
|
+│ │ 执行计划生成(任务保存时预生成) │ │
|
|
|
+│ │ 安全规则 → 有序节点列表 → JSON存储 │ │
|
|
|
+│ └──────────────────────────┬──────────────────────────────┘ │
|
|
|
+│ │ │
|
|
|
+│ ┌──────────────────────────▼──────────────────────────────┐ │
|
|
|
+│ │ 责任链节点执行器(NodeHandler Chain) │ │
|
|
|
+│ │ OPEN_GROUP / CLOSE_GROUP / START_PUMP / STOP_PUMP │ │
|
|
|
+│ │ SET_PUMP_PRESSURE / WAIT / ZONE_SWITCH_WAIT │ │
|
|
|
+│ └──────────────────────────┬──────────────────────────────┘ │
|
|
|
+│ │ │
|
|
|
+│ ┌──────────────────────────▼──────────────────────────────┐ │
|
|
|
+│ │ RocketMQ 延迟消息队列 │ │
|
|
|
+│ │ CHECK消息(10s) / TIMEOUT兜底(30s) / 推进消息(Nmin) │ │
|
|
|
+│ └──────────────────────────────────────────────────────────┘ │
|
|
|
+└─────────────────────────────────────────────────────────────────┘
|
|
|
+ │ MQTT指令下发
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ 设备通信层 │
|
|
|
+│ │
|
|
|
+│ ┌─────────────────────┐ ┌──────────────────────────┐ │
|
|
|
+│ │ EMQX MQTT Broker │◄──────►│ 电磁阀 / 球阀 / 水泵 │ │
|
|
|
+│ │ (指令下发 + ACK) │ │ 施肥机(4G连接) │ │
|
|
|
+│ └─────────────────────┘ └──────────────────────────┘ │
|
|
|
+└─────────────────────────────────────────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ 存储层 │
|
|
|
+│ │
|
|
|
+│ ┌──────────────┐ ┌────────────────┐ ┌────────────────────┐ │
|
|
|
+│ │ MySQL │ │ Redis │ │ InfluxDB │ │
|
|
|
+│ │ 执行计划JSON │ │ 幂等+ACK注册表 │ │ 传感器时序数据 │ │
|
|
|
+│ │ 执行记录 │ │ 冷却时间TTL │ │ (可选) │ │
|
|
|
+│ │ 报警记录 │ │ │ │ │ │
|
|
|
+│ └──────────────┘ └────────────────┘ └────────────────────┘ │
|
|
|
+└─────────────────────────────────────────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────────────────────────────┐
|
|
|
+│ 监控报警层 │
|
|
|
+│ │
|
|
|
+│ ┌──────────────┐ ┌─────────────┐ ┌────────────────────────┐ │
|
|
|
+│ │ 短信 │ │ 电话 │ │ 移动端推送(APP) │ │
|
|
|
+│ │ 报警通知 │ │ 语音报警 │ │ 报警通知 │ │
|
|
|
+│ └──────────────┘ └─────────────┘ └────────────────────────┘ │
|
|
|
+└─────────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 核心设计原则
|
|
|
+
|
|
|
+- **Job 只做触发,不做执行**:Quartz/联动触发后只发 MQ 消息,立即释放线程。
|
|
|
+- **消息驱动,无阻塞等待**:所有等待(ACK等待、灌溉等待)均通过 RocketMQ 延迟消息实现,不使用 Thread.sleep。
|
|
|
+- **状态持久化**:流程状态全部存储在 DB 中,MQ 只是触发器,即使 MQ 消息丢失,看门狗也能从 DB 恢复流程。
|
|
|
+- **幂等处理**:所有消息消费均做幂等校验,防止重复执行。
|
|
|
+- **4G 设备各自独立**:所有球阀/电磁阀通过 4G 独立连接 MQTT Broker,设备之间不互通,平台作为统一协调中心。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 触发层设计
|
|
|
+
|
|
|
+### 4.1 定时触发(Quartz)
|
|
|
+
|
|
|
+#### 4.1.1 模式 A:Cron 表达式调度
|
|
|
+
|
|
|
+- 用户配置的星期 + 时间,在任务保存时由后端自动转换为标准 Cron 表达式。
|
|
|
+- 示例:每周一、三、五早上 02:00 → `0 0 2 ? * MON,WED,FRI`
|
|
|
+- Quartz 以**集群模式**部署,使用 JDBC 存储(将调度信息持久化到 MySQL),避免单点故障。
|
|
|
+- 节点竞争抢锁触发,同一时间只有一个节点执行,防止重复触发。
|
|
|
+- **Misfire 策略**:设置为 `DoNothing`,错过的任务不补跑(轮灌场景下错过就跳过)。
|
|
|
+
|
|
|
+#### 4.1.2 模式 B:Simple 计数调度
|
|
|
+
|
|
|
+- 使用 `SimpleScheduleBuilder` 按间隔天数重复执行。
|
|
|
+- 数据库中记录已执行次数(`executed_count`),每次触发后自增。
|
|
|
+- 当已执行次数达到配置的总次数时,自动标记为停用,不再触发。
|
|
|
+
|
|
|
+#### 4.1.3 Quartz Job 设计原则
|
|
|
+
|
|
|
+**Job 只做触发,不做执行:**
|
|
|
+
|
|
|
+```
|
|
|
+[Quartz Job 触发]
|
|
|
+ │
|
|
|
+ ├─► 生成 task_execution 记录(status=PENDING)
|
|
|
+ │ 写入自动生成的 execution_plan JSON
|
|
|
+ │
|
|
|
+ ├─► 发送 MQ 消息:TASK_START(含 execution_id)
|
|
|
+ │
|
|
|
+ └─► 立即返回(Job 线程释放,不做任何等待)
|
|
|
+```
|
|
|
+
|
|
|
+#### 4.1.4 Quartz 集群配置要点
|
|
|
+
|
|
|
+- **存储模式**:JDBC(`job-store-type: jdbc`),支持集群。
|
|
|
+- **集群开关**:`isClustered: true`,自动ID分配。
|
|
|
+- **线程池**:`threadCount: 20`,Job 触发后立即返回,线程不会被长占。
|
|
|
+- **Misfire 策略**:`withMisfireHandlingInstructionDoNothing()`,服务重启后不补跑错过的任务。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 4.2 联动触发(传感器规则引擎)
|
|
|
+
|
|
|
+#### 4.2.1 触发流程
|
|
|
+
|
|
|
+```
|
|
|
+[传感器数据上报(MQTT)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [后端实时接收传感器数据]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [查询关联此传感器的联动规则(从DB缓存)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [规则比对:设备ID + 比较符 + 阈值]
|
|
|
+ │
|
|
|
+ ┌────┴────┐
|
|
|
+ 满足条件 不满足
|
|
|
+ │ │
|
|
|
+ ▼ └─► 忽略
|
|
|
+ [查询Redis: 冷却时间Key是否存在]
|
|
|
+ │
|
|
|
+ ┌────┴────┐
|
|
|
+ Key存在 Key不存在(冷却结束)
|
|
|
+ │ │
|
|
|
+ ▼ ▼
|
|
|
+ 忽略 [写入冷却时间Key(SET NX + TTL=冷却时长)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [生成 task_execution 记录]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [发送 MQ 消息:TASK_START]
|
|
|
+```
|
|
|
+
|
|
|
+#### 4.2.2 冷却时间实现
|
|
|
+
|
|
|
+- Redis Key 格式:`linkage:cooldown:{rule_id}`
|
|
|
+- 触发时使用 `SET NX` 写入,TTL = 冷却时长(分钟级别),Key 自然过期代表冷却结束。
|
|
|
+- `SET NX` 原子操作保证并发场景下只允许一次触发成功。
|
|
|
+
|
|
|
+#### 4.2.3 规则存储方案
|
|
|
+
|
|
|
+- 当前阶段:规则存 DB,后端实时比对(简单够用,单条件)。
|
|
|
+- 未来扩展:如需支持多条件组合(如 `湿度 < 30 且 温度 > 35`),可引入 Drools 规则引擎。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 流程编排设计
|
|
|
+
|
|
|
+### 5.0 三层模型:配置层与执行层解耦
|
|
|
+
|
|
|
+系统在设计上明确将"用户表达的意图"与"系统实际执行的动作"分为三层,彻底解决配置顺序与执行顺序不一致的问题。
|
|
|
+
|
|
|
+```
|
|
|
+┌────────────────────────────────────────────────────────────────┐
|
|
|
+│ 第一层:用户配置层(Intent Layer) │
|
|
|
+│ 用户只表达"我要什么",不关心怎么做 │
|
|
|
+│ 水泵类型+压力 / 灌区组选择+各灌区时长 │
|
|
|
+└──────────────────────┬─────────────────────────────────────────┘
|
|
|
+ │ 任务保存 / 运行时动态调整时
|
|
|
+ ▼
|
|
|
+┌────────────────────────────────────────────────────────────────┐
|
|
|
+│ 第二层:计划生成引擎(Plan Generator) │
|
|
|
+│ 系统根据配置 + 安全规则,自动生成完整有序节点列表 │
|
|
|
+│ 处理:先开后关 / 水泵开启时机 / 跳过灌区重算 │
|
|
|
+└──────────────────────┬─────────────────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌────────────────────────────────────────────────────────────────┐
|
|
|
+│ 第三层:执行层(Execution Layer) │
|
|
|
+│ 按物理节点列表顺序执行,支持暂停 / 恢复 / 跳过 / 动态重算 │
|
|
|
+└────────────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+**三层职责对照表:**
|
|
|
+
|
|
|
+| 用户配置的(意图) | 计划生成引擎转换为(物理执行节点) |
|
|
|
+| --------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
|
+| "水泵恒压-统一,300kPa" | 自动在 Zone1 成功后插入 `SET_PUMP_PRESSURE(300)` → `START_PUMP`,仅下发一次 |
|
|
|
+| "水泵恒压-分区" | Zone1 后插入 `SET_PUMP_PRESSURE(zone1.压力)` → `START_PUMP`,每次灌区切换时追加 `SET_PUMP_PRESSURE(新压力)` |
|
|
|
+| "球阀配置了目标压力" | 压力参数随 `OPEN_GROUP` 下发给球阀,与水泵压力模式无关,两者独立生效 |
|
|
|
+| "选择灌区组1→2→3,各配时长" | 自动展开为 `OPEN(1)→START_PUMP→WAIT(时长1)→OPEN(2)→ZONE_SWITCH_WAIT→CLOSE(1)→WAIT(时长2)→...→STOP_PUMP→CLOSE(N)`,严格遵循先开后关 |
|
|
|
+| "稳压等待 5 秒" | 在每个 `OPEN_GROUP(ZoneI)` 成功后、`CLOSE_GROUP(ZoneI-1)` 之前插入 `ZONE_SWITCH_WAIT(5s)` |
|
|
|
+| "跳过灌区2(运行时)" | 截断后续节点,重新生成,安全衔接已开启灌区 |
|
|
|
+| "人工修复后继续" | PAUSED 状态接受 Resume API,检测泵状态,自动插入 S-05 重建序列后接续执行 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.1 执行计划自动生成
|
|
|
+
|
|
|
+#### 5.1.1 设计原则
|
|
|
+
|
|
|
+执行计划在**任务保存时**(而非运行时)根据安全规则自动计算并生成完整的有序节点列表,以 JSON 格式存储到 `task_execution.execution_plan` 字段。
|
|
|
+
|
|
|
+**运行时只按顺序逐个执行,不再做任何安全判断逻辑**,保证执行路径的确定性和可追溯性。
|
|
|
+
|
|
|
+#### 5.1.2 自动生成算法
|
|
|
+
|
|
|
+```
|
|
|
+输入:
|
|
|
+ 灌溉组列表 [Zone1, Zone2, ... ZoneN] ← 任务中选择的灌溉组(§2.3.2)
|
|
|
+ 水泵压力模式 NONE / PUMP_UNIFIED / PUMP_ZONE
|
|
|
+ 统一目标压力值(PUMP_UNIFIED 模式)
|
|
|
+ 每组灌溉时长 ← 任务级配置(§2.3.2),每个灌区可不同
|
|
|
+ switch_stable_seconds ← 灌区切换稳压等待秒数(任务级配置)
|
|
|
+ (球阀目标压力在灌溉组设备列表中配置,若有则随 OPEN_GROUP 下发)
|
|
|
+
|
|
|
+输出节点列表:
|
|
|
+
|
|
|
+1. OPEN_GROUP(Zone1) ← 开启第一个灌区(S-01 前置)
|
|
|
+ │ 球阀配置了目标压力时:同时下发角度+压力给球阀
|
|
|
+2. [SET_PUMP_PRESSURE(压力值)] ← PUMP_UNIFIED/PUMP_ZONE 模式插入;NONE 模式不插入
|
|
|
+3. START_PUMP ← 开启水泵(S-01:灌区开启后)
|
|
|
+
|
|
|
+循环 i = 2 到 N:
|
|
|
+ WAIT(灌溉时长)
|
|
|
+ OPEN_GROUP(ZoneI) ← 先开下一个灌区(S-02/S-04)
|
|
|
+ [SET_PUMP_PRESSURE(ZoneI.压力值)] ← 仅 PUMP_ZONE 模式:灌区切换时调整水泵压力
|
|
|
+ ZONE_SWITCH_WAIT(switch_stable_seconds) ← 稳压等待(S-02,防止压力突变)
|
|
|
+ CLOSE_GROUP(ZoneI-1) ← 再关上一个灌区(S-02:稳压后才关)
|
|
|
+
|
|
|
+最后灌区结束:
|
|
|
+ WAIT(灌溉时长)
|
|
|
+ STOP_PUMP ← 关水泵(S-03)
|
|
|
+ CLOSE_GROUP(ZoneN) ← 最后关最后一个灌区(S-03/S-04)
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.3 特殊情况处理
|
|
|
+
|
|
|
+**只有一个灌区:**
|
|
|
+
|
|
|
+```
|
|
|
+index=0 OPEN_GROUP 灌区1
|
|
|
+index=1 SET_PUMP_PRESSURE (PUMP_UNIFIED/PUMP_ZONE 模式)
|
|
|
+index=2 START_PUMP
|
|
|
+index=3 WAIT N分钟
|
|
|
+index=4 STOP_PUMP
|
|
|
+index=5 CLOSE_GROUP 灌区1
|
|
|
+
|
|
|
+→ 没有中间的先开后关逻辑,直接安全关闭
|
|
|
+```
|
|
|
+
|
|
|
+**每个灌区等待时长不同:**
|
|
|
+
|
|
|
+```
|
|
|
+灌区1等3分钟,灌区2等5分钟,灌区3等10分钟,稳压等待5秒
|
|
|
+
|
|
|
+index=0 OPEN_GROUP 灌区1
|
|
|
+index=1 START_PUMP
|
|
|
+index=2 WAIT 3分钟 ← 灌区1的灌溉等待
|
|
|
+index=3 OPEN_GROUP 灌区2
|
|
|
+index=4 ZONE_SWITCH_WAIT 5秒 ← 稳压等待(S-02)
|
|
|
+index=5 CLOSE_GROUP 灌区1
|
|
|
+index=6 WAIT 5分钟 ← 灌区2的灌溉等待
|
|
|
+index=7 OPEN_GROUP 灌区3
|
|
|
+index=8 ZONE_SWITCH_WAIT 5秒 ← 稳压等待
|
|
|
+index=9 CLOSE_GROUP 灌区2
|
|
|
+index=10 WAIT 10分钟 ← 灌区3的灌溉等待
|
|
|
+index=11 STOP_PUMP
|
|
|
+index=12 CLOSE_GROUP 灌区3
|
|
|
+
|
|
|
+→ ZONE_SWITCH_WAIT 确保两个灌区同时开启的时间窗口内压力趋于平稳后再关旧灌区
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.4 施肥机执行方案设计
|
|
|
+
|
|
|
+本节设计施肥机在轮灌执行过程中的控制方案,包括子任务模型、状态机、触发流程、数据结构及安全约束。
|
|
|
+
|
|
|
+##### 5.1.4.1 核心设计思路:施肥子任务(FertTask)
|
|
|
+
|
|
|
+施肥机的两个子设备(搅拌电机、施肥泵)的启停时序**完全在单个灌区时间窗口内发生**,与主执行计划的节点链路**平行运行**,互不阻塞。灌溉水泵即施肥主水泵,由灌溉主流程统一控制,施肥子任务不单独管理水泵。
|
|
|
+
|
|
|
+设计采用**施肥子任务(FertTask)**模型:
|
|
|
+
|
|
|
+```
|
|
|
+主流程节点链路(顺序执行):
|
|
|
+ OPEN_GROUP(Z1) → START_PUMP → WAIT(30min) → ... → STOP_PUMP → CLOSE_GROUP(Z1)
|
|
|
+
|
|
|
+施肥子任务(并行,由延迟 MQ 驱动):
|
|
|
+ T+3min → 搅拌电机 ON (延迟MQ:延迟=施肥延迟时间)
|
|
|
+ T+5min → 施肥泵 ON (延迟MQ:延迟=施肥延迟+搅拌提前)
|
|
|
+ T+10min→ 施肥泵 OFF + 搅拌电机 OFF (延迟MQ 或 施肥机自动停止)
|
|
|
+```
|
|
|
+
|
|
|
+主流程的 `WAIT(30min)` 节点执行时,同步向 MQ 发送一条带参数的施肥调度消息,由施肥子任务处理器接管后续的子设备控制,**不影响主链路推进**。
|
|
|
+
|
|
|
+##### 5.1.4.2 施肥子任务状态机
|
|
|
+
|
|
|
+```
|
|
|
+FertTask 状态:
|
|
|
+ PENDING → STIR_WAITING → STIRRING → FERT_RUNNING → DONE
|
|
|
+ │ │ │
|
|
|
+ │ │ └─ 施肥泵 ON,等待完成
|
|
|
+ │ └─ 搅拌电机 ON,等待施肥泵启动
|
|
|
+ └─ 等待施肥延迟时间到
|
|
|
+ FAILED(任意子设备失败时进入,不影响主任务)
|
|
|
+```
|
|
|
+
|
|
|
+| 状态 | 说明 | 下一状态 |
|
|
|
+| -------------- | -------------------------------- | ------------------------- |
|
|
|
+| `PENDING` | 已创建,等待施肥延迟时间到 | `STIR_WAITING` → 发延迟MQ |
|
|
|
+| `STIR_WAITING` | 发出搅拌电机启动指令,等待ACK | `STIRRING` |
|
|
|
+| `STIRRING` | 搅拌中,等待搅拌提前时长到 | `FERT_RUNNING` → 发延迟MQ |
|
|
|
+| `FERT_RUNNING` | 施肥泵运行中,等待时长/容量达到 | `DONE` |
|
|
|
+| `DONE` | 施肥子任务成功完成 | — |
|
|
|
+| `FAILED` | 子设备故障,施肥放弃,主任务继续 | — |
|
|
|
+
|
|
|
+##### 5.1.4.3 施肥子任务触发流程
|
|
|
+
|
|
|
+```
|
|
|
+[主流程执行 WAIT(IRRIGATE) 节点]
|
|
|
+ │
|
|
|
+ ├─► 发主链路延迟MQ(N分钟后推进主节点)
|
|
|
+ │
|
|
|
+ └─ 检查任务是否选择了施肥机设备?
|
|
|
+ │ 是
|
|
|
+ ▼
|
|
|
+ [创建 FertTask 记录,status = PENDING]
|
|
|
+ [写入 fert_task 表:绑定 execution_id + zone_id]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [发施肥延迟MQ]
|
|
|
+ 消息类型:FERT_STIR_START
|
|
|
+ 延迟时间:施肥延迟时间(分钟)
|
|
|
+ │
|
|
|
+ ─── 施肥延迟时间后 ───────────────
|
|
|
+ │
|
|
|
+ [FERT_STIR_START 消息到达]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [下发搅拌电机 ON 指令,等待ACK]
|
|
|
+ ├─ ACK成功 → status = STIRRING
|
|
|
+ │ └─ 发 FERT_PUMP_START 延迟MQ(延迟=搅拌提前时长)
|
|
|
+ └─ ACK失败 → status = FAILED,发告警,不影响主任务
|
|
|
+
|
|
|
+ ─── 搅拌提前时长后 ───────────────
|
|
|
+ │
|
|
|
+ [FERT_PUMP_START 消息到达]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [下发施肥泵 ON 指令,等待ACK]
|
|
|
+ ├─ ACK成功 → status = FERT_RUNNING
|
|
|
+ │ ├─ [时间模式] 发 FERT_PUMP_STOP 延迟MQ(延迟=施肥时长)
|
|
|
+ │ └─ [容量模式] 下发定量施肥指令给施肥机(含目标施肥量参数)
|
|
|
+ └─ ACK失败 → 关搅拌电机 → status = FAILED,发告警
|
|
|
+
|
|
|
+ ─── 时间模式:施肥时长后 ─────────
|
|
|
+ │
|
|
|
+ [FERT_PUMP_STOP 消息到达]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [下发施肥泵 OFF 指令]
|
|
|
+ [下发搅拌电机 OFF 指令](同步停止)
|
|
|
+ [status = DONE]
|
|
|
+
|
|
|
+ ─── 容量模式:施肥机自动停止 ─────
|
|
|
+ │
|
|
|
+ 施肥机达到目标量后自动停止施肥泵
|
|
|
+ 搅拌电机由施肥机定量控制指令自行控制停止
|
|
|
+ 系统无需主动发送停止指令
|
|
|
+ [status = DONE]
|
|
|
+
|
|
|
+ ─── 灌区结束 / 灌区切换(两种模式共用)───
|
|
|
+ │
|
|
|
+ [主 WAIT 节点完成 或 灌区切换]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [检查 FertTask.status]
|
|
|
+ ├─ DONE → 已正常完成,灌区切换时额外发送关闭施肥机指令(安全兜底)
|
|
|
+ └─ FERT_RUNNING → 施肥未完成
|
|
|
+ → 发送关闭施肥机指令(施肥泵 OFF + 搅拌电机 OFF)
|
|
|
+ → 记录实际施肥信息
|
|
|
+ → status = DONE(标记未完成目标)
|
|
|
+ → 发送告警
|
|
|
+```
|
|
|
+
|
|
|
+##### 5.1.4.4 容量模式施肥控制
|
|
|
+
|
|
|
+```
|
|
|
+容量模式(status = FERT_RUNNING 期间):
|
|
|
+ 系统下发定量施肥指令给施肥机(含目标施肥量参数)
|
|
|
+ 施肥机内部通过流量计监控,达到目标量后自动停止施肥泵
|
|
|
+ 搅拌电机的停止由施肥机定量控制指令自行控制
|
|
|
+
|
|
|
+灌区灌溉结束时(主 WAIT 节点完成):
|
|
|
+ FertTask.status 仍为 FERT_RUNNING?
|
|
|
+ → 未达到目标量,发送关闭施肥机指令(施肥泵 OFF + 搅拌电机 OFF)
|
|
|
+ → 记录实际施肥量到 fert_task.actual_amount
|
|
|
+ → status = DONE(标记未完成目标)
|
|
|
+ → 发送告警:本次施肥未达目标量
|
|
|
+
|
|
|
+灌区切换时:
|
|
|
+ → 系统额外发送关闭施肥机指令,确保安全性
|
|
|
+```
|
|
|
+
|
|
|
+##### 5.1.4.5 FertTask 数据结构
|
|
|
+
|
|
|
+```java
|
|
|
+FertTask {
|
|
|
+ Long id;
|
|
|
+ Long executionId; // 关联主任务执行实例
|
|
|
+ Long zoneId; // 绑定的灌区ID
|
|
|
+
|
|
|
+ // 施肥参数(从任务级统一配置冗余存储,防止任务修改影响执行记录)
|
|
|
+ Integer delayMinutes; // 施肥延迟时间
|
|
|
+ Integer preStirMinutes; // 搅拌提前时长
|
|
|
+ Integer fertDurationMinutes; // 施肥时长(时间模式)
|
|
|
+ Integer fertTargetLiters; // 施肥目标量(容量模式,null=时间模式)
|
|
|
+
|
|
|
+ // 执行状态
|
|
|
+ FertStatus status; // PENDING/STIR_WAITING/STIRRING/FERT_RUNNING/DONE/FAILED
|
|
|
+ Integer actualFertSeconds; // 实际施肥时长
|
|
|
+ Integer actualLiters; // 实际施肥量(容量模式)
|
|
|
+ String failReason; // 失败原因
|
|
|
+
|
|
|
+ // 时间戳
|
|
|
+ DateTime stirStartAt; // 搅拌电机实际启动时间
|
|
|
+ DateTime pumpStartAt; // 施肥泵实际启动时间
|
|
|
+ DateTime pumpStopAt; // 施肥泵实际停止时间
|
|
|
+ DateTime doneAt; // 子任务完成时间
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+##### 5.1.4.6 施肥参数与灌区的关系
|
|
|
+
|
|
|
+施肥参数在创建任务时按任务级统一配置(见 §2.3.2),是否施肥由任务是否选择了施肥机设备来决定,所有灌区共用同一套施肥参数:
|
|
|
+
|
|
|
+```
|
|
|
+任务灌区配置示例(任务已选择施肥机,施肥参数:延迟3min,搅拌提前2min,施肥时长5min):
|
|
|
+ 灌区1(Z1):灌溉时长 30min → 创建 FertTask(时间模式,共用施肥参数)
|
|
|
+ 灌区2(Z2):灌溉时长 20min → 创建 FertTask(时间模式,共用施肥参数)
|
|
|
+ 灌区3(Z3):灌溉时长 25min → 创建 FertTask(时间模式,共用施肥参数)
|
|
|
+
|
|
|
+执行时:
|
|
|
+ 执行 Z1 的 WAIT 节点 → 创建 FertTask(共用施肥参数)
|
|
|
+ 执行 Z2 的 WAIT 节点 → 创建 FertTask(共用施肥参数)
|
|
|
+ 执行 Z3 的 WAIT 节点 → 创建 FertTask(共用施肥参数)
|
|
|
+```
|
|
|
+
|
|
|
+##### 5.1.4.7 安全约束
|
|
|
+
|
|
|
+| 约束 | 说明 |
|
|
|
+| ------------------------------ | -------------------------------------------------------------------- |
|
|
|
+| 施肥子任务不阻塞主链路 | FertTask 失败时,主任务继续;主任务可以无视 FertTask 状态 |
|
|
|
+| 灌区结束时强制停止所有施肥设备 | 无论 FertTask 处于什么状态,灌区 WAIT 完成时必须关停施肥泵和搅拌电机 |
|
|
|
+| 搅拌先于施肥泵关闭 | 施肥泵停止后,搅拌电机同步停止(防止残肥沉积) |
|
|
|
+| 灌区切换时发送关闭指令 | 容量模式下灌区切换时额外发送关闭施肥机指令,确保安全 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.1.5 计划生成引擎(Plan Generator)详细设计
|
|
|
+
|
|
|
+计划生成引擎是系统安全性的核心保障,负责将用户配置的"灌溉意图"转换为"安全有序的物理执行节点列表"。本节详细描述引擎的内部架构、Pipeline 各阶段处理逻辑及调用时机。
|
|
|
+
|
|
|
+#### 5.1.5.1 引擎职责与调用时机
|
|
|
+
|
|
|
+引擎在以下三种场景下被调用,每次调用均输出完整或增量的节点列表:
|
|
|
+
|
|
|
+| 调用场景 | 触发时机 | 输出 |
|
|
|
+| ------------ | --------------------------- | ------------------------------------------------------ |
|
|
|
+| **全量生成** | 任务首次保存 / 任务配置修改 | 完整的 `execution_plan.nodes` |
|
|
|
+| **跳过重算** | 运行时跳过某灌区(5.4 节) | 截断当前 index 后的 PENDING 节点,重新生成后续节点追加 |
|
|
|
+| **故障重建** | Resume API 触发(5.5.4 节) | 在 current_index 前插入启动重建序列节点 |
|
|
|
+
|
|
|
+#### 5.1.5.2 引擎输入契约(PlanGeneratorInput)
|
|
|
+
|
|
|
+```java
|
|
|
+PlanGeneratorInput {
|
|
|
+ // 基础信息
|
|
|
+ Long taskId;
|
|
|
+ Long executionId; // 执行实例ID(全量生成时为新ID)
|
|
|
+
|
|
|
+ // 灌区配置(有序列表,顺序即灌溉意图顺序)
|
|
|
+ // ZoneConfig 合并灌溉组模板(zoneName, zonePressureKpa, deviceList)与任务级配置(durationSeconds)
|
|
|
+ List<ZoneConfig> zones; // 每个灌区含:zoneId, zoneName, durationSeconds(任务级), zonePressureKpa(模板), deviceList(模板)
|
|
|
+
|
|
|
+ // 施肥配置(任务级统一,所有灌区共用;为null表示不施肥)
|
|
|
+ FertConfig fertConfig; // 含:delayMinutes, preStirMinutes, fertDurationMinutes/fertTargetLiters
|
|
|
+ Long fertPumpDeviceId; // 施肥泵设备ID(选择了施肥机时有值,否则null)
|
|
|
+ Long stirDeviceId; // 搅拌电机设备ID(选择了施肥机时有值,否则null)
|
|
|
+
|
|
|
+ // 压力控制配置
|
|
|
+ PressureMode pressureMode; // NONE / PUMP_UNIFIED / PUMP_ZONE
|
|
|
+ Integer targetPressureKpa; // 仅 PUMP_UNIFIED 模式有值
|
|
|
+ // 球阀目标压力在各灌溉组的设备列表中配置,若有则随 OPEN_GROUP 下发,与水泵压力模式无关
|
|
|
+
|
|
|
+ // 安全参数
|
|
|
+ Integer switchStableSeconds; // 灌区切换稳压等待时间
|
|
|
+
|
|
|
+ // 运行时增量参数(跳过重算 / 故障重建时使用)
|
|
|
+ GenerateMode mode; // FULL / SKIP_REBUILD / RESUME_REBUILD
|
|
|
+ Integer fromIndex; // 增量生成时的起始节点索引
|
|
|
+ Long currentOpenZoneId; // 当前已开启但未关闭的灌区ID(增量时用于衔接)
|
|
|
+ List<Long> skipZoneIds; // 运行时已跳过的灌区ID列表
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.5.3 引擎输出契约(PlanGeneratorOutput)
|
|
|
+
|
|
|
+```java
|
|
|
+PlanGeneratorOutput {
|
|
|
+ List<ExecutionNode> nodes; // 生成的节点列表(全量或增量)
|
|
|
+ Integer startIndex; // 节点在完整计划中的起始 index
|
|
|
+ Integer estimatedSeconds; // 本批节点预计总耗时(用于前端展示预估完成时间)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.5.4 五阶段处理 Pipeline
|
|
|
+
|
|
|
+引擎内部按固定顺序执行五个阶段,每个阶段向节点列表中插入对应节点:
|
|
|
+
|
|
|
+```
|
|
|
+输入(PlanGeneratorInput)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────┐
|
|
|
+│ Phase 1:输入校验(Validator) │
|
|
|
+│ - 灌区列表不能为空 │
|
|
|
+│ - switch_stable_seconds ≥ 0 │
|
|
|
+│ - PUMP_UNIFIED 模式必须有 targetPressureKpa │
|
|
|
+│ - PUMP_ZONE 模式每个灌区必须有 zonePressureKpa │
|
|
|
+│ - NONE 模式无额外压力校验 │
|
|
|
+│ - 跳过重算时 currentOpenZoneId 必须不为空 │
|
|
|
+└──────────────────────┬───────────────────────────────┘
|
|
|
+ │ 校验通过
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────┐
|
|
|
+│ Phase 2:首灌区启动序列生成(StartupBuilder) │
|
|
|
+│ │
|
|
|
+│ 输出节点(按 S-01 规则): │
|
|
|
+│ OPEN_GROUP(Zone1) │
|
|
|
+│ ← 球阀配置了目标压力时,压力参数随球阀指令下发 │
|
|
|
+│ [SET_PUMP_PRESSURE] ← PUMP_UNIFIED/PUMP_ZONE 模式 │
|
|
|
+│ START_PUMP │
|
|
|
+│ NONE 模式: │
|
|
|
+│ OPEN_GROUP(Zone1) │
|
|
|
+│ START_PUMP ← 不插入 SET_PUMP_PRESSURE │
|
|
|
+│ │
|
|
|
+│ 跳过重算(SKIP_REBUILD): │
|
|
|
+│ 直接跳过此阶段(接续之前的启动状态) │
|
|
|
+│ 故障重建(RESUME_REBUILD): │
|
|
|
+│ 生成 S-05 重建启动序列(先开灌区→再开泵) │
|
|
|
+└──────────────────────┬───────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────┐
|
|
|
+│ Phase 3:灌区轮转序列展开(ZoneRotationExpander) │
|
|
|
+│ │
|
|
|
+│ 输入:有效灌区列表(去掉 skipZoneIds), │
|
|
|
+│ 当前已开启灌区(增量模式时用于衔接) │
|
|
|
+│ │
|
|
|
+│ 对每个灌区(从第2个开始,或增量模式从下一个灌区开始): │
|
|
|
+│ 追加: WAIT(zone[i-1].durationSeconds) │
|
|
|
+│ 追加: OPEN_GROUP(zone[i]) ← S-04 先开 │
|
|
|
+│ 追加: [SET_PUMP_PRESSURE(zone[i].pressure)] │
|
|
|
+│ ← 仅 PUMP_ZONE 模式:灌区切换时调整压力 │
|
|
|
+│ 追加: ZONE_SWITCH_WAIT(switchStableSeconds) ← S-02 │
|
|
|
+│ 追加: CLOSE_GROUP(zone[i-1]) ← S-04 后关 │
|
|
|
+│ │
|
|
|
+│ 最后一个灌区额外追加: │
|
|
|
+│ 追加: WAIT(zone[N].durationSeconds) │
|
|
|
+└──────────────────────┬───────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────┐
|
|
|
+│ Phase 4:正常结束序列追加(SafeShutdownBuilder) │
|
|
|
+│ │
|
|
|
+│ 遵循 S-03(正常结束顺序): │
|
|
|
+│ STOP_PUMP │
|
|
|
+│ CLOSE_GROUP(ZoneN) ← 最后一个灌区(S-04 最后关) │
|
|
|
+│ │
|
|
|
+│ 注意:此处为正常结束流程的计划生成,异常安全关闭见§6.4 │
|
|
|
+│ 异常时只关水泵+施肥设备,阀门保持当前状态 │
|
|
|
+└──────────────────────┬───────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+┌──────────────────────────────────────────────────────┐
|
|
|
+│ Phase 5:节点索引分配与元数据填充(NodeIndexer) │
|
|
|
+│ │
|
|
|
+│ 对所有节点: │
|
|
|
+│ 分配连续 index(全量从0开始,增量从 fromIndex 开始) │
|
|
|
+│ 填充 nodeName(用于日志和报警展示) │
|
|
|
+│ 填充 maxRetry(ACK类节点=3,WAIT类节点=0) │
|
|
|
+│ 填充 params.source(IRRIGATE/ZONE_SWITCH/REBUILD) │
|
|
|
+│ 初始化 status = PENDING │
|
|
|
+│ 初始化 devices(从 DB 查关联设备信息填入) │
|
|
|
+│ │
|
|
|
+│ 施肥标注(当 fertConfig 不为 null 时): │
|
|
|
+│ 对每个 WAIT(source=IRRIGATE) 节点: │
|
|
|
+│ 标注 params.fertEnabled = true │
|
|
|
+│ 标注 params.fertPumpDeviceId / stirDeviceId │
|
|
|
+│ 运行时执行引擎据此创建 FertTask 并发送施肥MQ消息 │
|
|
|
+└──────────────────────┬───────────────────────────────┘
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ 输出(PlanGeneratorOutput)
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.5.5 三种调用模式的 Pipeline 差异
|
|
|
+
|
|
|
+| Phase | 全量生成(FULL) | 跳过重算(SKIP_REBUILD) | 故障重建(RESUME_REBUILD) |
|
|
|
+| ------------------ | ------------------ | ------------------------------------------------------- | ------------------------------------------------ |
|
|
|
+| Phase 1 校验 | 完整校验 | 额外校验 `currentOpenZoneId` | 额外校验泵状态 |
|
|
|
+| Phase 2 首启动序列 | ✅ 生成 Zone1 启动 | ❌ 跳过(接续之前的启动状态) | ✅ 生成重建启动序列(S-05) |
|
|
|
+| Phase 3 灌区展开 | 从 Zone2 开始展开 | 从跳过后的下一个灌区开始,以 `currentOpenZoneId` 为前驱 | 从当前灌区接续,跳过已完成灌区 |
|
|
|
+| Phase 4 安全关闭 | ✅ 追加 | ✅ 追加 | ✅ 追加 |
|
|
|
+| Phase 5 索引分配 | 从 index=0 | 从 `fromIndex` 续编 | 插入到 `current_index` 之前,已有节点 index 不变 |
|
|
|
+
|
|
|
+#### 5.1.5.6 引擎类结构(Java 伪代码)
|
|
|
+
|
|
|
+```java
|
|
|
+@Component
|
|
|
+public class PlanGenerator {
|
|
|
+
|
|
|
+ // 五个 Phase 处理器,按顺序注入
|
|
|
+ private final InputValidator validator;
|
|
|
+ private final StartupBuilder startupBuilder;
|
|
|
+ private final ZoneRotationExpander rotationExpander;
|
|
|
+ private final SafeShutdownBuilder shutdownBuilder;
|
|
|
+ private final NodeIndexer indexer;
|
|
|
+
|
|
|
+ public PlanGeneratorOutput generate(PlanGeneratorInput input) {
|
|
|
+ // Phase 1
|
|
|
+ validator.validate(input);
|
|
|
+
|
|
|
+ List<ExecutionNode> nodes = new ArrayList<>();
|
|
|
+
|
|
|
+ // Phase 2
|
|
|
+ if (input.getMode() != SKIP_REBUILD) {
|
|
|
+ nodes.addAll(startupBuilder.build(input));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Phase 3
|
|
|
+ nodes.addAll(rotationExpander.expand(input, nodes));
|
|
|
+
|
|
|
+ // Phase 4
|
|
|
+ nodes.addAll(shutdownBuilder.build(input, nodes));
|
|
|
+
|
|
|
+ // Phase 5
|
|
|
+ return indexer.assign(nodes, input);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.5.7 安全约束检查(内嵌在各 Phase 中)
|
|
|
+
|
|
|
+每个 Phase 在构建节点前都内嵌对应安全规则检查,违反则抛出异常(不生成计划):
|
|
|
+
|
|
|
+| Phase | 检查点 | 违反时行为 |
|
|
|
+| ------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
|
+| Phase 2 | START_PUMP 前必须已有 OPEN_GROUP(S-01);PUMP_UNIFIED/PUMP_ZONE 模式必须有 SET_PUMP_PRESSURE;NONE 模式不插入 SET_PUMP_PRESSURE | 抛出 SafetyViolationException |
|
|
|
+| Phase 3 | CLOSE_GROUP 前必须已有对应的 OPEN_GROUP 且中间有 ZONE_SWITCH_WAIT(S-02) | 抛出 SafetyViolationException |
|
|
|
+| Phase 4 | CLOSE_GROUP(ZoneN) 必须在 STOP_PUMP 之后(S-03) | 抛出 SafetyViolationException |
|
|
|
+
|
|
|
+> **设计意图**:安全检查在生成时一次性验证,运行时执行引擎无需重复校验,保证执行路径的确定性。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.2 责任链 + 延迟 MQ 执行流程
|
|
|
+
|
|
|
+#### 5.2.1 核心设计思路
|
|
|
+
|
|
|
+- 每个节点由对应的 `NodeHandler` 执行,执行只做**下发指令**,立即返回,不等待设备响应。
|
|
|
+- 通过 RocketMQ 延迟消息实现异步等待,释放线程资源。
|
|
|
+- 整个流程完全由消息驱动,无同步阻塞。
|
|
|
+
|
|
|
+#### 5.2.2 主执行流程图
|
|
|
+
|
|
|
+```
|
|
|
+[MQ消息到达: TASK_START / NEXT_NODE]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [从DB加载 task_execution(含 execution_plan JSON)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [读取 current_index,取当前节点]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [查找对应 NodeHandler(按节点类型路由)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [NodeHandler.execute()]
|
|
|
+ │ 下发 MQTT 指令给所有关联设备
|
|
|
+ │ 在 Redis 注册 ACK 期望列表(ACK 注册表)
|
|
|
+ │ 立即返回(不等待设备响应)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [发送两条延迟MQ消息]
|
|
|
+ ├─► 消息A: CHECK_ACK(10秒后)
|
|
|
+ └─► 消息B: ACK_TIMEOUT(30秒后,兜底)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [线程释放,等待延迟消息到达]
|
|
|
+
|
|
|
+
|
|
|
+─────── 10秒后 ──────────────────────────────────────────
|
|
|
+
|
|
|
+[MQ消息到达: CHECK_ACK]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [查询 Redis ACK 注册表,检查所有设备状态]
|
|
|
+ │
|
|
|
+ ┌─────┴──────────────┐
|
|
|
+ 全部成功 有失败 / 未全回
|
|
|
+ │ │
|
|
|
+ ▼ ▼
|
|
|
+ [更新节点状态=SUCCESS] [触发重试逻辑(见第6章)]
|
|
|
+ [发送延迟MQ消息] │
|
|
|
+ NEXT_NODE(N分钟后) [或5秒后再次CHECK]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [线程释放]
|
|
|
+
|
|
|
+
|
|
|
+─────── N分钟后 ─────────────────────────────────────────
|
|
|
+
|
|
|
+[MQ消息到达: NEXT_NODE]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [current_index + 1,更新DB(乐观锁)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ [index 超出节点列表长度?]
|
|
|
+ │ │
|
|
|
+ 否 是
|
|
|
+ │ │
|
|
|
+ ▼ ▼
|
|
|
+ [取下一节点] [status = SUCCESS]
|
|
|
+ [重复执行] [记录完成时间]
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.2.3 WAIT 节点处理
|
|
|
+
|
|
|
+WAIT 节点不下发任何设备指令,仅发送一条延迟 MQ 消息(延迟时长 = 配置的灌溉等待时长),消息到达后直接推进到下一节点。线程不做任何等待。
|
|
|
+
|
|
|
+> **施肥子任务触发**:当 WAIT 节点类型为 `IRRIGATE`(灌区灌溉等待)且任务选择了施肥机设备时,WAIT 节点执行时还会同步创建 FertTask 记录并发送施肥延迟 MQ 消息,由施肥子任务处理器接管后续子设备控制,不阻塞主链路。详见 §5.1.4.3。
|
|
|
+
|
|
|
+#### 5.2.4 线程资源对比
|
|
|
+
|
|
|
+```
|
|
|
+Thread.sleep方案:
|
|
|
+ [线程]═══════════════════════════════════════(占用3小时)
|
|
|
+
|
|
|
+责任链+延迟MQ方案:
|
|
|
+ [线程]═[释放] [线程]═[释放] [线程]═[释放]
|
|
|
+ ↑延迟消息触发↑ ↑延迟消息触发↑
|
|
|
+
|
|
|
+ 每次只占用毫秒级线程,等待期间零占用
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.3 节点类型定义
|
|
|
+
|
|
|
+#### 5.3.1 节点类型枚举
|
|
|
+
|
|
|
+| 节点类型 | 说明 | 关联设备 | 是否需要 ACK |
|
|
|
+| ------------------- | ------------------------------------------------------------------------------------------------------ | ------------ | ------------ |
|
|
|
+| `OPEN_GROUP` | 开启灌溉组(电磁阀开 + 球阀到目标角度;球阀配置了目标压力时同时下发压力指令) | 电磁阀、球阀 | 是 |
|
|
|
+| `CLOSE_GROUP` | 关闭灌溉组(电磁阀关 + 球阀归零) | 电磁阀、球阀 | 是 |
|
|
|
+| `START_PUMP` | 启动水泵 | 水泵 | 是 |
|
|
|
+| `STOP_PUMP` | 关闭水泵 | 水泵 | 是 |
|
|
|
+| `SET_PUMP_PRESSURE` | 设置变频恒压水泵目标压力(PUMP_UNIFIED 首次下发,PUMP_ZONE 每次灌区切换时下发;NONE 模式不插入此节点) | 水泵 | 是 |
|
|
|
+| `WAIT` | 灌溉等待指定时长(灌区灌溉中) | 无 | 否 |
|
|
|
+| `ZONE_SWITCH_WAIT` | 灌区切换稳压等待(`switch_stable_seconds`),在 OPEN 后、CLOSE 前执行,防止压力突变 | 无 | 否 |
|
|
|
+
|
|
|
+> **施肥消息类型**:施肥子任务通过独立的 MQ 延迟消息驱动,不在主节点链路中,因此不作为主链路节点类型。施肥相关的 MQ 消息类型定义如下:
|
|
|
+
|
|
|
+| 施肥消息类型 | 说明 | 触发时机 |
|
|
|
+| ------------------ | -------------------------------------------------- | -------------------------------- |
|
|
|
+| `FERT_STIR_START` | 搅拌电机启动 | 施肥延迟时间到达后触发 |
|
|
|
+| `FERT_PUMP_START` | 施肥泵启动 | 搅拌提前时长到达后触发 |
|
|
|
+| `FERT_PUMP_STOP` | 施肥泵+搅拌电机停止(仅时间模式) | 施肥时长到达后触发 |
|
|
|
+| `FERT_FORCE_STOP` | 强制关闭施肥设备(灌区结束/切换时兜底) | 灌区 WAIT 完成或灌区切换时触发 |
|
|
|
+
|
|
|
+#### 5.3.2 NodeHandler 统一接口
|
|
|
+
|
|
|
+```
|
|
|
+NodeHandler 接口:
|
|
|
+ ├── getType() 返回节点类型(用于路由匹配)
|
|
|
+ ├── execute(context) 执行本节点动作(发MQTT指令,立即返回)
|
|
|
+ ├── checkResult() 检查执行结果(查Redis ACK状态)
|
|
|
+ └── onFailure() 失败处理(记录日志、触发重试或终止)
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.3.3 扩展说明
|
|
|
+
|
|
|
+系统采用**策略模式 + 节点类型路由**的扩展设计:
|
|
|
+
|
|
|
+- 每种节点类型对应一个独立的 `NodeHandler` 实现类。
|
|
|
+- 执行引擎通过节点类型字符串查找对应 Handler,执行 `handle(context)` 方法。
|
|
|
+- **新增节点类型只需**:
|
|
|
+ 1. 定义新的节点类型枚举值
|
|
|
+ 2. 实现对应 `NodeHandler`
|
|
|
+ 3. 注册到 Handler 路由表
|
|
|
+- 无需修改任何现有 Handler 或执行引擎代码(开闭原则)。
|
|
|
+
|
|
|
+**未来可扩展的节点类型:**
|
|
|
+
|
|
|
+| 节点类型 | 说明 |
|
|
|
+| ---------------- | ---------------------------------- |
|
|
|
+| `WEATHER_CHECK` | 天气检查节点(恶劣天气自动终止) |
|
|
|
+| `SOIL_SENSOR` | 土壤传感器检查节点(湿度达标跳过) |
|
|
|
+| `NOTIFY` | 通知节点(流程中间发通知) |
|
|
|
+| `MANUAL_CONFIRM` | 人工确认节点(等待人工确认继续) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.4 运行时动态跳过灌区
|
|
|
+
|
|
|
+当某个灌区的设备开启失败且用户决定跳过时,不能简单标记该节点为跳过,因为先开后关的拓扑关系需要重新计算。系统采用**截断重算**方案:
|
|
|
+
|
|
|
+#### 5.4.1 执行计划中的逻辑层扩展
|
|
|
+
|
|
|
+在 `execution_plan` JSON 根节点新增两个字段,用于支持运行时调整:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "zone_sequence": [1, 2, 3],
|
|
|
+ "skip_zones": [],
|
|
|
+ "nodes": [ ... ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| 字段 | 说明 |
|
|
|
+| --------------- | ----------------------------------------------------- |
|
|
|
+| `zone_sequence` | 当前有效的灌区执行顺序(逻辑顺序,运行时可调整) |
|
|
|
+| `skip_zones` | 运行时被跳过的灌区 ID 列表 |
|
|
|
+| `nodes` | 物理执行节点列表(由生成引擎根据 zone_sequence 生成) |
|
|
|
+
|
|
|
+#### 5.4.2 跳过流程
|
|
|
+
|
|
|
+```
|
|
|
+[灌区 N 开启失败,用户/系统决定跳过]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+1. 将 zone_n 写入 skip_zones 列表
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+2. 截断 current_index 之后所有 PENDING 节点
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+3. 以【剩余灌区(去掉 skip_zones)】为输入,
|
|
|
+ 以【当前已开启但未关闭的灌区】为"前一个灌区",
|
|
|
+ 调用 Plan Generator 重新生成后续节点
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+4. 将新节点追加到 execution_plan.nodes 中
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+5. 更新 current_index,继续执行
|
|
|
+ (先开后关的安全逻辑在重新生成时自动保证)
|
|
|
+```
|
|
|
+
|
|
|
+**示例**(跳过灌区2):
|
|
|
+
|
|
|
+```
|
|
|
+原始计划:
|
|
|
+ OPEN(Z1) → START_PUMP → WAIT(10) → OPEN(Z2) → CLOSE(Z1)
|
|
|
+ → WAIT(10) → OPEN(Z3) → CLOSE(Z2) → WAIT(10) → STOP_PUMP → CLOSE(Z3)
|
|
|
+
|
|
|
+灌区2开启失败,跳过:
|
|
|
+ ✅ OPEN(Z1) → START_PUMP → WAIT(10)
|
|
|
+ ❌ OPEN(Z2) 失败 → 跳过,截断后续节点
|
|
|
+ 重新生成:→ OPEN(Z3) → CLOSE(Z1) ← 正确衔接已开启的 Zone1
|
|
|
+ → WAIT(10) → STOP_PUMP → CLOSE(Z3)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.5 故障接续执行(PAUSED 状态)
|
|
|
+
|
|
|
+#### 5.5.1 状态扩展
|
|
|
+
|
|
|
+新增 `PAUSED` 状态,用于区分"可接续的暂停"与"不可恢复的终止":
|
|
|
+
|
|
|
+| 状态 | 含义 | 后续操作 |
|
|
|
+| ----------- | --------------------------------- | ------------------------ |
|
|
|
+| `FAILED` | 硬件故障,不可恢复,触发安全关闭 | 无,任务结束 |
|
|
|
+| `PAUSED` | 网络/超时类重试耗尽,等待人工介入 | 调用 Resume API 接续执行 |
|
|
|
+| `CANCELLED` | 人工主动放弃 | 触发安全关闭 |
|
|
|
+
|
|
|
+#### 5.5.2 进入 PAUSED 的条件
|
|
|
+
|
|
|
+```
|
|
|
+重试策略判断(见 6.3):
|
|
|
+
|
|
|
+失败类型为"网络/超时" 且 retryCount >= 3:
|
|
|
+ → status = PAUSED
|
|
|
+ → 发送多通道报警(提示人工检查设备后接续)
|
|
|
+ → 停止发送任何新的 MQ 消息(流程冻结)
|
|
|
+ → 看门狗不再 recover(PAUSED 是预期状态)
|
|
|
+
|
|
|
+失败类型为"硬件故障":
|
|
|
+ → status = FAILED
|
|
|
+ → 触发安全关闭流程
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.5.3 Resume API
|
|
|
+
|
|
|
+```
|
|
|
+POST /task-execution/{id}/resume
|
|
|
+
|
|
|
+Body:
|
|
|
+{
|
|
|
+ "action": "RETRY_CURRENT" | "SKIP_CURRENT"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| Action | 行为 |
|
|
|
+| --------------- | ------------------------------------------------------------------------------------------------ |
|
|
|
+| `RETRY_CURRENT` | 重置当前节点状态为 `PENDING`,重置失败设备 ACK 为 `PENDING`,重新下发 MQTT 指令,流程继续 |
|
|
|
+| `SKIP_CURRENT` | 若当前节点为 `OPEN_GROUP`,触发 5.4 的跳过流程;否则直接标记当前节点为 `SKIPPED`,推进到下一节点 |
|
|
|
+
|
|
|
+**接续流程:**
|
|
|
+
|
|
|
+```
|
|
|
+[人工修复设备完成]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[调用 Resume API(action=RETRY_CURRENT)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[校验 status = PAUSED,否则拒绝]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[检查安全关闭记录:泵是否已停止?]
|
|
|
+ ├─ 已停止 ──► [触发 5.5.4 故障接续启动序列重建]
|
|
|
+ └─ 未停止 ──► [直接重置当前节点,继续执行]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[status = RUNNING]
|
|
|
+[流程从重建序列(或当前节点)继续执行]
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.5.4 故障接续启动序列重建(S-05)
|
|
|
+
|
|
|
+当任务从 PAUSED 恢复时,若安全关闭流程已停止水泵,直接继续原节点会导致**无泵运行灌区**的危险状态。系统必须在继续执行前插入重建序列。
|
|
|
+
|
|
|
+**重建序列生成规则:**
|
|
|
+
|
|
|
+```
|
|
|
+检查执行计划中已执行的 STOP_PUMP(安全关闭节点):
|
|
|
+
|
|
|
+如果水泵已停止:
|
|
|
+ → 检查当前应灌的灌区(从 zone_sequence 和 skip_zones 推算)
|
|
|
+ → 生成重建序列:
|
|
|
+
|
|
|
+ OPEN_GROUP(当前灌区) ← S-05:幂等确认灌区已开启(安全关闭未关阀门,灌区仍开启)
|
|
|
+ [SET_PUMP_PRESSURE(压力值)] ← PUMP_UNIFIED/PUMP_ZONE 模式
|
|
|
+ START_PUMP ← S-05:再开水泵
|
|
|
+
|
|
|
+ → 将重建序列插入 execution_plan.nodes 中 current_index 之前
|
|
|
+ → 更新 current_index 指向重建序列首节点
|
|
|
+
|
|
|
+如果水泵未停止(仅部分设备故障):
|
|
|
+ → 直接重置当前节点,不插入重建序列
|
|
|
+```
|
|
|
+
|
|
|
+**示例(灌区1灌溉中,故障停泵后恢复):**
|
|
|
+
|
|
|
+```
|
|
|
+原执行计划(执行到 index=3 的 WAIT 时发生故障):
|
|
|
+ [0] OPEN_GROUP(Z1) ✅
|
|
|
+ [1] START_PUMP ✅
|
|
|
+ [2] WAIT(10min) ← 执行中故障
|
|
|
+ ...
|
|
|
+
|
|
|
+安全关闭(仅关水泵+施肥设备,阀门保持当前状态,见§6.4):
|
|
|
+ [SC-1] STOP_PUMP ✅
|
|
|
+ (灌区1的电磁阀/球阀仍处于开启状态)
|
|
|
+
|
|
|
+用户 Resume,系统插入重建序列:
|
|
|
+ [R-0] OPEN_GROUP(Z1) ← 幂等确认灌区已开启(S-05)
|
|
|
+ [R-1] SET_PUMP_PRESSURE ← 设置压力(PUMP_UNIFIED/PUMP_ZONE 模式)
|
|
|
+ [R-2] START_PUMP ← 再开水泵(S-05)
|
|
|
+ [2'] WAIT(剩余灌溉时间) ← 继续灌区1的剩余灌溉时间
|
|
|
+ [3] OPEN_GROUP(Z2)
|
|
|
+ [4] ZONE_SWITCH_WAIT(5s)
|
|
|
+ [5] CLOSE_GROUP(Z1)
|
|
|
+ ...(后续节点不变)
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.5.5 手动取消 API(Cancel)
|
|
|
+
|
|
|
+用户可主动取消正在运行或暂停中的任务,触发安全关闭流程。
|
|
|
+
|
|
|
+```
|
|
|
+POST /task-execution/{id}/cancel
|
|
|
+
|
|
|
+Body:(无参数)
|
|
|
+```
|
|
|
+
|
|
|
+**取消流程:**
|
|
|
+
|
|
|
+```
|
|
|
+[用户调用 Cancel API]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[校验 status = RUNNING 或 PAUSED,否则拒绝]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[status = CANCELLED]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[触发安全关闭流程(见 §6.4)]
|
|
|
+ → 停止水泵 → 关闭施肥设备(阀门保持当前状态)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[清理 Redis 运行标记(task:running:{task_id})]
|
|
|
+[发送通知:任务已被手动取消]
|
|
|
+```
|
|
|
+
|
|
|
+> **与 FAILED 的区别**:CANCELLED 是用户主动行为,FAILED 是系统判定不可恢复。两者均触发安全关闭,但报警内容和日志类型不同。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 执行安全保障
|
|
|
+
|
|
|
+### 6.1 三层安全保障体系
|
|
|
+
|
|
|
+```
|
|
|
+第一层:生成时保障
|
|
|
+ → 任务保存时根据安全规则(S-01~S-05)自动生成执行顺序
|
|
|
+ → 人工无法配置出错误的执行顺序
|
|
|
+ → 先开后关(S-02/S-04)、稳压等待(ZONE_SWITCH_WAIT)、水泵启动时机全部自动处理
|
|
|
+
|
|
|
+第二层:执行时保障
|
|
|
+ → 每个节点必须ACK成功才能进入下一个节点
|
|
|
+ → ACK失败立即触发重试或终止
|
|
|
+ → 不会出现上一步没完成就执行下一步
|
|
|
+
|
|
|
+第三层:故障时保障
|
|
|
+ → 安全关闭:关闭水泵 + 施肥设备(施肥泵、搅拌电机),阀门保持当前状态
|
|
|
+ → 从执行记录查已开启设备,确保不遗漏
|
|
|
+ → 关闭失败的设备加入报警,提示人工处理
|
|
|
+ → 故障恢复时自动重建启动序列(S-05):先开灌区→再开泵
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 6.2 ACK 超时机制
|
|
|
+
|
|
|
+#### 6.2.1 ACK 注册表(Redis)
|
|
|
+
|
|
|
+发送 MQTT 指令后,在 Redis 中为每个设备写入期望 ACK 记录:
|
|
|
+
|
|
|
+- Key 格式:`ack:{execution_id}:{node_index}:{device_id}`
|
|
|
+- 初始状态:`PENDING`
|
|
|
+- 设备 MQTT 响应后,更新为 `SUCCESS` 或 `FAIL`
|
|
|
+- 设置 TTL = 5 分钟,自动清理过期数据。
|
|
|
+
|
|
|
+#### 6.2.2 ACK 超时处理流程
|
|
|
+
|
|
|
+```
|
|
|
+[发送MQTT指令]
|
|
|
+ │
|
|
|
+ ├─► Redis 注册所有设备为 PENDING
|
|
|
+ │
|
|
|
+ ├─► 发延迟消息A: CHECK_ACK(10秒后)
|
|
|
+ └─► 发延迟消息B: ACK_TIMEOUT(30秒后,兜底)
|
|
|
+
|
|
|
+
|
|
|
+─── 10秒后:CHECK_ACK 消息到达 ──────────────────────
|
|
|
+
|
|
|
+[检查 Redis ACK 注册表]
|
|
|
+ │
|
|
|
+ ├─ 所有设备 SUCCESS ──► [发 NEXT_NODE 延迟消息(N分钟后)]
|
|
|
+ │ └─► Redis 写幂等标记(已推进)
|
|
|
+ │
|
|
|
+ ├─ 有设备 FAIL ──────► [触发重试流程(见6.3)]
|
|
|
+ │
|
|
|
+ └─ 有设备仍 PENDING ─► [未超deadline → 再发CHECK_ACK(5秒后)]
|
|
|
+ [已超deadline → 按超时处理]
|
|
|
+
|
|
|
+
|
|
|
+─── 30秒后:ACK_TIMEOUT 消息到达 ────────────────────
|
|
|
+
|
|
|
+[检查 Redis 幂等标记]
|
|
|
+ │
|
|
|
+ ┌────┴────┐
|
|
|
+已推进 未推进
|
|
|
+ │ │
|
|
|
+ ▼ ▼
|
|
|
+ 忽略 [触发超时处理]
|
|
|
+(幂等) │
|
|
|
+ ├─► 记录超时设备列表
|
|
|
+ ├─► 进入重试流程(或直接终止)
|
|
|
+ └─► 更新 task_execution.status
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.2.3 幂等保障
|
|
|
+
|
|
|
+两条消息(CHECK 和 TIMEOUT)的幂等关系:
|
|
|
+
|
|
|
+```
|
|
|
+正常情况:
|
|
|
+ T+10s: CHECK消息到,ACK全部回来 → 状态推进 → 写幂等标记
|
|
|
+ T+30s: TIMEOUT消息到 → 发现幂等标记存在 → 忽略 ✅
|
|
|
+
|
|
|
+超时情况:
|
|
|
+ T+10s: CHECK消息到,ACK没全回 → 再发CHECK(T+15s)
|
|
|
+ T+15s: CHECK消息到,还没回 → 再发CHECK(T+20s)
|
|
|
+ T+30s: TIMEOUT消息到 → 无幂等标记 → 触发超时处理 ✅
|
|
|
+
|
|
|
+关键:用 status 字段 + Redis幂等标记 做幂等,两条消息不会重复处理
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 6.3 重试策略
|
|
|
+
|
|
|
+#### 6.3.1 重试范围
|
|
|
+
|
|
|
+- **重试粒度:单设备级别**,只对状态为 `ACK_FAIL` 或 `TIMEOUT` 的设备重新下发 MQTT 指令。
|
|
|
+- 状态为 `SUCCESS` 的设备不重复操作。
|
|
|
+
|
|
|
+#### 6.3.2 重试规则
|
|
|
+
|
|
|
+| 失败次数 | 处理方式 |
|
|
|
+| ------------------------ | ---------------------------------------------------------------------- |
|
|
|
+| 第 1 次失败 | 立即重试(无等待) |
|
|
|
+| 第 2 次失败 | 等待 5 秒后重试 |
|
|
|
+| 第 3 次失败 | 等待 15 秒后重试 |
|
|
|
+| 超过 3 次(网络/超时类) | 任务进入 `PAUSED` 状态 → 多通道报警 → 等待人工调用 Resume API 接续执行 |
|
|
|
+| 硬件故障类错误 | 不重试,直接 `FAILED` → 触发安全关闭 → 发送报警 |
|
|
|
+
|
|
|
+#### 6.3.3 重试流程
|
|
|
+
|
|
|
+```
|
|
|
+[ACK检查发现有失败设备]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[检查失败原因]
|
|
|
+ │
|
|
|
+ ├─► 硬件故障 → 直接 FAILED + 安全关闭 + 报警 ❌
|
|
|
+ │
|
|
|
+ └─► 网络/超时 → 检查 retryCount
|
|
|
+ │
|
|
|
+ ┌──────┴──────┐
|
|
|
+ retryCount < 3 retryCount >= 3
|
|
|
+ │ │
|
|
|
+ ▼ ▼
|
|
|
+ [只对失败设备重发指令] [status = PAUSED]
|
|
|
+ [retryCount + 1] [发送多通道报警]
|
|
|
+ [重置失败设备ACK=PENDING] [等待人工调用 Resume API]
|
|
|
+ [发延迟消息(等待间隔后CHECK)] [见 5.5 故障接续执行]
|
|
|
+ [更新last_heartbeat_at]
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.3.4 部分设备失败的处理
|
|
|
+
|
|
|
+```
|
|
|
+灌区1有5个球阀:
|
|
|
+ valve_001 → ACK_OK ✅
|
|
|
+ valve_002 → ACK_OK ✅
|
|
|
+ valve_003 → TIMEOUT ❌ → 重试
|
|
|
+ valve_004 → ACK_OK ✅
|
|
|
+ valve_005 → ACK_FAIL❌ → 重试
|
|
|
+
|
|
|
+重试时:只对 valve_003 和 valve_005 重发指令
|
|
|
+ valve_001、002、004 不重复发送
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 6.4 安全关闭流程
|
|
|
+
|
|
|
+当任务因异常(重试耗尽、不可恢复错误)而终止时,必须触发安全关闭流程。
|
|
|
+
|
|
|
+#### 6.4.1 安全关闭流程图
|
|
|
+
|
|
|
+```
|
|
|
+[任务异常终止触发]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[从 execution_plan JSON 查询已开启设备]
|
|
|
+(已执行且status=SUCCESS的OPEN/START类节点中的设备)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+步骤1: 关闭水泵(如已启动)
|
|
|
+ │ 下发 STOP_PUMP MQTT 指令
|
|
|
+ │ 等待 ACK(最多30秒)
|
|
|
+ │ 超时也继续(尽力而为原则)
|
|
|
+ └─► 失败设备记录入报警列表
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+步骤2: 关闭施肥设备(如已启动)
|
|
|
+ │ 下发施肥泵 OFF、搅拌电机 OFF 指令
|
|
|
+ │ 等待 ACK(最多30秒)
|
|
|
+ │ 超时也继续(尽力而为原则)
|
|
|
+ └─► 失败设备记录入报警列表
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+注意:电磁阀/球阀保持当前状态,不在异常关闭流程中关闭
|
|
|
+(管路中残余水自然排出,避免封闭管路导致水锤效应)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[汇总安全关闭结果]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[更新 task_execution.status = FAILED]
|
|
|
+[写入 fail_reason 字段]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[发送多通道报警]
|
|
|
+ ├─► 短信:失败原因 + 设备列表 + 安全关闭状态
|
|
|
+ ├─► 电话:语音播报关键失败信息(紧急告警时)
|
|
|
+ └─► 移动端推送:同短信内容,推送至 APP
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.4.2 尽力而为原则
|
|
|
+
|
|
|
+安全关闭过程中,即使某个设备关闭失败或超时,也继续执行后续关闭步骤。不因单个设备失败而中断整个安全关闭流程。最终将所有失败设备汇总到报警通知中,提示人工处理。
|
|
|
+
|
|
|
+> **为什么不关阀门**:异常情况下,水泵已关停、管路无水压,阀门保持当前状态不会造成危害。反之,在管路仍有残余压力时关闭阀门可能导致水锤效应或爆管。阀门的正常开关由计划执行引擎(S-03 正常结束序列)统一管理。
|
|
|
+
|
|
|
+#### 6.4.3 报警内容设计
|
|
|
+
|
|
|
+```
|
|
|
+轮灌任务执行失败
|
|
|
+─────────────────────────────
|
|
|
+任务名称:农场A区轮灌
|
|
|
+失败节点:开启灌区2
|
|
|
+失败时间:2026-02-25 02:03:35
|
|
|
+重试次数:3次(已达上限)
|
|
|
+
|
|
|
+失败设备:
|
|
|
+ valve_003 超时未响应
|
|
|
+ ball_005 设备离线
|
|
|
+
|
|
|
+成功设备:
|
|
|
+ valve_001 ✅
|
|
|
+ valve_002 ✅
|
|
|
+ valve_004 ✅
|
|
|
+
|
|
|
+安全关闭状态:
|
|
|
+ 水泵 已关闭 ✅
|
|
|
+ 施肥泵 已关闭 ✅
|
|
|
+ 搅拌电机 已关闭 ✅
|
|
|
+ 阀门 保持当前状态(不主动关闭)
|
|
|
+
|
|
|
+请检查失败设备后手动处理
|
|
|
+─────────────────────────────
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 6.5 看门狗机制(Watchdog)
|
|
|
+
|
|
|
+系统依赖 RocketMQ 延迟消息驱动流程推进。当 MQ 消息因 Broker 宕机、网络异常等原因丢失时,流程可能停滞。看门狗机制作为最后一道保障,定期扫描"疑似卡住"的任务并触发恢复。
|
|
|
+
|
|
|
+> **设计要点**:由于灌区灌溉时长可能长达数小时甚至 10 小时以上,采用简单的 **12 小时心跳超时**方案。每个节点执行时更新 `last_heartbeat_at`,超过 12 小时未更新则判定为流程卡住。该方案简单可靠,避免了基于预计完成时间的复杂计算,且不会因 PAUSED 状态产生误判逻辑冲突。
|
|
|
+
|
|
|
+#### 6.5.1 心跳更新时机
|
|
|
+
|
|
|
+每个节点执行时(包括 WAIT 节点开始执行时)更新 `last_heartbeat_at = NOW()`:
|
|
|
+
|
|
|
+| 更新时机 | 说明 |
|
|
|
+| ---------------------- | -------------------------------------------------------- |
|
|
|
+| 节点开始执行时 | 任何节点(ACK类、WAIT类)开始执行时立即更新 |
|
|
|
+| CHECK_ACK 消息处理时 | 每次检查 ACK 状态时更新,表明流程仍在活跃处理中 |
|
|
|
+| 重试发生时 | 每次重试下发指令时更新 |
|
|
|
+| NEXT_NODE 推进时 | 推进到下一节点时更新 |
|
|
|
+
|
|
|
+#### 6.5.2 看门狗扫描规则
|
|
|
+
|
|
|
+通过定时任务(Quartz/ScheduledExecutor)每 **30 分钟** 扫描一次:
|
|
|
+
|
|
|
+```sql
|
|
|
+SELECT * FROM task_execution
|
|
|
+WHERE status = 'RUNNING'
|
|
|
+ AND last_heartbeat_at < NOW() - INTERVAL 12 HOUR
|
|
|
+```
|
|
|
+
|
|
|
+> **为什么是 12 小时**:单个灌区灌溉时长最长约 10 小时,加上执行前的 WAIT 启动和切换等待,12 小时是一个合理的安全阈值。正常情况下每个节点执行都会更新心跳,只有 MQ 消息真正丢失时才会超过 12 小时无心跳。
|
|
|
+
|
|
|
+#### 6.5.3 恢复策略
|
|
|
+
|
|
|
+```
|
|
|
+[看门狗发现疑似卡住的任务]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[检查 task_execution.status]
|
|
|
+ │
|
|
|
+ ┌────┴──────────┐
|
|
|
+RUNNING PAUSED
|
|
|
+ │ │
|
|
|
+ ▼ ▼
|
|
|
+[重新发送当前节点] [忽略,PAUSED 是预期状态]
|
|
|
+[NEXT_NODE 消息] [等待人工 Resume]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[查 current_index 对应节点状态]
|
|
|
+ │
|
|
|
+ ├─ PENDING → 重新触发 execute()
|
|
|
+ ├─ RUNNING → 重新发送 CHECK_ACK
|
|
|
+ └─ SUCCESS → 发送 NEXT_NODE 推进
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.5.4 防重复恢复
|
|
|
+
|
|
|
+- 看门狗恢复也是通过发 MQ 消息触发,消费端通过 Redis 幂等标记和节点 status 做幂等校验。
|
|
|
+- 看门狗每次恢复后更新 `last_heartbeat_at`,防止下一轮扫描重复处理。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 6.6 并发触发防护
|
|
|
+
|
|
|
+同一轮灌任务不允许出现多个同时运行的执行实例。由于同一任务可关联多个触发来源(定时 + 联动 + 手动),任意触发来源在创建新执行实例前,必须检查该任务是否存在未结束的执行实例:
|
|
|
+
|
|
|
+```
|
|
|
+[触发信号到达(Quartz / 联动规则引擎 / 手动触发)]
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+[查询 task_execution 表:同一 task_id 是否存在 status = RUNNING / PAUSED 的记录]
|
|
|
+ │
|
|
|
+ ┌────┴────────────┐
|
|
|
+ 存在(正在执行) 不存在
|
|
|
+ │ │
|
|
|
+ ▼ ▼
|
|
|
+[本次触发放弃] [正常创建 task_execution]
|
|
|
+[记录日志] [发送 TASK_START MQ 消息]
|
|
|
+[可选:发送通知]
|
|
|
+```
|
|
|
+
|
|
|
+**实现方式:**
|
|
|
+
|
|
|
+所有触发来源统一使用 Redis 原子标记(`SET NX`)作为并发门禁:
|
|
|
+
|
|
|
+- Key 格式:`task:running:{task_id}`,值为 `execution_id`。
|
|
|
+- 触发时先 `SET NX`,成功则创建执行实例,失败则拒绝本次触发。
|
|
|
+- 任务结束(SUCCESS / FAILED / CANCELLED)时删除该 Key。
|
|
|
+- PAUSED 状态不删除 Key,防止暂停期间被重复触发。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 数据存储设计
|
|
|
+
|
|
|
+### 7.1 核心表结构
|
|
|
+
|
|
|
+#### 7.1.1 task_execution(任务执行实例表)
|
|
|
+
|
|
|
+| 字段名 | 类型 | 说明 |
|
|
|
+| -------------------- | ----------- | ------------------------------------------------------------------------------- |
|
|
|
+| `id` | BIGINT PK | 执行实例唯一ID |
|
|
|
+| `task_id` | BIGINT | 关联的轮灌任务ID |
|
|
|
+| `trigger_type` | VARCHAR(20) | 触发类型:`SCHEDULED` / `LINKAGE` / `MANUAL` |
|
|
|
+| `execution_plan` | JSON | 完整有序节点列表(核心字段,见7.2) |
|
|
|
+| `current_index` | INT | 当前正在执行的节点索引 |
|
|
|
+| `status` | VARCHAR(20) | 执行状态:`PENDING` / `RUNNING` / `SUCCESS` / `FAILED` / `PAUSED` / `CANCELLED` |
|
|
|
+| `version` | INT | 乐观锁版本号,防止并发更新冲突 |
|
|
|
+| `started_at` | DATETIME | 实际开始时间 |
|
|
|
+| `finished_at` | DATETIME | 完成时间 |
|
|
|
+| `expected_finish_at` | DATETIME | 预计完成时间(含重试顺延后更新,前端展示用) |
|
|
|
+| `last_heartbeat_at` | DATETIME | 最近一次心跳时间(看门狗主要判据,每个节点执行时更新,超过12小时未更新则判定卡住) |
|
|
|
+| `paused_at` | DATETIME | 进入 PAUSED 状态的时间 |
|
|
|
+| `fail_reason` | TEXT | 失败/暂停原因描述 |
|
|
|
+
|
|
|
+**乐观锁使用场景:**
|
|
|
+
|
|
|
+```sql
|
|
|
+UPDATE task_execution
|
|
|
+SET execution_plan = ?, current_index = ?, version = version + 1
|
|
|
+WHERE id = ? AND version = ?
|
|
|
+
|
|
|
+→ 防止集群下多节点同时更新同一个执行实例
|
|
|
+```
|
|
|
+
|
|
|
+#### 7.1.2 alarm_record(报警记录表)
|
|
|
+
|
|
|
+| 字段名 | 类型 | 说明 |
|
|
|
+| ------------------- | ------------ | ----------------------------------------------------- |
|
|
|
+| `id` | BIGINT PK | 报警记录唯一ID |
|
|
|
+| `execution_id` | BIGINT | 关联的执行实例ID |
|
|
|
+| `task_id` | BIGINT | 关联的任务ID |
|
|
|
+| `alarm_type` | VARCHAR(50) | 报警类型:`DEVICE_FAIL` / `TIMEOUT` / `SAFE_CLOSE` 等 |
|
|
|
+| `failed_node` | VARCHAR(100) | 失败节点名称 |
|
|
|
+| `fail_reason` | TEXT | 详细失败原因 |
|
|
|
+| `failed_devices` | JSON | 失败设备列表 |
|
|
|
+| `success_devices` | JSON | 成功设备列表 |
|
|
|
+| `safe_close_status` | VARCHAR(20) | 安全关闭结果:`SUCCESS` / `PARTIAL` / `FAILED` |
|
|
|
+| `notified_channels` | VARCHAR(100) | 已通知渠道(SMS/PHONE/APP) |
|
|
|
+| `created_at` | DATETIME | 报警时间 |
|
|
|
+
|
|
|
+#### 7.1.3 fert_task(施肥子任务表)
|
|
|
+
|
|
|
+> 数据结构详见 §5.1.4.5。
|
|
|
+
|
|
|
+| 字段名 | 类型 | 说明 |
|
|
|
+| ----------------------- | ----------- | ---------------------------------------------------------------------------------------- |
|
|
|
+| `id` | BIGINT PK | 施肥子任务唯一ID |
|
|
|
+| `execution_id` | BIGINT | 关联主任务执行实例ID |
|
|
|
+| `zone_id` | BIGINT | 绑定的灌区ID |
|
|
|
+| `delay_minutes` | INT | 施肥延迟时间(分钟,来自任务级灌区配置) |
|
|
|
+| `pre_stir_minutes` | INT | 搅拌提前时长(分钟) |
|
|
|
+| `fert_duration_minutes` | INT | 施肥时长(时间模式,分钟) |
|
|
|
+| `fert_target_liters` | INT | 施肥目标量(容量模式,升;NULL 表示时间模式) |
|
|
|
+| `status` | VARCHAR(20) | 子任务状态:`PENDING` / `STIR_WAITING` / `STIRRING` / `FERT_RUNNING` / `DONE` / `FAILED` |
|
|
|
+| `actual_fert_seconds` | INT | 实际施肥时长(秒) |
|
|
|
+| `actual_liters` | INT | 实际施肥量(容量模式) |
|
|
|
+| `fail_reason` | TEXT | 失败原因 |
|
|
|
+| `stir_start_at` | DATETIME | 搅拌电机实际启动时间 |
|
|
|
+| `pump_start_at` | DATETIME | 施肥泵实际启动时间 |
|
|
|
+| `pump_stop_at` | DATETIME | 施肥泵实际停止时间 |
|
|
|
+| `done_at` | DATETIME | 子任务完成时间 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 7.2 execution_plan JSON 结构
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "zone_sequence": [101, 102, 103],
|
|
|
+ "skip_zones": [],
|
|
|
+ "nodes": [
|
|
|
+ {
|
|
|
+ "index": 0,
|
|
|
+ "nodeType": "OPEN_GROUP",
|
|
|
+ "nodeName": "开启灌区1",
|
|
|
+ "refId": 101,
|
|
|
+ "params": {
|
|
|
+ "targetAngle": 80,
|
|
|
+ "targetPressureKpa": 250
|
|
|
+ },
|
|
|
+ "status": "SUCCESS",
|
|
|
+ "retryCount": 0,
|
|
|
+ "maxRetry": 3,
|
|
|
+ "startedAt": "2026-02-25T02:00:00",
|
|
|
+ "finishedAt": "2026-02-25T02:00:05",
|
|
|
+ "devices": [
|
|
|
+ {
|
|
|
+ "deviceId": "valve-001",
|
|
|
+ "deviceType": "SOLENOID_VALVE",
|
|
|
+ "ackStatus": "SUCCESS",
|
|
|
+ "retryCount": 0,
|
|
|
+ "failReason": null,
|
|
|
+ "ackAt": "2026-02-25T02:00:03"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "deviceId": "ball-valve-001",
|
|
|
+ "deviceType": "BALL_VALVE",
|
|
|
+ "ackStatus": "SUCCESS",
|
|
|
+ "retryCount": 1,
|
|
|
+ "failReason": null,
|
|
|
+ "ackAt": "2026-02-25T02:00:04"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "index": 1,
|
|
|
+ "nodeType": "SET_PUMP_PRESSURE",
|
|
|
+ "nodeName": "设置水泵压力",
|
|
|
+ "refId": 201,
|
|
|
+ "params": {
|
|
|
+ "pressureKpa": 300
|
|
|
+ },
|
|
|
+ "status": "SUCCESS",
|
|
|
+ "retryCount": 0,
|
|
|
+ "maxRetry": 3,
|
|
|
+ "startedAt": "2026-02-25T02:00:06",
|
|
|
+ "finishedAt": "2026-02-25T02:00:08",
|
|
|
+ "devices": [
|
|
|
+ {
|
|
|
+ "deviceId": "pump-001",
|
|
|
+ "deviceType": "PRESSURE_PUMP",
|
|
|
+ "ackStatus": "SUCCESS",
|
|
|
+ "retryCount": 0,
|
|
|
+ "failReason": null,
|
|
|
+ "ackAt": "2026-02-25T02:00:07"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "index": 2,
|
|
|
+ "nodeType": "START_PUMP",
|
|
|
+ "nodeName": "开启水泵",
|
|
|
+ "refId": 201,
|
|
|
+ "params": {},
|
|
|
+ "status": "RUNNING",
|
|
|
+ "retryCount": 0,
|
|
|
+ "maxRetry": 3,
|
|
|
+ "startedAt": "2026-02-25T02:00:09",
|
|
|
+ "finishedAt": null,
|
|
|
+ "devices": [
|
|
|
+ {
|
|
|
+ "deviceId": "pump-001",
|
|
|
+ "deviceType": "PRESSURE_PUMP",
|
|
|
+ "ackStatus": "PENDING",
|
|
|
+ "retryCount": 0,
|
|
|
+ "failReason": null,
|
|
|
+ "ackAt": null
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "index": 3,
|
|
|
+ "nodeType": "WAIT",
|
|
|
+ "nodeName": "等待3分钟",
|
|
|
+ "refId": null,
|
|
|
+ "params": {
|
|
|
+ "seconds": 180
|
|
|
+ },
|
|
|
+ "status": "PENDING",
|
|
|
+ "retryCount": 0,
|
|
|
+ "maxRetry": 0,
|
|
|
+ "startedAt": null,
|
|
|
+ "finishedAt": null,
|
|
|
+ "devices": []
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**字段说明:**
|
|
|
+
|
|
|
+| 字段 | 说明 |
|
|
|
+| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
+| `zone_sequence` | 当前有效的灌区执行顺序(灌区 ID 列表,运行时可调整) |
|
|
|
+| `skip_zones` | 运行时被跳过的灌区 ID 列表,跳过后重新生成后续节点 |
|
|
|
+| `index` | 节点顺序索引,从 0 开始 |
|
|
|
+| `nodeType` | 节点类型,对应 Handler 路由 |
|
|
|
+| `nodeName` | 可读的节点名称,用于日志和报警展示 |
|
|
|
+| `refId` | 关联实体 ID(灌溉组ID / 水泵ID) |
|
|
|
+| `params` | 节点执行参数;`OPEN_GROUP` 含 `targetAngle`、`targetPressureKpa`(球阀配置了目标压力时);`SET_PUMP_PRESSURE` 含 `pressureKpa`;`WAIT` 含 `seconds` |
|
|
|
+| `params.source` | WAIT 类节点来源标识:`IRRIGATE`(灌溉等待)/ `ZONE_SWITCH`(稳压等待)/ `RESUME_REBUILD`(故障接续重建节点) |
|
|
|
+| `status` | 节点状态:`PENDING` / `RUNNING` / `SUCCESS` / `FAILED` / `SKIPPED` |
|
|
|
+| `retryCount` | 节点级别已重试次数 |
|
|
|
+| `maxRetry` | 最大重试次数(默认3,WAIT/ZONE_SWITCH_WAIT 节点为0) |
|
|
|
+| `startedAt` / `finishedAt` | 节点执行时间记录 |
|
|
|
+| `devices[].ackStatus` | 单设备 ACK 状态:`PENDING` / `SUCCESS` / `FAIL` / `TIMEOUT` |
|
|
|
+| `devices[].retryCount` | 单设备已重试次数 |
|
|
|
+| `devices[].failReason` | 单设备失败原因 |
|
|
|
+
|
|
|
+### 7.3 JSON 存储优势
|
|
|
+
|
|
|
+```
|
|
|
+查某次执行的完整计划 → JSON一次读完 ✅
|
|
|
+查当前执行到哪一步 → current_index字段 ✅
|
|
|
+查某次执行是否成功 → status字段 ✅
|
|
|
+安全关闭查已开启设备 → 从JSON中内存过滤 ✅
|
|
|
+跳过灌区后重算后续计划 → 截断nodes + 重新追加新节点 ✅
|
|
|
+故障接续判断是否需要重建序列 → 查JSON中 STOP_PUMP 安全关闭节点 ✅
|
|
|
+执行历史归档 → task_execution记录保留,JSON即完整记录 ✅
|
|
|
+
|
|
|
+JSON大小估算:
|
|
|
+ 10个节点,每节点含10个设备 → 约5-10KB
|
|
|
+ 完全在MySQL JSON字段承受范围内
|
|
|
+```
|