From a3a5b6a71946303418143640f3146c6017c05409 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 1 Apr 2026 22:17:35 +0800 Subject: [PATCH] =?UTF-8?q?20260401:=E6=94=AF=E6=8C=81=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E8=BD=AE=E8=AF=A2=E4=BB=BB=E5=8A=A1=EF=BC=9B=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=8E=A8=E9=80=81=E6=A8=A1=E6=9D=BF=EF=BC=9B?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BD=91=E5=85=B3=E8=AE=BE=E5=A4=87=E8=B7=A8?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E6=A8=A1=E5=9E=8B=E6=95=B0=E6=8D=AE=E6=8E=A8?= =?UTF-8?q?=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastbee-iot-data/pom.xml | 6 + .../iot/data/push/ThingsModelPushHandler.java | 169 ++++++++++++++++-- .../service/impl/ModbusJobServiceImpl.java | 93 +++++++++- yack-web/src/api/iot/modbusJob.js | 8 +- .../views/iot/device/device-modbus-task.vue | 65 ++++++- 5 files changed, 320 insertions(+), 21 deletions(-) diff --git a/fastbee-iot-data/pom.xml b/fastbee-iot-data/pom.xml index c8f0196b..694de2e9 100644 --- a/fastbee-iot-data/pom.xml +++ b/fastbee-iot-data/pom.xml @@ -15,6 +15,12 @@ mqtt-broker + + + com.fastbee + fastbee-iot-service + + com.fastbee fastbee-mq diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java index 06ade0fc..8317238e 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java @@ -8,14 +8,20 @@ import com.fastbee.common.core.notify.config.WeChatConfigParams; import com.fastbee.common.core.notify.msg.WechatMsgParams; import com.fastbee.common.core.redis.RedisKeyBuilder; import com.fastbee.common.enums.DataLevelEnum; +import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.wechat.WechatUtils; import com.fastbee.common.wechat.WeChatAppResult; import com.fastbee.iot.cache.ITSLCache; +import com.fastbee.iot.domain.SubGateway; import com.fastbee.iot.domain.PushModelItem; import com.fastbee.iot.domain.SocialUser; +import com.fastbee.iot.domain.ThingsModel; import com.fastbee.iot.domain.ThingsModelPushConfig; import com.fastbee.iot.model.ThingsModels.ValueItem; +import com.fastbee.iot.model.gateWay.SubDeviceListVO; +import com.fastbee.iot.service.ISubGatewayService; +import com.fastbee.iot.service.IThingsModelService; import com.fastbee.iot.service.IThingsModelPushConfigService; import com.fastbee.iot.service.ISocialUserService; import com.fastbee.notify.core.wechat.vo.TemplateDataVo; @@ -52,6 +58,8 @@ public class ThingsModelPushHandler { private static final RestTemplate REST_TEMPLATE = new RestTemplate(); private static final Pattern VAR_PATTERN = Pattern.compile("\\{\\{([^}]+)}}"); + /** 公式中跨设备引用:{slaveId:N.identifier} */ + private static final Pattern CROSS_DEVICE_REF = Pattern.compile("\\{slaveId:(\\d+)\\.([^}]+)}"); private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -65,6 +73,10 @@ public class ThingsModelPushHandler { private ISocialUserService socialUserService; @Resource private ITSLCache itslCache; + @Resource + private ISubGatewayService subGatewayService; + @Resource + private IThingsModelService thingsModelService; /** * 按推送配置执行推送 @@ -148,13 +160,24 @@ public class ThingsModelPushHandler { levelTextMap.put(lvl, new ArrayList<>()); } Map cacheMap = itslCache.getSimpleThingsModelValueItemByCache(redisKey); + Map subDeviceBySlaveId = null; + Map calculatedModelCache = new HashMap<>(); for (PushModelItem item : config.getItems()) { ValueItem valueItem = cacheMap.get(item.getIdentifier()); - if (valueItem == null) { - continue; + String value; + Date ts; + if (valueItem != null) { + value = valueItem.getValue(); + ts = valueItem.getTs(); + } else { + // 如果缓存里没有,尝试按网关计算物模型公式计算(跨设备引用) + if (subDeviceBySlaveId == null) { + subDeviceBySlaveId = loadSubDeviceBySlaveId(config.getDeviceId()); + } + ComputedValue computed = tryComputeGatewayCalculatedValue(config, serialNumber, item.getIdentifier(), subDeviceBySlaveId, calculatedModelCache); + value = computed != null ? computed.value : null; + ts = computed != null ? computed.ts : null; } - String value = valueItem.getValue(); - Date ts = valueItem.getTs(); if (config.getLastPushTime() != null && ts != null && ts.before(config.getLastPushTime())) { log.warn("[Push] configId={} sn={} item={} value={} 时间{}早于上次推送时间{},跳过", config.getId(), serialNumber, item.getIdentifier(), value, ts, config.getLastPushTime()); @@ -195,6 +218,133 @@ public class ThingsModelPushHandler { return sendTemplateMsg(config, msgParams, serialNumber, chosen, dataText, pushUrl, openIdMap); } + private static final class ComputedValue { + private final String value; + private final Date ts; + + private ComputedValue(String value, Date ts) { + this.value = value; + this.ts = ts; + } + } + + private Map loadSubDeviceBySlaveId(Long gwDeviceId) { + if (gwDeviceId == null) { + return Collections.emptyMap(); + } + try { + SubGateway subQ = new SubGateway(); + subQ.setGwDeviceId(gwDeviceId); + List list = subGatewayService.selectGatewayList(subQ); + if (CollectionUtils.isEmpty(list)) { + return Collections.emptyMap(); + } + Map map = new HashMap<>(); + for (SubDeviceListVO vo : list) { + if (vo != null && vo.getSlaveId() != null) { + map.put(vo.getSlaveId(), vo); + } + } + return map; + } catch (Exception e) { + log.warn("[Push] loadSubDeviceBySlaveId gwDeviceId={} 异常: {}", gwDeviceId, e.getMessage()); + return Collections.emptyMap(); + } + } + + /** + * 计算网关跨设备计算物模型的当前值(只基于各从机当前缓存值) + */ + private ComputedValue tryComputeGatewayCalculatedValue(ThingsModelPushConfig config, + String gwSerialNumber, + String identifier, + Map subDeviceBySlaveId, + Map calculatedModelCache) { + if (config == null || config.getProductId() == null || StringUtils.isEmpty(identifier)) { + return null; + } + ThingsModel model; + if (calculatedModelCache != null && calculatedModelCache.containsKey(identifier)) { + model = calculatedModelCache.get(identifier); + } else { + ThingsModel query = new ThingsModel(); + query.setProductId(config.getProductId()); + query.setIdentifier(identifier); + query.setIsCalculated(1); + query.setSerialNumber(gwSerialNumber); + try { + model = thingsModelService.selectSingleThingsModel(query); + } catch (Exception e) { + return null; + } + if (calculatedModelCache != null) { + calculatedModelCache.put(identifier, model); + } + } + if (model == null || model.getIsCalculated() == null || + model.getIsCalculated() != 1 || StringUtils.isEmpty(model.getFormula())) { + return null; + } + Map refs = parseCrossDeviceRefs(model.getFormula()); + if (refs.isEmpty()) { + return null; + } + + Map replaceMap = new HashMap<>(); + Date maxTs = null; + for (Map.Entry ref : refs.entrySet()) { + String refKey = ref.getKey(); // slaveId:N.identifier + int slaveId; + try { + slaveId = Integer.parseInt(refKey.split("\\.")[0].replace("slaveId:", "")); + } catch (Exception e) { + continue; + } + SubDeviceListVO sub = subDeviceBySlaveId != null ? subDeviceBySlaveId.get(slaveId) : null; + String subSn = sub != null ? sub.getSubDeviceNo() : null; + Long subProductId = sub != null ? sub.getSubProductId() : null; + String subIdentifier = ref.getValue(); + String val = "0"; + if (subProductId != null && StringUtils.isNotEmpty(subSn) && StringUtils.isNotEmpty(subIdentifier)) { + try { + ValueItem v = thingsModelService.getCacheIdentifierValue(subProductId, subSn, subIdentifier); + if (v != null && StringUtils.isNotEmpty(v.getValue())) { + val = v.getValue(); + } + if (v != null && v.getTs() != null) { + if (maxTs == null || v.getTs().after(maxTs)) { + maxTs = v.getTs(); + } + } + } catch (Exception ignored) { + } + } + replaceMap.put("{" + refKey + "}", val); + } + try { + Object computed = CaculateUtils.execute(model.getFormula(), replaceMap); + String computedStr = computed != null ? computed.toString() : null; + return new ComputedValue(computedStr, maxTs); + } catch (Exception e) { + log.warn("[Push] configId={} sn={} identifier={} 公式计算失败 formula={} err={}", + config.getId(), gwSerialNumber, identifier, model.getFormula(), e.getMessage()); + return null; + } + } + + private Map parseCrossDeviceRefs(String formula) { + Map refs = new LinkedHashMap<>(); + if (StringUtils.isEmpty(formula)) { + return refs; + } + Matcher m = CROSS_DEVICE_REF.matcher(formula); + while (m.find()) { + String key = "slaveId:" + m.group(1) + "." + m.group(2); + refs.put(key, m.group(2)); + } + return refs; + } + /** * 发送单条微信模板消息。 * 从通知模板的 content 字段提取 {{变量.DATA}} 变量名顺序, @@ -206,12 +356,12 @@ public class ThingsModelPushHandler { List varNames = extractVarNames(msgParams.getContent()); Map map = new LinkedHashMap<>(); -// String deviceDisplay = StringUtils.isNotEmpty(config.getDeviceName()) -// ? config.getDeviceName() : serialNumber; + String deviceDisplay = StringUtils.isNotEmpty(config.getDeviceName()) + ? config.getDeviceName() : serialNumber; for (int i = 0; i < varNames.size(); i++) { String varName = varNames.get(i); if (i == 0) { - map.put(varName, serialNumber); + map.put(varName, deviceDisplay); } else if (i == 1) { map.put(varName, level.getLabel()); } else if (i == 2) { @@ -284,9 +434,4 @@ public class ThingsModelPushHandler { return resp; } - public static void main(String[] args) { - String value = "WaterT=1.76;WaterVol=-6.34;T=20"; - String value1 = "[修正]参术儿康糖浆10ml*10支1盒装儿童小..."; - System.out.println("value.length() = " + value.length()); - } } 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 88ed1e41..4b624e2b 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 @@ -3,7 +3,9 @@ package com.fastbee.iot.service.impl; import com.fastbee.common.core.device.DeviceAndProtocol; import com.fastbee.common.core.mq.SubDeviceBo; import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.exception.job.TaskException; import com.fastbee.common.utils.DateUtils; +import com.fastbee.common.utils.StringUtils; import com.fastbee.iot.domain.DeviceJob; import com.fastbee.iot.domain.ModbusJob; import com.fastbee.iot.enums.DeviceType; @@ -172,16 +174,103 @@ public class ModbusJobServiceImpl implements IModbusJobService { */ @Override public int updateModbusJob(ModbusJob modbusJob) { + if (modbusJob == null || modbusJob.getTaskId() == null) { + throw new ServiceException("任务ID不能为空"); + } + + ModbusJob existingJob = modbusJobMapper.selectModbusJobByTaskId(modbusJob.getTaskId()); + if (existingJob == null) { + throw new ServiceException("轮询任务不存在"); + } + // cycleType 由前端传递,不再根据 remark 判断 // 如果状态从1(暂停)改为0(正常),需要清空执行时间 if (modbusJob.getTaskId() != null && modbusJob.getStatus() != null && "0".equals(modbusJob.getStatus())) { - ModbusJob existingJob = modbusJobMapper.selectModbusJobByTaskId(modbusJob.getTaskId()); if (existingJob != null && "1".equals(existingJob.getStatus())) { // 状态从1改为0,清空执行时间 modbusJobMapper.clearExecuteTime(modbusJob.getTaskId()); } } - return modbusJobMapper.updateModbusJob(modbusJob); + + // 是否修改了时间周期(remark/cycleType),如修改则需要同步更新/重绑 iot_device_job 并刷新 quartz + boolean scheduleChanged = (modbusJob.getRemark() != null) || (modbusJob.getCycleType() != null); + Long oldDeviceJobId = existingJob.getJobId(); + + if (scheduleChanged) { + // 计算新的 remark / cycleType(允许只传 cycleType 或只传 remark) + String newRemark = modbusJob.getRemark() != null ? modbusJob.getRemark() : existingJob.getRemark(); + Integer newCycleType = modbusJob.getCycleType() != null ? modbusJob.getCycleType() : existingJob.getCycleType(); + if (newCycleType == null) { + newCycleType = 1; + } + + if (oldDeviceJobId == null) { + throw new ServiceException("轮询任务缺少关联的设备定时任务(jobId)"); + } + + DeviceJob oldDeviceJob = deviceJobService.selectJobById(oldDeviceJobId); + if (oldDeviceJob == null) { + throw new ServiceException("关联的设备定时任务不存在(jobId=" + oldDeviceJobId + ")"); + } + + // 优先复用同设备、同remark的 DeviceJob(避免重复创建) + DeviceJob query = new DeviceJob(); + query.setDeviceId(oldDeviceJob.getDeviceId()); + query.setJobType(5); + query.setRemark(newRemark); + List candidates = deviceJobService.selectJobList(query); + DeviceJob targetDeviceJob = CollectionUtils.isEmpty(candidates) ? null : candidates.get(0); + + if (targetDeviceJob == null) { + DeviceJob create = new DeviceJob(); + create.setDeviceId(oldDeviceJob.getDeviceId()); + 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.setIsAdvance(oldDeviceJob.getIsAdvance() != null ? oldDeviceJob.getIsAdvance() : 0); + create.setStatus("0"); + create.setRemark(newRemark); + + if (isPowerOnTrigger(newCycleType)) { + create.setCronExpression(null); + } else { + SceneModelTagCycleVO vo = JobCronUtils.handleCronCycle(newCycleType, newRemark); + create.setCronExpression(vo.getCron()); + } + + try { + deviceJobService.insertJob(create); + } catch (SchedulerException | TaskException e) { + throw new ServiceException(e.getMessage()); + } + targetDeviceJob = create; + } + + // 绑定到新的/复用的 DeviceJob + modbusJob.setJobId(targetDeviceJob.getJobId()); + } + + int rows = modbusJobMapper.updateModbusJob(modbusJob); + + // 如发生重绑且旧 jobId 无引用,则删除旧 DeviceJob(同时会清理 quartz trigger/job) + 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}); + } catch (SchedulerException e) { + throw new ServiceException(e.getMessage()); + } + } + } + + return rows; } /** diff --git a/yack-web/src/api/iot/modbusJob.js b/yack-web/src/api/iot/modbusJob.js index 53668960..12132dcd 100644 --- a/yack-web/src/api/iot/modbusJob.js +++ b/yack-web/src/api/iot/modbusJob.js @@ -27,15 +27,11 @@ export function addJob(data) { } // 修改轮训任务列 -export function updateJob(taskId, status) { - const data = { - taskId: taskId, - status: status, - }; +export function updateJob(data) { return request({ url: '/modbus/job', method: 'put', - data: data, + data, }); } 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 516a4a9d..c12ac9ed 100644 --- a/yack-web/src/views/iot/device/device-modbus-task.vue +++ b/yack-web/src/views/iot/device/device-modbus-task.vue @@ -86,6 +86,7 @@ @@ -517,6 +518,50 @@ export default { }; }, methods: { + // 回填时间周期(remark -> cycles1/cycles2) + parseRemarkToCycles(cycleType, remark) { + // 默认值 + this.cycles1 = [{ interval: '300', time: '', week: '', day: '' }]; + this.cycles2 = [{ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' }]; + + if (!remark) return; + let list = []; + try { + list = JSON.parse(remark); + } catch (e) { + return; + } + if (!Array.isArray(list) || list.length === 0) return; + const first = list[0] || {}; + + if (cycleType === 2) { + // 自定义时间段(当前页面若未展示 cycles2,则仅做数据回填保底) + this.cycles2 = list; + return; + } + + // 周期循环 + if (first.interval != null) { + this.cycles1 = [{ interval: String(first.interval), time: '', week: '', day: '' }]; + return; + } + if (first.type === 'hour') { + this.cycles1 = [{ interval: 'hour', time: '', week: '', day: '' }]; + return; + } + if (first.type === 'day') { + this.cycles1 = [{ interval: 'day', time: String(first.time || '01'), week: '', day: '' }]; + return; + } + if (first.type === 'week') { + this.cycles1 = [{ interval: 'week', week: String(first.week || '1'), time: String(first.time || '01'), day: '' }]; + return; + } + if (first.type === 'month') { + this.cycles1 = [{ interval: 'month', day: String(first.day || '1'), time: String(first.time || '01'), week: '' }]; + return; + } + }, /** 查询轮训任务列列表 */ getList() { this.loading = true; @@ -871,7 +916,7 @@ export default { this.$modal .confirm(this.$t('device.device-modbus-task.384302-44', [text + '""' + row.jobName])) .then(function () { - return updateJob(row.taskId, row.status); + return updateJob({ taskId: row.taskId, status: row.status }); }) .then(() => { this.$modal.msgSuccess(text + this.$t('device.device-modbus-task.384302-45')); @@ -880,6 +925,24 @@ export default { row.status = row.status === '0' ? '1' : '0'; }); }, + + // 编辑轮询任务(修改时间周期/指令等) + handleEdit(row) { + this.resetCreateForm(); + this.editName = true; + getJob(row.taskId).then((res) => { + const data = (res && res.data) || {}; + this.createForm = Object.assign({}, this.createForm, data); + // 回填指令 + this.createCode = data.command || ''; + // 回填周期 + const cycleType = data.cycleType != null ? data.cycleType : 1; + this.createForm.cycleType = cycleType; + this.parseRemarkToCycles(cycleType, data.remark); + // 打开弹窗 + this.editDialog = true; + }); + }, // 格式化显示CRON描述 formatCronDisplay(item) { let result = '';