learshaw пре 4 месеци
родитељ
комит
f055475cee
19 измењених фајлова са 525 додато и 543 уклоњено
  1. 0 34
      ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/controller/OpEnergyStrategyController.java
  2. 0 43
      ems/ems-core/src/main/java/com/ruoyi/ems/domain/OpEnergyStrategyContext.java
  3. 3 0
      ems/ems-core/src/main/java/com/ruoyi/ems/domain/OpEnergyStrategyStep.java
  4. 0 69
      ems/ems-core/src/main/java/com/ruoyi/ems/mapper/OpEnergyStrategyContextMapper.java
  5. 11 0
      ems/ems-core/src/main/java/com/ruoyi/ems/mapper/OpEnergyStrategyStepMapper.java
  6. 0 69
      ems/ems-core/src/main/java/com/ruoyi/ems/service/IOpEnergyStrategyContextService.java
  7. 8 0
      ems/ems-core/src/main/java/com/ruoyi/ems/service/IOpEnergyStrategyStepService.java
  8. 0 108
      ems/ems-core/src/main/java/com/ruoyi/ems/service/impl/OpEnergyStrategyContextServiceImpl.java
  9. 5 0
      ems/ems-core/src/main/java/com/ruoyi/ems/service/impl/OpEnergyStrategyStepServiceImpl.java
  10. 58 0
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/AttrValueChangeInterceptor.java
  11. 139 31
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/evaluator/ConditionEvaluator.java
  12. 45 68
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/AbilityCallStepExecutor.java
  13. 61 0
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/AttrQueryStepExecutor.java
  14. 165 0
      ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/LoopStepExecutor.java
  15. 0 93
      ems/ems-core/src/main/resources/mapper/ems/OpEnergyStrategyContextMapper.xml
  16. 25 4
      ems/ems-core/src/main/resources/mapper/ems/OpEnergyStrategyStepMapper.xml
  17. 0 1
      ems/sql/ems_init_data.sql
  18. 2 2
      ems/sql/ems_init_data_test.sql
  19. 3 21
      ems/sql/ems_server.sql

+ 0 - 34
ems/ems-cloud/ems-server/src/main/java/com/ruoyi/ems/controller/OpEnergyStrategyController.java

@@ -9,14 +9,12 @@ import com.ruoyi.common.log.annotation.Log;
 import com.ruoyi.common.log.enums.BusinessType;
 import com.ruoyi.common.security.annotation.RequiresPermissions;
 import com.ruoyi.ems.domain.OpEnergyStrategy;
-import com.ruoyi.ems.domain.OpEnergyStrategyContext;
 import com.ruoyi.ems.domain.OpEnergyStrategyExecLog;
 import com.ruoyi.ems.domain.OpEnergyStrategyParam;
 import com.ruoyi.ems.domain.OpEnergyStrategyStep;
 import com.ruoyi.ems.domain.OpEnergyStrategyStepLog;
 import com.ruoyi.ems.domain.OpEnergyStrategyTemplate;
 import com.ruoyi.ems.domain.OpEnergyStrategyTrigger;
-import com.ruoyi.ems.service.IOpEnergyStrategyContextService;
 import com.ruoyi.ems.service.IOpEnergyStrategyExecLogService;
 import com.ruoyi.ems.service.IOpEnergyStrategyParamService;
 import com.ruoyi.ems.service.IOpEnergyStrategyService;
@@ -72,9 +70,6 @@ public class OpEnergyStrategyController extends BaseController {
     private IOpEnergyStrategyExecLogService execLogService;
 
     @Autowired
-    private IOpEnergyStrategyContextService contextService;
-
-    @Autowired
     private StrategyExecutor strategyExecutor;
 
     /**
@@ -317,35 +312,6 @@ public class OpEnergyStrategyController extends BaseController {
     }
 
     /**
-     * 查询上下文参数
-     *
-     * @param strategyCode
-     * @return
-     */
-    @GetMapping("/context")
-    public AjaxResult getStrategyContext(@RequestParam String strategyCode) {
-        OpEnergyStrategyContext context = contextService.selectContextByStrategyCode(strategyCode);
-        return success(context);
-    }
-
-    /**
-     * 保存上下文参数
-     *
-     * @param context
-     * @return
-     */
-    @PostMapping("/context")
-    public AjaxResult saveStrategyContext(@RequestBody OpEnergyStrategyContext context) {
-        int cnt = contextService.insertContext(context);
-        return success(cnt);
-    }
-
-    @DeleteMapping("/context/{id}")
-    public AjaxResult deleteStrategyContext(@PathVariable Long id) {
-        return toAjax(contextService.deleteContextById(id));
-    }
-
-    /**
      * 手动执行策略
      */
     @PostMapping("/execute/{strategyCode}")

+ 0 - 43
ems/ems-core/src/main/java/com/ruoyi/ems/domain/OpEnergyStrategyContext.java

@@ -1,43 +0,0 @@
-package com.ruoyi.ems.domain;
-
-import com.huashe.common.domain.BaseEntity;
-import lombok.Data;
-
-/**
- * 策略上下文变量对象 adm_op_energy_strategy_context
- * 
- * @author ruoyi
- * @date 2025-12-02
- */
-@Data
-public class OpEnergyStrategyContext extends BaseEntity
-{
-    private static final long serialVersionUID = 1L;
-
-    /** 序号 */
-    private Long id;
-
-    /** 策略代码 */
-    private String strategyCode;
-
-    /** 变量键 */
-    private String varKey;
-
-    /** 变量名称 */
-    private String varName;
-
-    /** 变量类型:INPUT-输入,OUTPUT-输出,TEMP-临时 */
-    private String varType;
-
-    /** 数据类型:STRING,NUMBER,BOOLEAN,OBJECT */
-    private String dataType;
-
-    /** 默认值 */
-    private String defaultValue;
-
-    /** 值来源:设备代码.属性键 */
-    private String valueSource;
-
-    /** 描述 */
-    private String description;
-}

+ 3 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/domain/OpEnergyStrategyStep.java

@@ -38,6 +38,9 @@ public class OpEnergyStrategyStep
     private Integer timeout;
     private Integer enable;
     private String remark;
+    private Integer loopMaxCount;
+    private Integer loopInterval;
+    private String loopCondition;
     private Date createTime;
     private Date updateTime;
 }

+ 0 - 69
ems/ems-core/src/main/java/com/ruoyi/ems/mapper/OpEnergyStrategyContextMapper.java

@@ -1,69 +0,0 @@
-package com.ruoyi.ems.mapper;
-
-import java.util.List;
-import com.ruoyi.ems.domain.OpEnergyStrategyContext;
-
-/**
- * 策略上下文变量Mapper接口
- * 
- * @author ruoyi
- * @date 2025-12-02
- */
-public interface OpEnergyStrategyContextMapper 
-{
-    /**
-     * 查询策略上下文变量
-     * 
-     * @param id 策略上下文变量主键
-     * @return 策略上下文变量
-     */
-     OpEnergyStrategyContext selectContextById(Long id);
-
-    /**
-     * 查询策略上下文变量
-     *
-     * @param strategyCode 策略上下文变量主键
-     * @return 策略上下文变量
-     */
-    OpEnergyStrategyContext selectContextByStrategyCode(String strategyCode);
-
-    /**
-     * 查询策略上下文变量列表
-     * 
-     * @param context 策略上下文变量
-     * @return 策略上下文变量集合
-     */
-     List<OpEnergyStrategyContext> selectContextList(OpEnergyStrategyContext context);
-
-    /**
-     * 新增策略上下文变量
-     * 
-     * @param context 策略上下文变量
-     * @return 结果
-     */
-     int insertContext(OpEnergyStrategyContext context);
-
-    /**
-     * 修改策略上下文变量
-     * 
-     * @param context 策略上下文变量
-     * @return 结果
-     */
-     int updateContext(OpEnergyStrategyContext context);
-
-    /**
-     * 删除策略上下文变量
-     * 
-     * @param id 策略上下文变量主键
-     * @return 结果
-     */
-     int deleteContextById(Long id);
-
-    /**
-     * 批量删除策略上下文变量
-     * 
-     * @param ids 需要删除的数据主键集合
-     * @return 结果
-     */
-     int deleteContextByIds(Long[] ids);
-}

+ 11 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/mapper/OpEnergyStrategyStepMapper.java

@@ -14,6 +14,7 @@ import java.util.List;
 public interface OpEnergyStrategyStepMapper {
     /**
      * 查询策略步骤列表
+     *
      * @param strategyCode 策略代码
      * @return 步骤列表
      */
@@ -21,6 +22,7 @@ public interface OpEnergyStrategyStepMapper {
 
     /**
      * 查询步骤详情
+     *
      * @param id 步骤ID
      * @return 步骤详情
      */
@@ -28,6 +30,7 @@ public interface OpEnergyStrategyStepMapper {
 
     /**
      * 新增步骤
+     *
      * @param step 步骤
      * @return 结果
      */
@@ -35,6 +38,7 @@ public interface OpEnergyStrategyStepMapper {
 
     /**
      * 修改步骤
+     *
      * @param step 步骤
      * @return 结果
      */
@@ -42,6 +46,7 @@ public interface OpEnergyStrategyStepMapper {
 
     /**
      * 删除步骤
+     *
      * @param id 步骤ID
      * @return 结果
      */
@@ -49,6 +54,7 @@ public interface OpEnergyStrategyStepMapper {
 
     /**
      * 批量删除步骤
+     *
      * @param ids 步骤ID数组
      * @return 结果
      */
@@ -56,6 +62,7 @@ public interface OpEnergyStrategyStepMapper {
 
     /**
      * 根据策略代码删除步骤
+     *
      * @param strategyCode 策略代码
      * @return 结果
      */
@@ -63,8 +70,12 @@ public interface OpEnergyStrategyStepMapper {
 
     /**
      * 批量新增步骤
+     *
      * @param steps 步骤列表
      * @return 结果
      */
     int insertStepBatch(@Param("list") List<OpEnergyStrategyStep> steps);
+
+    List<OpEnergyStrategyStep> selectStepsByParentCode(@Param("strategyCode") String strategyCode,
+        @Param("parentStepCode") String parentStepCode);
 }

+ 0 - 69
ems/ems-core/src/main/java/com/ruoyi/ems/service/IOpEnergyStrategyContextService.java

@@ -1,69 +0,0 @@
-package com.ruoyi.ems.service;
-
-import java.util.List;
-
-import com.ruoyi.ems.domain.OpEnergyStrategyContext;
-
-/**
- * 策略上下文变量Service接口
- *
- * @author ruoyi
- * @date 2025-12-02
- */
-public interface IOpEnergyStrategyContextService {
-    /**
-     * 查询策略上下文变量
-     *
-     * @param id 策略上下文变量主键
-     * @return 策略上下文变量
-     */
-    OpEnergyStrategyContext selectContextById(Long id);
-
-    /**
-     * 查询策略上下文变量
-     *
-     * @param strategyCode 策略上下文变量主键
-     * @return 策略上下文变量
-     */
-    OpEnergyStrategyContext selectContextByStrategyCode(String strategyCode);
-
-    /**
-     * 查询策略上下文变量列表
-     *
-     * @param context 策略上下文变量
-     * @return 策略上下文变量集合
-     */
-    List<OpEnergyStrategyContext> selectContextList(OpEnergyStrategyContext context);
-
-    /**
-     * 新增策略上下文变量
-     *
-     * @param context 策略上下文变量
-     * @return 结果
-     */
-    int insertContext(OpEnergyStrategyContext context);
-
-    /**
-     * 修改策略上下文变量
-     *
-     * @param context 策略上下文变量
-     * @return 结果
-     */
-    int updateContext(OpEnergyStrategyContext context);
-
-    /**
-     * 批量删除策略上下文变量
-     *
-     * @param ids 需要删除的策略上下文变量主键集合
-     * @return 结果
-     */
-    int deleteContextByIds(Long[] ids);
-
-    /**
-     * 删除策略上下文变量信息
-     *
-     * @param id 策略上下文变量主键
-     * @return 结果
-     */
-    int deleteContextById(Long id);
-}

+ 8 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/service/IOpEnergyStrategyStepService.java

@@ -73,4 +73,12 @@ public interface IOpEnergyStrategyStepService {
      * @return 结果
      */
     int insertStepBatch(List<OpEnergyStrategyStep> steps);
+
+    /**
+     * 查询父步骤下的子步骤列表
+     * @param strategyCode 策略代码
+     * @param parentStepCode 父步骤代码
+     * @return 子步骤列表
+     */
+    List<OpEnergyStrategyStep> selectStepsByParentCode(String strategyCode, String parentStepCode);
 }

+ 0 - 108
ems/ems-core/src/main/java/com/ruoyi/ems/service/impl/OpEnergyStrategyContextServiceImpl.java

@@ -1,108 +0,0 @@
-package com.ruoyi.ems.service.impl;
-
-import java.util.List;
-
-import com.huashe.common.utils.DateUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import com.ruoyi.ems.mapper.OpEnergyStrategyContextMapper;
-import com.ruoyi.ems.domain.OpEnergyStrategyContext;
-import com.ruoyi.ems.service.IOpEnergyStrategyContextService;
-
-/**
- * 策略上下文变量Service业务层处理
- * 
- * @author ruoyi
- * @date 2025-12-02
- */
-@Service
-public class OpEnergyStrategyContextServiceImpl implements IOpEnergyStrategyContextService
-{
-    @Autowired
-    private OpEnergyStrategyContextMapper contextMapper;
-
-    /**
-     * 查询策略上下文变量
-     * 
-     * @param id 策略上下文变量主键
-     * @return 策略上下文变量
-     */
-    @Override
-    public OpEnergyStrategyContext selectContextById(Long id)
-    {
-        return contextMapper.selectContextById(id);
-    }
-
-    /**
-     * 查询策略上下文变量
-     *
-     * @param strategyCode 策略上下文变量主键
-     * @return 策略上下文变量
-     */
-    @Override
-    public OpEnergyStrategyContext selectContextByStrategyCode(String strategyCode)
-    {
-        return contextMapper.selectContextByStrategyCode(strategyCode);
-    }
-
-    /**
-     * 查询策略上下文变量列表
-     * 
-     * @param context 策略上下文变量
-     * @return 策略上下文变量
-     */
-    @Override
-    public List<OpEnergyStrategyContext> selectContextList(OpEnergyStrategyContext context)
-    {
-        return contextMapper.selectContextList(context);
-    }
-
-    /**
-     * 新增策略上下文变量
-     * 
-     * @param context 策略上下文变量
-     * @return 结果
-     */
-    @Override
-    public int insertContext(OpEnergyStrategyContext context)
-    {
-        context.setCreateTime(DateUtils.getNowDate());
-        return contextMapper.insertContext(context);
-    }
-
-    /**
-     * 修改策略上下文变量
-     * 
-     * @param context 策略上下文变量
-     * @return 结果
-     */
-    @Override
-    public int updateContext(OpEnergyStrategyContext context)
-    {
-        return contextMapper.updateContext(context);
-    }
-
-    /**
-     * 批量删除策略上下文变量
-     * 
-     * @param ids 需要删除的策略上下文变量主键
-     * @return 结果
-     */
-    @Override
-    public int deleteContextByIds(Long[] ids)
-    {
-        return contextMapper.deleteContextByIds(ids);
-    }
-
-    /**
-     * 删除策略上下文变量信息
-     * 
-     * @param id 策略上下文变量主键
-     * @return 结果
-     */
-    @Override
-    public int deleteContextById(Long id)
-    {
-        return contextMapper.deleteContextById(id);
-    }
-}

+ 5 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/service/impl/OpEnergyStrategyStepServiceImpl.java

@@ -63,4 +63,9 @@ public class OpEnergyStrategyStepServiceImpl implements IOpEnergyStrategyStepSer
     public int insertStepBatch(List<OpEnergyStrategyStep> steps) {
         return stepMapper.insertStepBatch(steps);
     }
+
+    @Override
+    public List<OpEnergyStrategyStep> selectStepsByParentCode(String strategyCode, String parentStepCode) {
+        return stepMapper.selectStepsByParentCode(strategyCode, parentStepCode);
+    }
 }

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

@@ -0,0 +1,58 @@
+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);
+    }
+}

+ 139 - 31
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/evaluator/ConditionEvaluator.java

@@ -1,16 +1,7 @@
-/*
- * 文 件 名:  ConditionEvaluator
- * 版    权:  华设设计集团股份有限公司
- * 描    述:  <描述>
- * 修 改 人:  lvwenbin
- * 修改时间:  2025/11/27
- * 跟踪单号:  <跟踪单号>
- * 修改单号:  <修改单号>
- * 修改内容:  <修改内容>
- */
 package com.ruoyi.ems.strategy.evaluator;
 
 import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
@@ -18,19 +9,15 @@ import org.springframework.stereotype.Component;
 import java.util.Map;
 
 /**
- * 条件表达式求值器
- * <功能详细描述>
- *
- * @author lvwenbin
- * @version [版本号, 2025/11/27]
- * @see [相关类/方法]
- * @since [产品/模块版本]
+ * 条件表达式求值器(支持前端简单和复杂格式)
  */
 @Slf4j
 @Component
 public class ConditionEvaluator {
+
     /**
      * 评估条件表达式
+     *
      * @param conditionExpr 条件表达式JSON
      * @param context 上下文变量
      * @return 是否满足条件
@@ -42,27 +29,94 @@ public class ConditionEvaluator {
 
         try {
             JSONObject condition = JSON.parseObject(conditionExpr);
-            return evaluateCondition(condition, context);
+
+            // ✅ 支持简单格式: {"left":"Switch","op":"==","right":"1"}
+            if (condition.containsKey("left") && condition.containsKey("op")) {
+                return evaluateSimple(condition, context);
+            }
+
+            // ✅ 支持复杂格式: {"logic":"AND","conditions":[...]}
+            if (condition.containsKey("logic")) {
+                return evaluateComplex(condition, context);
+            }
+
+            // ✅ 支持旧格式: {"operator":"AND","conditions":[...]}
+            if (condition.containsKey("operator")) {
+                return evaluateLegacy(condition, context);
+            }
+
+            log.warn("未识别的条件格式: {}", conditionExpr);
+            return false;
+
         } catch (Exception e) {
             log.error("条件表达式解析失败: {}", conditionExpr, e);
             return false;
         }
     }
 
-    private boolean evaluateCondition(JSONObject condition, Map<String, Object> context) {
+    /**
+     * 评估简单条件
+     * 格式: {"left":"Switch","op":"==","right":"1"}
+     */
+    private boolean evaluateSimple(JSONObject condition, Map<String, Object> context) {
+        String left = condition.getString("left");
+        String op = condition.getString("op");
+        Object right = condition.get("right");
+
+        // 从上下文获取左侧值
+        Object leftValue = getValueFromContext(left, context);
+
+        return compare(leftValue, op, right);
+    }
+
+    /**
+     * 评估复杂条件(AND/OR)
+     * 格式: {"logic":"AND","conditions":[...]}
+     */
+    private boolean evaluateComplex(JSONObject condition, Map<String, Object> context) {
+        String logic = condition.getString("logic");
+        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 false;
+    }
+
+    /**
+     * 评估旧格式条件(兼容)
+     * 格式: {"operator":"AND","conditions":[...]}
+     */
+    private boolean evaluateLegacy(JSONObject condition, Map<String, Object> context) {
         String operator = condition.getString("operator");
+        JSONArray conditions = condition.getJSONArray("conditions");
 
         if ("AND".equals(operator)) {
-            return condition.getJSONArray("conditions").stream()
+            return conditions.stream()
                 .allMatch(c -> evaluateCondition((JSONObject) c, context));
         } else if ("OR".equals(operator)) {
-            return condition.getJSONArray("conditions").stream()
+            return conditions.stream()
                 .anyMatch(c -> evaluateCondition((JSONObject) c, context));
         } else {
             return evaluateSimpleCondition(condition, context);
         }
     }
 
+    private boolean evaluateCondition(JSONObject condition, Map<String, Object> context) {
+        String operator = condition.getString("operator");
+
+        if ("AND".equals(operator) || "OR".equals(operator)) {
+            return evaluateLegacy(condition, context);
+        } else {
+            return evaluateSimpleCondition(condition, context);
+        }
+    }
+
     private boolean evaluateSimpleCondition(JSONObject condition, Map<String, Object> context) {
         String field = condition.getString("field");
         String operator = condition.getString("operator");
@@ -73,50 +127,104 @@ public class ConditionEvaluator {
         return compare(actualValue, operator, expectedValue);
     }
 
+    /**
+     * 从上下文获取值(支持嵌套路径)
+     *
+     * 支持格式:
+     * - "Switch" → context.get("Switch")
+     * - "context.Switch" → context.get("Switch")
+     * - "newValue" → context.get("newValue")(用于属性变化触发)
+     * - "data.temperature" → context.get("data").temperature
+     */
     private Object getValueFromContext(String field, Map<String, Object> context) {
+        if (field == null || field.trim().isEmpty()) {
+            return null;
+        }
+
+        // 移除 "context." 前缀
+        if (field.startsWith("context.")) {
+            field = field.substring(8);
+        }
+
+        // 支持嵌套路径
         if (field.contains(".")) {
             String[] parts = field.split("\\.");
-            Object current = context;
-            for (String part : parts) {
+            Object current = context.get(parts[0]);
+
+            for (int i = 1; i < parts.length && current != null; i++) {
                 if (current instanceof Map) {
-                    current = ((Map<?, ?>) current).get(part);
+                    current = ((Map<?, ?>) current).get(parts[i]);
+                } else if (current instanceof JSONObject) {
+                    current = ((JSONObject) current).get(parts[i]);
                 } else {
                     return null;
                 }
             }
+
             return current;
         }
+
         return context.get(field);
     }
 
+    /**
+     * 比较两个值
+     */
     private boolean compare(Object actual, String operator, Object expected) {
         if (actual == null) return false;
 
+        // 数值类型统一转字符串比较(去除小数点后的0)
+        String actualStr = String.valueOf(actual).replaceAll("\\.0+$", "");
+        String expectedStr = String.valueOf(expected).replaceAll("\\.0+$", "");
+
         switch (operator) {
             case "==":
-                return actual.equals(expected);
+            case "equals":
+                return actualStr.equals(expectedStr);
+
             case "!=":
-                return !actual.equals(expected);
+            case "notEquals":
+                return !actualStr.equals(expectedStr);
+
             case ">":
+            case "greaterThan":
                 return compareNumeric(actual, expected) > 0;
+
             case ">=":
+            case "greaterThanOrEqual":
                 return compareNumeric(actual, expected) >= 0;
+
             case "<":
+            case "lessThan":
                 return compareNumeric(actual, expected) < 0;
+
             case "<=":
+            case "lessThanOrEqual":
                 return compareNumeric(actual, expected) <= 0;
+
             case "contains":
-                return actual.toString().contains(expected.toString());
+                return actualStr.contains(expectedStr);
+
             case "in":
-                return expected.toString().contains(actual.toString());
+                return expectedStr.contains(actualStr);
+
             default:
+                log.warn("未知的比较运算符: {}", operator);
                 return false;
         }
     }
 
+    /**
+     * 数值比较
+     */
     private int compareNumeric(Object a, Object b) {
-        double va = Double.parseDouble(a.toString());
-        double vb = Double.parseDouble(b.toString());
-        return Double.compare(va, vb);
+        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;
+        }
     }
 }

+ 45 - 68
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/AbilityCallStepExecutor.java

@@ -1,13 +1,3 @@
-/*
- * 文 件 名:  AbilityCallStepExecutor
- * 版    权:  华设设计集团股份有限公司
- * 描    述:  <描述>
- * 修 改 人:  lvwenbin
- * 修改时间:  2025/11/27
- * 跟踪单号:  <跟踪单号>
- * 修改单号:  <修改单号>
- * 修改内容:  <修改内容>
- */
 package com.ruoyi.ems.strategy.executor;
 
 import com.alibaba.fastjson2.JSON;
@@ -23,17 +13,17 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 /**
- *  能力调用执行器
- * <功能详细描述>
+ * 能力调用执行器(完全通用化版本)
  *
- * @author lvwenbin
- * @version [版本号, 2025/11/27]
- * @see [相关类/方法]
- * @since [产品/模块版本]
+ * 支持三种参数类型:
+ * 1. null - 无参数能力
+ * 2. Slider/Input - 单值参数
+ * 3. Options - 枚举值参数
  */
 @Slf4j
 @Component
 public class AbilityCallStepExecutor implements IStepExecutor {
+
     @Autowired
     private IAbilityCallService abilityCallService;
 
@@ -49,11 +39,12 @@ public class AbilityCallStepExecutor implements IStepExecutor {
         payload.setModelCode(step.getTargetModelCode());
         payload.setAbilityKey(step.getAbilityKey());
 
-        // 解析能力参数(支持多种参数来源
-        String abilityParam = parseParam(step, context);
+        // 解析能力参数(完全通用化
+        String abilityParam = resolveAbilityParam(step, context);
         payload.setAbilityParam(abilityParam);
 
-        log.info("策略步骤[{}]执行能力调用: {}", step.getStepCode(), payload);
+        log.info("策略步骤[{}]执行能力调用: objCode={}, abilityKey={}, param={}",
+            step.getStepCode(), payload.getObjCode(), payload.getAbilityKey(), abilityParam);
 
         // 调用设备能力服务
         String result = abilityCallService.devAbilityCall(payload);
@@ -66,9 +57,8 @@ public class AbilityCallStepExecutor implements IStepExecutor {
     /**
      * 解析能力参数(支持静态、上下文、属性值三种来源)
      */
-    private String parseParam(OpEnergyStrategyStep step, StrategyExecutionContext context) {
+    private String resolveAbilityParam(OpEnergyStrategyStep step, StrategyExecutionContext context) {
         String paramSource = step.getParamSource();
-
         if (paramSource == null) {
             paramSource = "STATIC";
         }
@@ -80,11 +70,11 @@ public class AbilityCallStepExecutor implements IStepExecutor {
 
             case "CONTEXT":
                 // 上下文参数:从执行上下文中获取动态值
-                return parseContextParam(step, context);
+                return resolveFromContext(step, context);
 
             case "ATTR":
                 // 属性参数:从设备属性中获取实时值
-                return parseAttrParam(step, context);
+                return resolveFromAttr(step, context);
 
             default:
                 log.warn("未知的参数来源类型: {}, 使用静态参数", paramSource);
@@ -94,37 +84,33 @@ public class AbilityCallStepExecutor implements IStepExecutor {
 
     /**
      * 从上下文解析参数
-     * paramMapping格式: {"brightness": "context.lightLevel", "mode": "context.workMode"}
+     *
+     * 支持格式:
+     * 1. 单值: "1" 或 "50"(直接返回)
+     * 2. 映射: {"value": "context.switchState"} → 从上下文获取
      */
-    private String parseContextParam(OpEnergyStrategyStep step, StrategyExecutionContext context) {
+    private String resolveFromContext(OpEnergyStrategyStep step, StrategyExecutionContext context) {
         String paramMapping = step.getParamMapping();
 
+        // 如果没有映射配置,直接使用 abilityParam
         if (paramMapping == null || paramMapping.trim().isEmpty()) {
-            log.warn("上下文参数映射配置为空,使用静态参数");
             return step.getAbilityParam();
         }
 
         try {
             JSONObject mapping = JSON.parseObject(paramMapping);
-            JSONObject resultParams = new JSONObject();
-
-            // 遍历映射配置,从上下文中获取值
-            mapping.forEach((paramKey, contextPath) -> {
-                String path = contextPath.toString();
-                Object value = getValueFromContext(path, context);
-                if (value != null) {
-                    resultParams.put(paramKey, value);
-                } else {
-                    log.warn("上下文路径 {} 未找到值", path);
-                }
-            });
 
-            // 如果没有映射到任何值,返回静态参数
-            if (resultParams.isEmpty()) {
-                return step.getAbilityParam();
+            // 如果是单值映射 {"value": "context.xxx"}
+            if (mapping.containsKey("value")) {
+                String contextPath = mapping.getString("value");
+                Object value = getValueFromContext(contextPath, context);
+                return value != null ? value.toString() : step.getAbilityParam();
             }
 
-            return resultParams.toJSONString();
+            // 其他映射情况(暂不支持多值)
+            log.warn("不支持的上下文参数映射格式: {}", paramMapping);
+            return step.getAbilityParam();
+
         } catch (Exception e) {
             log.error("解析上下文参数失败: {}", e.getMessage(), e);
             return step.getAbilityParam();
@@ -133,52 +119,40 @@ public class AbilityCallStepExecutor implements IStepExecutor {
 
     /**
      * 从设备属性解析参数
-     * paramMapping格式: {"targetTemp": "deviceCode.temperature", "mode": "deviceCode.workMode"}
+     *
+     * 支持格式:
+     * 1. 单值: {"value": "D-B-QS-10000001.Switch"} → 从设备属性获取
      */
-    private String parseAttrParam(OpEnergyStrategyStep step, StrategyExecutionContext context) {
+    private String resolveFromAttr(OpEnergyStrategyStep step, StrategyExecutionContext context) {
         if (attrValueService == null) {
             log.warn("属性值服务未注入,无法获取设备属性,使用静态参数");
             return step.getAbilityParam();
         }
 
         String paramMapping = step.getParamMapping();
-
         if (paramMapping == null || paramMapping.trim().isEmpty()) {
-            log.warn("属性参数映射配置为空,使用静态参数");
             return step.getAbilityParam();
         }
 
         try {
             JSONObject mapping = JSON.parseObject(paramMapping);
-            JSONObject resultParams = new JSONObject();
 
-            // 遍历映射配置,从设备属性中获取值
-            mapping.forEach((paramKey, attrPath) -> {
-                String path = attrPath.toString();
-                String[] parts = path.split("\\.");
+            // 如果是单值映射 {"value": "deviceCode.attrKey"}
+            if (mapping.containsKey("value")) {
+                String attrPath = mapping.getString("value");
+                String[] parts = attrPath.split("\\.");
 
                 if (parts.length == 2) {
                     String deviceCode = parts[0];
                     String attrKey = parts[1];
-
-                    // 调用属性查询服务获取设备属性值
                     Object attrValue = queryDeviceAttr(step.getTargetModelCode(), deviceCode, attrKey);
-                    if (attrValue != null) {
-                        resultParams.put(paramKey, attrValue);
-                    } else {
-                        log.warn("设备 {} 的属性 {} 未找到值", deviceCode, attrKey);
-                    }
-                } else {
-                    log.warn("属性路径格式错误: {}, 应为 deviceCode.attrKey", path);
+                    return attrValue != null ? attrValue.toString() : step.getAbilityParam();
                 }
-            });
-
-            // 如果没有映射到任何值,返回静态参数
-            if (resultParams.isEmpty()) {
-                return step.getAbilityParam();
             }
 
-            return resultParams.toJSONString();
+            log.warn("不支持的属性参数映射格式: {}", paramMapping);
+            return step.getAbilityParam();
+
         } catch (Exception e) {
             log.error("解析属性参数失败: {}", e.getMessage(), e);
             return step.getAbilityParam();
@@ -187,7 +161,11 @@ public class AbilityCallStepExecutor implements IStepExecutor {
 
     /**
      * 从上下文中获取值(支持嵌套路径)
-     * 例如: "context.lightLevel" 或 "stepResult.STEP_001.value"
+     *
+     * 示例:
+     * - "switchState" → context.getVariable("switchState")
+     * - "context.switchState" → context.getVariable("switchState")
+     * - "stepResult.STEP_001.value" → context.getStepResult("STEP_001").value
      */
     private Object getValueFromContext(String path, StrategyExecutionContext context) {
         if (path == null || path.trim().isEmpty()) {
@@ -199,7 +177,6 @@ public class AbilityCallStepExecutor implements IStepExecutor {
             path = path.substring(8);
         }
 
-        // 支持嵌套路径
         String[] parts = path.split("\\.");
         Object current = null;
 

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

@@ -0,0 +1,61 @@
+package com.ruoyi.ems.strategy.executor;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.ems.domain.OpEnergyStrategyStep;
+import com.ruoyi.ems.domain.StrategyExecutionContext;
+import com.ruoyi.ems.service.IEmsObjAttrValueService;
+import com.ruoyi.ems.service.IStepExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 属性查询执行器
+ * 用于查询设备属性值并保存到上下文
+ */
+@Slf4j
+@Component
+public class AttrQueryStepExecutor implements IStepExecutor {
+
+    @Autowired
+    private IEmsObjAttrValueService attrValueService;
+
+    @Override
+    public Object execute(OpEnergyStrategyStep step, StrategyExecutionContext context) throws Exception {
+        String deviceCode = step.getTargetObjCode();
+        String modelCode = step.getTargetModelCode();
+        String attrKey = step.getAbilityKey(); // ✅ 复用 abilityKey 字段存储属性键
+
+        log.info("查询设备属性: device={}, model={}, attr={}",
+            deviceCode, modelCode, attrKey);
+
+        // 查询属性值
+        Object attrValue = attrValueService.selectObjAttrValue(modelCode, deviceCode, attrKey);
+
+        if (attrValue == null) {
+            log.warn("设备属性不存在: device={}, attr={}", deviceCode, attrKey);
+            attrValue = "";
+        }
+
+        // 保存到上下文(使用步骤代码作为键)
+        context.setVariable(step.getStepCode() + "_value", attrValue);
+        context.setVariable("current_" + attrKey, attrValue); // ✅ 也保存为通用键名
+
+        log.info("属性查询成功: device={}, attr={}, value={}",
+            deviceCode, attrKey, attrValue);
+
+        // 返回JSON格式结果
+        JSONObject result = new JSONObject();
+        result.put("deviceCode", deviceCode);
+        result.put("attrKey", attrKey);
+        result.put("attrValue", attrValue);
+
+        return result;
+    }
+
+    @Override
+    public String getSupportedType() {
+        return "ATTR_QUERY";
+    }
+}

+ 165 - 0
ems/ems-core/src/main/java/com/ruoyi/ems/strategy/executor/LoopStepExecutor.java

@@ -0,0 +1,165 @@
+package com.ruoyi.ems.strategy.executor;
+
+import com.ruoyi.ems.domain.OpEnergyStrategyStep;
+import com.ruoyi.ems.domain.StrategyExecutionContext;
+import com.ruoyi.ems.service.IOpEnergyStrategyStepService;
+import com.ruoyi.ems.service.IStepExecutor;
+import com.ruoyi.ems.strategy.evaluator.ConditionEvaluator;
+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.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 循环执行器
+ * ✅ 使用 ApplicationContext 延迟获取 StepExecutorFactory,打破循环依赖
+ */
+@Slf4j
+@Component
+public class LoopStepExecutor implements IStepExecutor {
+
+    @Autowired
+    private IOpEnergyStrategyStepService stepService;
+
+    @Autowired
+    private ConditionEvaluator conditionEvaluator;
+
+    // ✅ 改为注入 ApplicationContext,延迟获取 StepExecutorFactory
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    // ✅ 延迟获取的 StepExecutorFactory 引用
+    private StepExecutorFactory stepExecutorFactory;
+
+    /**
+     * 获取 StepExecutorFactory(延迟加载)
+     */
+    private StepExecutorFactory getStepExecutorFactory() {
+        if (stepExecutorFactory == null) {
+            stepExecutorFactory = applicationContext.getBean(StepExecutorFactory.class);
+        }
+        return stepExecutorFactory;
+    }
+
+    @Override
+    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();
+
+        log.info("循环步骤[{}]开始执行: maxCount={}, interval={}ms, condition={}",
+            step.getStepCode(), maxCount == 0 ? "无限" : maxCount, interval, breakCondition);
+
+        // 加载子步骤
+        List<OpEnergyStrategyStep> childSteps = stepService.selectStepsByParentCode(
+                step.getStrategyCode(), step.getStepCode()
+            ).stream()
+            .filter(s -> s.getEnable() == 1)
+            .sorted(Comparator.comparingInt(OpEnergyStrategyStep::getStepIndex))
+            .collect(Collectors.toList());
+
+        if (childSteps.isEmpty()) {
+            log.warn("循环步骤[{}]没有子步骤,跳过执行", step.getStepCode());
+            return "循环步骤无子步骤";
+        }
+
+        int loopCount = 0;
+        boolean shouldBreak = false;
+
+        // 循环执行
+        while (true) {
+            loopCount++;
+            log.debug("循环步骤[{}]第{}次执行开始", step.getStepCode(), loopCount);
+
+            // 执行所有子步骤
+            for (OpEnergyStrategyStep childStep : childSteps) {
+                if (!executeChildStep(childStep, context)) {
+                    // 子步骤执行失败且不允许继续
+                    log.error("循环步骤[{}]中的子步骤[{}]执行失败,终止循环",
+                        step.getStepCode(), childStep.getStepCode());
+                    shouldBreak = true;
+                    break;
+                }
+            }
+
+            if (shouldBreak) {
+                break;
+            }
+
+            // 检查跳出条件
+            if (breakCondition != null && !breakCondition.trim().isEmpty()) {
+                boolean conditionMet = conditionEvaluator.evaluate(breakCondition, context.getVariables());
+                if (conditionMet) {
+                    log.info("循环步骤[{}]满足跳出条件,退出循环(共执行{}次)",
+                        step.getStepCode(), loopCount);
+                    break;
+                }
+            }
+
+            // 检查最大循环次数
+            if (maxCount > 0 && loopCount >= maxCount) {
+                log.info("循环步骤[{}]达到最大循环次数{},退出循环",
+                    step.getStepCode(), maxCount);
+                break;
+            }
+
+            // 等待下一次循环
+            if (interval > 0) {
+                Thread.sleep(interval);
+            }
+        }
+
+        log.info("循环步骤[{}]执行完成,共循环{}次", step.getStepCode(), loopCount);
+        return "循环完成,共执行" + loopCount + "次";
+    }
+
+    /**
+     * 执行子步骤
+     * @return true=继续执行, false=终止循环
+     */
+    private boolean executeChildStep(OpEnergyStrategyStep childStep, StrategyExecutionContext context) {
+        try {
+            // 评估子步骤的执行条件
+            if (childStep.getConditionExpr() != null && !childStep.getConditionExpr().isEmpty()) {
+                boolean conditionMet = conditionEvaluator.evaluate(
+                    childStep.getConditionExpr(), context.getVariables()
+                );
+                if (!conditionMet) {
+                    log.debug("子步骤[{}]条件不满足,跳过", childStep.getStepCode());
+                    return true;
+                }
+            }
+
+            // ✅ 延迟获取 StepExecutorFactory
+            IStepExecutor executor = getStepExecutorFactory().getExecutor(childStep.getStepType());
+
+            // 执行子步骤
+            Object result = executor.execute(childStep, context);
+
+            // 保存子步骤结果到上下文
+            context.setStepResult(childStep.getStepCode(), result);
+
+            return true;
+
+        } catch (Exception e) {
+            log.error("子步骤[{}]执行失败", childStep.getStepCode(), e);
+
+            // 根据子步骤的continueOnFail决定是否继续
+            if (childStep.getContinueOnFail() == 1) {
+                log.warn("子步骤[{}]执行失败,但继续执行", childStep.getStepCode());
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public String getSupportedType() {
+        return "LOOP";
+    }
+}

+ 0 - 93
ems/ems-core/src/main/resources/mapper/ems/OpEnergyStrategyContextMapper.xml

@@ -1,93 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE mapper
-PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.ruoyi.ems.mapper.OpEnergyStrategyContextMapper">
-    
-    <resultMap type="com.ruoyi.ems.domain.OpEnergyStrategyContext" id="OpEnergyStrategyContextResult">
-        <result property="id"    column="id"    />
-        <result property="strategyCode"    column="strategy_code"    />
-        <result property="varKey"    column="var_key"    />
-        <result property="varName"    column="var_name"    />
-        <result property="varType"    column="var_type"    />
-        <result property="dataType"    column="data_type"    />
-        <result property="defaultValue"    column="default_value"    />
-        <result property="valueSource"    column="value_source"    />
-        <result property="description"    column="description"    />
-        <result property="createTime"    column="create_time"    />
-    </resultMap>
-
-    <sql id="selectContextVo">
-        select id, strategy_code, var_key, var_name, var_type, data_type, default_value, value_source, description, create_time from adm_op_energy_strategy_context
-    </sql>
-
-    <select id="selectContextList" parameterType="com.ruoyi.ems.domain.OpEnergyStrategyContext" resultMap="OpEnergyStrategyContextResult">
-        <include refid="selectContextVo"/>
-        <where>  
-            <if test="strategyCode != null  and strategyCode != ''"> and strategy_code = #{strategyCode}</if>
-        </where>
-    </select>
-
-    <select id="selectContextById" parameterType="Long" resultMap="OpEnergyStrategyContextResult">
-        <include refid="selectContextVo"/>
-        where id = #{id}
-    </select>
-
-    <select id="selectContextByStrategyCode" parameterType="String" resultMap="OpEnergyStrategyContextResult">
-        <include refid="selectContextVo"/>
-        where strategy_code = #{strategyCode}
-    </select>
-        
-    <insert id="insertContext" parameterType="com.ruoyi.ems.domain.OpEnergyStrategyContext" useGeneratedKeys="true" keyProperty="id">
-        insert into adm_op_energy_strategy_context
-        <trim prefix="(" suffix=")" suffixOverrides=",">
-            <if test="strategyCode != null and strategyCode != ''">strategy_code,</if>
-            <if test="varKey != null and varKey != ''">var_key,</if>
-            <if test="varName != null and varName != ''">var_name,</if>
-            <if test="varType != null and varType != ''">var_type,</if>
-            <if test="dataType != null">data_type,</if>
-            <if test="defaultValue != null">default_value,</if>
-            <if test="valueSource != null">value_source,</if>
-            <if test="description != null">description,</if>
-            <if test="createTime != null">create_time,</if>
-         </trim>
-        <trim prefix="values (" suffix=")" suffixOverrides=",">
-            <if test="strategyCode != null and strategyCode != ''">#{strategyCode},</if>
-            <if test="varKey != null and varKey != ''">#{varKey},</if>
-            <if test="varName != null and varName != ''">#{varName},</if>
-            <if test="varType != null and varType != ''">#{varType},</if>
-            <if test="dataType != null">#{dataType},</if>
-            <if test="defaultValue != null">#{defaultValue},</if>
-            <if test="valueSource != null">#{valueSource},</if>
-            <if test="description != null">#{description},</if>
-            <if test="createTime != null">#{createTime},</if>
-         </trim>
-    </insert>
-
-    <update id="updateContext" parameterType="com.ruoyi.ems.domain.OpEnergyStrategyContext">
-        update adm_op_energy_strategy_context
-        <trim prefix="SET" suffixOverrides=",">
-            <if test="strategyCode != null and strategyCode != ''">strategy_code = #{strategyCode},</if>
-            <if test="varKey != null and varKey != ''">var_key = #{varKey},</if>
-            <if test="varName != null and varName != ''">var_name = #{varName},</if>
-            <if test="varType != null and varType != ''">var_type = #{varType},</if>
-            <if test="dataType != null">data_type = #{dataType},</if>
-            <if test="defaultValue != null">default_value = #{defaultValue},</if>
-            <if test="valueSource != null">value_source = #{valueSource},</if>
-            <if test="description != null">`description` = #{description},</if>
-            <if test="createTime != null">create_time = #{createTime},</if>
-        </trim>
-        where id = #{id}
-    </update>
-
-    <delete id="deleteContextById" parameterType="Long">
-        delete from adm_op_energy_strategy_context where id = #{id}
-    </delete>
-
-    <delete id="deleteContextByIds" parameterType="String">
-        delete from adm_op_energy_strategy_context where id in 
-        <foreach item="id" collection="array" open="(" separator="," close=")">
-            #{id}
-        </foreach>
-    </delete>
-</mapper>

+ 25 - 4
ems/ems-core/src/main/resources/mapper/ems/OpEnergyStrategyStepMapper.xml

@@ -26,6 +26,9 @@
         <result property="timeout" column="timeout"/>
         <result property="enable" column="enable"/>
         <result property="remark" column="remark"/>
+        <result property="loopMaxCount" column="loop_max_count"/>
+        <result property="loopInterval" column="loop_interval"/>
+        <result property="loopCondition" column="loop_condition"/>
         <result property="createTime" column="create_time"/>
         <result property="updateTime" column="update_time"/>
     </resultMap>
@@ -34,7 +37,8 @@
         select id, 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, create_time, update_time
+               retry_interval, continue_on_fail, timeout, enable, remark, loop_max_count, loop_interval, loop_condition,
+               create_time, update_time
         from adm_op_energy_strategy_step
     </sql>
 
@@ -72,8 +76,11 @@
             <if test="retryInterval != null">retry_interval,</if>
             <if test="continueOnFail != null">continue_on_fail,</if>
             <if test="timeout != null">timeout,</if>
-            <if test="enable != null">enable,</if>
+            <if test="enable != null">`enable`,</if>
             <if test="remark != null">remark,</if>
+            <if test="loopMaxCount != null">loop_max_count,</if>
+            <if test="loopInterval != null">loop_interval,</if>
+            <if test="loopCondition != null">loop_condition,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="strategyCode != null and strategyCode != ''">#{strategyCode},</if>
@@ -98,6 +105,9 @@
             <if test="timeout != null">#{timeout},</if>
             <if test="enable != null">#{enable},</if>
             <if test="remark != null">#{remark},</if>
+            <if test="loopMaxCount != null">#{loopMaxCount},</if>
+            <if test="loopInterval != null">#{loopInterval},</if>
+            <if test="loopCondition != null">#{loopCondition},</if>
         </trim>
     </insert>
 
@@ -124,6 +134,9 @@
             <if test="timeout != null">timeout = #{timeout},</if>
             <if test="enable != null">enable = #{enable},</if>
             <if test="remark != null">remark = #{remark},</if>
+            <if test="loopMaxCount != null">loop_max_count = #{loopMaxCount},</if>
+            <if test="loopInterval != null">loop_interval = #{loopInterval},</if>
+            <if test="loopCondition != null">loop_condition = #{loopCondition},</if>
         </trim>
         where id = #{id}
     </update>
@@ -148,7 +161,7 @@
         (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)
+        retry_interval, continue_on_fail, timeout, enable, remark, loop_max_count, loop_interval, loop_condition)
         values
         <foreach collection="list" item="item" separator=",">
             (#{item.strategyCode}, #{item.stepCode}, #{item.stepName}, #{item.stepType},
@@ -156,7 +169,15 @@
             #{item.targetObjCode}, #{item.targetModelCode}, #{item.abilityKey}, #{item.abilityParam},
             #{item.paramSource}, #{item.paramMapping}, #{item.delaySeconds}, #{item.retryOnFail},
             #{item.retryTimes}, #{item.retryInterval}, #{item.continueOnFail}, #{item.timeout},
-            #{item.enable}, #{item.remark})
+            #{item.enable}, #{item.remark}, #{item.loopMaxCount}, #{item.loopInterval}, #{item.loopCondition})
         </foreach>
     </insert>
+
+    <select id="selectStepsByParentCode" resultMap="StepResult">
+        <include refid="selectStepVo"/>
+        WHERE strategy_code = #{strategyCode}
+        AND parent_step_code = #{parentStepCode}
+        AND del_flag = '0'
+        ORDER BY step_index ASC
+    </select>
 </mapper>

+ 0 - 1
ems/sql/ems_init_data.sql

@@ -1781,7 +1781,6 @@ INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `mo
 INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `model_code`, `man_facturer`, `contact_person`, `contact_number`, `maintainer_person`, `maintainer_number`, `descr`) VALUES ('SYS_DLJK', '电力监控系统', '电力监控', 'M_W4_SYS_ELEC_MONITOR', '安科瑞', 'gzl', '1212121121', 'gzl', '1212221111', NULL);
 INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `model_code`, `man_facturer`, `contact_person`, `contact_number`, `maintainer_person`, `maintainer_number`, `descr`) VALUES ('SYS_GF', '光伏系统', '光伏', 'M_TEST', '南通', '张', '1380000', '李工', '123123', NULL);
 INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `model_code`, `man_facturer`, `contact_person`, `contact_number`, `maintainer_person`, `maintainer_number`, `descr`) VALUES ('SYS_CN', '储能系统', '储能', 'M_TEST','储能厂商', '王', '122112', '陈', '21212', NULL);
-INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `model_code`, `man_facturer`, `contact_person`, `contact_number`, `maintainer_person`, `maintainer_number`, `descr`) VALUES ('SYS_BA', 'BA', '楼控', 'M_TEST','楼控', '李', '12', '王', '121', NULL);
 INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `model_code`, `man_facturer`, `contact_person`, `contact_number`, `maintainer_person`, `maintainer_number`, `descr`) VALUES ('SYS_CD', '充电桩系统', '充电桩', 'M_TEST', '特来电', '张三', '1212121121', '刘工', '1212221111', NULL);
 INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `model_code`, `man_facturer`, `contact_person`, `contact_number`, `maintainer_person`, `maintainer_number`, `descr`) VALUES ('SYS_GCC', '光储充系统', '光储充', 'M_TEST', '光储充厂商', '张三', '1212121121', '刘工', '1212221111', NULL);
 INSERT INTO `adm_ems_subsystem` (`system_code`, `system_name`, `short_name`, `model_code`, `man_facturer`, `contact_person`, `contact_number`, `maintainer_person`, `maintainer_number`, `descr`) VALUES ('SYS_GCZR', '光储直柔系统', '光储直柔', 'M_TEST', '光储直柔厂商', '张三', '1212121121', '刘工', '1212221111', NULL);

+ 2 - 2
ems/sql/ems_init_data_test.sql

@@ -122,5 +122,5 @@ 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', 2, 'overload', '过载', '功率过载', 'e-gy-0001', '0x0001');
-INSERT INTO `adm_ems_obj_event` (`model_code`, `event_type`, `event_key`, `event_name`, `event_desc`, `event_code`, `ext_event_code`) VALUES ('M_W2', 2, 'undervoltage', '欠压', '电压不足', 'e-gy-0002', '0x0002');
+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);

+ 3 - 21
ems/sql/ems_server.sql

@@ -1123,6 +1123,9 @@ CREATE TABLE adm_op_energy_strategy_step (
   `timeout` INT DEFAULT 60 COMMENT '超时时间(秒)',
   `enable` INT DEFAULT 1 COMMENT '是否启用',
   `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 '循环跳出条件表达式',
   `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (`id`),
@@ -1183,27 +1186,6 @@ CREATE TABLE adm_op_energy_strategy_step_log (
   KEY `idx_strategy_step` (`strategy_code`, `step_code`)
 ) ENGINE=INNODB AUTO_INCREMENT=1 COMMENT='策略步骤执行日志表';
 
-
--- ----------------------------
--- 策略上下文变量表
--- ----------------------------
-drop table if exists adm_op_energy_strategy_context;
-CREATE TABLE adm_op_energy_strategy_context (
-  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '序号',
-  `strategy_code` VARCHAR(16) NOT NULL COMMENT '策略代码',
-  `var_key` VARCHAR(64) NOT NULL COMMENT '变量键',
-  `var_name` VARCHAR(128) NOT NULL COMMENT '变量名称',
-  `var_type` VARCHAR(32) NOT NULL COMMENT '变量类型:INPUT-输入,OUTPUT-输出,TEMP-临时',
-  `data_type` VARCHAR(32) DEFAULT 'STRING' COMMENT '数据类型:STRING,NUMBER,BOOLEAN,OBJECT',
-  `default_value` TEXT DEFAULT NULL COMMENT '默认值',
-  `value_source` VARCHAR(64) DEFAULT NULL COMMENT '值来源:设备代码.属性键',
-  `description` VARCHAR(256) DEFAULT NULL COMMENT '描述',
-  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `ux_strategy_var` (`strategy_code`, `var_key`)
-) ENGINE=INNODB AUTO_INCREMENT=1 COMMENT='策略上下文变量表';
-
-
 -- ----------------------------
 -- 碳核算账户配置表
 -- ----------------------------