diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/ConditionTriggerTaskExecutor.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/ConditionTriggerTaskExecutor.java
new file mode 100644
index 00000000..92f87225
--- /dev/null
+++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/ConditionTriggerTaskExecutor.java
@@ -0,0 +1,218 @@
+package com.fastbee.iot.data.executor;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.fastbee.common.constant.FastBeeConstant;
+import com.fastbee.common.core.mq.message.DeviceMessage;
+import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
+import com.fastbee.common.utils.StringUtils;
+import com.fastbee.iot.data.service.IDeviceMessageService;
+import com.fastbee.iot.domain.ModbusJob;
+import com.fastbee.iot.service.IModbusJobService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 条件触发任务执行器(cycleType=4)
+ *
+ * 每次设备上报属性/功能数据时,检查是否存在满足条件的 Modbus 任务,
+ * 满足则立即下发对应的 Modbus 指令(与上电触发使用同一套指令下发通道)。
+ *
+ * 条件存储在 {@code iot_modbus_job.remark} 字段中,JSON 格式:
+ *
{"identifier":"temp","operator":">","value":"50"}
+ * 支持的 operator:{@code = != > < >= <=}
+ *
+ * @author Sumuxi
+ */
+@Slf4j
+@Component
+public class ConditionTriggerTaskExecutor {
+
+ @Resource
+ private IModbusJobService modbusJobService;
+
+ @Resource
+ private IDeviceMessageService deviceMessageService;
+
+ /**
+ * 异步检查并执行条件触发任务。
+ * 由 {@code DataHandlerImpl.reportData()} 在每次属性/功能上报后调用。
+ *
+ * @param serialNumber 上报数据的设备编号(子设备或直连设备)
+ * @param thingsModelSimpleItems 本次上报的物模型值列表
+ */
+ @Async(FastBeeConstant.TASK.TRIGGER_POWER_ON_TASK_ASYNC)
+ public void checkAndTrigger(String serialNumber, List thingsModelSimpleItems) {
+ if (StringUtils.isEmpty(serialNumber) || CollectionUtils.isEmpty(thingsModelSimpleItems)) {
+ return;
+ }
+
+ // 查询该设备所有状态正常的条件触发任务
+ ModbusJob query = new ModbusJob();
+ query.setSubSerialNumber(serialNumber);
+ query.setStatus("0");
+ query.setCycleType(4);
+ List jobList = modbusJobService.selectModbusJobList(query);
+ if (CollectionUtils.isEmpty(jobList)) {
+ return;
+ }
+
+ for (ModbusJob job : jobList) {
+ try {
+ if (isConditionMet(job, thingsModelSimpleItems)) {
+ dispatchCommand(serialNumber, job);
+ }
+ } catch (Exception e) {
+ log.error("[条件触发] 执行异常,设备:{},任务ID:{}", serialNumber, job.getTaskId(), e);
+ }
+ }
+ }
+
+ /**
+ * 判断本次上报数据是否满足任务配置的条件。
+ *
+ * 支持两种 remark 格式:
+ *
+ * - 多条件:{@code {"logic":"and","conditions":[{...},{...}]}}
+ *
+ * logic 为 "or" 时任一条件满足即触发;默认("and")要求全部满足。
+ */
+ private boolean isConditionMet(ModbusJob job, List thingsModelSimpleItems) {
+ String remark = job.getRemark();
+ if (StringUtils.isEmpty(remark)) {
+ log.warn("[条件触发] 任务 {} remark 为空,跳过", job.getTaskId());
+ return false;
+ }
+
+ JSONObject json;
+ try {
+ json = JSON.parseObject(remark);
+ } catch (Exception e) {
+ log.warn("[条件触发] 任务 {} remark 解析失败:{}", job.getTaskId(), remark);
+ return false;
+ }
+
+ JSONArray conditions = json.getJSONArray("conditions");
+ if (conditions == null || conditions.isEmpty()) {
+ log.warn("[条件触发] 任务 {} conditions 为空", job.getTaskId());
+ return false;
+ }
+ String logic = json.getString("logic");
+ boolean isOr = "or".equalsIgnoreCase(logic);
+ for (int i = 0; i < conditions.size(); i++) {
+ JSONObject cond = conditions.getJSONObject(i);
+ boolean met = evaluateSingleCondition(cond, thingsModelSimpleItems, job.getTaskId());
+ if (isOr && met) return true;
+ if (!isOr && !met) return false;
+ }
+ return !isOr; // AND:全部通过; OR:无一通过
+ }
+
+ /**
+ * 评估单个条件对象(含 identifier / operator / value 字段)。
+ */
+ private boolean evaluateSingleCondition(JSONObject condition,
+ List items,
+ Long taskId) {
+ String identifier = condition.getString("identifier");
+ String operator = condition.getString("operator");
+ String condValue = condition.getString("value");
+
+ if (StringUtils.isEmpty(identifier) || StringUtils.isEmpty(operator) || condValue == null) {
+ log.warn("[条件触发] 任务 {} 条件配置不完整:{}", taskId, condition);
+ return false;
+ }
+
+ String reportedValue = null;
+ for (ThingsModelSimpleItem item : items) {
+ if (identifier.equals(item.getId())) {
+ reportedValue = item.getValue();
+ break;
+ }
+ }
+ if (reportedValue == null) {
+ // 本次上报不含该属性,条件视为不满足
+ return false;
+ }
+
+ return evaluate(reportedValue, operator, condValue);
+ }
+
+ /**
+ * 条件表达式计算。
+ * 优先尝试数值比较,失败则降级为字符串比较。
+ */
+ private boolean evaluate(String actual, String operator, String expected) {
+ try {
+ BigDecimal a = new BigDecimal(actual.trim());
+ BigDecimal b = new BigDecimal(expected.trim());
+ int cmp = a.compareTo(b);
+ switch (operator) {
+ case "=": return cmp == 0;
+ case "!=": return cmp != 0;
+ case ">": return cmp > 0;
+ case "<": return cmp < 0;
+ case ">=": return cmp >= 0;
+ case "<=": return cmp <= 0;
+ default:
+ log.warn("[条件触发] 不支持的操作符:{}", operator);
+ return false;
+ }
+ } catch (NumberFormatException e) {
+ // 非数值,降级为字符串等于/不等于
+ switch (operator) {
+ case "=": return actual.equals(expected);
+ case "!=": return !actual.equals(expected);
+ default:
+ log.warn("[条件触发] 非数值不支持操作符 {},actual={}", operator, actual);
+ return false;
+ }
+ }
+ }
+
+ /**
+ * 下发 Modbus 指令(HEX 格式)。
+ * 复用与上电触发相同的消息通道。
+ */
+ private void dispatchCommand(String serialNumber, ModbusJob job) {
+ String command = job.getCommand();
+ if (StringUtils.isEmpty(command)) {
+ log.warn("[条件触发] 任务 {} 指令为空,跳过下发", job.getTaskId());
+ return;
+ }
+
+ DeviceMessage msg = new DeviceMessage();
+ msg.setSerialNumber(serialNumber);
+ msg.setTopicName("/function/get");
+ msg.setDataType("hex");
+ msg.setMessage(command);
+
+ try {
+ deviceMessageService.messagePost(msg);
+ // 记录执行时间
+ updateExecuteTime(job.getTaskId());
+ log.info("[条件触发] 下发成功,设备:{},任务ID:{},指令:{}", serialNumber, job.getTaskId(), command);
+ } catch (Exception e) {
+ log.error("[条件触发] 下发异常,设备:{},任务ID:{}", serialNumber, job.getTaskId(), e);
+ }
+ }
+
+ /**
+ * 更新任务最近执行时间。
+ */
+ private void updateExecuteTime(Long taskId) {
+ if (taskId == null) return;
+ ModbusJob update = new ModbusJob();
+ update.setTaskId(taskId);
+ update.setExecuteTime(new Date());
+ modbusJobService.updateModbusJob(update);
+ }
+}
diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/ruleEngine/SceneContext.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/ruleEngine/SceneContext.java
index 718e6dd6..090de17b 100644
--- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/ruleEngine/SceneContext.java
+++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/ruleEngine/SceneContext.java
@@ -89,6 +89,7 @@ public class SceneContext {
/**
* 处理规则脚本
* 静默时间:silent,在静默时间内即使满足触发条件也不执行动作
+ *
* @return
*/
private boolean process(String json) throws InterruptedException {
@@ -96,7 +97,8 @@ public class SceneContext {
ScriptTemplate scriptTemplate = JSON.parseObject(json, ScriptTemplate.class);
String requestId = "scene/" + scriptTemplate.getChainName();
scene_logger.info("[{}]=====+>场景联动规则开始执行!", requestId);
- scene_logger.info("[{}]=====+>规则引擎脚本配置:{},规则引擎上下文:{}", requestId, JSON.toJSONString(scriptTemplate), JSON.toJSONString(this));
+ scene_logger.info("[{}]=====+>规则引擎脚本配置:{},规则引擎上下文:{}", requestId, JSON.toJSONString(scriptTemplate),
+ JSON.toJSONString(this));
// MDC.put("id", scriptTemplate.getChainName());
// 触发器
if (scriptTemplate.getPurpose() == 2) {
@@ -513,7 +515,8 @@ public class SceneContext {
alertPushParams.setDeviceId(device.getDeviceId());
alertPushParams.setDeviceName(device.getDeviceName());
alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber());
- // List deviceUserList = deviceUserService.getDeviceUserAndShare(device.getDeviceId());
+ // List deviceUserList =
+ // deviceUserService.getDeviceUserAndShare(device.getDeviceId());
// 多租户改版查询自己配置的告警用户 默认优先推送本账号超级管理员
Set userIdSet = new HashSet<>();
try {
@@ -547,7 +550,8 @@ public class SceneContext {
return alertPushParams;
}
- private AlertLog getTerminalUserAlertLog(SceneTerminalUserVO sceneTerminalUserVO, Device device, SceneThingsModelItem sceneThingsModelItem) {
+ private AlertLog getTerminalUserAlertLog(SceneTerminalUserVO sceneTerminalUserVO, Device device,
+ SceneThingsModelItem sceneThingsModelItem) {
AlertLog alertLog = new AlertLog();
alertLog.setAlertName("设备告警");
alertLog.setAlertLevel(1L);
diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DataHandlerImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DataHandlerImpl.java
index 8a9546bb..7eae4549 100644
--- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DataHandlerImpl.java
+++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DataHandlerImpl.java
@@ -17,6 +17,7 @@ import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.date.LocalDateTimeUtils;
import com.fastbee.common.utils.gateway.mq.TopicsUtils;
import com.fastbee.iot.cache.SceneModelTagCache;
+import com.fastbee.iot.data.executor.ConditionTriggerTaskExecutor;
import com.fastbee.iot.data.service.IDataHandler;
import com.fastbee.iot.data.service.IMqttMessagePublish;
import com.fastbee.iot.data.service.IRuleEngine;
@@ -59,6 +60,8 @@ public class DataHandlerImpl implements IDataHandler {
@Resource
private IRuleEngine ruleEngine;
@Resource
+ private ConditionTriggerTaskExecutor conditionTriggerExecutor;
+ @Resource
private MqttRemoteManager remoteManager;
@Resource
private TopicsUtils topicsUtils;
@@ -100,6 +103,8 @@ public class DataHandlerImpl implements IDataHandler {
// todo 只有设备上报进入规则引擎流程
if (bo.isRuleEngine() && !bo.getSerialNumber().startsWith("server-")) {
ruleEngine.ruleMatch(bo);
+ // 条件触发任务检查:根据本次上报值决定是否下发 Modbus 指令
+ conditionTriggerExecutor.checkAndTrigger(bo.getSerialNumber(), result);
}
// 发送至前端
PushMessageBo messageBo = new PushMessageBo();
diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusJob.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusJob.java
index 67e7c89b..d4a43d23 100644
--- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusJob.java
+++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusJob.java
@@ -84,13 +84,13 @@ public class ModbusJob extends BaseEntity {
private String remarkStr;
/**
- * 时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发)
+ * 时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发,4-条件触发)
*/
@ApiModelProperty("时间周期类型")
private Integer cycleType;
/**
- * 执行时间(仅设备上电触发任务使用,记录任务执行的时间)
+ * 执行时间(仅设备上电触发/条件触发任务使用,记录最近一次执行的时间)
*/
@ApiModelProperty("执行时间")
private Date executeTime;
diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ModbusJobServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ModbusJobServiceImpl.java
index 0e118fcd..0396fab7 100644
--- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ModbusJobServiceImpl.java
+++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ModbusJobServiceImpl.java
@@ -1,5 +1,6 @@
package com.fastbee.iot.service.impl;
+import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.core.device.DeviceAndProtocol;
import com.fastbee.common.core.mq.SubDeviceBo;
import com.fastbee.common.exception.ServiceException;
@@ -69,9 +70,13 @@ public class ModbusJobServiceImpl implements IModbusJobService {
List modbusJobList = modbusJobMapper.selectModbusJobList(modbusJob);
if (!CollectionUtils.isEmpty(modbusJobList)) {
for (ModbusJob job : modbusJobList) {
- // 直接使用数据库中的 cycleType 判断
- if (job.getCycleType() != null && job.getCycleType() == 3) {
+ Integer ct = job.getCycleType();
+ if (ct != null && ct == 3) {
job.setRemarkStr("设备上电触发");
+ } else if (ct != null && ct == 4) {
+ // 条件触发:remark 是 JSON 对象,解析后展示为可读描述
+ String condDesc = buildConditionDesc(job.getRemark());
+ job.setRemarkStr("条件触发:" + condDesc);
} else {
job.setRemarkStr(JobCronUtils.handleCronCycle(1, job.getRemark()).getDesc());
job.setCycleType(1); // 周期循环(默认)
@@ -83,13 +88,14 @@ public class ModbusJobServiceImpl implements IModbusJobService {
}
/**
- * 检查是否是设备上电触发(直接使用 cycleType 判断)
+ * 检查是否是非定时触发类型(上电触发 cycleType=3 或条件触发 cycleType=4)
+ * 这两种类型不需要创建 Quartz 定时任务,而是通过事件驱动触发。
*
* @param cycleType 时间周期类型
- * @return true表示是设备上电触发
+ * @return true 表示不需要创建定时任务
*/
- private boolean isPowerOnTrigger(Integer cycleType) {
- return cycleType != null && cycleType == 3;
+ private boolean isNonScheduleTrigger(Integer cycleType) {
+ return cycleType != null && (cycleType == 3 || cycleType == 4);
}
/**
@@ -107,7 +113,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
}
// 查询关联的网关设备
- DeviceAndProtocol deviceAndProtocol = deviceService.selectProtocolBySerialNumber(modbusJob.getSubSerialNumber());
+ DeviceAndProtocol deviceAndProtocol = deviceService
+ .selectProtocolBySerialNumber(modbusJob.getSubSerialNumber());
// 如果是网关子设备
if (deviceAndProtocol.getDeviceType() == DeviceType.SUB_GATEWAY.getCode() ||
deviceAndProtocol.getDeviceType() == DeviceType.GATEWAY.getCode()) {
@@ -129,14 +136,12 @@ public class ModbusJobServiceImpl implements IModbusJobService {
deviceJob.setSerialNumber(deviceAndProtocol.getGwSerialNumber());
deviceJob.setProductId(deviceAndProtocol.getGwProductId());
deviceJob.setCreateTime(DateUtils.getNowDate());
- // 处理时间转换
- // 检查是否是设备上电触发(使用 cycleType 判断)
- if (isPowerOnTrigger(modbusJob.getCycleType())) {
- // 设备上电触发不需要 cron 表达式,设置为null
- // DeviceJobServiceImpl.insertJob 会检查并跳过创建定时任务
+ // 上电触发(3)和条件触发(4)不需要 cron 表达式
+ if (isNonScheduleTrigger(modbusJob.getCycleType())) {
deviceJob.setCronExpression(null);
} else {
- SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1, deviceJob.getRemark());
+ SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1,
+ deviceJob.getRemark());
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
}
deviceJobService.insertJob(deviceJob);
@@ -150,7 +155,7 @@ public class ModbusJobServiceImpl implements IModbusJobService {
DeviceJob deviceJob = new DeviceJob();
deviceJob.setDeviceId(deviceAndProtocol.getDeviceId()); // 用网关自己的deviceId
buildSaveDeviceJob(modbusJob, deviceAndProtocol, deviceJob);
- } else {
+ } else {
throw new ServiceException("请先绑定网关");
}
} else {
@@ -200,7 +205,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
if (scheduleChanged) {
// 计算新的 remark / cycleType(允许只传 cycleType 或只传 remark)
String newRemark = modbusJob.getRemark() != null ? modbusJob.getRemark() : existingJob.getRemark();
- Integer newCycleType = modbusJob.getCycleType() != null ? modbusJob.getCycleType() : existingJob.getCycleType();
+ Integer newCycleType = modbusJob.getCycleType() != null ? modbusJob.getCycleType()
+ : existingJob.getCycleType();
if (newCycleType == null) {
newCycleType = 1;
}
@@ -228,15 +234,20 @@ public class ModbusJobServiceImpl implements IModbusJobService {
create.setSerialNumber(oldDeviceJob.getSerialNumber());
create.setProductId(oldDeviceJob.getProductId());
create.setJobType(5);
- create.setJobName(StringUtils.isNotEmpty(oldDeviceJob.getJobName()) ? oldDeviceJob.getJobName() : "modbus poll");
- create.setJobGroup(StringUtils.isNotEmpty(oldDeviceJob.getJobGroup()) ? oldDeviceJob.getJobGroup() : "MODBUS");
- create.setMisfirePolicy(StringUtils.isNotEmpty(oldDeviceJob.getMisfirePolicy()) ? oldDeviceJob.getMisfirePolicy() : "2");
- create.setConcurrent(StringUtils.isNotEmpty(oldDeviceJob.getConcurrent()) ? oldDeviceJob.getConcurrent() : "1");
+ create.setJobName(
+ StringUtils.isNotEmpty(oldDeviceJob.getJobName()) ? oldDeviceJob.getJobName() : "modbus poll");
+ create.setJobGroup(
+ StringUtils.isNotEmpty(oldDeviceJob.getJobGroup()) ? oldDeviceJob.getJobGroup() : "MODBUS");
+ create.setMisfirePolicy(
+ StringUtils.isNotEmpty(oldDeviceJob.getMisfirePolicy()) ? oldDeviceJob.getMisfirePolicy()
+ : "2");
+ create.setConcurrent(
+ StringUtils.isNotEmpty(oldDeviceJob.getConcurrent()) ? oldDeviceJob.getConcurrent() : "1");
create.setIsAdvance(oldDeviceJob.getIsAdvance() != null ? oldDeviceJob.getIsAdvance() : 0);
create.setStatus("0");
create.setRemark(newRemark);
- if (isPowerOnTrigger(newCycleType)) {
+ if (isNonScheduleTrigger(newCycleType)) {
create.setCronExpression(null);
} else {
SceneModelTagCycleVO vo = JobCronUtils.handleCronCycle(newCycleType, newRemark);
@@ -258,13 +269,14 @@ public class ModbusJobServiceImpl implements IModbusJobService {
int rows = modbusJobMapper.updateModbusJob(modbusJob);
// 如发生重绑且旧 jobId 无引用,则删除旧 DeviceJob(同时会清理 quartz trigger/job)
- if (scheduleChanged && oldDeviceJobId != null && modbusJob.getJobId() != null && !oldDeviceJobId.equals(modbusJob.getJobId())) {
+ if (scheduleChanged && oldDeviceJobId != null && modbusJob.getJobId() != null
+ && !oldDeviceJobId.equals(modbusJob.getJobId())) {
ModbusJob q = new ModbusJob();
q.setJobId(oldDeviceJobId);
List left = modbusJobMapper.selectModbusJobList(q);
if (CollectionUtils.isEmpty(left)) {
try {
- deviceJobService.deleteJobByIds(new Long[]{oldDeviceJobId});
+ deviceJobService.deleteJobByIds(new Long[] { oldDeviceJobId });
} catch (SchedulerException e) {
throw new ServiceException(e.getMessage());
}
@@ -332,7 +344,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
case GATEWAY:
// 网关设备需要查询所有子设备的modbus任务
List subDeviceList = subGatewayService.getSubDeviceListByGw(serialNumber);
- List serialNumberList = subDeviceList.stream().map(SubDeviceBo::getSubDeviceNo).collect(Collectors.toList());
+ List serialNumberList = subDeviceList.stream().map(SubDeviceBo::getSubDeviceNo)
+ .collect(Collectors.toList());
// 把网关自身SN也加入查询
serialNumberList.add(serialNumber);
modbusJob.setSubDeviceList(serialNumberList);
@@ -360,9 +373,10 @@ public class ModbusJobServiceImpl implements IModbusJobService {
/**
* 组装并保存设备任务
- * @param modbusJob 待新增的modbus任务
- * @param deviceAndProtocol 设备协议
- * @param deviceJob 设备任务
+ *
+ * @param modbusJob 待新增的modbus任务
+ * @param deviceAndProtocol 设备协议
+ * @param deviceJob 设备任务
*/
private void buildSaveDeviceJob(ModbusJob modbusJob, DeviceAndProtocol deviceAndProtocol, DeviceJob deviceJob) {
deviceJob.setRemark(modbusJob.getRemark());
@@ -379,13 +393,10 @@ public class ModbusJobServiceImpl implements IModbusJobService {
deviceJob.setSerialNumber(deviceAndProtocol.getSerialNumber()); // 网关自己的SN
deviceJob.setProductId(deviceAndProtocol.getProductId());
deviceJob.setCreateTime(DateUtils.getNowDate());
- // 检查是否是设备上电触发(使用 cycleType 判断)
- if (isPowerOnTrigger(modbusJob.getCycleType())) {
- // 设备上电触发不需要 cron 表达式,设置为null
- // DeviceJobServiceImpl.insertJob 会检查并跳过创建定时任务
+ // 上电触发(3)和条件触发(4)不需要 cron 表达式
+ if (isNonScheduleTrigger(modbusJob.getCycleType())) {
deviceJob.setCronExpression(null);
} else {
- // 处理时间转换
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1, deviceJob.getRemark());
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
}
@@ -400,4 +411,48 @@ public class ModbusJobServiceImpl implements IModbusJobService {
modbusJob.setJobId(job.getJobId());
}
}
+
+ /**
+ * 将条件触发的 remark JSON 转换为可读描述,用于列表展示。
+ * 示例:{"identifier":"temperature","operator":">","value":"50"} → "temperature >
+ * 50"
+ */
+ private String buildConditionDesc(String remark) {
+ if (StringUtils.isEmpty(remark)) {
+ return "(条件未配置)";
+ }
+ try {
+ JSONObject json = JSONObject.parseObject(remark);
+ // 多条件格式
+ if (json.containsKey("conditions")) {
+ com.alibaba.fastjson2.JSONArray conditions = json.getJSONArray("conditions");
+ if (conditions == null || conditions.isEmpty()) {
+ return "(条件未配置)";
+ }
+ String logic = json.getString("logic");
+ String sep = "or".equalsIgnoreCase(logic) ? " 或 " : " 且 ";
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < conditions.size(); i++) {
+ JSONObject cond = conditions.getJSONObject(i);
+ String id = cond.getString("identifier");
+ String op = cond.getString("operator");
+ String val = cond.getString("value");
+ if (i > 0)
+ sb.append(sep);
+ sb.append(id).append(op).append(val);
+ }
+ return sb.toString();
+ }
+ // 单条件旧格式
+ String id = json.getString("identifier");
+ String op = json.getString("operator");
+ String val = json.getString("value");
+ if (StringUtils.isEmpty(id) || StringUtils.isEmpty(op) || val == null) {
+ return remark;
+ }
+ return id + " " + op + " " + val;
+ } catch (Exception e) {
+ return remark;
+ }
+ }
}
diff --git a/yack-web/src/views/iot/device/device-modbus-task.vue b/yack-web/src/views/iot/device/device-modbus-task.vue
index 226d3507..7c82d971 100644
--- a/yack-web/src/views/iot/device/device-modbus-task.vue
+++ b/yack-web/src/views/iot/device/device-modbus-task.vue
@@ -66,6 +66,7 @@
周期循环
自定义时间段
设备上电触发
+ 条件触发
-
@@ -83,7 +84,11 @@
-
-
+
+
+ {{ formatModbusScheduleDisplay(scope.row) }}
+
+
编辑
@@ -331,6 +336,84 @@
+
+
+ 条件触发
+
+
+ 当设备上报的指定属性满足条件时,自动下发本条指令
+
+
+
+
+
+
+
+ 条件关系:
+
+ 全部满足(且)
+ 任一满足(或)
+
+
+
+
+
+
+ {{ createForm.conditionLogic === 'or' ? '或' : '且' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
添加条件
+
+
@@ -363,6 +446,26 @@ import { parseTime } from '@/utils/ruoyi';
import { encode } from '@/api/iot/mqttTest';
import { listJob, getJob, delJob, addJob, updateJob, getSlaveId } from '@/api/iot/modbusJob';
import { listGateway } from '@/api/iot/gateway';
+import { listByProductIdAndSerialNumber, cacheJsonThingsModel } from '@/api/iot/model';
+
+/**
+ * 计算型物模型且公式含跨从机引用 {slaveId:N.xxx}(与后端 CalculateUtils 一致)。
+ * 条件触发依赖与上报同一链路的物模型值,跨设备计算量为查询/聚合侧数据,不纳入可选监听属性。
+ */
+function isCrossDeviceCalculatedModel(isCalculated, formula) {
+ const ic = isCalculated === 1 || isCalculated === '1';
+ if (!ic) {
+ return false;
+ }
+ if (formula == null) {
+ return false;
+ }
+ const s = String(formula);
+ if (s.trim() === '') {
+ return false;
+ }
+ return /\{slaveId:\d+\.[^}]+}/.test(s);
+}
export default {
name: 'device-modbus-task',
@@ -515,6 +618,8 @@ export default {
},
cycles1: [{ interval: '300', time: '', week: '', day: '' }],
cycles2: [{ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' }],
+ // 条件触发:当前产品的属性列表({id, name} 结构)
+ conditionProperties: [],
};
},
methods: {
@@ -522,7 +627,7 @@ export default {
showExecuteTime(row) {
const t = row.cycleType;
if (t === 1 || t === 2) return true;
- if (t === 3) return row.status === '1';
+ if (t === 3 || t === 4) return !!row.executeTime;
return false;
},
// 回填时间周期(remark -> cycles1/cycles2)
@@ -672,7 +777,7 @@ export default {
/**计算定时时间*/
let cycle = '';
- // 如果是设备上电触发,不需要 cron 表达式
+ // 设备上电触发(3)和条件触发(4)不需要 cron 表达式
if (this.createForm.cycleType === 3) {
this.createForm.subDeviceId = this.device.deviceId;
this.createForm.subSerialNumber = this.device.serialNumber;
@@ -683,6 +788,31 @@ export default {
this.editDialog = false;
return;
}
+ if (this.createForm.cycleType === 4) {
+ const validConds = (this.createForm.conditions || []).filter(
+ (c) => c.identifier && c.operator && c.value !== undefined && c.value !== ''
+ );
+ if (validConds.length === 0) {
+ this.$message({ type: 'warning', message: '条件触发:至少填写一条完整的条件(属性 + 操作符 + 阈值)' });
+ return;
+ }
+ this.createForm.subDeviceId = this.device.deviceId;
+ this.createForm.subSerialNumber = this.device.serialNumber;
+ this.createForm.command = this.createCode;
+ // 多条件 JSON 存入 remark(统一使用新格式,后端向下兼容旧格式)
+ this.createForm.remark = JSON.stringify({
+ logic: this.createForm.conditionLogic || 'and',
+ conditions: validConds.map((c) => ({
+ identifier: c.identifier,
+ operator: c.operator,
+ value: String(c.value),
+ })),
+ });
+ this.createForm.cronExpression = '';
+ this.submitForm();
+ this.editDialog = false;
+ return;
+ }
// 原有的周期循环和自定义时间段逻辑
const c = this.cycles1.map((item) => {
@@ -707,9 +837,72 @@ export default {
this.editDialog = false;
},
+ /**
+ * 加载属性列表供条件触发下拉使用。
+ * 主设备:listByProductIdAndSerialNumber,含本设备链路上可用的属性与本机/公式内自引用型计算量;
+ * 排除公式中含 {slaveId:N.xxx} 的跨设备计算物模型(与上报值不同路,无法作为条件比较)。
+ * 网关模式:另追加子产品物模型(cacheJson),同样按公式过滤跨设备计算量。
+ */
+ loadConditionProperties() {
+ if (!this.device || !this.device.productId) return;
+ const allProperties = [];
+ const seen = new Set();
+
+ /** 统一格式并追加到列表(去重) */
+ const mergeItem = (id, name, isCalculated) => {
+ if (!id || seen.has(id)) return;
+ seen.add(id);
+ allProperties.push({ id, name, isCalculated: !!isCalculated });
+ };
+
+ // 1. 主设备:产品级 + 设备级计算物模型(type=1 属性),过滤跨设备计算量
+ const mainPromise = listByProductIdAndSerialNumber({
+ productId: this.device.productId,
+ serialNumber: this.device.serialNumber,
+ pageSize: 999,
+ pageNum: 1,
+ }).then((res) => {
+ (res.rows || []).forEach((item) => {
+ // type=1 为属性,只取属性,不取功能/事件
+ if (item.type === 1) {
+ if (isCrossDeviceCalculatedModel(item.isCalculated, item.formula)) {
+ return;
+ }
+ mergeItem(item.identifier, item.modelName, item.isCalculated === 1);
+ }
+ });
+ }).catch(() => {});
+
+ // 2. 网关模式:追加子产品物模型,过滤跨设备计算量
+ const subPromises = [];
+ if (this.isGatewayMode && this.subDeviceList && this.subDeviceList.length > 0) {
+ const subProductIds = [...new Set(
+ this.subDeviceList.map((s) => s.subProductId).filter(Boolean)
+ )];
+ subProductIds.forEach((pid) => {
+ subPromises.push(
+ cacheJsonThingsModel(pid).then((res) => {
+ const data = res.data ? JSON.parse(res.data) : {};
+ (data.properties || []).forEach((p) => {
+ if (isCrossDeviceCalculatedModel(p.isCalculated, p.formula)) {
+ return;
+ }
+ mergeItem(p.id, p.name, p.isCalculated === 1);
+ });
+ }).catch(() => {})
+ );
+ });
+ }
+
+ Promise.all([mainPromise, ...subPromises]).then(() => {
+ this.conditionProperties = allProperties;
+ });
+ },
+
/**打开编辑框 */
openEdit() {
this.resetCreateForm();
+ this.loadConditionProperties();
// 网关设备(deviceType=2)直接轮询:跳过从机地址校验,由用户手动填写
if (this.device && this.device.deviceType === 2) {
this.createForm.path = '';
@@ -747,7 +940,9 @@ export default {
setValue16: '0000',
setValueSwitch: 'Dec',
status: '0',
- cycleType: 1, // 1周期,2自定义
+ cycleType: 1, // 1周期,2自定义,3上电触发,4条件触发
+ conditionLogic: 'and',
+ conditions: [{ identifier: '', operator: '>', value: '' }],
};
this.createCode = '';
},
@@ -886,6 +1081,8 @@ export default {
listGateway({ gwDeviceId: this.device.deviceId, pageSize: 999 })
.then((res) => {
this.subDeviceList = res.rows || [];
+ // 子设备加载完成后,刷新条件触发属性下拉(合并子设备物模型)
+ this.loadConditionProperties();
})
.finally(() => {
this.subListLoading = false;
@@ -936,6 +1133,7 @@ export default {
// 编辑轮询任务(修改时间周期/指令等)
handleEdit(row) {
this.resetCreateForm();
+ this.loadConditionProperties();
this.editName = true;
getJob(row.taskId).then((res) => {
const data = (res && res.data) || {};
@@ -945,11 +1143,100 @@ export default {
// 回填周期
const cycleType = data.cycleType != null ? data.cycleType : 1;
this.createForm.cycleType = cycleType;
- this.parseRemarkToCycles(cycleType, data.remark);
+ // 回填条件触发配置(兼容新多条件格式和旧单条件格式)
+ if (cycleType === 4 && data.remark) {
+ try {
+ const parsed = JSON.parse(data.remark);
+ if (parsed.conditions && Array.isArray(parsed.conditions)) {
+ // 新多条件格式
+ this.createForm.conditionLogic = parsed.logic || 'and';
+ this.createForm.conditions = parsed.conditions.map((c) => ({
+ identifier: c.identifier || '',
+ operator: c.operator || '>',
+ value: c.value !== undefined ? c.value : '',
+ }));
+ } else {
+ // 旧单条件格式
+ this.createForm.conditionLogic = 'and';
+ this.createForm.conditions = [{
+ identifier: parsed.identifier || '',
+ operator: parsed.operator || '>',
+ value: parsed.value !== undefined ? parsed.value : '',
+ }];
+ }
+ } catch (e) {
+ this.createForm.conditionLogic = 'and';
+ this.createForm.conditions = [{ identifier: '', operator: '>', value: '' }];
+ }
+ } else {
+ this.parseRemarkToCycles(cycleType, data.remark);
+ }
// 打开弹窗
this.editDialog = true;
});
},
+ /** 列表「定时时间」列:条件触发时避免展示原始 JSON */
+ formatModbusScheduleDisplay(row) {
+ const ct = row.cycleType;
+ if (ct === 3) {
+ return '设备上电触发';
+ }
+ if (ct === 4) {
+ return this.formatConditionTriggerRemark(row);
+ }
+ if (row.remarkStr != null && row.remarkStr !== '') {
+ return row.remarkStr;
+ }
+ return '-';
+ },
+ /** 将条件触发的 remark JSON 转为可读句,如:条件触发:T > 20 且 OP3 < 11 */
+ formatConditionTriggerRemark(row) {
+ let raw = row.remark;
+ if (raw == null || String(raw).trim() === '') {
+ const rs = row.remarkStr;
+ if (typeof rs === 'string' && rs.indexOf('条件触发:') === 0) {
+ raw = rs.replace(/^条件触发:\s*/, '');
+ }
+ }
+ if (raw == null || String(raw).trim() === '') {
+ return '条件触发:—';
+ }
+ const str = String(raw).trim();
+ if (str.charAt(0) !== '{' && str.charAt(0) !== '[') {
+ return '条件触发:' + (str.length > 120 ? str.slice(0, 120) + '…' : str);
+ }
+ try {
+ const obj = JSON.parse(str);
+ if (obj.conditions && Array.isArray(obj.conditions) && obj.conditions.length) {
+ const isOr = obj.logic && String(obj.logic).toLowerCase() === 'or';
+ const sep = isOr ? ' 或 ' : ' 且 ';
+ const parts = obj.conditions
+ .map((c) => {
+ if (!c) {
+ return '';
+ }
+ const id = c.identifier != null ? c.identifier : '';
+ const op = c.operator != null ? c.operator : '';
+ const val = c.value != null ? c.value : '';
+ if (id === '' && op === '' && val === '') {
+ return '';
+ }
+ return `${id} ${op} ${val}`.replace(/\s+/g, ' ').trim();
+ })
+ .filter(Boolean);
+ return '条件触发:' + (parts.length ? parts.join(sep) : '—');
+ }
+ if (obj.identifier != null || obj.operator) {
+ const id = obj.identifier != null ? obj.identifier : '';
+ const op = obj.operator != null ? obj.operator : '';
+ const val = obj.value != null ? obj.value : '';
+ return '条件触发:' + `${id} ${op} ${val}`.replace(/\s+/g, ' ').trim();
+ }
+ } catch (e) {
+ /* 保持下方兜底 */
+ }
+ return '条件触发:' + (str.length > 100 ? str.slice(0, 100) + '…' : str);
+ },
// 格式化显示CRON描述
formatCronDisplay(item) {
let result = '';
@@ -1024,9 +1311,21 @@ export default {
this.cycles1 = [{ interval: 'hour', time: '', week: '', day: '' }];
} else if (val === 3) {
// 设备上电触发,不需要时间周期配置
- // 可以清空或保留,但不使用
+ } else if (val === 4) {
+ // 条件触发,不需要时间周期配置
+ if (!this.createForm.conditions || this.createForm.conditions.length === 0) {
+ this.createForm.conditions = [{ identifier: '', operator: '>', value: '' }];
+ }
}
},
+ /** 条件触发:新增一行条件 */
+ addCondition() {
+ this.createForm.conditions.push({ identifier: '', operator: '>', value: '' });
+ },
+ /** 条件触发:删除指定行条件 */
+ removeCondition(idx) {
+ this.createForm.conditions.splice(idx, 1);
+ },
handleCustomIntervalDelete(index) {
this.cycles2.splice(index, 1);
},
@@ -1081,6 +1380,15 @@ $right-btn-color: #1890ff;
min-height: 400px;
}
+.modbus-task-schedule-text {
+ white-space: normal;
+ line-height: 1.45;
+ word-break: break-word;
+ text-align: left;
+ display: inline-block;
+ max-width: 100%;
+}
+
.sub-device-side-panel {
border: 1px solid $border-color;
border-radius: 4px;