20260426:条件触发任务
This commit is contained in:
parent
d433c25f7b
commit
49cd12e1e3
@ -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)
|
||||||
|
* <p>
|
||||||
|
* 每次设备上报属性/功能数据时,检查是否存在满足条件的 Modbus 任务,
|
||||||
|
* 满足则立即下发对应的 Modbus 指令(与上电触发使用同一套指令下发通道)。
|
||||||
|
* <p>
|
||||||
|
* 条件存储在 {@code iot_modbus_job.remark} 字段中,JSON 格式:
|
||||||
|
* <pre>{"identifier":"temp","operator":">","value":"50"}</pre>
|
||||||
|
* 支持的 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<ThingsModelSimpleItem> thingsModelSimpleItems) {
|
||||||
|
if (StringUtils.isEmpty(serialNumber) || CollectionUtils.isEmpty(thingsModelSimpleItems)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该设备所有状态正常的条件触发任务
|
||||||
|
ModbusJob query = new ModbusJob();
|
||||||
|
query.setSubSerialNumber(serialNumber);
|
||||||
|
query.setStatus("0");
|
||||||
|
query.setCycleType(4);
|
||||||
|
List<ModbusJob> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断本次上报数据是否满足任务配置的条件。
|
||||||
|
* <p>
|
||||||
|
* 支持两种 remark 格式:
|
||||||
|
* <ul>
|
||||||
|
* <li>多条件:{@code {"logic":"and","conditions":[{...},{...}]}}</li>
|
||||||
|
* </ul>
|
||||||
|
* logic 为 "or" 时任一条件满足即触发;默认("and")要求全部满足。
|
||||||
|
*/
|
||||||
|
private boolean isConditionMet(ModbusJob job, List<ThingsModelSimpleItem> 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<ThingsModelSimpleItem> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -89,6 +89,7 @@ public class SceneContext {
|
|||||||
/**
|
/**
|
||||||
* 处理规则脚本
|
* 处理规则脚本
|
||||||
* 静默时间:silent,在静默时间内即使满足触发条件也不执行动作
|
* 静默时间:silent,在静默时间内即使满足触发条件也不执行动作
|
||||||
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private boolean process(String json) throws InterruptedException {
|
private boolean process(String json) throws InterruptedException {
|
||||||
@ -96,7 +97,8 @@ public class SceneContext {
|
|||||||
ScriptTemplate scriptTemplate = JSON.parseObject(json, ScriptTemplate.class);
|
ScriptTemplate scriptTemplate = JSON.parseObject(json, ScriptTemplate.class);
|
||||||
String requestId = "scene/" + scriptTemplate.getChainName();
|
String requestId = "scene/" + scriptTemplate.getChainName();
|
||||||
scene_logger.info("[{}]=====+>场景联动规则开始执行!", requestId);
|
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());
|
// MDC.put("id", scriptTemplate.getChainName());
|
||||||
// 触发器
|
// 触发器
|
||||||
if (scriptTemplate.getPurpose() == 2) {
|
if (scriptTemplate.getPurpose() == 2) {
|
||||||
@ -513,7 +515,8 @@ public class SceneContext {
|
|||||||
alertPushParams.setDeviceId(device.getDeviceId());
|
alertPushParams.setDeviceId(device.getDeviceId());
|
||||||
alertPushParams.setDeviceName(device.getDeviceName());
|
alertPushParams.setDeviceName(device.getDeviceName());
|
||||||
alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber());
|
alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber());
|
||||||
// List<DeviceUser> deviceUserList = deviceUserService.getDeviceUserAndShare(device.getDeviceId());
|
// List<DeviceUser> deviceUserList =
|
||||||
|
// deviceUserService.getDeviceUserAndShare(device.getDeviceId());
|
||||||
// 多租户改版查询自己配置的告警用户 默认优先推送本账号超级管理员
|
// 多租户改版查询自己配置的告警用户 默认优先推送本账号超级管理员
|
||||||
Set<Long> userIdSet = new HashSet<>();
|
Set<Long> userIdSet = new HashSet<>();
|
||||||
try {
|
try {
|
||||||
@ -547,7 +550,8 @@ public class SceneContext {
|
|||||||
return alertPushParams;
|
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 alertLog = new AlertLog();
|
||||||
alertLog.setAlertName("设备告警");
|
alertLog.setAlertName("设备告警");
|
||||||
alertLog.setAlertLevel(1L);
|
alertLog.setAlertLevel(1L);
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import com.fastbee.common.utils.StringUtils;
|
|||||||
import com.fastbee.common.utils.date.LocalDateTimeUtils;
|
import com.fastbee.common.utils.date.LocalDateTimeUtils;
|
||||||
import com.fastbee.common.utils.gateway.mq.TopicsUtils;
|
import com.fastbee.common.utils.gateway.mq.TopicsUtils;
|
||||||
import com.fastbee.iot.cache.SceneModelTagCache;
|
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.IDataHandler;
|
||||||
import com.fastbee.iot.data.service.IMqttMessagePublish;
|
import com.fastbee.iot.data.service.IMqttMessagePublish;
|
||||||
import com.fastbee.iot.data.service.IRuleEngine;
|
import com.fastbee.iot.data.service.IRuleEngine;
|
||||||
@ -59,6 +60,8 @@ public class DataHandlerImpl implements IDataHandler {
|
|||||||
@Resource
|
@Resource
|
||||||
private IRuleEngine ruleEngine;
|
private IRuleEngine ruleEngine;
|
||||||
@Resource
|
@Resource
|
||||||
|
private ConditionTriggerTaskExecutor conditionTriggerExecutor;
|
||||||
|
@Resource
|
||||||
private MqttRemoteManager remoteManager;
|
private MqttRemoteManager remoteManager;
|
||||||
@Resource
|
@Resource
|
||||||
private TopicsUtils topicsUtils;
|
private TopicsUtils topicsUtils;
|
||||||
@ -100,6 +103,8 @@ public class DataHandlerImpl implements IDataHandler {
|
|||||||
// todo 只有设备上报进入规则引擎流程
|
// todo 只有设备上报进入规则引擎流程
|
||||||
if (bo.isRuleEngine() && !bo.getSerialNumber().startsWith("server-")) {
|
if (bo.isRuleEngine() && !bo.getSerialNumber().startsWith("server-")) {
|
||||||
ruleEngine.ruleMatch(bo);
|
ruleEngine.ruleMatch(bo);
|
||||||
|
// 条件触发任务检查:根据本次上报值决定是否下发 Modbus 指令
|
||||||
|
conditionTriggerExecutor.checkAndTrigger(bo.getSerialNumber(), result);
|
||||||
}
|
}
|
||||||
// 发送至前端
|
// 发送至前端
|
||||||
PushMessageBo messageBo = new PushMessageBo();
|
PushMessageBo messageBo = new PushMessageBo();
|
||||||
|
|||||||
@ -84,13 +84,13 @@ public class ModbusJob extends BaseEntity {
|
|||||||
private String remarkStr;
|
private String remarkStr;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发)
|
* 时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发,4-条件触发)
|
||||||
*/
|
*/
|
||||||
@ApiModelProperty("时间周期类型")
|
@ApiModelProperty("时间周期类型")
|
||||||
private Integer cycleType;
|
private Integer cycleType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行时间(仅设备上电触发任务使用,记录任务执行的时间)
|
* 执行时间(仅设备上电触发/条件触发任务使用,记录最近一次执行的时间)
|
||||||
*/
|
*/
|
||||||
@ApiModelProperty("执行时间")
|
@ApiModelProperty("执行时间")
|
||||||
private Date executeTime;
|
private Date executeTime;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.fastbee.iot.service.impl;
|
package com.fastbee.iot.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.fastbee.common.core.device.DeviceAndProtocol;
|
import com.fastbee.common.core.device.DeviceAndProtocol;
|
||||||
import com.fastbee.common.core.mq.SubDeviceBo;
|
import com.fastbee.common.core.mq.SubDeviceBo;
|
||||||
import com.fastbee.common.exception.ServiceException;
|
import com.fastbee.common.exception.ServiceException;
|
||||||
@ -69,9 +70,13 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
List<ModbusJob> modbusJobList = modbusJobMapper.selectModbusJobList(modbusJob);
|
List<ModbusJob> modbusJobList = modbusJobMapper.selectModbusJobList(modbusJob);
|
||||||
if (!CollectionUtils.isEmpty(modbusJobList)) {
|
if (!CollectionUtils.isEmpty(modbusJobList)) {
|
||||||
for (ModbusJob job : modbusJobList) {
|
for (ModbusJob job : modbusJobList) {
|
||||||
// 直接使用数据库中的 cycleType 判断
|
Integer ct = job.getCycleType();
|
||||||
if (job.getCycleType() != null && job.getCycleType() == 3) {
|
if (ct != null && ct == 3) {
|
||||||
job.setRemarkStr("设备上电触发");
|
job.setRemarkStr("设备上电触发");
|
||||||
|
} else if (ct != null && ct == 4) {
|
||||||
|
// 条件触发:remark 是 JSON 对象,解析后展示为可读描述
|
||||||
|
String condDesc = buildConditionDesc(job.getRemark());
|
||||||
|
job.setRemarkStr("条件触发:" + condDesc);
|
||||||
} else {
|
} else {
|
||||||
job.setRemarkStr(JobCronUtils.handleCronCycle(1, job.getRemark()).getDesc());
|
job.setRemarkStr(JobCronUtils.handleCronCycle(1, job.getRemark()).getDesc());
|
||||||
job.setCycleType(1); // 周期循环(默认)
|
job.setCycleType(1); // 周期循环(默认)
|
||||||
@ -83,13 +88,14 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否是设备上电触发(直接使用 cycleType 判断)
|
* 检查是否是非定时触发类型(上电触发 cycleType=3 或条件触发 cycleType=4)
|
||||||
|
* 这两种类型不需要创建 Quartz 定时任务,而是通过事件驱动触发。
|
||||||
*
|
*
|
||||||
* @param cycleType 时间周期类型
|
* @param cycleType 时间周期类型
|
||||||
* @return true表示是设备上电触发
|
* @return true 表示不需要创建定时任务
|
||||||
*/
|
*/
|
||||||
private boolean isPowerOnTrigger(Integer cycleType) {
|
private boolean isNonScheduleTrigger(Integer cycleType) {
|
||||||
return cycleType != null && cycleType == 3;
|
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() ||
|
if (deviceAndProtocol.getDeviceType() == DeviceType.SUB_GATEWAY.getCode() ||
|
||||||
deviceAndProtocol.getDeviceType() == DeviceType.GATEWAY.getCode()) {
|
deviceAndProtocol.getDeviceType() == DeviceType.GATEWAY.getCode()) {
|
||||||
@ -129,14 +136,12 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
deviceJob.setSerialNumber(deviceAndProtocol.getGwSerialNumber());
|
deviceJob.setSerialNumber(deviceAndProtocol.getGwSerialNumber());
|
||||||
deviceJob.setProductId(deviceAndProtocol.getGwProductId());
|
deviceJob.setProductId(deviceAndProtocol.getGwProductId());
|
||||||
deviceJob.setCreateTime(DateUtils.getNowDate());
|
deviceJob.setCreateTime(DateUtils.getNowDate());
|
||||||
// 处理时间转换
|
// 上电触发(3)和条件触发(4)不需要 cron 表达式
|
||||||
// 检查是否是设备上电触发(使用 cycleType 判断)
|
if (isNonScheduleTrigger(modbusJob.getCycleType())) {
|
||||||
if (isPowerOnTrigger(modbusJob.getCycleType())) {
|
|
||||||
// 设备上电触发不需要 cron 表达式,设置为null
|
|
||||||
// DeviceJobServiceImpl.insertJob 会检查并跳过创建定时任务
|
|
||||||
deviceJob.setCronExpression(null);
|
deviceJob.setCronExpression(null);
|
||||||
} else {
|
} else {
|
||||||
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1, deviceJob.getRemark());
|
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1,
|
||||||
|
deviceJob.getRemark());
|
||||||
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
|
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
|
||||||
}
|
}
|
||||||
deviceJobService.insertJob(deviceJob);
|
deviceJobService.insertJob(deviceJob);
|
||||||
@ -200,7 +205,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
if (scheduleChanged) {
|
if (scheduleChanged) {
|
||||||
// 计算新的 remark / cycleType(允许只传 cycleType 或只传 remark)
|
// 计算新的 remark / cycleType(允许只传 cycleType 或只传 remark)
|
||||||
String newRemark = modbusJob.getRemark() != null ? modbusJob.getRemark() : existingJob.getRemark();
|
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) {
|
if (newCycleType == null) {
|
||||||
newCycleType = 1;
|
newCycleType = 1;
|
||||||
}
|
}
|
||||||
@ -228,15 +234,20 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
create.setSerialNumber(oldDeviceJob.getSerialNumber());
|
create.setSerialNumber(oldDeviceJob.getSerialNumber());
|
||||||
create.setProductId(oldDeviceJob.getProductId());
|
create.setProductId(oldDeviceJob.getProductId());
|
||||||
create.setJobType(5);
|
create.setJobType(5);
|
||||||
create.setJobName(StringUtils.isNotEmpty(oldDeviceJob.getJobName()) ? oldDeviceJob.getJobName() : "modbus poll");
|
create.setJobName(
|
||||||
create.setJobGroup(StringUtils.isNotEmpty(oldDeviceJob.getJobGroup()) ? oldDeviceJob.getJobGroup() : "MODBUS");
|
StringUtils.isNotEmpty(oldDeviceJob.getJobName()) ? oldDeviceJob.getJobName() : "modbus poll");
|
||||||
create.setMisfirePolicy(StringUtils.isNotEmpty(oldDeviceJob.getMisfirePolicy()) ? oldDeviceJob.getMisfirePolicy() : "2");
|
create.setJobGroup(
|
||||||
create.setConcurrent(StringUtils.isNotEmpty(oldDeviceJob.getConcurrent()) ? oldDeviceJob.getConcurrent() : "1");
|
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.setIsAdvance(oldDeviceJob.getIsAdvance() != null ? oldDeviceJob.getIsAdvance() : 0);
|
||||||
create.setStatus("0");
|
create.setStatus("0");
|
||||||
create.setRemark(newRemark);
|
create.setRemark(newRemark);
|
||||||
|
|
||||||
if (isPowerOnTrigger(newCycleType)) {
|
if (isNonScheduleTrigger(newCycleType)) {
|
||||||
create.setCronExpression(null);
|
create.setCronExpression(null);
|
||||||
} else {
|
} else {
|
||||||
SceneModelTagCycleVO vo = JobCronUtils.handleCronCycle(newCycleType, newRemark);
|
SceneModelTagCycleVO vo = JobCronUtils.handleCronCycle(newCycleType, newRemark);
|
||||||
@ -258,13 +269,14 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
int rows = modbusJobMapper.updateModbusJob(modbusJob);
|
int rows = modbusJobMapper.updateModbusJob(modbusJob);
|
||||||
|
|
||||||
// 如发生重绑且旧 jobId 无引用,则删除旧 DeviceJob(同时会清理 quartz trigger/job)
|
// 如发生重绑且旧 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();
|
ModbusJob q = new ModbusJob();
|
||||||
q.setJobId(oldDeviceJobId);
|
q.setJobId(oldDeviceJobId);
|
||||||
List<ModbusJob> left = modbusJobMapper.selectModbusJobList(q);
|
List<ModbusJob> left = modbusJobMapper.selectModbusJobList(q);
|
||||||
if (CollectionUtils.isEmpty(left)) {
|
if (CollectionUtils.isEmpty(left)) {
|
||||||
try {
|
try {
|
||||||
deviceJobService.deleteJobByIds(new Long[]{oldDeviceJobId});
|
deviceJobService.deleteJobByIds(new Long[] { oldDeviceJobId });
|
||||||
} catch (SchedulerException e) {
|
} catch (SchedulerException e) {
|
||||||
throw new ServiceException(e.getMessage());
|
throw new ServiceException(e.getMessage());
|
||||||
}
|
}
|
||||||
@ -332,7 +344,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
case GATEWAY:
|
case GATEWAY:
|
||||||
// 网关设备需要查询所有子设备的modbus任务
|
// 网关设备需要查询所有子设备的modbus任务
|
||||||
List<SubDeviceBo> subDeviceList = subGatewayService.getSubDeviceListByGw(serialNumber);
|
List<SubDeviceBo> subDeviceList = subGatewayService.getSubDeviceListByGw(serialNumber);
|
||||||
List<String> serialNumberList = subDeviceList.stream().map(SubDeviceBo::getSubDeviceNo).collect(Collectors.toList());
|
List<String> serialNumberList = subDeviceList.stream().map(SubDeviceBo::getSubDeviceNo)
|
||||||
|
.collect(Collectors.toList());
|
||||||
// 把网关自身SN也加入查询
|
// 把网关自身SN也加入查询
|
||||||
serialNumberList.add(serialNumber);
|
serialNumberList.add(serialNumber);
|
||||||
modbusJob.setSubDeviceList(serialNumberList);
|
modbusJob.setSubDeviceList(serialNumberList);
|
||||||
@ -360,6 +373,7 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装并保存设备任务
|
* 组装并保存设备任务
|
||||||
|
*
|
||||||
* @param modbusJob 待新增的modbus任务
|
* @param modbusJob 待新增的modbus任务
|
||||||
* @param deviceAndProtocol 设备协议
|
* @param deviceAndProtocol 设备协议
|
||||||
* @param deviceJob 设备任务
|
* @param deviceJob 设备任务
|
||||||
@ -379,13 +393,10 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
deviceJob.setSerialNumber(deviceAndProtocol.getSerialNumber()); // 网关自己的SN
|
deviceJob.setSerialNumber(deviceAndProtocol.getSerialNumber()); // 网关自己的SN
|
||||||
deviceJob.setProductId(deviceAndProtocol.getProductId());
|
deviceJob.setProductId(deviceAndProtocol.getProductId());
|
||||||
deviceJob.setCreateTime(DateUtils.getNowDate());
|
deviceJob.setCreateTime(DateUtils.getNowDate());
|
||||||
// 检查是否是设备上电触发(使用 cycleType 判断)
|
// 上电触发(3)和条件触发(4)不需要 cron 表达式
|
||||||
if (isPowerOnTrigger(modbusJob.getCycleType())) {
|
if (isNonScheduleTrigger(modbusJob.getCycleType())) {
|
||||||
// 设备上电触发不需要 cron 表达式,设置为null
|
|
||||||
// DeviceJobServiceImpl.insertJob 会检查并跳过创建定时任务
|
|
||||||
deviceJob.setCronExpression(null);
|
deviceJob.setCronExpression(null);
|
||||||
} else {
|
} else {
|
||||||
// 处理时间转换
|
|
||||||
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1, deviceJob.getRemark());
|
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1, deviceJob.getRemark());
|
||||||
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
|
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
|
||||||
}
|
}
|
||||||
@ -400,4 +411,48 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
|||||||
modbusJob.setJobId(job.getJobId());
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,7 @@
|
|||||||
<el-tag v-if="scope.row.cycleType === 1" type="primary" size="small">周期循环</el-tag>
|
<el-tag v-if="scope.row.cycleType === 1" type="primary" size="small">周期循环</el-tag>
|
||||||
<el-tag v-else-if="scope.row.cycleType === 2" type="warning" size="small">自定义时间段</el-tag>
|
<el-tag v-else-if="scope.row.cycleType === 2" type="warning" size="small">自定义时间段</el-tag>
|
||||||
<el-tag v-else-if="scope.row.cycleType === 3" type="success" size="small">设备上电触发</el-tag>
|
<el-tag v-else-if="scope.row.cycleType === 3" type="success" size="small">设备上电触发</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.cycleType === 4" type="danger" size="small">条件触发</el-tag>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -83,7 +84,11 @@
|
|||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('device.device-modbus-task.384302-59')" align="center" prop="remarkStr"></el-table-column>
|
<el-table-column :label="$t('device.device-modbus-task.384302-59')" align="center" min-width="200" class-name="modbus-task-schedule-col">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span class="modbus-task-schedule-text">{{ formatModbusScheduleDisplay(scope.row) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column :label="$t('device.device-modbus-task.384302-60')" align="center" class-name="small-padding fixed-width">
|
<el-table-column :label="$t('device.device-modbus-task.384302-60')" align="center" class-name="small-padding fixed-width">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)" v-hasPermi="['modbus:job:edit']">编辑</el-button>
|
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)" v-hasPermi="['modbus:job:edit']">编辑</el-button>
|
||||||
@ -331,6 +336,84 @@
|
|||||||
<i class="el-icon-question" style="color: #909399; margin-left: 5px;"></i>
|
<i class="el-icon-question" style="color: #909399; margin-left: 5px;"></i>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
|
<!-- 条件触发 -->
|
||||||
|
<el-radio :label="4" style="display: block; margin-top: 10px;">
|
||||||
|
<span>条件触发</span>
|
||||||
|
<el-tooltip placement="right">
|
||||||
|
<div slot="content">
|
||||||
|
当设备上报的指定属性满足条件时,自动下发本条指令
|
||||||
|
</div>
|
||||||
|
<i class="el-icon-question" style="color: #909399; margin-left: 5px;"></i>
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- 条件配置区域(多条件) -->
|
||||||
|
<div v-show="createForm.cycleType === 4" style="margin-top: 10px; padding: 10px; background: #f5f7fa; border-radius: 4px;">
|
||||||
|
<!-- 多条件逻辑选择(≥2条时显示) -->
|
||||||
|
<div v-if="createForm.conditions.length > 1" style="margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
|
||||||
|
<span style="font-size:12px; color:#606266;">条件关系:</span>
|
||||||
|
<el-radio-group v-model="createForm.conditionLogic" size="mini">
|
||||||
|
<el-radio-button label="and">全部满足(且)</el-radio-button>
|
||||||
|
<el-radio-button label="or">任一满足(或)</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<!-- 条件行列表 -->
|
||||||
|
<div
|
||||||
|
v-for="(cond, idx) in createForm.conditions"
|
||||||
|
:key="idx"
|
||||||
|
style="display: flex; align-items: flex-end; gap: 6px; margin-bottom: 6px;"
|
||||||
|
>
|
||||||
|
<!-- 多条件连接词标签 -->
|
||||||
|
<span v-if="idx > 0" style="font-size:12px; color:#409EFF; white-space: nowrap; padding-bottom: 6px; min-width: 30px;">
|
||||||
|
{{ createForm.conditionLogic === 'or' ? '或' : '且' }}
|
||||||
|
</span>
|
||||||
|
<span v-else style="min-width: 30px;"></span>
|
||||||
|
<el-select
|
||||||
|
v-model="cond.identifier"
|
||||||
|
size="small"
|
||||||
|
placeholder="监听属性"
|
||||||
|
filterable
|
||||||
|
style="flex: 2;"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="prop in conditionProperties"
|
||||||
|
:key="prop.id"
|
||||||
|
:label="prop.name + '(' + prop.id + ')' + (prop.isCalculated ? ' [计算]' : '')"
|
||||||
|
:value="prop.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="cond.operator" size="small" style="flex: 1.2;">
|
||||||
|
<el-option label="等于(=)" value="=" />
|
||||||
|
<el-option label="不等于(!=)" value="!=" />
|
||||||
|
<el-option label="大于(>)" value=">" />
|
||||||
|
<el-option label="小于(<)" value="<" />
|
||||||
|
<el-option label="大于等于(>=)" value=">=" />
|
||||||
|
<el-option label="小于等于(<=)" value="<=" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="cond.value"
|
||||||
|
size="small"
|
||||||
|
placeholder="阈值"
|
||||||
|
style="flex: 1;"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
v-if="createForm.conditions.length > 1"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-minus"
|
||||||
|
size="mini"
|
||||||
|
circle
|
||||||
|
@click.stop="removeCondition(idx)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 添加条件按钮 -->
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
@click.stop="addCondition"
|
||||||
|
style="margin-top: 2px;"
|
||||||
|
>添加条件</el-button>
|
||||||
|
</div>
|
||||||
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -363,6 +446,26 @@ import { parseTime } from '@/utils/ruoyi';
|
|||||||
import { encode } from '@/api/iot/mqttTest';
|
import { encode } from '@/api/iot/mqttTest';
|
||||||
import { listJob, getJob, delJob, addJob, updateJob, getSlaveId } from '@/api/iot/modbusJob';
|
import { listJob, getJob, delJob, addJob, updateJob, getSlaveId } from '@/api/iot/modbusJob';
|
||||||
import { listGateway } from '@/api/iot/gateway';
|
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 {
|
export default {
|
||||||
name: 'device-modbus-task',
|
name: 'device-modbus-task',
|
||||||
@ -515,6 +618,8 @@ export default {
|
|||||||
},
|
},
|
||||||
cycles1: [{ interval: '300', time: '', week: '', day: '' }],
|
cycles1: [{ interval: '300', time: '', week: '', day: '' }],
|
||||||
cycles2: [{ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' }],
|
cycles2: [{ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' }],
|
||||||
|
// 条件触发:当前产品的属性列表({id, name} 结构)
|
||||||
|
conditionProperties: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -522,7 +627,7 @@ export default {
|
|||||||
showExecuteTime(row) {
|
showExecuteTime(row) {
|
||||||
const t = row.cycleType;
|
const t = row.cycleType;
|
||||||
if (t === 1 || t === 2) return true;
|
if (t === 1 || t === 2) return true;
|
||||||
if (t === 3) return row.status === '1';
|
if (t === 3 || t === 4) return !!row.executeTime;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
// 回填时间周期(remark -> cycles1/cycles2)
|
// 回填时间周期(remark -> cycles1/cycles2)
|
||||||
@ -672,7 +777,7 @@ export default {
|
|||||||
/**计算定时时间*/
|
/**计算定时时间*/
|
||||||
let cycle = '';
|
let cycle = '';
|
||||||
|
|
||||||
// 如果是设备上电触发,不需要 cron 表达式
|
// 设备上电触发(3)和条件触发(4)不需要 cron 表达式
|
||||||
if (this.createForm.cycleType === 3) {
|
if (this.createForm.cycleType === 3) {
|
||||||
this.createForm.subDeviceId = this.device.deviceId;
|
this.createForm.subDeviceId = this.device.deviceId;
|
||||||
this.createForm.subSerialNumber = this.device.serialNumber;
|
this.createForm.subSerialNumber = this.device.serialNumber;
|
||||||
@ -683,6 +788,31 @@ export default {
|
|||||||
this.editDialog = false;
|
this.editDialog = false;
|
||||||
return;
|
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) => {
|
const c = this.cycles1.map((item) => {
|
||||||
@ -707,9 +837,72 @@ export default {
|
|||||||
this.editDialog = false;
|
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() {
|
openEdit() {
|
||||||
this.resetCreateForm();
|
this.resetCreateForm();
|
||||||
|
this.loadConditionProperties();
|
||||||
// 网关设备(deviceType=2)直接轮询:跳过从机地址校验,由用户手动填写
|
// 网关设备(deviceType=2)直接轮询:跳过从机地址校验,由用户手动填写
|
||||||
if (this.device && this.device.deviceType === 2) {
|
if (this.device && this.device.deviceType === 2) {
|
||||||
this.createForm.path = '';
|
this.createForm.path = '';
|
||||||
@ -747,7 +940,9 @@ export default {
|
|||||||
setValue16: '0000',
|
setValue16: '0000',
|
||||||
setValueSwitch: 'Dec',
|
setValueSwitch: 'Dec',
|
||||||
status: '0',
|
status: '0',
|
||||||
cycleType: 1, // 1周期,2自定义
|
cycleType: 1, // 1周期,2自定义,3上电触发,4条件触发
|
||||||
|
conditionLogic: 'and',
|
||||||
|
conditions: [{ identifier: '', operator: '>', value: '' }],
|
||||||
};
|
};
|
||||||
this.createCode = '';
|
this.createCode = '';
|
||||||
},
|
},
|
||||||
@ -886,6 +1081,8 @@ export default {
|
|||||||
listGateway({ gwDeviceId: this.device.deviceId, pageSize: 999 })
|
listGateway({ gwDeviceId: this.device.deviceId, pageSize: 999 })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.subDeviceList = res.rows || [];
|
this.subDeviceList = res.rows || [];
|
||||||
|
// 子设备加载完成后,刷新条件触发属性下拉(合并子设备物模型)
|
||||||
|
this.loadConditionProperties();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.subListLoading = false;
|
this.subListLoading = false;
|
||||||
@ -936,6 +1133,7 @@ export default {
|
|||||||
// 编辑轮询任务(修改时间周期/指令等)
|
// 编辑轮询任务(修改时间周期/指令等)
|
||||||
handleEdit(row) {
|
handleEdit(row) {
|
||||||
this.resetCreateForm();
|
this.resetCreateForm();
|
||||||
|
this.loadConditionProperties();
|
||||||
this.editName = true;
|
this.editName = true;
|
||||||
getJob(row.taskId).then((res) => {
|
getJob(row.taskId).then((res) => {
|
||||||
const data = (res && res.data) || {};
|
const data = (res && res.data) || {};
|
||||||
@ -945,11 +1143,100 @@ export default {
|
|||||||
// 回填周期
|
// 回填周期
|
||||||
const cycleType = data.cycleType != null ? data.cycleType : 1;
|
const cycleType = data.cycleType != null ? data.cycleType : 1;
|
||||||
this.createForm.cycleType = cycleType;
|
this.createForm.cycleType = cycleType;
|
||||||
|
// 回填条件触发配置(兼容新多条件格式和旧单条件格式)
|
||||||
|
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.parseRemarkToCycles(cycleType, data.remark);
|
||||||
|
}
|
||||||
// 打开弹窗
|
// 打开弹窗
|
||||||
this.editDialog = true;
|
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描述
|
// 格式化显示CRON描述
|
||||||
formatCronDisplay(item) {
|
formatCronDisplay(item) {
|
||||||
let result = '';
|
let result = '';
|
||||||
@ -1024,8 +1311,20 @@ export default {
|
|||||||
this.cycles1 = [{ interval: 'hour', time: '', week: '', day: '' }];
|
this.cycles1 = [{ interval: 'hour', time: '', week: '', day: '' }];
|
||||||
} else if (val === 3) {
|
} 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) {
|
handleCustomIntervalDelete(index) {
|
||||||
this.cycles2.splice(index, 1);
|
this.cycles2.splice(index, 1);
|
||||||
@ -1081,6 +1380,15 @@ $right-btn-color: #1890ff;
|
|||||||
min-height: 400px;
|
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 {
|
.sub-device-side-panel {
|
||||||
border: 1px solid $border-color;
|
border: 1px solid $border-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user