# 智能轮灌系统 技术需求与技术方案文档 **版本:** 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 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 skipZoneIds; // 运行时已跳过的灌区ID列表 } ``` #### 5.1.5.3 引擎输出契约(PlanGeneratorOutput) ```java PlanGeneratorOutput { List 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 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字段承受范围内 ```