球阀轮灌技术方案.md 97 KB

智能轮灌系统 技术需求与技术方案文档

版本: 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 数据结构
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)

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)

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 伪代码)

@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 根节点新增两个字段,用于支持运行时调整:

{
  "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 响应后,更新为 SUCCESSFAIL
  • 设置 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_FAILTIMEOUT 的设备重新下发 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 分钟 扫描一次:

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 失败/暂停原因描述

乐观锁使用场景:

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 结构

{
  "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_GROUPtargetAngletargetPressureKpa(球阀配置了目标压力时);SET_PUMP_PRESSUREpressureKpaWAITseconds
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字段承受范围内