learshaw 4 месяцев назад
Родитель
Сommit
b20cc71095
21 измененных файлов с 2856 добавлено и 402 удалено
  1. 35 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/config/EmsConfig.java
  2. 349 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/core/EmsApiTemplate.java
  3. 32 30
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/handle/Keka86BsHandler.java
  4. 87 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/retrofit/EmsApi.java
  5. 6 0
      ems/ems-cloud/ems-dev-adapter/src/main/resources/application-local.yml
  6. 6 0
      ems/ems-cloud/ems-dev-adapter/src/main/resources/application-prod-ct.yml
  7. 36 0
      ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/config/TaskSchedulerConfig.java
  8. 342 96
      ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/controller/OpEnergyStrategyController.java
  9. 346 0
      ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/task/StrategyScheduler.java
  10. 482 0
      ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/task/StrategyTriggerListener.java
  11. 88 22
      ems/ems-core/src/main/java/com/ruoyi/ems/domain/StrategyExecutionContext.java
  12. 0 58
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/AttrValueChangeInterceptor.java
  13. 623 0
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/PollingMonitorService.java
  14. 26 22
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/evaluator/ConditionEvaluator.java
  15. 1 1
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/AttrQueryStepExecutor.java
  16. 265 27
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/LoopStepExecutor.java
  17. 119 20
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/StrategyExecutor.java
  18. 0 118
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/listener/StrategyTriggerListener.java
  19. 0 1
      ems/ems-core/src/main/resources/mapper/ems/OpEnergyStrategyStepMapper.xml
  20. 6 0
      ems/sql/ems_init_data_test.sql
  21. 7 7
      ems/sql/ems_server.sql

+ 35 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/config/EmsConfig.java

@@ -0,0 +1,35 @@
+/*
+ * 文 件 名:  EmsConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Ems配置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/9]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "adapter.ems")
+public class EmsConfig {
+    private String url;
+    private int connectTimeout;
+    private int readTimeout;
+    private int writeTimeout;
+    private boolean notifyEnabled;
+}

+ 349 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/core/EmsApiTemplate.java

@@ -0,0 +1,349 @@
+/*
+ * 文 件 名:  EmsApiTemplate
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  EMS Server API 调用模板
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ */
+package com.ruoyi.ems.core;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.support.retrofit.Retrofit2ConverterFactory;
+import com.ruoyi.ems.config.EmsConfig;
+import com.ruoyi.ems.retrofit.EmsApi;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.OkHttpClient;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * EMS Server API 调用模板
+ * 用于 ems-dev-adapter 调用 ems-server 的策略相关接口
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/9]
+ */
+@Slf4j
+public class EmsApiTemplate extends BaseApiTemplate {
+    /**
+     * EMS Server 地址
+     */
+    protected String url;
+
+    private boolean notifyEnabled;
+
+    /**
+     * API 代理
+     */
+    protected final EmsApi api;
+
+    /**
+     * 异步执行线程池
+     */
+    private final ExecutorService asyncExecutor = Executors.newFixedThreadPool(3);
+
+    /**
+     * 构造调用模板(默认超时配置)
+     *
+     * @param emsConfig EMS Server 地址 (http://ip:port)
+     */
+    public EmsApiTemplate(EmsConfig emsConfig) {
+        super.connectTimeout = emsConfig.getConnectTimeout();
+        super.readTimeout = emsConfig.getReadTimeout();
+        super.writeTimeout = emsConfig.getWriteTimeout();
+        this.url = emsConfig.getUrl();
+        this.notifyEnabled = emsConfig.isNotifyEnabled();
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(this.url)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        this.api = retrofit.create(EmsApi.class);
+    }
+
+    // ==================== 同步调用方法 ====================
+
+    /**
+     * 通知属性值变化(同步)
+     *
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param oldValue 旧值(可为null)
+     * @param newValue 新值
+     * @return 触发的策略数量,失败返回 -1
+     */
+    public int notifyAttrValueChanged(String objCode, String attrKey, Object oldValue, Object newValue) {
+        if (!notifyEnabled) {
+            log.debug("策略通知已禁用,跳过: obj={}, attr={}", objCode, attrKey);
+            return 0;
+        }
+
+        try {
+            String oldValueStr = oldValue != null ? String.valueOf(oldValue) : null;
+            String newValueStr = newValue != null ? String.valueOf(newValue) : "";
+
+            Call<String> call = api.onAttrValueChanged(objCode, attrKey, newValueStr, oldValueStr);
+            Response<String> response = call.execute();
+
+            log.debug("notifyAttrValueChanged response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    JSONObject data = resJson.getJSONObject("data");
+                    int triggeredCount = data != null ? data.getIntValue("triggeredStrategies") : 0;
+                    log.debug("属性变化通知成功: obj={}, attr={}, {} -> {}, triggered={}", objCode, attrKey, oldValue,
+                        newValue, triggeredCount);
+                    return triggeredCount;
+                }
+                else {
+                    log.warn("属性变化通知失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("属性变化通知HTTP失败: status={}, body={}", response.code(), response.body());
+            }
+        }
+        catch (Exception e) {
+            log.error("属性变化通知异常: obj={}, attr={}, error={}", objCode, attrKey, e.getMessage());
+        }
+
+        return -1;
+    }
+
+    /**
+     * 批量通知属性值变化(同步)
+     *
+     * @param objCode    设备代码
+     * @param attributes 属性键值对
+     * @return 触发的策略数量,失败返回 -1
+     */
+    public int notifyAttrValueChangedBatch(String objCode, Map<String, Object> attributes) {
+        if (!notifyEnabled) {
+            log.debug("策略通知已禁用,跳过批量通知: obj={}", objCode);
+            return 0;
+        }
+
+        if (attributes == null || attributes.isEmpty()) {
+            log.debug("属性为空,跳过批量通知: obj={}", objCode);
+            return 0;
+        }
+
+        try {
+            Map<String, Object> request = new HashMap<>();
+            request.put("objCode", objCode);
+            request.put("attributes", attributes);
+
+            Call<String> call = api.onAttrValueChangedBatch(request);
+            Response<String> response = call.execute();
+
+            log.debug("notifyAttrValueChangedBatch response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    JSONObject data = resJson.getJSONObject("data");
+                    int triggeredCount = data != null ? data.getIntValue("triggeredStrategies") : 0;
+                    log.debug("批量属性变化通知成功: obj={}, attrCount={}, triggered={}", objCode, attributes.size(),
+                        triggeredCount);
+                    return triggeredCount;
+                }
+                else {
+                    log.warn("批量属性变化通知失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("批量属性变化通知HTTP失败: status={}", response.code());
+            }
+        }
+        catch (Exception e) {
+            log.error("批量属性变化通知异常: obj={}, error={}", objCode, e.getMessage());
+        }
+
+        return -1;
+    }
+
+    /**
+     * 通知设备事件(同步)
+     *
+     * @param objCode   设备代码
+     * @param eventKey  事件键
+     * @param eventData 事件数据(可为null)
+     * @return 触发的策略数量,失败返回 -1
+     */
+    public int notifyDeviceEvent(String objCode, String eventKey, Map<String, Object> eventData) {
+        if (!notifyEnabled) {
+            log.debug("策略通知已禁用,跳过事件通知: obj={}, event={}", objCode, eventKey);
+            return 0;
+        }
+
+        try {
+            Map<String, Object> data = eventData != null ? eventData : new HashMap<>();
+
+            Call<String> call = api.onDeviceEvent(objCode, eventKey, data);
+            Response<String> response = call.execute();
+
+            log.debug("notifyDeviceEvent response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    JSONObject respData = resJson.getJSONObject("data");
+                    int triggeredCount = respData != null ? respData.getIntValue("triggeredStrategies") : 0;
+                    log.debug("设备事件通知成功: obj={}, event={}, triggered={}", objCode, eventKey, triggeredCount);
+                    return triggeredCount;
+                }
+                else {
+                    log.warn("设备事件通知失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("设备事件通知HTTP失败: status={}", response.code());
+            }
+        }
+        catch (Exception e) {
+            log.error("设备事件通知异常: obj={}, event={}, error={}", objCode, eventKey, e.getMessage());
+        }
+
+        return -1;
+    }
+
+    /**
+     * 获取调度器状态(同步)
+     *
+     * @return 调度器状态信息,失败返回 null
+     */
+    public JSONObject getSchedulerStatus() {
+        try {
+            Call<String> call = api.getSchedulerStatus();
+            Response<String> response = call.execute();
+
+            log.debug("getSchedulerStatus response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    return resJson.getJSONObject("data");
+                }
+                else {
+                    log.warn("获取调度器状态失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("获取调度器状态HTTP失败: status={}", response.code());
+            }
+        }
+        catch (Exception e) {
+            log.error("获取调度器状态异常: error={}", e.getMessage());
+        }
+
+        return null;
+    }
+
+    /**
+     * 重新加载调度器(同步)
+     *
+     * @return 是否成功
+     */
+    public boolean reloadScheduler() {
+        try {
+            Call<String> call = api.reloadScheduler();
+            Response<String> response = call.execute();
+
+            log.debug("reloadScheduler response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+                return code != null && code == 200;
+            }
+        }
+        catch (Exception e) {
+            log.error("重新加载调度器异常: error={}", e.getMessage());
+        }
+
+        return false;
+    }
+
+    // ==================== 异步调用方法 ====================
+
+    /**
+     * 通知属性值变化(异步)
+     * 不阻塞主流程,适合在数据采集时使用
+     *
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param oldValue 旧值
+     * @param newValue 新值
+     */
+    public void notifyAttrValueChangedAsync(String objCode, String attrKey, Object oldValue, Object newValue) {
+        if (!notifyEnabled) {
+            return;
+        }
+
+        asyncExecutor.submit(() -> {
+            try {
+                notifyAttrValueChanged(objCode, attrKey, oldValue, newValue);
+            }
+            catch (Exception e) {
+                log.error("异步属性变化通知异常: obj={}, attr={}", objCode, attrKey, e);
+            }
+        });
+    }
+
+    /**
+     * 批量通知属性值变化(异步)
+     *
+     * @param objCode    设备代码
+     * @param attributes 属性键值对
+     */
+    public void notifyAttrValueChangedBatchAsync(String objCode, Map<String, Object> attributes) {
+        if (!notifyEnabled || attributes == null || attributes.isEmpty()) {
+            return;
+        }
+
+        asyncExecutor.submit(() -> {
+            try {
+                notifyAttrValueChangedBatch(objCode, attributes);
+            }
+            catch (Exception e) {
+                log.error("异步批量属性变化通知异常: obj={}", objCode, e);
+            }
+        });
+    }
+
+    /**
+     * 通知设备事件(异步)
+     *
+     * @param objCode   设备代码
+     * @param eventKey  事件键
+     * @param eventData 事件数据
+     */
+    public void notifyDeviceEventAsync(String objCode, String eventKey, Map<String, Object> eventData) {
+        if (!notifyEnabled) {
+            return;
+        }
+
+        asyncExecutor.submit(() -> {
+            try {
+                notifyDeviceEvent(objCode, eventKey, eventData);
+            }
+            catch (Exception e) {
+                log.error("异步设备事件通知异常: obj={}, event={}", objCode, eventKey, e);
+            }
+        });
+    }
+}

+ 32 - 30
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/handle/Keka86BsHandler.java

@@ -12,6 +12,8 @@ package com.ruoyi.ems.handle;
 
 import com.huashe.common.exception.Assert;
 import com.huashe.common.utils.DateUtils;
+import com.ruoyi.ems.config.EmsConfig;
+import com.ruoyi.ems.core.EmsApiTemplate;
 import com.ruoyi.ems.core.MqttTemplate;
 import com.ruoyi.ems.domain.EmsDevice;
 import com.ruoyi.ems.domain.EmsObjAttrValue;
@@ -25,6 +27,7 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
@@ -52,6 +55,9 @@ public class Keka86BsHandler extends BaseDevHandler {
     @Qualifier("mqttTemplate")
     protected MqttTemplate mqttTemplate;
 
+    @Autowired
+    private EmsConfig emsConfig;
+
     // 主题前置
     private static final String TOPIC_PREFIX = "/sc/dtu/ctl/";
 
@@ -151,14 +157,15 @@ public class Keka86BsHandler extends BaseDevHandler {
             }
             else if ((funcCode & 0x80) != 0) {
                 // 错误响应 (功能码最高位为1,如83H、86H)
-                byte originalFuncCode = (byte)(funcCode & 0x7F);
+                byte originalFuncCode = (byte) (funcCode & 0x7F);
                 handleErrorResponse(gatewayId, payload, originalFuncCode);
             }
             else {
                 log.warn("[Keka86] 未知功能码: 0x{}, 消息: {}", funcCodeStr, payload);
             }
 
-        } catch (Exception e) {
+        }
+        catch (Exception e) {
             log.error("[Keka86] 网关:{}, 消息处理异常", gatewayId, e);
         }
     }
@@ -388,8 +395,7 @@ public class Keka86BsHandler extends BaseDevHandler {
      * @param command Modbus指令对象 (包含字节数组和十六进制字符串)
      */
     public void sendMqttHex(String topic, ModbusCommand command) {
-        log.info("[Send] Topic:{}, message:{}, qos:{}, retained:{}",
-            topic, command.getCommandHex(), 2, false);
+        log.info("[Send] Topic:{}, message:{}, qos:{}, retained:{}", topic, command.getCommandHex(), 2, false);
         mqttTemplate.sendHex(topic, command.getCommandBytes(), 2, false);
     }
 
@@ -419,12 +425,12 @@ public class Keka86BsHandler extends BaseDevHandler {
                 // 更新设备状态
                 updateDeviceStatus(deviceCode, deviceStatus);
 
-                log.info("[Keka86-Read] 网关:{}, 设备:{}, 按键{}, 状态:{}",
-                    gatewayId, deviceCode, buttonId,
+                log.info("[Keka86-Read] 网关:{}, 设备:{}, 按键{}, 状态:{}", gatewayId, deviceCode, buttonId,
                     deviceStatus.getStatus() == 1 ? "开" : "关");
             }
 
-        } catch (Exception e) {
+        }
+        catch (Exception e) {
             log.error("[Keka86-Read] 网关:{}, 解析失败, Hex:{}", gatewayId, hexMessage, e);
         }
     }
@@ -442,8 +448,7 @@ public class Keka86BsHandler extends BaseDevHandler {
             byte[] dataWithoutCrc = new byte[response.length - 2];
             System.arraycopy(response, 0, dataWithoutCrc, 0, response.length - 2);
             int calculatedCrc = calculateCRC16(dataWithoutCrc);
-            int receivedCrc = (response[response.length - 2] & 0xFF) |
-                ((response[response.length - 1] & 0xFF) << 8);
+            int receivedCrc = (response[response.length - 2] & 0xFF) | ((response[response.length - 1] & 0xFF) << 8);
 
             if (calculatedCrc != receivedCrc) {
                 log.error("[Keka86-Write] CRC校验失败, 网关:{}, Hex:{}", gatewayId, hexMessage);
@@ -458,21 +463,18 @@ public class Keka86BsHandler extends BaseDevHandler {
             int buttonId = registerAddr - LIGHT_BASE_ADDR + 1;
             int status = (value == VALUE_ON) ? 1 : 0;
 
-            log.info("[Keka86-Write] 网关:{}, 写入确认成功, 寄存器:0x{}, 按键:{}, 状态:{}",
-                gatewayId,
-                String.format("%04X", registerAddr),
-                buttonId,
-                status == 1 ? "开" : "关");
+            log.info("[Keka86-Write] 网关:{}, 写入确认成功, 寄存器:0x{}, 按键:{}, 状态:{}", gatewayId,
+                String.format("%04X", registerAddr), buttonId, status == 1 ? "开" : "关");
 
             // 写入成功后更新数据库状态
             String deviceCode = getDeviceCode(gatewayId, buttonId);
             if (deviceCode != null) {
-                DeviceStatus deviceStatus = new DeviceStatus(
-                    response[0] & 0xFF, buttonId, status, hexMessage);
+                DeviceStatus deviceStatus = new DeviceStatus(response[0] & 0xFF, buttonId, status, hexMessage);
                 updateDeviceStatus(deviceCode, deviceStatus);
             }
 
-        } catch (Exception e) {
+        }
+        catch (Exception e) {
             log.error("[Keka86-Write] 网关:{}, 解析失败, Hex:{}", gatewayId, hexMessage, e);
         }
     }
@@ -480,7 +482,7 @@ public class Keka86BsHandler extends BaseDevHandler {
     /**
      * 处理错误响应
      * 响应格式: 01 83 01 CRC_L CRC_H (读取错误)
-     *          01 86 01 CRC_L CRC_H (写入错误)
+     * 01 86 01 CRC_L CRC_H (写入错误)
      */
     private void handleErrorResponse(String gatewayId, String hexMessage, byte originalFuncCode) {
         try {
@@ -489,15 +491,14 @@ public class Keka86BsHandler extends BaseDevHandler {
                 byte errorCode = response[2];
                 String errorMsg = getModbusErrorMessage(errorCode);
 
-                String funcName = (originalFuncCode == 0x03) ? "读取" :
-                    (originalFuncCode == 0x06) ? "写入" : "未知";
+                String funcName = (originalFuncCode == 0x03) ? "读取" : (originalFuncCode == 0x06) ? "写入" : "未知";
 
-                log.error("[Keka86-Error] 网关:{}, {}操作失败, 错误:{}, Hex:{}",
-                    gatewayId, funcName, errorMsg, hexMessage);
+                log.error("[Keka86-Error] 网关:{}, {}操作失败, 错误:{}, Hex:{}", gatewayId, funcName, errorMsg,
+                    hexMessage);
             }
-        } catch (Exception e) {
-            log.error("[Keka86-Error] 网关:{}, 错误响应解析失败, Hex:{}",
-                gatewayId, hexMessage, e);
+        }
+        catch (Exception e) {
+            log.error("[Keka86-Error] 网关:{}, 错误响应解析失败, Hex:{}", gatewayId, hexMessage, e);
         }
     }
 
@@ -506,21 +507,22 @@ public class Keka86BsHandler extends BaseDevHandler {
      */
     private void updateDeviceStatus(String deviceCode, DeviceStatus deviceStatus) {
         try {
-            EmsObjAttrValue value = objAttrValueService.selectObjAttrValue(
-                MODE_CODE, deviceCode, "Switch");
+            EmsObjAttrValue value = objAttrValueService.selectObjAttrValue(MODE_CODE, deviceCode, "Switch");
             String newValue = String.valueOf(deviceStatus.getStatus());
 
             if (null != value && !StringUtils.equals(value.getAttrValue(), newValue)) {
                 objAttrValueService.updateObjAttrValue(MODE_CODE, deviceCode, "Switch", newValue);
-                log.info("[Keka86-Update] 设备:{}, 状态更新: {} -> {}",
-                    deviceCode, value.getAttrValue(), newValue);
+                new EmsApiTemplate(emsConfig).notifyAttrValueChangedAsync(deviceCode, "Switch", value.getAttrValue(),
+                    newValue);
+                log.info("[Keka86-Update] 设备:{}, 状态更新: {} -> {}", deviceCode, value.getAttrValue(), newValue);
             }
             else if (null == value) {
                 value = new EmsObjAttrValue(deviceCode, MODE_CODE, "Switch", newValue);
                 objAttrValueService.mergeObjAttrValue(value);
                 log.info("[Keka86-Update] 设备:{}, 状态新增: {}", deviceCode, newValue);
             }
-        } catch (Exception e) {
+        }
+        catch (Exception e) {
             log.error("[Keka86-Update] 设备:{}, 状态更新失败", deviceCode, e);
         }
     }

+ 87 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/retrofit/EmsApi.java

@@ -0,0 +1,87 @@
+/*
+ * 文 件 名:  EmsApi
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  EMS Server API 接口定义
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ */
+package com.ruoyi.ems.retrofit;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+import retrofit2.http.Query;
+
+import java.util.Map;
+
+/**
+ * EMS Server API 接口
+ * 用于 ems-dev-adapter 调用 ems-server 的策略相关接口
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/9]
+ */
+public interface EmsApi {
+
+    /**
+     * 属性值变化通知(单个)
+     *
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param newValue 新值
+     * @param oldValue 旧值(可选)
+     * @return 响应
+     */
+    @Headers({"Content-Type: application/json"})
+    @POST("/ems/energyStrategy/onAttrValueChanged")
+    Call<String> onAttrValueChanged(
+        @Query("objCode") String objCode,
+        @Query("attrKey") String attrKey,
+        @Query("newValue") String newValue,
+        @Query("oldValue") String oldValue
+    );
+
+    /**
+     * 属性值变化通知(批量)
+     *
+     * @param request 请求体 { "objCode": "xxx", "attributes": { "key1": "val1", ... } }
+     * @return 响应
+     */
+    @Headers({"Content-Type: application/json"})
+    @POST("/ems/energyStrategy/onAttrValueChangedBatch")
+    Call<String> onAttrValueChangedBatch(@Body Map<String, Object> request);
+
+    /**
+     * 设备事件通知
+     *
+     * @param objCode   设备代码
+     * @param eventKey  事件键
+     * @param eventData 事件数据
+     * @return 响应
+     */
+    @Headers({"Content-Type: application/json"})
+    @POST("/ems/energyStrategy/onDeviceEvent")
+    Call<String> onDeviceEvent(
+        @Query("objCode") String objCode,
+        @Query("eventKey") String eventKey,
+        @Body Map<String, Object> eventData
+    );
+
+    /**
+     * 获取调度器状态
+     *
+     * @return 响应
+     */
+    @GET("/ems/energyStrategy/scheduler/status")
+    Call<String> getSchedulerStatus();
+
+    /**
+     * 重新加载调度器
+     *
+     * @return 响应
+     */
+    @POST("/ems/energyStrategy/scheduler/reload")
+    Call<String> reloadScheduler();
+}

+ 6 - 0
ems/ems-cloud/ems-dev-adapter/src/main/resources/application-local.yml

@@ -55,6 +55,12 @@ mqtt:
       namePrefix: 'mqttHandle-'
 
 adapter:
+  ems:
+    url: http://127.0.0.1:9202
+    connectTimeout: 3
+    readTimeout: 5
+    writeTimeout: 5
+    notifyEnabled: true
   # 安科瑞
   acrel:
     url: http://127.0.0.1:8090

+ 6 - 0
ems/ems-cloud/ems-dev-adapter/src/main/resources/application-prod-ct.yml

@@ -55,6 +55,12 @@ mqtt:
       namePrefix: 'mqttHandle-'
 
 adapter:
+  ems:
+    url: http://127.0.0.1:9202
+    connectTimeout: 3
+    readTimeout: 5
+    writeTimeout: 5
+    notifyEnabled: true
   # 安科瑞
   acrel:
     url: http://172.61.55.66:8090

+ 36 - 0
ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/config/TaskSchedulerConfig.java

@@ -0,0 +1,36 @@
+/*
+ * 文 件 名:  TaskSchedulerConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+/**
+ * 任务调度器配置
+ */
+@Configuration
+@EnableScheduling
+public class TaskSchedulerConfig {
+
+    @Bean("strategyTaskScheduler")
+    public TaskScheduler strategyTaskScheduler() {
+        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+        scheduler.setPoolSize(10);
+        scheduler.setThreadNamePrefix("strategy-scheduler-");
+        scheduler.setWaitForTasksToCompleteOnShutdown(true);
+        scheduler.setAwaitTerminationSeconds(30);
+        scheduler.initialize();
+        return scheduler;
+    }
+}

+ 342 - 96
ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/controller/OpEnergyStrategyController.java

@@ -8,6 +8,7 @@ import com.ruoyi.common.core.web.page.TableDataInfo;
 import com.ruoyi.common.log.annotation.Log;
 import com.ruoyi.common.log.enums.BusinessType;
 import com.ruoyi.common.security.annotation.RequiresPermissions;
+import com.ruoyi.common.security.utils.SecurityUtils;
 import com.ruoyi.ems.domain.OpEnergyStrategy;
 import com.ruoyi.ems.domain.OpEnergyStrategyExecLog;
 import com.ruoyi.ems.domain.OpEnergyStrategyParam;
@@ -21,9 +22,14 @@ import com.ruoyi.ems.service.IOpEnergyStrategyService;
 import com.ruoyi.ems.service.IOpEnergyStrategyStepService;
 import com.ruoyi.ems.service.IOpEnergyStrategyTemplateService;
 import com.ruoyi.ems.service.IOpEnergyStrategyTriggerService;
+import com.ruoyi.ems.strategy.PollingMonitorService;
 import com.ruoyi.ems.strategy.executor.StrategyExecutor;
+import com.ruoyi.ems.task.StrategyScheduler;
+import com.ruoyi.ems.task.StrategyTriggerListener;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.DeleteMapping;
@@ -43,14 +49,13 @@ import java.util.stream.Collectors;
 
 /**
  * 能源策略Controller
- *
- * @author ruoyi
- * @date 2024-08-08
  */
+@Slf4j
 @RestController
 @RequestMapping("/energyStrategy")
 @Api(value = "OpEnergyStrategyController", description = "能源策略管理接口")
 public class OpEnergyStrategyController extends BaseController {
+
     @Autowired
     private IOpEnergyStrategyService strategyService;
 
@@ -72,9 +77,20 @@ public class OpEnergyStrategyController extends BaseController {
     @Autowired
     private StrategyExecutor strategyExecutor;
 
-    /**
-     * 查询能源策略列表
-     */
+    // 策略调度器
+    @Autowired
+    private StrategyScheduler strategyScheduler;
+
+    // 触发监听器
+    @Autowired
+    private StrategyTriggerListener triggerListener;
+
+    // 轮询监控服务
+    @Autowired
+    private PollingMonitorService pollingMonitorService;
+
+    // ==================== 策略基础管理 ====================
+
     @RequiresPermissions("power-mgr:strategy:list")
     @GetMapping("/list")
     public TableDataInfo list(OpEnergyStrategy strategy) {
@@ -83,25 +99,16 @@ public class OpEnergyStrategyController extends BaseController {
         return getDataTable(list);
     }
 
-    /**
-     * 获取能源策略详细信息
-     */
     @GetMapping(value = "/{id}")
     public AjaxResult getInfo(@PathVariable("id") Long id) {
         return success(strategyService.selectStrategyById(id));
     }
 
-    /**
-     * 获取能源策略详细信息
-     */
     @GetMapping(value = "/code/{strategyCode}")
     public AjaxResult getByCode(@PathVariable("strategyCode") String strategyCode) {
         return success(strategyService.selectStrategyByCode(strategyCode));
     }
 
-    /**
-     * 新增能源策略
-     */
     @RequiresPermissions("power-mgr:strategy:add")
     @Log(title = "能源策略", businessType = BusinessType.INSERT)
     @PostMapping
@@ -111,26 +118,56 @@ public class OpEnergyStrategyController extends BaseController {
 
     /**
      * 修改能源策略
+     * -修改后刷新调度器
      */
     @RequiresPermissions("power-mgr:strategy:edit")
     @Log(title = "能源策略", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody OpEnergyStrategy strategy) {
-        return toAjax(strategyService.updateStrategy(strategy));
+        int result = strategyService.updateStrategy(strategy);
+
+        if (result > 0) {
+            // 刷新调度器中的策略配置
+            strategyScheduler.refreshStrategy(strategy.getStrategyCode());
+
+            // 【新增】如果是轮询触发类型且已启用,刷新轮询配置
+            if (strategy.getStrategyState() != null && strategy.getStrategyState() == 1) {
+                if (strategy.getTriggerType() == 4 || strategy.getTriggerType() == 5) {
+                    pollingMonitorService.refreshPollingStrategy(strategy.getStrategyCode());
+                }
+            }
+
+            log.info("策略[{}]已更新并刷新调度器", strategy.getStrategyCode());
+        }
+
+        return toAjax(result);
     }
 
     /**
      * 删除能源策略
+     * 删除前从调度器注销
      */
     @RequiresPermissions("power-mgr:strategy:remove")
     @Log(title = "能源策略", businessType = BusinessType.DELETE)
     @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids) {
+        // 先从调度器注销
+        for (Long id : ids) {
+            OpEnergyStrategy strategy = strategyService.selectStrategyById(id);
+            if (strategy != null) {
+                strategyScheduler.unregisterStrategy(strategy.getStrategyCode());
+                // 【新增】同时注销轮询监控
+                pollingMonitorService.unregisterPollingStrategy(strategy.getStrategyCode());
+                log.info("策略[{}]已从调度器注销", strategy.getStrategyCode());
+            }
+        }
+
         return toAjax(strategyService.deleteStrategyByIds(ids));
     }
 
     /**
      * 启用/停用策略
+     * 核心修改:同时更新调度器
      */
     @PutMapping("/state/{strategyCode}/{state}")
     @ApiOperation("启用/停用策略")
@@ -139,8 +176,30 @@ public class OpEnergyStrategyController extends BaseController {
         if (strategy == null) {
             return error("策略不存在");
         }
+
         strategy.setStrategyState(state);
-        return toAjax(strategyService.updateStrategy(strategy));
+        int result = strategyService.updateStrategy(strategy);
+
+        if (result > 0) {
+            // 根据状态更新调度器
+            if (state == 1) {
+                strategyScheduler.registerStrategy(strategy);
+
+                // 如果是轮询触发类型,同时注册轮询监控
+                if (strategy.getTriggerType() == 4 || strategy.getTriggerType() == 5) {
+                    pollingMonitorService.registerPollingStrategy(strategy);
+                }
+
+                log.info("策略[{}]已启用并注册到调度器", strategyCode);
+            }
+            else {
+                strategyScheduler.unregisterStrategy(strategyCode);
+                pollingMonitorService.unregisterPollingStrategy(strategyCode);
+                log.info("策略[{}]已停用并从调度器注销", strategyCode);
+            }
+        }
+
+        return toAjax(result);
     }
 
     @GetMapping("/sceneCount")
@@ -148,23 +207,198 @@ public class OpEnergyStrategyController extends BaseController {
         return success(strategyService.getSceneTypeCount(areaCode));
     }
 
-    // ==================== 策略参数管理 ====================
+    // ==================== 属性变化通知接口(供采集模块调用) ====================
 
     /**
-     * 获取能源策略参数
+     * 属性值变化通知接口
+     * 由 ems-dev-adapter 采集模块调用
      *
-     * @param strategyCode 策略编码
-     * @return 参数集合
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param oldValue 旧值(可选)
+     * @param newValue 新值
+     * @return 触发的策略数量
      */
+    @PostMapping("/onAttrValueChanged")
+    @ApiOperation("属性值变化通知(供采集模块调用)")
+    public AjaxResult onAttrValueChanged(@ApiParam("设备代码") @RequestParam String objCode,
+        @ApiParam("属性键") @RequestParam String attrKey,
+        @ApiParam("旧值") @RequestParam(required = false) String oldValue,
+        @ApiParam("新值") @RequestParam String newValue) {
+
+        log.debug("收到属性变化通知: obj={}, attr={}, {} -> {}", objCode, attrKey, oldValue, newValue);
+
+        int triggeredCount = triggerListener.handleAttrChange(objCode, attrKey, oldValue, newValue);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("objCode", objCode);
+        result.put("attrKey", attrKey);
+        result.put("triggeredStrategies", triggeredCount);
+
+        return success(result);
+    }
+
+    /**
+     * 批量属性变化通知接口
+     *
+     * @param request 包含 objCode 和 attributes 的请求体
+     */
+    @PostMapping("/onAttrValueChangedBatch")
+    @ApiOperation("批量属性值变化通知(供采集模块调用)")
+    public AjaxResult onAttrValueChangedBatch(@RequestBody Map<String, Object> request) {
+        String objCode = (String) request.get("objCode");
+        @SuppressWarnings("unchecked") Map<String, Object> attributes = (Map<String, Object>) request.get("attributes");
+
+        if (objCode == null || attributes == null) {
+            return error("参数不完整,需要 objCode 和 attributes");
+        }
+
+        log.debug("收到批量属性变化通知: obj={}, attrCount={}", objCode, attributes.size());
+
+        int totalTriggered = 0;
+        for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+            String attrKey = entry.getKey();
+            Object newValue = entry.getValue();
+
+            int triggered = triggerListener.handleAttrChange(objCode, attrKey, null, newValue);
+            totalTriggered += triggered;
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("objCode", objCode);
+        result.put("attributeCount", attributes.size());
+        result.put("triggeredStrategies", totalTriggered);
+
+        return success(result);
+    }
+
+    /**
+     * 设备事件通知接口
+     */
+    @PostMapping("/onDeviceEvent")
+    @ApiOperation("设备事件通知(供采集模块调用)")
+    public AjaxResult onDeviceEvent(@ApiParam("设备代码") @RequestParam String objCode,
+        @ApiParam("事件键") @RequestParam String eventKey,
+        @RequestBody(required = false) Map<String, Object> eventData) {
+
+        log.debug("收到设备事件通知: obj={}, event={}", objCode, eventKey);
+
+        if (eventData == null) {
+            eventData = new HashMap<>();
+        }
+
+        int triggeredCount = triggerListener.handleDeviceEvent(objCode, eventKey, eventData);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("objCode", objCode);
+        result.put("eventKey", eventKey);
+        result.put("triggeredStrategies", triggeredCount);
+
+        return success(result);
+    }
+
+    // ==================== 调度器状态接口 ====================
+
+    /**
+     * 获取调度器状态
+     */
+    @GetMapping("/scheduler/status")
+    @ApiOperation("获取调度器状态")
+    public AjaxResult getSchedulerStatus() {
+        Map<String, Object> status = new HashMap<>();
+        status.put("registeredStrategies", strategyScheduler.getRegisteredCount());
+        status.put("scheduledTasks", strategyScheduler.getScheduledTaskCount());
+        status.put("attrTriggers", strategyScheduler.getAttrTriggerCount());
+        status.put("triggers", triggerListener.getRegisteredTriggers());
+
+        // 添加轮询监控状态
+        status.put("pollingTasks", pollingMonitorService.getPollingTaskCount());
+        status.put("pollingStatus", pollingMonitorService.getPollingStatus());
+
+        return success(status);
+    }
+
+    /**
+     * 重新加载所有策略
+     */
+    @PostMapping("/scheduler/reload")
+    @ApiOperation("重新加载所有策略")
+    public AjaxResult reloadScheduler() {
+        strategyScheduler.loadEnabledStrategies();
+        return success("调度器已重新加载");
+    }
+
+    // ==================== 轮询监控接口 ====================
+
+    /**
+     * 获取轮询监控状态
+     */
+    @GetMapping("/polling/status")
+    @ApiOperation("获取轮询监控状态")
+    public AjaxResult getPollingStatus() {
+        return success(pollingMonitorService.getPollingStatus());
+    }
+
+    /**
+     * 刷新指定策略的轮询配置
+     */
+    @PostMapping("/polling/refresh/{strategyCode}")
+    @ApiOperation("刷新策略轮询配置")
+    public AjaxResult refreshPollingStrategy(@PathVariable String strategyCode) {
+        pollingMonitorService.refreshPollingStrategy(strategyCode);
+        return success("轮询配置已刷新");
+    }
+
+    /**
+     * 手动触发一次轮询检查(用于调试)
+     * 【改造点】:完善实现,调用 PollingMonitorService.triggerPollingCheck()
+     */
+    @PostMapping("/polling/trigger/{strategyCode}")
+    @ApiOperation("手动触发轮询检查")
+    public AjaxResult manualTriggerPolling(@PathVariable String strategyCode) {
+        if (!pollingMonitorService.hasPollingTask(strategyCode)) {
+            return error("该策略未配置轮询监控或未启用");
+        }
+
+        try {
+            // 【改造】调用轮询服务的手动触发方法
+            pollingMonitorService.triggerPollingCheck(strategyCode);
+
+            Map<String, Object> result = new HashMap<>();
+            result.put("strategyCode", strategyCode);
+            result.put("message", "已触发轮询检查,请查看日志确认执行结果");
+
+            return success(result);
+        } catch (Exception e) {
+            log.error("手动触发轮询检查失败: strategy={}", strategyCode, e);
+            return error("触发轮询检查失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 【新增】检查策略是否有轮询任务
+     */
+    @GetMapping("/polling/check/{strategyCode}")
+    @ApiOperation("检查策略是否有轮询任务")
+    public AjaxResult checkPollingTask(@PathVariable String strategyCode) {
+        boolean hasTask = pollingMonitorService.hasPollingTask(strategyCode);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("strategyCode", strategyCode);
+        result.put("hasPollingTask", hasTask);
+        result.put("message", hasTask ? "已注册轮询监控" : "未注册轮询监控");
+
+        return success(result);
+    }
+
+    // ==================== 策略参数管理 ====================
+
     @GetMapping(value = "/param")
     public AjaxResult getStrategyParam(@RequestParam(name = "strategyCode") String strategyCode) {
         List<OpEnergyStrategyParam> paramList = paramService.selectParamByStrategyCode(strategyCode);
         return success(buildStrategyParams(paramList));
     }
 
-    /**
-     * 修改能源策略参数
-     */
     @RequiresPermissions("power-mgr:strategy:edit")
     @Log(title = "能源策略参数", businessType = BusinessType.UPDATE)
     @PutMapping("/param")
@@ -180,14 +414,12 @@ public class OpEnergyStrategyController extends BaseController {
 
     private Map<String, Map<String, JSONObject>> buildStrategyParams(List<OpEnergyStrategyParam> paramList) {
         Map<String, Map<String, JSONObject>> params = new HashMap<>();
-
         Map<String, List<OpEnergyStrategyParam>> groupedMap = paramList.stream()
             .collect(Collectors.groupingBy(OpEnergyStrategyParam::getParamGroup, Collectors.toList()));
 
         for (Map.Entry<String, List<OpEnergyStrategyParam>> entry : groupedMap.entrySet()) {
             String groupName = entry.getKey();
             List<OpEnergyStrategyParam> groupParams = entry.getValue();
-
             Map<String, JSONObject> groupParamMap = new HashMap<>();
             for (OpEnergyStrategyParam param : groupParams) {
                 JSONObject option = new JSONObject();
@@ -199,62 +431,31 @@ public class OpEnergyStrategyController extends BaseController {
             }
             params.put(groupName, groupParamMap);
         }
-
         return params;
     }
 
     // ==================== 策略步骤管理 ====================
 
-    /**
-     * 获取能源策略执行步骤
-     *
-     * @param strategyCode 策略编码
-     * @return 步骤列表
-     */
     @GetMapping(value = "/step")
     public AjaxResult getStrategyStep(@RequestParam(name = "strategyCode") String strategyCode) {
         return success(stepService.selectStepByStrategyCode(strategyCode));
     }
 
-    /**
-     * 新增能源策略执行步骤
-     *
-     * @param strategyStep 策略步骤
-     * @return 执行结果
-     */
     @PostMapping(value = "/step")
     public AjaxResult addStrategyStep(@RequestBody OpEnergyStrategyStep strategyStep) {
         return toAjax(stepService.insertStep(strategyStep));
     }
 
-    /**
-     * 修改能源策略执行步骤
-     *
-     * @param strategyStep 策略步骤
-     * @return 执行结果
-     */
     @PutMapping(value = "/step")
     public AjaxResult editStrategyStep(@RequestBody OpEnergyStrategyStep strategyStep) {
         return toAjax(stepService.updateStep(strategyStep));
     }
 
-    /**
-     * 删除能源策略执行步骤
-     *
-     * @param id 步骤ID
-     * @return 执行结果
-     */
     @DeleteMapping(value = "/step/{id}")
     public AjaxResult delStrategyStep(@PathVariable Long id) {
         return toAjax(stepService.deleteStep(id));
     }
 
-    /**
-     * 批量修改能源策略执行步骤
-     *
-     * @param strategySteps 策略步骤列表
-     * @return 执行结果
-     */
     @PutMapping(value = "/step/batch")
     public AjaxResult editStrategyStepBatch(@RequestBody List<OpEnergyStrategyStep> strategySteps) {
         if (CollectionUtils.isEmpty(strategySteps)) {
@@ -264,11 +465,8 @@ public class OpEnergyStrategyController extends BaseController {
         return toAjax(stepService.insertStepBatch(strategySteps));
     }
 
-    // ==================== 触发器管理(新增) ====================
+    // ==================== 触发器管理 ====================
 
-    /**
-     * 获取策略触发器列表
-     */
     @GetMapping("/trigger/{strategyCode}")
     @ApiOperation("获取触发器列表")
     public AjaxResult getTriggers(@PathVariable String strategyCode) {
@@ -276,44 +474,48 @@ public class OpEnergyStrategyController extends BaseController {
         return success(triggers);
     }
 
-    /**
-     * 保存策略触发器
-     */
     @PostMapping("/trigger")
     @ApiOperation("保存触发器")
     public AjaxResult saveTrigger(@RequestBody OpEnergyStrategyTrigger trigger) {
+        int result;
         if (trigger.getId() == null) {
-            return toAjax(triggerService.insertTrigger(trigger));
+            result = triggerService.insertTrigger(trigger);
+        } else {
+            result = triggerService.updateTrigger(trigger);
         }
-        else {
-            return toAjax(triggerService.updateTrigger(trigger));
+
+        if (result > 0) {
+            // 同步更新策略的触发类型
+            syncStrategyTriggerType(trigger.getStrategyCode());
+
+            // 刷新调度器
+            strategyScheduler.refreshStrategy(trigger.getStrategyCode());
+
+            // 如果是轮询触发器,刷新轮询配置
+            if ("POLLING".equals(trigger.getTriggerType())) {
+                pollingMonitorService.refreshPollingStrategy(trigger.getStrategyCode());
+            }
         }
+
+        return toAjax(result);
     }
 
-    /**
-     * 删除触发器
-     */
     @DeleteMapping("/trigger/{id}")
     @ApiOperation("删除触发器")
     public AjaxResult deleteTrigger(@PathVariable Long id) {
         return toAjax(triggerService.deleteTrigger(id));
     }
 
-    /**
-     * 策略模板
-     *
-     * @param template
-     * @return
-     */
+    // ==================== 模板管理 ====================
+
     @GetMapping("/template/list")
     public AjaxResult listTemplate(OpEnergyStrategyTemplate template) {
         List<OpEnergyStrategyTemplate> list = templateService.selectTemplateList(template);
         return success(list);
     }
 
-    /**
-     * 手动执行策略
-     */
+    // ==================== 策略执行 ====================
+
     @PostMapping("/execute/{strategyCode}")
     @ApiOperation("手动执行策略")
     public AjaxResult executeStrategy(@PathVariable String strategyCode,
@@ -322,26 +524,41 @@ public class OpEnergyStrategyController extends BaseController {
             if (params == null) {
                 params = new HashMap<>();
             }
+
+            // 设置触发类型和触发源
             params.put("trigger_type", "MANUAL");
-            params.put("trigger_source", "USER");
+            params.put("trigger_source", "USER_MANUAL");
+
+            // 设置执行人(尝试获取当前登录用户)
+            try {
+                String username = SecurityUtils.getUsername();
+                if (username != null && !username.isEmpty()) {
+                    params.put("exec_by", username);
+                } else {
+                    params.put("exec_by", "UNKNOWN_USER");
+                }
+            } catch (Exception e) {
+                params.put("exec_by", "ANONYMOUS");
+                log.debug("无法获取当前用户: {}", e.getMessage());
+            }
 
             String execId = strategyExecutor.executeStrategy(strategyCode, params);
 
             Map<String, Object> result = new HashMap<>();
             result.put("execId", execId);
             result.put("message", "策略执行已启动");
+            result.put("triggerType", "MANUAL");
+            result.put("execBy", params.get("exec_by"));
 
             return success(result);
-        }
-        catch (Exception e) {
+        } catch (Exception e) {
+            log.error("手动执行策略失败: {}", strategyCode, e);
             return error("策略执行失败: " + e.getMessage());
         }
     }
 
-    // ==================== 执行日志(新增) ====================
-    /**
-     * 获取策略执行日志列表(支持分页和多条件查询)
-     */
+    // ==================== 执行日志 ====================
+
     @GetMapping("/execLog/list")
     @ApiOperation("获取执行日志列表")
     public TableDataInfo getExecLogList(OpEnergyStrategyExecLog param) {
@@ -350,9 +567,6 @@ public class OpEnergyStrategyController extends BaseController {
         return getDataTable(execLogs);
     }
 
-    /**
-     * 获取策略执行日志详情
-     */
     @GetMapping("/execLog/{execId}")
     @ApiOperation("获取执行日志详情")
     public AjaxResult getExecLog(@PathVariable String execId) {
@@ -360,24 +574,56 @@ public class OpEnergyStrategyController extends BaseController {
         if (execLog == null) {
             return error("执行日志不存在");
         }
-
-        // 获取步骤日志
         List<OpEnergyStrategyStepLog> stepLogs = execLogService.selectStepLogsByExecId(execId);
-
         Map<String, Object> result = new HashMap<>();
         result.put("execLog", execLog);
         result.put("stepLogs", stepLogs);
-
         return success(result);
     }
 
-    /**
-     * 获取步骤执行日志
-     */
     @GetMapping("/execLog/steps/{execId}")
     @ApiOperation("获取步骤执行日志")
     public AjaxResult getStepExecLog(@PathVariable String execId) {
         List<OpEnergyStrategyStepLog> stepLogs = execLogService.selectStepLogsByExecId(execId);
         return success(stepLogs);
     }
+
+    /**
+     * 保存触发器后同步更新策略的触发类型
+     */
+    private void syncStrategyTriggerType(String strategyCode) {
+        List<OpEnergyStrategyTrigger> triggers = triggerService.selectByStrategyCode(strategyCode);
+        if (triggers.isEmpty()) {
+            return;
+        }
+
+        // 取第一个启用的触发器的类型
+        OpEnergyStrategyTrigger firstTrigger = triggers.stream()
+            .filter(t -> t.getEnable() == 1)
+            .findFirst()
+            .orElse(null);
+
+        if (firstTrigger != null) {
+            OpEnergyStrategy strategy = strategyService.selectStrategyByCode(strategyCode);
+            if (strategy != null) {
+                Integer triggerType = mapTriggerType(firstTrigger.getTriggerType());
+                strategy.setTriggerType(triggerType);
+                strategyService.updateStrategy(strategy);
+            }
+        }
+    }
+
+    /**
+     * 触发器类型字符串转整数
+     */
+    private Integer mapTriggerType(String triggerType) {
+        if (triggerType == null) return 3;
+        switch (triggerType) {
+            case "EVENT": return 1;
+            case "TIME": return 2;
+            case "ATTR": return 4;
+            case "POLLING": return 5;
+            default: return 3;
+        }
+    }
 }

+ 346 - 0
ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/task/StrategyScheduler.java

@@ -0,0 +1,346 @@
+/*
+ * 文 件 名:  StrategyScheduler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.task;
+
+import com.ruoyi.ems.domain.OpEnergyStrategy;
+import com.ruoyi.ems.domain.OpEnergyStrategyTrigger;
+import com.ruoyi.ems.service.IOpEnergyStrategyService;
+import com.ruoyi.ems.service.IOpEnergyStrategyTriggerService;
+import com.ruoyi.ems.strategy.PollingMonitorService;
+import com.ruoyi.ems.strategy.executor.StrategyExecutor;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.support.CronTrigger;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+
+/**
+ * 策略调度器
+ * 支持的触发类型:
+ * 1. 事件触发(1) - 监听设备事件
+ * 2. 定时触发(2) - CRON定时执行
+ * 3. 手动触发(3) - 用户手动执行
+ * 4. 条件触发(4) - 属性变化触发
+ * 5. 轮询触发(5) - 主动轮询监控
+ */
+@Slf4j
+@Component
+public class StrategyScheduler {
+
+    @Autowired
+    private IOpEnergyStrategyService strategyService;
+
+    @Autowired
+    private IOpEnergyStrategyTriggerService triggerService;
+
+    @Autowired
+    private StrategyExecutor strategyExecutor;
+
+    @Autowired
+    private StrategyTriggerListener triggerListener;
+
+    /**
+     * 轮询监控服务
+     */
+    @Autowired
+    private PollingMonitorService pollingMonitorService;
+
+    @Qualifier("strategyTaskScheduler")
+    @Autowired
+    private TaskScheduler taskScheduler;
+
+    /**
+     * 存储定时任务的Future
+     */
+    private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
+
+    /**
+     * 存储已注册的策略
+     */
+    private final Map<String, OpEnergyStrategy> registeredStrategies = new ConcurrentHashMap<>();
+
+    @PostConstruct
+    public void init() {
+        log.info("====== 策略调度器V2初始化开始 ======");
+        loadEnabledStrategies();
+        log.info("====== 策略调度器V2初始化完成 ======");
+        log.info("  - 已注册策略: {}", registeredStrategies.size());
+        log.info("  - 定时任务: {}", scheduledTasks.size());
+        log.info("  - 属性触发器: {}", triggerListener.getAttrTriggerCount());
+        log.info("  - 事件触发器: {}", triggerListener.getEventTriggerCount());
+        log.info("  - 轮询任务: {}", pollingMonitorService.getPollingTaskCount());
+    }
+
+    @PreDestroy
+    public void destroy() {
+        log.info("====== 策略调度器V2销毁 ======");
+        scheduledTasks.values().forEach(future -> future.cancel(true));
+        scheduledTasks.clear();
+        registeredStrategies.clear();
+    }
+
+    /**
+     * 加载所有已启用的策略
+     */
+    public void loadEnabledStrategies() {
+        OpEnergyStrategy query = new OpEnergyStrategy();
+        query.setStrategyState(1);
+        List<OpEnergyStrategy> strategies = strategyService.selectStrategyList(query);
+
+        log.info("加载到 {} 个已启用的策略", strategies.size());
+
+        for (OpEnergyStrategy strategy : strategies) {
+            try {
+                registerStrategy(strategy);
+            }
+            catch (Exception e) {
+                log.error("注册策略失败: {}", strategy.getStrategyCode(), e);
+            }
+        }
+    }
+
+    /**
+     * 注册策略
+     */
+    public void registerStrategy(OpEnergyStrategy strategy) {
+        String strategyCode = strategy.getStrategyCode();
+        Integer triggerType = strategy.getTriggerType();
+
+        log.info("注册策略: code={}, name={}, triggerType={}",
+            strategyCode, strategy.getStrategyName(), triggerType);
+
+        // 先取消已有的注册
+        unregisterStrategy(strategyCode);
+
+        switch (triggerType) {
+            case 1: // 事件触发
+                registerEventTrigger(strategy);
+                break;
+
+            case 2: // 定时触发
+                registerTimeTrigger(strategy);
+                break;
+
+            case 3: // 手动触发
+                log.info("策略[{}]为手动触发类型,不自动执行", strategyCode);
+                break;
+
+            case 4: // 条件触发(属性变化)
+                // 同时注册被动触发和轮询触发
+                registerConditionTrigger(strategy);
+                registerPollingTrigger(strategy);
+                break;
+
+            case 5: // 轮询触发
+                registerPollingTrigger(strategy);
+                break;
+
+            default:
+                log.warn("未知的触发类型: {}", triggerType);
+        }
+
+        registeredStrategies.put(strategyCode, strategy);
+    }
+
+    /**
+     * 注销策略
+     */
+    public void unregisterStrategy(String strategyCode) {
+        // 取消定时任务
+        ScheduledFuture<?> future = scheduledTasks.remove(strategyCode);
+        if (future != null) {
+            future.cancel(true);
+            log.info("已取消策略[{}]的定时任务", strategyCode);
+        }
+
+        // 从触发监听器中移除
+        triggerListener.unregisterStrategy(strategyCode);
+
+        // 从轮询监控服务中移除
+        pollingMonitorService.unregisterPollingStrategy(strategyCode);
+
+        registeredStrategies.remove(strategyCode);
+    }
+
+    /**
+     * 注册事件触发策略
+     */
+    private void registerEventTrigger(OpEnergyStrategy strategy) {
+        String strategyCode = strategy.getStrategyCode();
+        List<OpEnergyStrategyTrigger> triggers = triggerService.selectByStrategyCode(strategyCode);
+
+        for (OpEnergyStrategyTrigger trigger : triggers) {
+            if (trigger.getEnable() != 1) continue;
+
+            if ("EVENT".equals(trigger.getTriggerType())) {
+                triggerListener.registerEventTrigger(
+                    strategyCode,
+                    trigger.getSourceObjCode(),
+                    trigger.getEventKey(),
+                    trigger.getConditionExpr()
+                );
+                log.info("策略[{}]注册事件触发: obj={}, event={}",
+                    strategyCode, trigger.getSourceObjCode(), trigger.getEventKey());
+            }
+        }
+    }
+
+    /**
+     * 注册定时触发策略
+     */
+    private void registerTimeTrigger(OpEnergyStrategy strategy) {
+        String strategyCode = strategy.getStrategyCode();
+        String cronExpr = strategy.getExecRule();
+
+        if (cronExpr == null || cronExpr.trim().isEmpty()) {
+            log.warn("策略[{}]的CRON表达式为空,跳过注册", strategyCode);
+            return;
+        }
+
+        try {
+            ScheduledFuture<?> future = taskScheduler.schedule(
+                () -> executeStrategyAsync(strategyCode, "TIME", "SCHEDULER"),
+                new CronTrigger(cronExpr)
+            );
+
+            scheduledTasks.put(strategyCode, future);
+            log.info("策略[{}]注册定时触发: cron={}", strategyCode, cronExpr);
+
+        } catch (Exception e) {
+            log.error("策略[{}]注册定时触发失败: cron={}", strategyCode, cronExpr, e);
+        }
+    }
+
+    /**
+     * 注册条件触发策略(属性变化触发 - 被动模式)
+     */
+    private void registerConditionTrigger(OpEnergyStrategy strategy) {
+        String strategyCode = strategy.getStrategyCode();
+        List<OpEnergyStrategyTrigger> triggers = triggerService.selectByStrategyCode(strategyCode);
+
+        for (OpEnergyStrategyTrigger trigger : triggers) {
+            if (trigger.getEnable() != 1) continue;
+
+            if ("ATTR".equals(trigger.getTriggerType())) {
+                triggerListener.registerAttrTrigger(
+                    strategyCode,
+                    trigger.getSourceObjCode(),
+                    trigger.getAttrKey(),
+                    trigger.getConditionExpr()
+                );
+                log.info("策略[{}]注册属性触发(被动): obj={}, attr={}, condition={}",
+                    strategyCode, trigger.getSourceObjCode(), trigger.getAttrKey(),
+                    trigger.getConditionExpr());
+            }
+        }
+    }
+
+    /**
+     * 注册轮询触发策略(主动轮询模式)
+     */
+    private void registerPollingTrigger(OpEnergyStrategy strategy) {
+        pollingMonitorService.registerPollingStrategy(strategy);
+    }
+
+    /**
+     * 异步执行策略
+     */
+    private void executeStrategyAsync(String strategyCode, String triggerType, String triggerSource) {
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("trigger_type", triggerType);
+            params.put("trigger_source", triggerSource);
+
+            // 根据触发类型设置执行人
+            switch (triggerType) {
+                case "TIME":
+                    params.put("exec_by", "SYSTEM_SCHEDULER");
+                    break;
+                case "EVENT":
+                    params.put("exec_by", "SYSTEM_EVENT");
+                    break;
+                case "ATTR":
+                    params.put("exec_by", "SYSTEM_ATTR_CHANGE");
+                    break;
+                case "POLLING":
+                    params.put("exec_by", "SYSTEM_POLLING");
+                    break;
+                default:
+                    params.put("exec_by", "SYSTEM");
+            }
+
+            strategyExecutor.executeStrategy(strategyCode, params);
+        } catch (Exception e) {
+            log.error("自动执行策略失败: {}", strategyCode, e);
+        }
+    }
+
+    /**
+     * 刷新策略
+     */
+    public void refreshStrategy(String strategyCode) {
+        OpEnergyStrategy strategy = strategyService.selectStrategyByCode(strategyCode);
+
+        if (strategy == null) {
+            unregisterStrategy(strategyCode);
+            return;
+        }
+
+        if (strategy.getStrategyState() == 1) {
+            registerStrategy(strategy);
+        } else {
+            unregisterStrategy(strategyCode);
+        }
+    }
+
+    // ==================== 统计方法 ====================
+
+    public int getRegisteredCount() {
+        return registeredStrategies.size();
+    }
+
+    public int getScheduledTaskCount() {
+        return scheduledTasks.size();
+    }
+
+    public int getAttrTriggerCount() {
+        return triggerListener.getAttrTriggerCount();
+    }
+
+    public int getPollingTaskCount() {
+        return pollingMonitorService.getPollingTaskCount();
+    }
+
+    /**
+     * 获取完整的调度器状态
+     */
+    public Map<String, Object> getFullStatus() {
+        Map<String, Object> status = new HashMap<>();
+        status.put("registeredStrategies", registeredStrategies.size());
+        status.put("scheduledTasks", scheduledTasks.size());
+        status.put("attrTriggers", triggerListener.getAttrTriggerCount());
+        status.put("eventTriggers", triggerListener.getEventTriggerCount());
+        status.put("pollingTasks", pollingMonitorService.getPollingTaskCount());
+        status.put("triggers", triggerListener.getRegisteredTriggers());
+        status.put("polling", pollingMonitorService.getPollingStatus());
+        return status;
+    }
+}

+ 482 - 0
ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/task/StrategyTriggerListener.java

@@ -0,0 +1,482 @@
+/*
+ * 文 件 名:  StrategyTriggerListener
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.task;
+
+import com.ruoyi.ems.strategy.evaluator.ConditionEvaluator;
+import com.ruoyi.ems.strategy.executor.StrategyExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 策略触发监听器
+ * 由 Controller 的 onAttrValueChanged 接口调用
+ */
+@Slf4j
+@Component
+public class StrategyTriggerListener {
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    @Autowired
+    private ConditionEvaluator conditionEvaluator;
+
+    /**
+     * StrategyExecutor 延迟加载,避免循环依赖
+     */
+    private StrategyExecutor strategyExecutor;
+
+    /**
+     * 属性触发器注册表
+     * key: objCode + ":" + attrKey
+     * value: 触发器配置列表
+     */
+    private final Map<String, List<AttrTriggerConfig>> attrTriggers = new ConcurrentHashMap<>();
+
+    /**
+     * 事件触发器注册表
+     * key: objCode + ":" + eventKey
+     * value: 触发器配置列表
+     */
+    private final Map<String, List<EventTriggerConfig>> eventTriggers = new ConcurrentHashMap<>();
+
+    /**
+     * 策略代码到触发器Key的映射(用于注销时快速查找)
+     * key: strategyCode
+     * value: 触发器Key列表 (格式: "ATTR:objCode:attrKey" 或 "EVENT:objCode:eventKey")
+     */
+    private final Map<String, List<String>> strategyTriggerKeys = new ConcurrentHashMap<>();
+
+    /**
+     * 异步执行线程池
+     */
+    private final ExecutorService asyncExecutor = Executors.newFixedThreadPool(5);
+
+    /**
+     * 获取 StrategyExecutor(延迟加载,避免循环依赖)
+     */
+    private StrategyExecutor getStrategyExecutor() {
+        if (strategyExecutor == null) {
+            synchronized (this) {
+                if (strategyExecutor == null) {
+                    strategyExecutor = applicationContext.getBean(StrategyExecutor.class);
+                }
+            }
+        }
+        return strategyExecutor;
+    }
+
+    // ==================== 触发器注册方法 ====================
+
+    /**
+     * 注册属性变化触发器
+     *
+     * @param strategyCode  策略代码
+     * @param objCode       设备代码
+     * @param attrKey       属性键
+     * @param conditionExpr 条件表达式(JSON格式)
+     */
+    public void registerAttrTrigger(String strategyCode, String objCode, String attrKey, String conditionExpr) {
+        String triggerKey = objCode + ":" + attrKey;
+
+        // 创建触发器配置
+        AttrTriggerConfig config = new AttrTriggerConfig();
+        config.strategyCode = strategyCode;
+        config.objCode = objCode;
+        config.attrKey = attrKey;
+        config.conditionExpr = conditionExpr;
+
+        // 添加到触发器列表
+        List<AttrTriggerConfig> configList = attrTriggers.computeIfAbsent(triggerKey, k -> new ArrayList<>());
+
+        // 避免重复注册
+        boolean exists = configList.stream().anyMatch(c -> c.strategyCode.equals(strategyCode));
+        if (!exists) {
+            configList.add(config);
+        }
+
+        // 记录策略到触发器的映射(用于注销)
+        List<String> keys = strategyTriggerKeys.computeIfAbsent(strategyCode, k -> new ArrayList<>());
+        String mappingKey = "ATTR:" + triggerKey;
+        if (!keys.contains(mappingKey)) {
+            keys.add(mappingKey);
+        }
+
+        log.debug("注册属性触发器: triggerKey={}, strategy={}, condition={}", triggerKey, strategyCode, conditionExpr);
+    }
+
+    /**
+     * 注册事件触发器
+     *
+     * @param strategyCode  策略代码
+     * @param objCode       设备代码
+     * @param eventKey      事件键
+     * @param conditionExpr 条件表达式(JSON格式)
+     */
+    public void registerEventTrigger(String strategyCode, String objCode, String eventKey, String conditionExpr) {
+        String triggerKey = objCode + ":" + eventKey;
+
+        // 创建触发器配置
+        EventTriggerConfig config = new EventTriggerConfig();
+        config.strategyCode = strategyCode;
+        config.objCode = objCode;
+        config.eventKey = eventKey;
+        config.conditionExpr = conditionExpr;
+
+        // 添加到触发器列表
+        List<EventTriggerConfig> configList = eventTriggers.computeIfAbsent(triggerKey, k -> new ArrayList<>());
+
+        // 避免重复注册
+        boolean exists = configList.stream().anyMatch(c -> c.strategyCode.equals(strategyCode));
+        if (!exists) {
+            configList.add(config);
+        }
+
+        // 记录策略到触发器的映射
+        List<String> keys = strategyTriggerKeys.computeIfAbsent(strategyCode, k -> new ArrayList<>());
+        String mappingKey = "EVENT:" + triggerKey;
+        if (!keys.contains(mappingKey)) {
+            keys.add(mappingKey);
+        }
+
+        log.debug("注册事件触发器: triggerKey={}, strategy={}", triggerKey, strategyCode);
+    }
+
+    /**
+     * 注销策略的所有触发器
+     *
+     * @param strategyCode 策略代码
+     */
+    public void unregisterStrategy(String strategyCode) {
+        List<String> keys = strategyTriggerKeys.remove(strategyCode);
+        if (keys == null || keys.isEmpty()) {
+            log.debug("策略[{}]没有注册的触发器", strategyCode);
+            return;
+        }
+
+        for (String key : keys) {
+            if (key.startsWith("ATTR:")) {
+                // 属性触发器
+                String triggerKey = key.substring(5); // 去掉 "ATTR:" 前缀
+                List<AttrTriggerConfig> configList = attrTriggers.get(triggerKey);
+                if (configList != null) {
+                    configList.removeIf(c -> strategyCode.equals(c.strategyCode));
+                    if (configList.isEmpty()) {
+                        attrTriggers.remove(triggerKey);
+                    }
+                }
+            }
+            else if (key.startsWith("EVENT:")) {
+                // 事件触发器
+                String triggerKey = key.substring(6); // 去掉 "EVENT:" 前缀
+                List<EventTriggerConfig> configList = eventTriggers.get(triggerKey);
+                if (configList != null) {
+                    configList.removeIf(c -> strategyCode.equals(c.strategyCode));
+                    if (configList.isEmpty()) {
+                        eventTriggers.remove(triggerKey);
+                    }
+                }
+            }
+        }
+
+        log.debug("注销策略[{}]的所有触发器,共{}个", strategyCode, keys.size());
+    }
+
+    // ==================== 触发器处理方法 ====================
+
+    /**
+     * 处理属性变化事件
+     * 由 Controller 的 /onAttrValueChanged 接口调用
+     *
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param oldValue 旧值(可为null)
+     * @param newValue 新值
+     * @return 触发的策略数量
+     */
+    public int handleAttrChange(String objCode, String attrKey, Object oldValue, Object newValue) {
+        String triggerKey = objCode + ":" + attrKey;
+        List<AttrTriggerConfig> configList = attrTriggers.get(triggerKey);
+
+        if (configList == null || configList.isEmpty()) {
+            log.debug("无匹配的属性触发器: {}", triggerKey);
+            return 0;
+        }
+
+        log.info("属性变化触发检查: obj={}, attr={}, {} -> {}", objCode, attrKey, oldValue, newValue);
+
+        int triggeredCount = 0;
+
+        // 遍历所有匹配的触发器
+        for (AttrTriggerConfig config : configList) {
+            try {
+                boolean triggered = checkAndExecuteAttrTrigger(config, oldValue, newValue);
+                if (triggered) {
+                    triggeredCount++;
+                }
+            }
+            catch (Exception e) {
+                log.error("属性触发器执行异常: strategy={}", config.strategyCode, e);
+            }
+        }
+
+        return triggeredCount;
+    }
+
+    /**
+     * 处理设备事件
+     *
+     * @param objCode   设备代码
+     * @param eventKey  事件键
+     * @param eventData 事件数据
+     * @return 触发的策略数量
+     */
+    public int handleDeviceEvent(String objCode, String eventKey, Map<String, Object> eventData) {
+        String triggerKey = objCode + ":" + eventKey;
+        List<EventTriggerConfig> configList = eventTriggers.get(triggerKey);
+
+        if (configList == null || configList.isEmpty()) {
+            log.debug("无匹配的事件触发器: {}", triggerKey);
+            return 0;
+        }
+
+        log.info("设备事件触发检查: obj={}, event={}", objCode, eventKey);
+
+        int triggeredCount = 0;
+
+        for (EventTriggerConfig config : configList) {
+            try {
+                boolean triggered = checkAndExecuteEventTrigger(config, eventData);
+                if (triggered) {
+                    triggeredCount++;
+                }
+            }
+            catch (Exception e) {
+                log.error("事件触发器执行异常: strategy={}", config.strategyCode, e);
+            }
+        }
+
+        return triggeredCount;
+    }
+
+    /**
+     * 检查属性触发条件并执行策略
+     * 修复:完善触发信息传递
+     *
+     * @return 是否触发执行
+     */
+    private boolean checkAndExecuteAttrTrigger(AttrTriggerConfig config, Object oldValue, Object newValue) {
+        // 构建条件评估上下文
+        Map<String, Object> context = new HashMap<>();
+        context.put("oldValue", oldValue);
+        context.put("newValue", newValue);
+        context.put(config.attrKey, newValue);
+
+        // 评估条件
+        boolean shouldTrigger = true;
+        if (config.conditionExpr != null && !config.conditionExpr.trim().isEmpty()) {
+            shouldTrigger = conditionEvaluator.evaluate(config.conditionExpr, context);
+        }
+
+        if (shouldTrigger) {
+            log.info("属性触发条件满足,执行策略: strategy={}, obj={}.{}, value={}",
+                config.strategyCode, config.objCode, config.attrKey, newValue);
+
+            // 异步执行策略
+            asyncExecutor.submit(() -> {
+                try {
+                    Map<String, Object> params = new HashMap<>();
+
+                    // 触发类型和触发源
+                    params.put("trigger_type", "ATTR");
+                    params.put("trigger_source", config.objCode + "." + config.attrKey);
+
+                    // 执行人(属性变化为系统自动触发)
+                    params.put("exec_by", "SYSTEM_ATTR_CHANGE");
+
+                    // 属性变化信息
+                    params.put("old_value", oldValue);
+                    params.put("new_value", newValue);
+                    params.put("device_code", config.objCode);
+                    params.put("attr_key", config.attrKey);
+
+                    // 方便上下文变量访问
+                    params.put(config.attrKey, newValue);
+                    params.put("current_" + config.attrKey, newValue);
+
+                    getStrategyExecutor().executeStrategy(config.strategyCode, params);
+                } catch (Exception e) {
+                    log.error("策略异步执行失败: strategy={}", config.strategyCode, e);
+                }
+            });
+
+            return true;
+        } else {
+            log.debug("属性触发条件不满足: strategy={}, condition={}", config.strategyCode, config.conditionExpr);
+            return false;
+        }
+    }
+
+    /**
+     * 检查事件触发条件并执行策略
+     * 修复:完善触发信息传递
+     *
+     * @return 是否触发执行
+     */
+    private boolean checkAndExecuteEventTrigger(EventTriggerConfig config, Map<String, Object> eventData) {
+        // 评估条件
+        boolean shouldTrigger = true;
+        if (config.conditionExpr != null && !config.conditionExpr.trim().isEmpty()) {
+            Map<String, Object> context = eventData != null ? eventData : new HashMap<>();
+            shouldTrigger = conditionEvaluator.evaluate(config.conditionExpr, context);
+        }
+
+        if (shouldTrigger) {
+            log.info("事件触发条件满足,执行策略: strategy={}, obj={}.{}",
+                config.strategyCode, config.objCode, config.eventKey);
+
+            // 异步执行策略
+            asyncExecutor.submit(() -> {
+                try {
+                    Map<String, Object> params = new HashMap<>();
+                    if (eventData != null) {
+                        params.putAll(eventData);
+                    }
+
+                    // 触发类型和触发源
+                    params.put("trigger_type", "EVENT");
+                    params.put("trigger_source", config.objCode + "." + config.eventKey);
+
+                    // 执行人(事件触发为系统自动执行)
+                    params.put("exec_by", "SYSTEM_EVENT");
+
+                    // 事件信息
+                    params.put("device_code", config.objCode);
+                    params.put("event_key", config.eventKey);
+
+                    getStrategyExecutor().executeStrategy(config.strategyCode, params);
+                } catch (Exception e) {
+                    log.error("策略异步执行失败: strategy={}", config.strategyCode, e);
+                }
+            });
+
+            return true;
+        } else {
+            log.debug("事件触发条件不满足: strategy={}, condition={}", config.strategyCode, config.conditionExpr);
+            return false;
+        }
+    }
+
+    // ==================== 统计和调试方法 ====================
+
+    /**
+     * 获取已注册的属性触发器数量
+     */
+    public int getAttrTriggerCount() {
+        int count = 0;
+        for (List<AttrTriggerConfig> list : attrTriggers.values()) {
+            count += list.size();
+        }
+        return count;
+    }
+
+    /**
+     * 获取已注册的事件触发器数量
+     */
+    public int getEventTriggerCount() {
+        int count = 0;
+        for (List<EventTriggerConfig> list : eventTriggers.values()) {
+            count += list.size();
+        }
+        return count;
+    }
+
+    /**
+     * 获取所有已注册的触发器信息(用于调试)
+     */
+    public Map<String, Object> getRegisteredTriggers() {
+        Map<String, Object> result = new HashMap<>();
+
+        // 属性触发器列表
+        List<String> attrTriggerList = new ArrayList<>(attrTriggers.keySet());
+        result.put("attrTriggers", attrTriggerList);
+
+        // 事件触发器列表
+        List<String> eventTriggerList = new ArrayList<>(eventTriggers.keySet());
+        result.put("eventTriggers", eventTriggerList);
+
+        // 数量统计
+        result.put("attrTriggerCount", getAttrTriggerCount());
+        result.put("eventTriggerCount", getEventTriggerCount());
+
+        // 策略与触发器的映射关系
+        Map<String, List<String>> strategyMappings = new HashMap<>(strategyTriggerKeys);
+        result.put("strategyMappings", strategyMappings);
+
+        return result;
+    }
+
+    /**
+     * 检查某个属性是否有触发器监听
+     */
+    public boolean hasAttrTrigger(String objCode, String attrKey) {
+        String triggerKey = objCode + ":" + attrKey;
+        List<AttrTriggerConfig> list = attrTriggers.get(triggerKey);
+        return list != null && !list.isEmpty();
+    }
+
+    /**
+     * 检查某个事件是否有触发器监听
+     */
+    public boolean hasEventTrigger(String objCode, String eventKey) {
+        String triggerKey = objCode + ":" + eventKey;
+        List<EventTriggerConfig> list = eventTriggers.get(triggerKey);
+        return list != null && !list.isEmpty();
+    }
+
+    // ==================== 内部配置类 ====================
+
+    /**
+     * 属性触发器配置
+     */
+    private static class AttrTriggerConfig {
+        String strategyCode;   // 策略代码
+
+        String objCode;        // 设备代码
+
+        String attrKey;        // 属性键
+
+        String conditionExpr;  // 条件表达式
+    }
+
+    /**
+     * 事件触发器配置
+     */
+    private static class EventTriggerConfig {
+        String strategyCode;   // 策略代码
+
+        String objCode;        // 设备代码
+
+        String eventKey;       // 事件键
+
+        String conditionExpr;  // 条件表达式
+    }
+}

+ 88 - 22
ems/ems-core/src/main/java/com/ruoyi/ems/domain/StrategyExecutionContext.java

@@ -1,13 +1,3 @@
-/*
- * 文 件 名:  StrategyExecutionContext
- * 版    权:  华设设计集团股份有限公司
- * 描    述:  <描述>
- * 修 改 人:  lvwenbin
- * 修改时间:  2025/11/27
- * 跟踪单号:  <跟踪单号>
- * 修改单号:  <修改单号>
- * 修改内容:  <修改内容>
- */
 package com.ruoyi.ems.domain;
 
 import lombok.Data;
@@ -16,37 +6,113 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * StrategyExecutionContext
- * <功能详细描述>
- *
- * @author lvwenbin
- * @version [版本号, 2025/11/27]
- * @see [相关类/方法]
- * @since [产品/模块版本]
+ * 策略执行上下文
+ * 存储执行过程中的变量、步骤结果等信息
  */
 @Data
 public class StrategyExecutionContext {
+
+    /**
+     * 执行ID
+     */
     private String execId;
+
+    /**
+     * 策略代码
+     */
     private String strategyCode;
+
+    /**
+     * 开始时间戳
+     */
+    private long startTime;
+
+    /**
+     * 触发类型
+     */
     private String triggerType;
+
+    /**
+     * 触发源
+     */
     private String triggerSource;
+
+    /**
+     * 执行人
+     */
+    private String execBy;
+
+    /**
+     * 上下文变量
+     */
     private Map<String, Object> variables = new HashMap<>();
+
+    /**
+     * 步骤执行结果
+     */
     private Map<String, Object> stepResults = new HashMap<>();
-    private Long startTime;
 
+    /**
+     * 获取触发类型
+     * 优先从专用字段获取,其次从 variables 中获取
+     */
+    public String getTriggerType() {
+        if (this.triggerType != null && !this.triggerType.isEmpty()) {
+            return this.triggerType;
+        }
+        Object value = variables.get("trigger_type");
+        return value != null ? value.toString() : null;
+    }
+
+    /**
+     * 获取触发源
+     * 优先从专用字段获取,其次从 variables 中获取
+     */
+    public String getTriggerSource() {
+        if (this.triggerSource != null && !this.triggerSource.isEmpty()) {
+            return this.triggerSource;
+        }
+        Object value = variables.get("trigger_source");
+        return value != null ? value.toString() : null;
+    }
+
+    /**
+     * 获取执行人
+     * 优先从专用字段获取,其次从 variables 中获取
+     */
+    public String getExecBy() {
+        if (this.execBy != null && !this.execBy.isEmpty()) {
+            return this.execBy;
+        }
+        Object value = variables.get("exec_by");
+        return value != null ? value.toString() : null;
+    }
+
+    /**
+     * 设置变量
+     */
     public void setVariable(String key, Object value) {
-        variables.put(key, value);
+        this.variables.put(key, value);
     }
 
+    /**
+     * 获取变量
+     */
     public Object getVariable(String key) {
-        return variables.get(key);
+        return this.variables.get(key);
     }
 
+    /**
+     * 设置步骤结果
+     */
     public void setStepResult(String stepCode, Object result) {
-        stepResults.put(stepCode, result);
+        this.stepResults.put(stepCode, result);
     }
 
+    /**
+     * 获取步骤结果
+     */
     public Object getStepResult(String stepCode) {
-        return stepResults.get(stepCode);
+        return this.stepResults.get(stepCode);
     }
 }

+ 0 - 58
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/AttrValueChangeInterceptor.java

@@ -1,58 +0,0 @@
-package com.ruoyi.ems.strategy;
-
-import com.ruoyi.ems.strategy.listener.StrategyTriggerListener;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-/**
- * 属性值变化拦截器
- * ✅ 放置在采集更新模块中
- */
-@Slf4j
-@Component
-public class AttrValueChangeInterceptor {
-
-    @Autowired(required = false)
-    private StrategyTriggerListener triggerListener;
-
-    /**
-     * 属性值更新后的回调
-     *
-     * @param objCode 对象代码
-     * @param modelCode 模型代码
-     * @param attrKey 属性键
-     * @param oldValue 旧值
-     * @param newValue 新值
-     */
-    public void onAttrValueChanged(String objCode, String modelCode,
-        String attrKey, Object oldValue, Object newValue) {
-
-        // 触发策略监听器
-        if (triggerListener != null) {
-            try {
-                triggerListener.handleAttrChange(objCode, attrKey, oldValue, newValue);
-            } catch (Exception e) {
-                log.error("触发策略监听器失败: obj={}, attr={}", objCode, attrKey, e);
-            }
-        }
-    }
-
-    /**
-     * 判断值是否相等(处理null和数值类型)
-     */
-    private boolean isValueEqual(Object oldValue, Object newValue) {
-        if (oldValue == null && newValue == null) {
-            return true;
-        }
-        if (oldValue == null || newValue == null) {
-            return false;
-        }
-
-        // 数值类型统一转为字符串比较(去除小数点后的0)
-        String oldStr = String.valueOf(oldValue).replaceAll("\\.0+$", "");
-        String newStr = String.valueOf(newValue).replaceAll("\\.0+$", "");
-
-        return oldStr.equals(newStr);
-    }
-}

+ 623 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/PollingMonitorService.java

@@ -0,0 +1,623 @@
+/*
+ * 文 件 名:  PollingMonitorService
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/11
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.strategy;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.domain.OpEnergyStrategy;
+import com.ruoyi.ems.domain.OpEnergyStrategyTrigger;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.service.IAbilityCallService;
+import com.ruoyi.ems.service.IEmsObjAttrValueService;
+import com.ruoyi.ems.service.IOpEnergyStrategyService;
+import com.ruoyi.ems.service.IOpEnergyStrategyTriggerService;
+import com.ruoyi.ems.strategy.evaluator.ConditionEvaluator;
+import com.ruoyi.ems.strategy.executor.StrategyExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 轮询监控服务
+ * <功能详细描述>
+ * 与属性变化触发的区别:
+ * - 属性变化触发:被动等待采集模块推送变化
+ * - 轮询监控触发:主动定期查询并判断
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/11]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Slf4j
+@Component
+public class PollingMonitorService {
+
+    @Autowired
+    private IOpEnergyStrategyService strategyService;
+
+    @Autowired
+    private IOpEnergyStrategyTriggerService triggerService;
+
+    @Autowired
+    private IEmsObjAttrValueService attrValueService;
+
+    @Autowired
+    private ConditionEvaluator conditionEvaluator;
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    /**
+     * 能力调用服务 - 用于下发主动查询指令
+     * 复用 AbilityCallStepExecutor 中使用的服务
+     */
+    @Autowired
+    private IAbilityCallService abilityCallService;
+
+    /**
+     * 延迟加载 StrategyExecutor 避免循环依赖
+     */
+    private StrategyExecutor strategyExecutor;
+
+    /**
+     * 轮询任务调度器
+     */
+    private final ScheduledExecutorService pollingScheduler = Executors.newScheduledThreadPool(5);
+
+    /**
+     * 已注册的轮询任务
+     * key: strategyCode
+     * value: ScheduledFuture
+     */
+    private final Map<String, ScheduledFuture<?>> pollingTasks = new ConcurrentHashMap<>();
+
+    /**
+     * 轮询配置缓存
+     * key: strategyCode
+     * value: PollingConfig
+     */
+    private final Map<String, PollingConfig> pollingConfigs = new ConcurrentHashMap<>();
+
+    /**
+     * 上次属性值缓存(用于判断是否变化)
+     * key: strategyCode + ":" + deviceCode + ":" + attrKey
+     * value: 上次的属性值
+     */
+    private final Map<String, String> lastAttrValues = new ConcurrentHashMap<>();
+
+    /**
+     * 获取 StrategyExecutor(延迟加载)
+     */
+    private StrategyExecutor getStrategyExecutor() {
+        if (strategyExecutor == null) {
+            synchronized (this) {
+                if (strategyExecutor == null) {
+                    strategyExecutor = applicationContext.getBean(StrategyExecutor.class);
+                }
+            }
+        }
+        return strategyExecutor;
+    }
+
+    @PreDestroy
+    public void destroy() {
+        log.info("====== 轮询监控服务销毁 ======");
+        pollingTasks.values().forEach(future -> future.cancel(true));
+        pollingTasks.clear();
+        pollingConfigs.clear();
+        pollingScheduler.shutdown();
+    }
+
+    // ==================== 注册与注销 ====================
+
+    /**
+     * 注册轮询监控策略
+     *
+     * @param strategy 策略对象
+     */
+    public void registerPollingStrategy(OpEnergyStrategy strategy) {
+        String strategyCode = strategy.getStrategyCode();
+
+        // 获取策略的触发器配置
+        List<OpEnergyStrategyTrigger> triggers = triggerService.selectByStrategyCode(strategyCode);
+
+        // 查找轮询类型的触发器
+        OpEnergyStrategyTrigger pollingTrigger = null;
+        for (OpEnergyStrategyTrigger trigger : triggers) {
+            if (trigger.getEnable() == 1 && "POLLING".equals(trigger.getTriggerType())) {
+                pollingTrigger = trigger;
+                break;
+            }
+        }
+
+        // 如果没有专门的轮询触发器,检查是否是条件触发(4)且配置了轮询
+        if (pollingTrigger == null && strategy.getTriggerType() == 4) {
+            // 从触发器中查找 ATTR 类型,并检查是否配置了轮询
+            for (OpEnergyStrategyTrigger trigger : triggers) {
+                if (trigger.getEnable() == 1 && "ATTR".equals(trigger.getTriggerType())) {
+                    // 检查条件表达式中是否配置了轮询
+                    PollingConfig config = parsePollingConfig(trigger);
+                    if (config != null && config.pollingEnabled) {
+                        pollingTrigger = trigger;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (pollingTrigger == null) {
+            log.debug("策略[{}]未配置轮询触发器", strategyCode);
+            return;
+        }
+
+        // 解析轮询配置
+        PollingConfig config = parsePollingConfig(pollingTrigger);
+        if (config == null || !config.pollingEnabled) {
+            log.warn("策略[{}]轮询配置解析失败或未启用", strategyCode);
+            return;
+        }
+
+        // 先取消已有的轮询任务
+        unregisterPollingStrategy(strategyCode);
+
+        // 保存配置
+        pollingConfigs.put(strategyCode, config);
+
+        // 创建轮询任务
+        ScheduledFuture<?> future = pollingScheduler.scheduleWithFixedDelay(
+            () -> executePollingCheck(strategyCode, config), config.initialDelay, config.pollingInterval,
+            TimeUnit.MILLISECONDS);
+
+        pollingTasks.put(strategyCode, future);
+
+        log.info("策略[{}]已注册轮询监控: device={}, attr={}, interval={}ms, activeQuery={}, queryAbility={}",
+            strategyCode, config.deviceCode, config.attrKey, config.pollingInterval, config.activeQuery,
+            config.queryAbilityKey);
+    }
+
+    /**
+     * 注销轮询监控策略
+     */
+    public void unregisterPollingStrategy(String strategyCode) {
+        ScheduledFuture<?> future = pollingTasks.remove(strategyCode);
+        if (future != null) {
+            future.cancel(true);
+            log.info("策略[{}]的轮询监控已注销", strategyCode);
+        }
+        pollingConfigs.remove(strategyCode);
+
+        // 清理相关的缓存
+        lastAttrValues.entrySet().removeIf(entry -> entry.getKey().startsWith(strategyCode + ":"));
+    }
+
+    /**
+     * 刷新策略的轮询配置
+     */
+    public void refreshPollingStrategy(String strategyCode) {
+        OpEnergyStrategy strategy = strategyService.selectStrategyByCode(strategyCode);
+        if (strategy == null || strategy.getStrategyState() != 1) {
+            unregisterPollingStrategy(strategyCode);
+            return;
+        }
+
+        registerPollingStrategy(strategy);
+    }
+
+    // ==================== 轮询执行 ====================
+
+    /**
+     * 执行轮询检查
+     */
+    private void executePollingCheck(String strategyCode, PollingConfig config) {
+        try {
+            log.debug("执行轮询检查: strategy={}, device={}, attr={}", strategyCode, config.deviceCode, config.attrKey);
+
+            // 1. 如果配置了主动查询,先下发查询指令
+            if (config.activeQuery) {
+                boolean querySuccess = executeActiveQuery(config);
+                if (!querySuccess) {
+                    log.warn("策略[{}]主动查询指令下发失败", strategyCode);
+                    // 可以选择继续读取缓存值或跳过本次检查
+                }
+                // 等待设备响应
+                Thread.sleep(config.queryWaitTime);
+            }
+
+            // 2. 从数据库读取最新属性值
+            EmsObjAttrValue attrValue = attrValueService.selectObjAttrValue(config.modelCode, config.deviceCode,
+                config.attrKey);
+
+            if (attrValue == null) {
+                log.debug("策略[{}]轮询: 属性值不存在 device={}, attr={}", strategyCode, config.deviceCode,
+                    config.attrKey);
+                return;
+            }
+
+            String currentValue = attrValue.getAttrValue();
+            String cacheKey = strategyCode + ":" + config.deviceCode + ":" + config.attrKey;
+            String lastValue = lastAttrValues.get(cacheKey);
+
+            log.debug("策略[{}]轮询检查: attr={}, lastValue={}, currentValue={}", strategyCode, config.attrKey,
+                lastValue, currentValue);
+
+            // 3. 判断是否满足触发条件
+            boolean shouldTrigger = evaluateTriggerCondition(config, currentValue, lastValue);
+
+            // 4. 更新缓存
+            lastAttrValues.put(cacheKey, currentValue);
+
+            // 5. 如果满足条件,触发策略执行
+            if (shouldTrigger) {
+                log.info("策略[{}]轮询触发条件满足: device={}, attr={}, value={}", strategyCode, config.deviceCode,
+                    config.attrKey, currentValue);
+
+                triggerStrategyExecution(strategyCode, config, currentValue, lastValue);
+            }
+
+        }
+        catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("策略[{}]轮询被中断", strategyCode);
+        }
+        catch (Exception e) {
+            log.error("策略[{}]轮询检查异常", strategyCode, e);
+        }
+    }
+
+    /**
+     * 评估触发条件
+     */
+    private boolean evaluateTriggerCondition(PollingConfig config, String currentValue, String lastValue) {
+        // 构建条件评估上下文
+        Map<String, Object> context = new HashMap<>();
+        context.put(config.attrKey, currentValue);
+        context.put("current_" + config.attrKey, currentValue);
+        context.put("newValue", currentValue);
+        context.put("oldValue", lastValue);
+
+        // 使用条件求值器评估
+        boolean conditionMet = conditionEvaluator.evaluate(config.conditionExpr, context);
+
+        // 根据触发模式判断是否真正触发
+        switch (config.triggerMode) {
+            case "ALWAYS":
+                // 只要条件满足就触发
+                return conditionMet;
+
+            case "ON_CHANGE":
+                // 条件满足且值发生变化才触发
+                return conditionMet && !currentValue.equals(lastValue);
+
+            case "ON_FIRST_MATCH":
+                // 条件满足且上次不满足才触发(边沿触发)
+                if (!conditionMet) {
+                    return false;
+                }
+                // 检查上次是否满足条件
+                if (lastValue == null) {
+                    return true;
+                }
+                Map<String, Object> lastContext = new HashMap<>();
+                lastContext.put(config.attrKey, lastValue);
+                boolean lastConditionMet = conditionEvaluator.evaluate(config.conditionExpr, lastContext);
+                return !lastConditionMet;
+
+            default:
+                return conditionMet;
+        }
+    }
+
+    /**
+     * 执行主动查询(下发查询指令)
+     * 复用 AbilityCallStepExecutor 中的能力调用逻辑:
+     * 1. 构建 AbilityPayload 对象
+     * 2. 调用 IAbilityCallService.devAbilityCall() 方法
+     * 3. 设备会通过 MQTT 下发查询指令
+     * 4. 设备回包后,由采集模块(如 Keka86BsHandler)更新数据库
+     *
+     * @param config 轮询配置(包含查询能力信息)
+     * @return 是否下发成功
+     */
+    private boolean executeActiveQuery(PollingConfig config) {
+        try {
+            // 检查是否配置了查询能力
+            if (config.queryAbilityKey == null || config.queryAbilityKey.isEmpty()) {
+                log.warn("策略[{}]未配置查询能力(queryAbilityKey)", config.strategyCode);
+                return false;
+            }
+
+            log.debug("下发主动查询指令: device={}, ability={}, param={}", config.deviceCode, config.queryAbilityKey,
+                config.queryAbilityParam);
+
+            // 构建能力调用参数(与 AbilityCallStepExecutor 相同)
+            AbilityPayload payload = new AbilityPayload();
+            payload.setObjCode(config.deviceCode);          // 目标设备编码
+            payload.setObjType(2);                           // 2 = 设备类型
+            payload.setModelCode(config.modelCode);          // 物模型编码
+            payload.setAbilityKey(config.queryAbilityKey);   // 能力键(如 syncState)
+
+            // 设置能力参数(如果有)
+            if (config.queryAbilityParam != null && !config.queryAbilityParam.isEmpty()) {
+                payload.setAbilityParam(config.queryAbilityParam);
+            }
+
+            // 调用设备能力服务(复用 AbilityCallStepExecutor 的逻辑)
+            // 这里调用的是 IAbilityCallService.devAbilityCall()
+            // 内部会通过 MQTT 下发指令到网关
+            abilityCallService.devAbilityCall(payload);
+
+            log.debug("主动查询指令下发成功: device={}, ability={}", config.deviceCode, config.queryAbilityKey);
+
+            return true;
+
+        }
+        catch (Exception e) {
+            log.error("主动查询指令下发失败: device={}, ability={}", config.deviceCode, config.queryAbilityKey, e);
+            return false;
+        }
+    }
+
+    /**
+     * 触发策略执行
+     * 修复:完善触发信息传递
+     */
+    private void triggerStrategyExecution(String strategyCode, PollingConfig config, String currentValue, String lastValue) {
+        try {
+            Map<String, Object> params = new HashMap<>();
+
+            // 触发类型和触发源
+            params.put("trigger_type", "POLLING");
+            params.put("trigger_source", config.deviceCode + "." + config.attrKey);
+
+            // 执行人(轮询监控为系统自动执行)
+            params.put("exec_by", "SYSTEM_POLLING");
+
+            // 设备和属性信息
+            params.put("device_code", config.deviceCode);
+            params.put("model_code", config.modelCode);
+            params.put("attr_key", config.attrKey);
+            params.put("current_value", currentValue);
+            params.put("old_value", lastValue);
+
+            // 方便上下文变量访问
+            params.put(config.attrKey, currentValue);
+            params.put("current_" + config.attrKey, currentValue);
+
+            // 轮询配置信息(用于调试)
+            params.put("polling_interval", config.pollingInterval);
+            params.put("trigger_mode", config.triggerMode);
+
+            log.info("轮询触发策略执行: strategy={}, device={}.{}, value={} -> {}",
+                strategyCode, config.deviceCode, config.attrKey, lastValue, currentValue);
+
+            getStrategyExecutor().executeStrategy(strategyCode, params);
+
+        } catch (Exception e) {
+            log.error("策略[{}]轮询触发执行失败", strategyCode, e);
+        }
+    }
+
+    // ==================== 配置解析 ====================
+
+    /**
+     * 解析轮询配置
+     * 支持的配置格式(在 conditionExpr 中):
+     * {
+     * "polling": {
+     * "enabled": true,
+     * "interval": 2000,
+     * "activeQuery": true,
+     * "queryWaitTime": 500,
+     * "triggerMode": "ON_FIRST_MATCH",
+     * "initialDelay": 1000,
+     * "queryAbility": {
+     * "abilityKey": "syncState",
+     * "abilityParam": ""
+     * }
+     * },
+     * "left": "Switch",
+     * "op": "==",
+     * "right": "1"
+     * }
+     */
+    private PollingConfig parsePollingConfig(OpEnergyStrategyTrigger trigger) {
+        PollingConfig config = new PollingConfig();
+
+        config.strategyCode = trigger.getStrategyCode();
+        config.deviceCode = trigger.getSourceObjCode();
+        config.modelCode = trigger.getSourceModelCode();
+        config.attrKey = trigger.getAttrKey();
+        config.conditionExpr = trigger.getConditionExpr();
+
+        // 解析条件表达式中的轮询配置
+        if (trigger.getConditionExpr() != null && !trigger.getConditionExpr().isEmpty()) {
+            try {
+                JSONObject conditionJson = JSON.parseObject(trigger.getConditionExpr());
+
+                // 检查是否有 polling 配置
+                JSONObject pollingJson = conditionJson.getJSONObject("polling");
+                if (pollingJson != null) {
+                    config.pollingEnabled = pollingJson.getBooleanValue("enabled");
+                    config.pollingInterval = pollingJson.getIntValue("interval");
+                    if (config.pollingInterval <= 0) {
+                        config.pollingInterval = 2000; // 默认2秒
+                    }
+                    config.activeQuery = pollingJson.getBooleanValue("activeQuery");
+                    config.queryWaitTime = pollingJson.getIntValue("queryWaitTime");
+                    if (config.queryWaitTime <= 0) {
+                        config.queryWaitTime = 500; // 默认500ms
+                    }
+                    config.triggerMode = pollingJson.getString("triggerMode");
+                    if (config.triggerMode == null) {
+                        config.triggerMode = "ON_FIRST_MATCH";
+                    }
+                    config.initialDelay = pollingJson.getIntValue("initialDelay");
+                    if (config.initialDelay <= 0) {
+                        config.initialDelay = 1000; // 默认1秒后开始
+                    }
+
+                    // 解析查询能力配置
+                    JSONObject queryAbilityJson = pollingJson.getJSONObject("queryAbility");
+                    if (queryAbilityJson != null) {
+                        config.queryAbilityKey = queryAbilityJson.getString("abilityKey");
+                        config.queryAbilityParam = queryAbilityJson.getString("abilityParam");
+                    }
+                    else {
+                        // 兼容旧配置:直接使用 abilityKey
+                        config.queryAbilityKey = pollingJson.getString("queryAbilityKey");
+                        config.queryAbilityParam = pollingJson.getString("queryAbilityParam");
+                    }
+
+                    // 如果开启了主动查询但没有配置能力,使用默认的 syncState
+                    if (config.activeQuery && (config.queryAbilityKey == null || config.queryAbilityKey.isEmpty())) {
+                        config.queryAbilityKey = "syncState";
+                        config.queryAbilityParam = "";
+                        log.info("策略[{}]未配置查询能力,使用默认能力: syncState", config.strategyCode);
+                    }
+
+                }
+                else {
+                    // 兼容旧格式:如果没有 polling 配置,使用默认值
+                    config.pollingEnabled = true;
+                    config.pollingInterval = 2000;
+                    config.activeQuery = false;
+                    config.queryWaitTime = 500;
+                    config.triggerMode = "ON_FIRST_MATCH";
+                    config.initialDelay = 1000;
+                }
+
+            }
+            catch (Exception e) {
+                log.warn("解析轮询配置失败: {}", trigger.getConditionExpr(), e);
+                // 使用默认配置
+                config.pollingEnabled = true;
+                config.pollingInterval = 2000;
+                config.triggerMode = "ON_FIRST_MATCH";
+            }
+        }
+
+        return config;
+    }
+
+    // ==================== 统计方法 ====================
+
+    /**
+     * 获取已注册的轮询任务数量
+     */
+    public int getPollingTaskCount() {
+        return pollingTasks.size();
+    }
+
+    /**
+     * 获取轮询任务详情
+     */
+    public Map<String, Object> getPollingStatus() {
+        Map<String, Object> status = new HashMap<>();
+        status.put("taskCount", pollingTasks.size());
+        status.put("registeredStrategies", pollingConfigs.keySet());
+
+        Map<String, Object> configDetails = new HashMap<>();
+        for (Map.Entry<String, PollingConfig> entry : pollingConfigs.entrySet()) {
+            PollingConfig config = entry.getValue();
+            Map<String, Object> detail = new HashMap<>();
+            detail.put("deviceCode", config.deviceCode);
+            detail.put("modelCode", config.modelCode);
+            detail.put("attrKey", config.attrKey);
+            detail.put("interval", config.pollingInterval);
+            detail.put("triggerMode", config.triggerMode);
+            detail.put("activeQuery", config.activeQuery);
+            detail.put("queryAbilityKey", config.queryAbilityKey);
+            detail.put("queryAbilityParam", config.queryAbilityParam);
+            configDetails.put(entry.getKey(), detail);
+        }
+        status.put("configs", configDetails);
+
+        return status;
+    }
+
+    /**
+     * 检查某策略是否有轮询任务
+     */
+    public boolean hasPollingTask(String strategyCode) {
+        return pollingTasks.containsKey(strategyCode);
+    }
+
+    /**
+     * 手动触发一次轮询检查(用于测试)
+     */
+    public void triggerPollingCheck(String strategyCode) {
+        PollingConfig config = pollingConfigs.get(strategyCode);
+        if (config != null) {
+            log.info("手动触发轮询检查: strategy={}", strategyCode);
+            executePollingCheck(strategyCode, config);
+        }
+        else {
+            log.warn("策略[{}]未注册轮询监控", strategyCode);
+        }
+    }
+
+    // ==================== 内部类 ====================
+
+    /**
+     * 轮询配置
+     */
+    private static class PollingConfig {
+        String strategyCode;      // 策略代码
+
+        String deviceCode;        // 设备代码
+
+        String modelCode;         // 模型代码
+
+        String attrKey;           // 属性键
+
+        String conditionExpr;     // 条件表达式
+
+        boolean pollingEnabled;   // 是否启用轮询
+
+        int pollingInterval;      // 轮询间隔(毫秒)
+
+        int initialDelay;         // 初始延迟(毫秒)
+
+        boolean activeQuery;      // 是否主动下发查询指令
+
+        int queryWaitTime;        // 查询等待时间(毫秒)
+
+        /**
+         * 查询能力配置
+         * 复用物模型中定义的能力(如 syncState)
+         * 与界面上"能力调用"步骤使用相同的机制
+         */
+        String queryAbilityKey;   // 查询能力键(如 syncState)
+
+        String queryAbilityParam; // 查询能力参数
+
+        /**
+         * 触发模式:
+         * - ALWAYS: 只要条件满足就触发
+         * - ON_CHANGE: 条件满足且值变化才触发
+         * - ON_FIRST_MATCH: 条件满足且上次不满足才触发(边沿触发)
+         */
+        String triggerMode;
+    }
+}

+ 26 - 22
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/evaluator/ConditionEvaluator.java

@@ -19,7 +19,7 @@ public class ConditionEvaluator {
      * 评估条件表达式
      *
      * @param conditionExpr 条件表达式JSON
-     * @param context 上下文变量
+     * @param context       上下文变量
      * @return 是否满足条件
      */
     public boolean evaluate(String conditionExpr, Map<String, Object> context) {
@@ -30,17 +30,17 @@ public class ConditionEvaluator {
         try {
             JSONObject condition = JSON.parseObject(conditionExpr);
 
-            // 支持简单格式: {"left":"Switch","op":"==","right":"1"}
+            // 支持简单格式: {"left":"Switch","op":"==","right":"1"}
             if (condition.containsKey("left") && condition.containsKey("op")) {
                 return evaluateSimple(condition, context);
             }
 
-            // 支持复杂格式: {"logic":"AND","conditions":[...]}
+            // 支持复杂格式: {"logic":"AND","conditions":[...]}
             if (condition.containsKey("logic")) {
                 return evaluateComplex(condition, context);
             }
 
-            // 支持旧格式: {"operator":"AND","conditions":[...]}
+            // 支持旧格式: {"operator":"AND","conditions":[...]}
             if (condition.containsKey("operator")) {
                 return evaluateLegacy(condition, context);
             }
@@ -48,7 +48,8 @@ public class ConditionEvaluator {
             log.warn("未识别的条件格式: {}", conditionExpr);
             return false;
 
-        } catch (Exception e) {
+        }
+        catch (Exception e) {
             log.error("条件表达式解析失败: {}", conditionExpr, e);
             return false;
         }
@@ -78,11 +79,10 @@ public class ConditionEvaluator {
         JSONArray conditions = condition.getJSONArray("conditions");
 
         if ("AND".equals(logic)) {
-            return conditions.stream()
-                .allMatch(c -> evaluateSimple((JSONObject) c, context));
-        } else if ("OR".equals(logic)) {
-            return conditions.stream()
-                .anyMatch(c -> evaluateSimple((JSONObject) c, context));
+            return conditions.stream().allMatch(c -> evaluateSimple((JSONObject) c, context));
+        }
+        else if ("OR".equals(logic)) {
+            return conditions.stream().anyMatch(c -> evaluateSimple((JSONObject) c, context));
         }
 
         return false;
@@ -97,12 +97,12 @@ public class ConditionEvaluator {
         JSONArray conditions = condition.getJSONArray("conditions");
 
         if ("AND".equals(operator)) {
-            return conditions.stream()
-                .allMatch(c -> evaluateCondition((JSONObject) c, context));
-        } else if ("OR".equals(operator)) {
-            return conditions.stream()
-                .anyMatch(c -> evaluateCondition((JSONObject) c, context));
-        } else {
+            return conditions.stream().allMatch(c -> evaluateCondition((JSONObject) c, context));
+        }
+        else if ("OR".equals(operator)) {
+            return conditions.stream().anyMatch(c -> evaluateCondition((JSONObject) c, context));
+        }
+        else {
             return evaluateSimpleCondition(condition, context);
         }
     }
@@ -112,7 +112,8 @@ public class ConditionEvaluator {
 
         if ("AND".equals(operator) || "OR".equals(operator)) {
             return evaluateLegacy(condition, context);
-        } else {
+        }
+        else {
             return evaluateSimpleCondition(condition, context);
         }
     }
@@ -129,7 +130,6 @@ public class ConditionEvaluator {
 
     /**
      * 从上下文获取值(支持嵌套路径)
-     *
      * 支持格式:
      * - "Switch" → context.get("Switch")
      * - "context.Switch" → context.get("Switch")
@@ -154,9 +154,11 @@ public class ConditionEvaluator {
             for (int i = 1; i < parts.length && current != null; i++) {
                 if (current instanceof Map) {
                     current = ((Map<?, ?>) current).get(parts[i]);
-                } else if (current instanceof JSONObject) {
+                }
+                else if (current instanceof JSONObject) {
                     current = ((JSONObject) current).get(parts[i]);
-                } else {
+                }
+                else {
                     return null;
                 }
             }
@@ -171,7 +173,8 @@ public class ConditionEvaluator {
      * 比较两个值
      */
     private boolean compare(Object actual, String operator, Object expected) {
-        if (actual == null) return false;
+        if (actual == null)
+            return false;
 
         // 数值类型统一转字符串比较(去除小数点后的0)
         String actualStr = String.valueOf(actual).replaceAll("\\.0+$", "");
@@ -222,7 +225,8 @@ public class ConditionEvaluator {
             double va = Double.parseDouble(String.valueOf(a));
             double vb = Double.parseDouble(String.valueOf(b));
             return Double.compare(va, vb);
-        } catch (NumberFormatException e) {
+        }
+        catch (NumberFormatException e) {
             log.warn("数值比较失败: {} vs {}", a, b);
             return 0;
         }

+ 1 - 1
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/AttrQueryStepExecutor.java

@@ -40,7 +40,7 @@ public class AttrQueryStepExecutor implements IStepExecutor {
 
         // 保存到上下文(使用步骤代码作为键)
         context.setVariable(step.getStepCode() + "_value", attrValue);
-        context.setVariable("current_" + attrKey, attrValue); // ✅ 也保存为通用键名
+        context.setVariable("current_" + attrKey, attrValue);
 
         log.info("属性查询成功: device={}, attr={}, value={}",
             deviceCode, attrKey, attrValue);

+ 265 - 27
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/LoopStepExecutor.java

@@ -1,7 +1,11 @@
 package com.ruoyi.ems.strategy.executor;
 
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
 import com.ruoyi.ems.domain.OpEnergyStrategyStep;
 import com.ruoyi.ems.domain.StrategyExecutionContext;
+import com.ruoyi.ems.service.IEmsObjAttrValueService;
 import com.ruoyi.ems.service.IOpEnergyStrategyStepService;
 import com.ruoyi.ems.service.IStepExecutor;
 import com.ruoyi.ems.strategy.evaluator.ConditionEvaluator;
@@ -16,7 +20,9 @@ import java.util.stream.Collectors;
 
 /**
  * 循环执行器
- * ✅ 使用 ApplicationContext 延迟获取 StepExecutorFactory,打破循环依赖
+ * 支持两种跳出条件模式:
+ * 1. ATTR - 属性监测模式:实时查询设备属性,满足条件时跳出
+ * 2. EXPR - 表达式模式:基于上下文变量判断(旧模式)
  */
 @Slf4j
 @Component
@@ -28,16 +34,14 @@ public class LoopStepExecutor implements IStepExecutor {
     @Autowired
     private ConditionEvaluator conditionEvaluator;
 
-    // ✅ 改为注入 ApplicationContext,延迟获取 StepExecutorFactory
     @Autowired
     private ApplicationContext applicationContext;
 
-    // ✅ 延迟获取的 StepExecutorFactory 引用
+    @Autowired(required = false)
+    private IEmsObjAttrValueService attrValueService;
+
     private StepExecutorFactory stepExecutorFactory;
 
-    /**
-     * 获取 StepExecutorFactory(延迟加载)
-     */
     private StepExecutorFactory getStepExecutorFactory() {
         if (stepExecutorFactory == null) {
             stepExecutorFactory = applicationContext.getBean(StepExecutorFactory.class);
@@ -49,10 +53,16 @@ public class LoopStepExecutor implements IStepExecutor {
     public Object execute(OpEnergyStrategyStep step, StrategyExecutionContext context) throws Exception {
         int maxCount = step.getLoopMaxCount() != null ? step.getLoopMaxCount() : 0;
         int interval = step.getLoopInterval() != null ? step.getLoopInterval() : 1000;
-        String breakCondition = step.getLoopCondition();
+        String loopCondition = step.getLoopCondition();
+
+        LoopBreakConfig breakConfig = parseBreakConfig(loopCondition);
 
-        log.info("循环步骤[{}]开始执行: maxCount={}, interval={}ms, condition={}",
-            step.getStepCode(), maxCount == 0 ? "无限" : maxCount, interval, breakCondition);
+        log.info("循环步骤[{}]开始执行: maxCount={}, interval={}ms, breakType={}, breakConfig={}",
+            step.getStepCode(),
+            maxCount == 0 ? "无限" : maxCount,
+            interval,
+            breakConfig.breakType,
+            breakConfig);
 
         // 加载子步骤
         List<OpEnergyStrategyStep> childSteps = stepService.selectStepsByParentCode(
@@ -69,6 +79,7 @@ public class LoopStepExecutor implements IStepExecutor {
 
         int loopCount = 0;
         boolean shouldBreak = false;
+        String breakReason = "";
 
         // 循环执行
         while (true) {
@@ -78,10 +89,10 @@ public class LoopStepExecutor implements IStepExecutor {
             // 执行所有子步骤
             for (OpEnergyStrategyStep childStep : childSteps) {
                 if (!executeChildStep(childStep, context)) {
-                    // 子步骤执行失败且不允许继续
                     log.error("循环步骤[{}]中的子步骤[{}]执行失败,终止循环",
                         step.getStepCode(), childStep.getStepCode());
                     shouldBreak = true;
+                    breakReason = "子步骤执行失败";
                     break;
                 }
             }
@@ -90,20 +101,19 @@ public class LoopStepExecutor implements IStepExecutor {
                 break;
             }
 
-            // 检查跳出条件
-            if (breakCondition != null && !breakCondition.trim().isEmpty()) {
-                boolean conditionMet = conditionEvaluator.evaluate(breakCondition, context.getVariables());
-                if (conditionMet) {
-                    log.info("循环步骤[{}]满足跳出条件,退出循环(共执行{}次)",
-                        step.getStepCode(), loopCount);
-                    break;
-                }
+            BreakCheckResult checkResult = checkBreakCondition(breakConfig, context);
+            if (checkResult.shouldBreak) {
+                log.info("循环步骤[{}]满足跳出条件,退出循环(共执行{}次)。原因: {}",
+                    step.getStepCode(), loopCount, checkResult.reason);
+                breakReason = checkResult.reason;
+                break;
             }
 
             // 检查最大循环次数
             if (maxCount > 0 && loopCount >= maxCount) {
                 log.info("循环步骤[{}]达到最大循环次数{},退出循环",
                     step.getStepCode(), maxCount);
+                breakReason = "达到最大循环次数";
                 break;
             }
 
@@ -113,13 +123,211 @@ public class LoopStepExecutor implements IStepExecutor {
             }
         }
 
-        log.info("循环步骤[{}]执行完成,共循环{}次", step.getStepCode(), loopCount);
-        return "循环完成,共执行" + loopCount + "次";
+        String resultMsg = String.format("循环完成,共执行%d次,退出原因: %s", loopCount, breakReason);
+        log.info("循环步骤[{}]执行完成: {}", step.getStepCode(), resultMsg);
+
+        return resultMsg;
+    }
+
+    /**
+     * 解析跳出条件配置
+     */
+    private LoopBreakConfig parseBreakConfig(String loopCondition) {
+        LoopBreakConfig config = new LoopBreakConfig();
+
+        if (loopCondition == null || loopCondition.trim().isEmpty()) {
+            config.breakType = "NONE";
+            return config;
+        }
+
+        try {
+            JSONObject condition = JSON.parseObject(loopCondition);
+
+            // 新格式:属性监测模式
+            if ("ATTR".equals(condition.getString("breakType"))) {
+                config.breakType = "ATTR";
+                config.deviceCode = condition.getString("deviceCode");
+                config.modelCode = condition.getString("modelCode");
+                config.attrKey = condition.getString("attrKey");
+                config.operator = condition.getString("operator");
+                config.value = condition.getString("value");
+
+                log.debug("解析到属性监测跳出条件: device={}, attr={}, op={}, value={}",
+                    config.deviceCode, config.attrKey, config.operator, config.value);
+            }
+            // 表达式模式
+            else if (condition.containsKey("left") || condition.containsKey("logic")) {
+                config.breakType = "EXPR";
+                config.expression = loopCondition;
+            }
+            else {
+                config.breakType = "EXPR";
+                config.expression = loopCondition;
+            }
+
+        } catch (Exception e) {
+            log.warn("解析跳出条件失败,使用表达式模式: {}", loopCondition);
+            config.breakType = "EXPR";
+            config.expression = loopCondition;
+        }
+
+        return config;
+    }
+
+    /**
+     * 检查跳出条件(核心方法)
+     */
+    private BreakCheckResult checkBreakCondition(LoopBreakConfig config, StrategyExecutionContext context) {
+        BreakCheckResult result = new BreakCheckResult();
+        result.shouldBreak = false;
+
+        if ("NONE".equals(config.breakType)) {
+            return result;
+        }
+
+        // 属性监测模式:实时查询设备属性
+        if ("ATTR".equals(config.breakType)) {
+            return checkAttrBreakCondition(config, context);
+        }
+
+        // 表达式模式:基于上下文变量判断
+        if ("EXPR".equals(config.breakType)) {
+            return checkExprBreakCondition(config, context);
+        }
+
+        return result;
+    }
+
+    /**
+     * 属性监测模式:实时查询设备属性并判断
+     */
+    private BreakCheckResult checkAttrBreakCondition(LoopBreakConfig config, StrategyExecutionContext context) {
+        BreakCheckResult result = new BreakCheckResult();
+        result.shouldBreak = false;
+
+        if (attrValueService == null) {
+            log.warn("属性值服务未注入,无法使用属性监测模式");
+            return result;
+        }
+
+        if (config.deviceCode == null || config.attrKey == null) {
+            log.warn("属性监测配置不完整: deviceCode={}, attrKey={}", config.deviceCode, config.attrKey);
+            return result;
+        }
+
+        try {
+            // 实时查询设备属性值
+            EmsObjAttrValue attrValue = attrValueService.selectObjAttrValue(
+                config.modelCode,
+                config.deviceCode,
+                config.attrKey
+            );
+
+            log.debug("实时查询属性值: device={}, attr={}, value={}",
+                config.deviceCode, config.attrKey, attrValue.getAttrValue());
+
+            // 将查询到的值同步到上下文(供子步骤使用)
+            String contextKey = "current_" + config.attrKey;
+            context.setVariable(contextKey, attrValue.getAttrValue());
+
+            // 比较属性值是否满足跳出条件
+            boolean conditionMet = compareValue(attrValue.getAttrValue(), config.operator, config.value);
+
+            if (conditionMet) {
+                result.shouldBreak = true;
+                result.reason = String.format("设备[%s]的属性[%s]值为[%s],满足条件[%s %s]",
+                    config.deviceCode, config.attrKey, attrValue.getAttrValue(), config.operator, config.value);
+            }
+
+        } catch (Exception e) {
+            log.error("查询设备属性失败: device={}, attr={}", config.deviceCode, config.attrKey, e);
+        }
+
+        return result;
+    }
+
+    /**
+     * 表达式模式:基于上下文变量判断
+     */
+    private BreakCheckResult checkExprBreakCondition(LoopBreakConfig config, StrategyExecutionContext context) {
+        BreakCheckResult result = new BreakCheckResult();
+        result.shouldBreak = false;
+
+        if (config.expression == null || config.expression.trim().isEmpty()) {
+            return result;
+        }
+
+        try {
+            boolean conditionMet = conditionEvaluator.evaluate(config.expression, context.getVariables());
+
+            if (conditionMet) {
+                result.shouldBreak = true;
+                result.reason = "表达式条件满足: " + config.expression;
+            }
+        } catch (Exception e) {
+            log.error("评估表达式条件失败: {}", config.expression, e);
+        }
+
+        return result;
+    }
+
+    /**
+     * 比较值是否满足条件
+     */
+    private boolean compareValue(Object actual, String operator, String expected) {
+        if (actual == null) {
+            return false;
+        }
+
+        // 统一转为字符串比较(去除小数点后的0)
+        String actualStr = String.valueOf(actual).replaceAll("\\.0+$", "").trim();
+        String expectedStr = String.valueOf(expected).replaceAll("\\.0+$", "").trim();
+
+        log.debug("比较值: actual=[{}], operator=[{}], expected=[{}]", actualStr, operator, expectedStr);
+
+        switch (operator) {
+            case "==":
+            case "equals":
+                return actualStr.equals(expectedStr);
+
+            case "!=":
+            case "notEquals":
+                return !actualStr.equals(expectedStr);
+
+            case ">":
+                return compareNumeric(actual, expected) > 0;
+
+            case ">=":
+                return compareNumeric(actual, expected) >= 0;
+
+            case "<":
+                return compareNumeric(actual, expected) < 0;
+
+            case "<=":
+                return compareNumeric(actual, expected) <= 0;
+
+            default:
+                log.warn("未知的比较运算符: {}", operator);
+                return false;
+        }
+    }
+
+    /**
+     * 数值比较
+     */
+    private int compareNumeric(Object a, Object b) {
+        try {
+            double va = Double.parseDouble(String.valueOf(a));
+            double vb = Double.parseDouble(String.valueOf(b));
+            return Double.compare(va, vb);
+        } catch (NumberFormatException e) {
+            log.warn("数值比较失败: {} vs {}", a, b);
+            return 0;
+        }
     }
 
     /**
      * 执行子步骤
-     * @return true=继续执行, false=终止循环
      */
     private boolean executeChildStep(OpEnergyStrategyStep childStep, StrategyExecutionContext context) {
         try {
@@ -134,13 +342,8 @@ public class LoopStepExecutor implements IStepExecutor {
                 }
             }
 
-            // ✅ 延迟获取 StepExecutorFactory
             IStepExecutor executor = getStepExecutorFactory().getExecutor(childStep.getStepType());
-
-            // 执行子步骤
             Object result = executor.execute(childStep, context);
-
-            // 保存子步骤结果到上下文
             context.setStepResult(childStep.getStepCode(), result);
 
             return true;
@@ -148,7 +351,6 @@ public class LoopStepExecutor implements IStepExecutor {
         } catch (Exception e) {
             log.error("子步骤[{}]执行失败", childStep.getStepCode(), e);
 
-            // 根据子步骤的continueOnFail决定是否继续
             if (childStep.getContinueOnFail() == 1) {
                 log.warn("子步骤[{}]执行失败,但继续执行", childStep.getStepCode());
                 return true;
@@ -162,4 +364,40 @@ public class LoopStepExecutor implements IStepExecutor {
     public String getSupportedType() {
         return "LOOP";
     }
+
+    /**
+     * 跳出条件配置
+     */
+    private static class LoopBreakConfig {
+        String breakType = "NONE"; // NONE, ATTR, EXPR
+
+        // ATTR 模式配置
+        String deviceCode;
+        String modelCode;
+        String attrKey;
+        String operator;
+        String value;
+
+        // EXPR 模式配置
+        String expression;
+
+        @Override
+        public String toString() {
+            if ("ATTR".equals(breakType)) {
+                return String.format("ATTR[device=%s, attr=%s, op=%s, value=%s]",
+                    deviceCode, attrKey, operator, value);
+            } else if ("EXPR".equals(breakType)) {
+                return String.format("EXPR[%s]", expression);
+            }
+            return "NONE";
+        }
+    }
+
+    /**
+     * 跳出检查结果
+     */
+    private static class BreakCheckResult {
+        boolean shouldBreak;
+        String reason = "";
+    }
 }

+ 119 - 20
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/StrategyExecutor.java

@@ -1,16 +1,7 @@
-/*
- * 文 件 名:  StrategyExecutor
- * 版    权:  华设设计集团股份有限公司
- * 描    述:  <描述>
- * 修 改 人:  lvwenbin
- * 修改时间:  2025/11/27
- * 跟踪单号:  <跟踪单号>
- * 修改单号:  <修改单号>
- * 修改内容:  <修改内容>
- */
 package com.ruoyi.ems.strategy.executor;
 
 import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.security.utils.SecurityUtils;
 import com.ruoyi.ems.domain.OpEnergyStrategy;
 import com.ruoyi.ems.domain.OpEnergyStrategyExecLog;
 import com.ruoyi.ems.domain.OpEnergyStrategyStep;
@@ -38,16 +29,11 @@ import java.util.stream.Collectors;
 
 /**
  * 策略执行器
- * <功能详细描述>
- *
- * @author lvwenbin
- * @version [版本号, 2025/11/27]
- * @see [相关类/方法]
- * @since [产品/模块版本]
  */
 @Slf4j
 @Component
 public class StrategyExecutor {
+
     @Autowired
     private IOpEnergyStrategyService strategyService;
 
@@ -70,6 +56,13 @@ public class StrategyExecutor {
 
     /**
      * 执行策略
+     *
+     * @param strategyCode 策略代码
+     * @param triggerData 触发数据,应包含:
+     *                    - trigger_type: 触发类型 (MANUAL/TIME/EVENT/ATTR/POLLING)
+     *                    - trigger_source: 触发源 (如: D-B-QS-10000001.Switch)
+     *                    - exec_by: 执行人 (可选)
+     * @return 执行ID
      */
     public String executeStrategy(String strategyCode, Map<String, Object> triggerData) {
         String execId = UUID.randomUUID().toString();
@@ -91,8 +84,43 @@ public class StrategyExecutor {
         context.setExecId(execId);
         context.setStrategyCode(strategyCode);
         context.setStartTime(System.currentTimeMillis());
+
+        // ✅ 从 triggerData 中提取触发信息
         if (triggerData != null) {
             context.getVariables().putAll(triggerData);
+
+            // 显式设置触发类型
+            if (triggerData.containsKey("trigger_type")) {
+                context.setTriggerType(String.valueOf(triggerData.get("trigger_type")));
+            }
+
+            // 显式设置触发源
+            if (triggerData.containsKey("trigger_source")) {
+                context.setTriggerSource(String.valueOf(triggerData.get("trigger_source")));
+            }
+
+            // 显式设置执行人
+            if (triggerData.containsKey("exec_by")) {
+                context.setExecBy(String.valueOf(triggerData.get("exec_by")));
+            }
+        }
+
+        // ✅ 如果是手动触发且没有指定执行人,尝试获取当前登录用户
+        if ("MANUAL".equals(context.getTriggerType()) && context.getExecBy() == null) {
+            try {
+                String username = SecurityUtils.getUsername();
+                if (username != null) {
+                    context.setExecBy(username);
+                }
+            } catch (Exception e) {
+                // 如果无法获取用户信息(如在定时任务中),使用默认值
+                log.debug("无法获取当前用户信息: {}", e.getMessage());
+            }
+        }
+
+        // ✅ 设置默认触发源(如果未指定)
+        if (context.getTriggerSource() == null || context.getTriggerSource().isEmpty()) {
+            context.setTriggerSource(buildDefaultTriggerSource(context.getTriggerType(), strategy));
         }
 
         // 记录执行日志
@@ -101,9 +129,14 @@ public class StrategyExecutor {
         execLog.setStrategyCode(strategyCode);
         execLog.setTriggerType(context.getTriggerType());
         execLog.setTriggerSource(context.getTriggerSource());
+        execLog.setExecBy(context.getExecBy());
         execLog.setExecStatus(0); // 执行中
         execLog.setStartTime(new Date());
         execLog.setContextData(JSON.toJSONString(context.getVariables()));
+
+        log.info("策略[{}]开始执行: execId={}, triggerType={}, triggerSource={}, execBy={}",
+            strategyCode, execId, execLog.getTriggerType(), execLog.getTriggerSource(), execLog.getExecBy());
+
         execLogService.insertExecLog(execLog);
 
         // 异步执行策略
@@ -119,13 +152,39 @@ public class StrategyExecutor {
         return execId;
     }
 
+    /**
+     * 构建默认触发源
+     */
+    private String buildDefaultTriggerSource(String triggerType, OpEnergyStrategy strategy) {
+        if (triggerType == null) {
+            return "UNKNOWN";
+        }
+
+        switch (triggerType) {
+            case "MANUAL":
+                return "USER";
+            case "TIME":
+                return "SCHEDULER";
+            case "EVENT":
+                return "EVENT_LISTENER";
+            case "ATTR":
+                return "ATTR_CHANGE";
+            case "POLLING":
+                return "POLLING_MONITOR";
+            default:
+                return triggerType;
+        }
+    }
+
     private void executeStrategySteps(OpEnergyStrategy strategy,
         StrategyExecutionContext context,
         OpEnergyStrategyExecLog execLog) {
+
         List<OpEnergyStrategyStep> steps = stepService.selectStepsByStrategyCode(strategy.getStrategyCode());
 
-        // 按步骤顺序排序
+        // 按步骤顺序排序,只取顶层步骤(parentStepCode 为空)
         steps = steps.stream()
+            .filter(s -> s.getParentStepCode() == null || s.getParentStepCode().isEmpty())
             .sorted(Comparator.comparingInt(OpEnergyStrategyStep::getStepIndex))
             .collect(Collectors.toList());
 
@@ -174,6 +233,9 @@ public class StrategyExecutor {
 
         // 更新策略统计
         updateStrategyStats(strategy, allSuccess);
+
+        log.info("策略[{}]执行完成: execId={}, 耗时={}ms, 结果={}",
+            strategy.getStrategyCode(), context.getExecId(), duration, allSuccess ? "成功" : "失败");
     }
 
     private StepExecutionResult executeStep(OpEnergyStrategyStep step,
@@ -189,6 +251,10 @@ public class StrategyExecutor {
         stepLog.setStepIndex(step.getStepIndex());
         stepLog.setExecStatus(0);
         stepLog.setStartTime(new Date());
+
+        // ✅ 记录输入参数
+        stepLog.setInputParam(buildStepInputParam(step, context));
+
         stepLogService.insertStepLog(stepLog);
 
         try {
@@ -224,6 +290,38 @@ public class StrategyExecutor {
         }
     }
 
+    /**
+     * 构建步骤输入参数(用于日志记录)
+     */
+    private String buildStepInputParam(OpEnergyStrategyStep step, StrategyExecutionContext context) {
+        try {
+            Map<String, Object> inputParam = new java.util.HashMap<>();
+            inputParam.put("stepType", step.getStepType());
+
+            if ("ABILITY".equals(step.getStepType())) {
+                inputParam.put("targetObjCode", step.getTargetObjCode());
+                inputParam.put("abilityKey", step.getAbilityKey());
+                inputParam.put("abilityParam", step.getAbilityParam());
+                inputParam.put("paramSource", step.getParamSource());
+            } else if ("DELAY".equals(step.getStepType())) {
+                inputParam.put("delaySeconds", step.getDelaySeconds());
+            } else if ("ATTR_QUERY".equals(step.getStepType())) {
+                inputParam.put("targetObjCode", step.getTargetObjCode());
+                inputParam.put("attrKey", step.getAbilityKey());
+            } else if ("LOOP".equals(step.getStepType())) {
+                inputParam.put("loopMaxCount", step.getLoopMaxCount());
+                inputParam.put("loopInterval", step.getLoopInterval());
+                inputParam.put("loopCondition", step.getLoopCondition());
+            } else if ("CONDITION".equals(step.getStepType())) {
+                inputParam.put("conditionExpr", step.getConditionExpr());
+            }
+
+            return JSON.toJSONString(inputParam);
+        } catch (Exception e) {
+            return "{}";
+        }
+    }
+
     private void recordStepSkipped(String execId, OpEnergyStrategyStep step) {
         OpEnergyStrategyStepLog stepLog = new OpEnergyStrategyStepLog();
         stepLog.setExecId(execId);
@@ -234,6 +332,7 @@ public class StrategyExecutor {
         stepLog.setExecStatus(3); // 跳过
         stepLog.setStartTime(new Date());
         stepLog.setEndTime(new Date());
+        stepLog.setDuration(0);
         stepLogService.insertStepLog(stepLog);
     }
 
@@ -245,12 +344,12 @@ public class StrategyExecutor {
     }
 
     private void updateStrategyStats(OpEnergyStrategy strategy, boolean success) {
-        strategy.setExecCount(strategy.getExecCount() + 1);
+        strategy.setExecCount((strategy.getExecCount() != null ? strategy.getExecCount() : 0) + 1);
         if (success) {
-            strategy.setSuccessCount(strategy.getSuccessCount() + 1);
+            strategy.setSuccessCount((strategy.getSuccessCount() != null ? strategy.getSuccessCount() : 0) + 1);
             strategy.setLastExecResult(0);
         } else {
-            strategy.setFailCount(strategy.getFailCount() + 1);
+            strategy.setFailCount((strategy.getFailCount() != null ? strategy.getFailCount() : 0) + 1);
             strategy.setLastExecResult(1);
         }
         strategy.setLastExecTime(new Date());

+ 0 - 118
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/listener/StrategyTriggerListener.java

@@ -1,118 +0,0 @@
-/*
- * 文 件 名:  StrategyTriggerListener
- * 版    权:  华设设计集团股份有限公司
- * 描    述:  <描述>
- * 修 改 人:  lvwenbin
- * 修改时间:  2025/11/27
- * 跟踪单号:  <跟踪单号>
- * 修改单号:  <修改单号>
- * 修改内容:  <修改内容>
- */
-package com.ruoyi.ems.strategy.listener;
-
-import com.ruoyi.ems.domain.OpEnergyStrategyTrigger;
-import com.ruoyi.ems.service.IOpEnergyStrategyTriggerService;
-import com.ruoyi.ems.strategy.evaluator.ConditionEvaluator;
-import com.ruoyi.ems.strategy.executor.StrategyExecutor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 事件监听器
- * <功能详细描述>
- *
- * @author lvwenbin
- * @version [版本号, 2025/11/27]
- * @see [相关类/方法]
- * @since [产品/模块版本]
- */
-@Slf4j
-@Component
-public class StrategyTriggerListener {
-    @Autowired
-    private IOpEnergyStrategyTriggerService triggerService;
-
-    @Autowired
-    private ConditionEvaluator conditionEvaluator;
-
-    @Autowired
-    private StrategyExecutor strategyExecutor;
-
-    /**
-     * 处理设备事件
-     */
-    public void handleDeviceEvent(String deviceCode, String eventKey, Map<String, Object> eventData) {
-        log.info("接收到设备事件: device={}, event={}", deviceCode, eventKey);
-
-        // 查询匹配的触发器
-        List<OpEnergyStrategyTrigger> triggers = triggerService.findBySourceAndEvent(deviceCode, eventKey);
-
-        for (OpEnergyStrategyTrigger trigger : triggers) {
-            if (trigger.getEnable() != 1) {
-                continue;
-            }
-
-            // 评估触发条件
-            boolean shouldTrigger = true;
-            if (trigger.getConditionExpr() != null && !trigger.getConditionExpr().isEmpty()) {
-                shouldTrigger = conditionEvaluator.evaluate(trigger.getConditionExpr(), eventData);
-            }
-
-            if (shouldTrigger) {
-                log.info("触发策略: {}", trigger.getStrategyCode());
-
-                // 准备触发数据
-                Map<String, Object> triggerData = new HashMap<>(eventData);
-                triggerData.put("trigger_type", "EVENT");
-                triggerData.put("trigger_source", deviceCode);
-                triggerData.put("event_key", eventKey);
-
-                // 执行策略
-                strategyExecutor.executeStrategy(trigger.getStrategyCode(), triggerData);
-            }
-        }
-    }
-
-    /**
-     * 处理属性变化
-     */
-    public void handleAttrChange(String objCode, String attrKey, Object oldValue, Object newValue) {
-        log.info("接收到属性变化: obj={}, attr={}, old={}, new={}", objCode, attrKey, oldValue, newValue);
-
-        // 查询匹配的触发器
-        List<OpEnergyStrategyTrigger> triggers = triggerService.findBySourceAndAttr(objCode, attrKey);
-
-        for (OpEnergyStrategyTrigger trigger : triggers) {
-            if (trigger.getEnable() != 1) {
-                continue;
-            }
-
-            // 构建上下文数据
-            Map<String, Object> context = new HashMap<>();
-            context.put("objCode", objCode);
-            context.put("attrKey", attrKey);
-            context.put("oldValue", oldValue);
-            context.put("newValue", newValue);
-
-            // 评估触发条件
-            boolean shouldTrigger = true;
-            if (trigger.getConditionExpr() != null && !trigger.getConditionExpr().isEmpty()) {
-                shouldTrigger = conditionEvaluator.evaluate(trigger.getConditionExpr(), context);
-            }
-
-            if (shouldTrigger) {
-                log.info("触发策略: {}", trigger.getStrategyCode());
-
-                context.put("trigger_type", "ATTR_CHANGE");
-                context.put("trigger_source", objCode);
-
-                strategyExecutor.executeStrategy(trigger.getStrategyCode(), context);
-            }
-        }
-    }
-}

+ 0 - 1
ems/ems-core/src/main/resources/mapper/ems/OpEnergyStrategyStepMapper.xml

@@ -177,7 +177,6 @@
         <include refid="selectStepVo"/>
         WHERE strategy_code = #{strategyCode}
         AND parent_step_code = #{parentStepCode}
-        AND del_flag = '0'
         ORDER BY step_index ASC
     </select>
 </mapper>

+ 6 - 0
ems/sql/ems_init_data_test.sql

@@ -124,3 +124,9 @@ INSERT INTO `adm_ems_obj_ability` (`model_code`, `ability_key`, `ability_name`,
 -- 对象事件DEMO数据
 INSERT INTO `adm_ems_obj_event` (`model_code`, `event_type`, `event_key`, `event_name`, `event_desc`, `event_code`, `ext_event_code`) VALUES ('M_W2_QF_GEEKOPEN', 2, 'key-on', '设备上电', '设备上电', 'e-qf-on-0001', null);
 INSERT INTO `adm_ems_obj_event` (`model_code`, `event_type`, `event_key`, `event_name`, `event_desc`, `event_code`, `ext_event_code`) VALUES ('M_W2_QF_GEEKOPEN', 2, 'key-off', '设备断电', '设备断电', 'e-qf-off-0001', null);
+
+
+INSERT INTO `adm_op_energy_strategy` (`area_code`, `strategy_code`, `strategy_name`, `scene_type`, `strategy_category`, `trigger_type`, `trigger_config`, `strategy_state`, `priority`, `strategy_desc`, `exec_mode`, `exec_rule`, `timeout`, `retry_times`, `last_exec_time`, `last_exec_result`, `exec_count`, `success_count`, `fail_count`, `version`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('320100', 'POLLING_LIGHT_001', '灯1开启后打开灯2(轮询监控V2)', 'ENERGY_SAVE', 'AUTO', 5, NULL, 1, 50, '使用轮询监控机制:每2秒调用syncState能力查询灯1状态,当灯1开启时自动打开灯2', 1, NULL, 300, 0, '2025-12-11 17:10:06', 0, 16, 16, 0, 1, NULL, '2025-12-11 14:16:20', NULL, '2025-12-11 17:08:33');
+INSERT INTO `adm_op_energy_strategy_trigger` (`strategy_code`, `trigger_name`, `trigger_type`, `source_obj_type`, `source_obj_code`, `source_model_code`, `event_key`, `attr_key`, `condition_expr`, `enable`, `priority`) VALUES ('POLLING_LIGHT_001', '监控灯1开关状态', 'POLLING', 2, 'D-B-QS-10000001', 'M_W2_QS_KEKA_86', '', 'Switch', '{\"left\":\"Switch\",\"op\":\"==\",\"right\":\"1\",\"polling\":{\"enabled\":true,\"interval\":2000,\"initialDelay\":1000,\"activeQuery\":true,\"queryWaitTime\":500,\"triggerMode\":\"ALWAYS\",\"queryAbility\":{\"abilityKey\":\"syncState\",\"abilityParam\":\"\"}}}', 1, 50);
+INSERT INTO `adm_op_energy_strategy_step` (`strategy_code`, `step_code`, `step_name`, `step_type`, `step_index`, `parent_step_code`, `condition_expr`, `target_obj_type`, `target_obj_code`, `target_model_code`, `ability_key`, `ability_param`, `param_source`, `param_mapping`, `delay_seconds`, `retry_on_fail`, `retry_times`, `retry_interval`, `continue_on_fail`, `timeout`, `enable`, `remark`, `loop_max_count`, `loop_interval`, `loop_condition`) VALUES ('POLLING_LIGHT_001', 'STEP_1765444159961_kin3j', '能力调用', 'ABILITY', 1, NULL, NULL, 2, 'D-B-QS-10000002', 'M_W2_QS_KEKA_86', 'on-off', '1', 'STATIC', NULL, 10, NULL, NULL, NULL, 0, NULL, 1, NULL, 100, 1000, '');
+

+ 7 - 7
ems/sql/ems_server.sql

@@ -886,7 +886,7 @@ drop table if exists adm_op_energy_strategy;
 CREATE TABLE adm_op_energy_strategy (
   `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '序号',
   `area_code` VARCHAR(32) NOT NULL COMMENT '地块代码',
-  `strategy_code` VARCHAR(16) NOT NULL COMMENT '策略代码',
+  `strategy_code` VARCHAR(64) NOT NULL COMMENT '策略代码',
   `strategy_name` VARCHAR(64) NOT NULL COMMENT '策略名称',
   `scene_type` VARCHAR(32) DEFAULT NULL COMMENT '场景类型:PV_ESS-光储协同,DEMAND_RESP-需求响应,PEAK_VALLEY-削峰填谷,EMERGENCY-应急保供,ENERGY_SAVE-节能优化',
   `strategy_category` VARCHAR(32) DEFAULT NULL COMMENT '策略分类:AUTO-自动,MANUAL-手动,SCHEDULE-定时,EVENT-事件',
@@ -1057,7 +1057,7 @@ VALUES
 drop table if exists adm_op_energy_strategy_trigger;
 CREATE TABLE adm_op_energy_strategy_trigger (
   `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '序号',
-  `strategy_code` VARCHAR(16) NOT NULL COMMENT '策略代码',
+  `strategy_code` VARCHAR(64) NOT NULL COMMENT '策略代码',
   `trigger_name` VARCHAR(64) NOT NULL COMMENT '触发器名称',
   `trigger_type` VARCHAR(32) NOT NULL COMMENT '触发类型:EVENT-事件,ATTR-属性变化,TIME-时间,CONDITION-条件',
   `source_obj_type` INT DEFAULT NULL COMMENT '源对象类型:2-设备,3-系统',
@@ -1083,7 +1083,7 @@ CREATE TABLE adm_op_energy_strategy_trigger (
 drop table if exists adm_op_energy_strategy_param;
 create table adm_op_energy_strategy_param  (
   `id`                  bigint(20)      not null auto_increment      comment '序号',
-  `strategy_code`       varchar(16)     not null                     comment '策略代码',
+  `strategy_code`       varchar(64)     not null                     comment '策略代码',
   `param_group`         varchar(128)    not null                     comment '策略分组',
   `param_key`           varchar(128)    not null                     comment '参数键值',
   `param_name`          varchar(256)    not null                     comment '参数名称',
@@ -1101,7 +1101,7 @@ create table adm_op_energy_strategy_param  (
 drop table if exists adm_op_energy_strategy_step;
 CREATE TABLE adm_op_energy_strategy_step (
   `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '序号',
-  `strategy_code` VARCHAR(16) NOT NULL COMMENT '策略代码',
+  `strategy_code` VARCHAR(64) NOT NULL COMMENT '策略代码',
   `step_code` VARCHAR(32) NOT NULL COMMENT '步骤代码',
   `step_name` VARCHAR(64) NOT NULL COMMENT '步骤名称',
   `step_type` VARCHAR(32) NOT NULL COMMENT '步骤类型:ABILITY-能力调用,DELAY-延时,CONDITION-条件判断,PARALLEL-并行,LOOP-循环',
@@ -1125,7 +1125,7 @@ CREATE TABLE adm_op_energy_strategy_step (
   `remark` VARCHAR(256) DEFAULT NULL COMMENT '备注',
   `loop_max_count` INT DEFAULT 0 COMMENT '最大循环次数(0=无限循环)',
   `loop_interval` INT DEFAULT 1000 COMMENT '循环间隔(毫秒)',
-  `loop_condition` VARCHAR(500) COMMENT '循环跳出条件表达式',
+  `loop_condition` TEXT DEFAULT NULL COMMENT '循环跳出条件表达式',
   `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (`id`),
@@ -1140,7 +1140,7 @@ CREATE TABLE adm_op_energy_strategy_step (
 drop table if exists adm_op_energy_strategy_exec_log;
 CREATE TABLE adm_op_energy_strategy_exec_log (
   `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '序号',
-  `strategy_code` VARCHAR(16) NOT NULL COMMENT '策略代码',
+  `strategy_code` VARCHAR(64) NOT NULL COMMENT '策略代码',
   `exec_id` VARCHAR(64) NOT NULL COMMENT '执行ID(UUID)',
   `trigger_type` VARCHAR(32) DEFAULT NULL COMMENT '触发类型',
   `trigger_source` VARCHAR(128) DEFAULT NULL COMMENT '触发源',
@@ -1168,7 +1168,7 @@ drop table if exists adm_op_energy_strategy_step_log;
 CREATE TABLE adm_op_energy_strategy_step_log (
   `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '序号',
   `exec_id` VARCHAR(64) NOT NULL COMMENT '策略执行ID',
-  `strategy_code` VARCHAR(16) NOT NULL COMMENT '策略代码',
+  `strategy_code` VARCHAR(64) NOT NULL COMMENT '策略代码',
   `step_code` VARCHAR(32) NOT NULL COMMENT '步骤代码',
   `step_name` VARCHAR(64) NOT NULL COMMENT '步骤名称',
   `step_index` INT NOT NULL COMMENT '步骤顺序',