package cn.iocoder.yudao.module.mes.service.collect; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper; import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum; import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties; import tools.jackson.databind.JsonNode; import jakarta.annotation.PreDestroy; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; /** * MES 数采报文分发器 * *

Paho 回调线程只负责入队,解析与落库在单线程中串行执行,保证同一设备报警等状态按顺序处理。

* * @author 超级管理员 */ @Component @Slf4j public class MesCollectMessageDispatcher { /** * 网关格式里承载设备属性快照的主题末段 */ private static final String ATTR_TOPIC_SUFFIX = "attributes"; @Resource private MesCollectProperties properties; @Resource private MesCollectDataParser parser; @Resource private MesCollectDataService dataService; @Resource private MesDvMachineryMapper machineryMapper; @Resource private MesCollectTaskResolver taskResolver; private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10_000), new ThreadFactory() { private final AtomicInteger index = new AtomicInteger(); @Override public Thread newThread(Runnable runnable) { Thread thread = new Thread(runnable, "mes-collect-dispatch-" + index.incrementAndGet()); thread.setDaemon(true); return thread; } }, new ThreadPoolExecutor.CallerRunsPolicy()); /** * 设备未绑定台账时的告警缓存,避免每条报文重复刷日志 */ private final ConcurrentHashMap unboundWarnCache = new ConcurrentHashMap<>(); public void dispatch(String topic, String payload) { executor.execute(() -> process(topic, payload)); } private void process(String topic, String payload) { // 网关格式报文结构完全不同(主题固定、设备以报文首层 key 区分),单独处理 if (properties.getMqtt().isGatewayFormat()) { processGateway(topic, payload); return; } JsonNode root; try { root = parser.parsePayload(payload); } catch (Exception e) { log.warn("[process][主题({}) 报文不是合法 JSON:{}]", topic, StrUtil.maxLength(payload, 200)); dataService.markRawMessageFailed( dataService.saveRawMessage(null, null, topic, MesCollectMsgTypeEnum.UNKNOWN, LocalDateTime.now(), payload), "报文不是合法 JSON:" + e.getMessage()); return; } JsonNode data = parser.parseData(root); MesCollectMsgTypeEnum msgType = parser.resolveMsgType(parser.getText(root, "topic"), topic, data, properties.getMqtt().getAlarmTopicSuffix()); String deviceCode = parser.resolveDeviceId(root, topic); LocalDateTime eventTime = parser.parseEventTime(root); MesDvMachineryDO machinery = StrUtil.isBlank(deviceCode) ? null : machineryMapper.selectByCollectDeviceCode(deviceCode); if (machinery == null && StrUtil.isNotBlank(deviceCode)) { warnUnboundDevice(deviceCode); } Long machineryId = machinery == null ? null : machinery.getId(); Long taskId = null; Long workOrderId = null; if (machinery != null && machinery.getWorkstationId() != null) { MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId()); if (task != null) { taskId = task.getId(); workOrderId = task.getWorkOrderId(); } } Long rawId = dataService.saveRawMessage(deviceCode, machineryId, topic, msgType, eventTime, payload); try { switch (msgType) { case REALTIME -> dataService.saveRealtime(deviceCode, machineryId, taskId, workOrderId, eventTime, data); case SPC -> dataService.saveCycle(deviceCode, machineryId, taskId, workOrderId, eventTime, data); case TECH -> dataService.saveTech(deviceCode, machineryId, taskId, workOrderId, eventTime, data); case OP_LOG -> dataService.saveOperateLog(deviceCode, machineryId, taskId, workOrderId, eventTime, data); case ALARM -> dataService.saveAlarm(deviceCode, machineryId, taskId, workOrderId, eventTime, data); default -> log.debug("[process][忽略未知类型报文,主题({})]", topic); } } catch (Exception e) { dataService.markRawMessageFailed(rawId, e.getMessage()); log.error("[process][主题({}) 设备({}) 处理失败]", topic, deviceCode, e); } } /** * 网关格式报文处理。 * *

attributes:报文首层 key 是设备名、value 是属性字典,按设备逐台落实时数据; * connect / disconnect:连接事件,只留痕不落实时数据,便于排查设备离线。

*/ private void processGateway(String topic, String payload) { JsonNode root; try { root = parser.parsePayload(payload); } catch (Exception e) { log.warn("[processGateway][主题({}) 报文不是合法 JSON:{}]", topic, StrUtil.maxLength(payload, 200)); dataService.markRawMessageFailed( dataService.saveRawMessage(null, null, topic, MesCollectMsgTypeEnum.UNKNOWN, LocalDateTime.now(), payload), "报文不是合法 JSON:" + e.getMessage()); return; } // 连接事件:{"device":"大拉"},没有属性字典 if (!ATTR_TOPIC_SUFFIX.equalsIgnoreCase(parser.extractTopicSuffix(topic))) { String deviceCode = StrUtil.trim(parser.getText(root, "device")); MesDvMachineryDO machinery = StrUtil.isBlank(deviceCode) ? null : machineryMapper.selectByCollectDeviceCode(deviceCode); dataService.saveRawMessage(deviceCode, machinery == null ? null : machinery.getId(), topic, MesCollectMsgTypeEnum.getByType(parser.extractTopicSuffix(topic)), LocalDateTime.now(), payload); return; } Map devices = parser.splitGatewayDevices(root); if (devices.isEmpty()) { log.debug("[processGateway][主题({}) 报文里没有设备属性:{}]", topic, StrUtil.maxLength(payload, 200)); dataService.saveRawMessage(null, null, topic, MesCollectMsgTypeEnum.UNKNOWN, LocalDateTime.now(), payload); return; } // 网关报文不带时间戳,统一按接收时间落库 LocalDateTime eventTime = parser.parseEventTime(root); devices.forEach((deviceCode, data) -> { MesDvMachineryDO machinery = machineryMapper.selectByCollectDeviceCode(deviceCode); if (machinery == null) { warnUnboundDevice(deviceCode); } Long machineryId = machinery == null ? null : machinery.getId(); Long taskId = null; Long workOrderId = null; if (machinery != null && machinery.getWorkstationId() != null) { MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId()); if (task != null) { taskId = task.getId(); workOrderId = task.getWorkOrderId(); } } Long rawId = dataService.saveRawMessage(deviceCode, machineryId, topic, MesCollectMsgTypeEnum.REALTIME, eventTime, payload); try { dataService.saveRealtime(deviceCode, machineryId, taskId, workOrderId, eventTime, data); } catch (Exception e) { dataService.markRawMessageFailed(rawId, e.getMessage()); log.error("[processGateway][主题({}) 设备({}) 处理失败]", topic, deviceCode, e); } }); } private void warnUnboundDevice(String deviceCode) { long now = System.currentTimeMillis(); Long last = unboundWarnCache.get(deviceCode); if (last != null && now - last < 60_000L) { return; } unboundWarnCache.put(deviceCode, now); log.warn("[process][数采设备({}) 未绑定任何设备台账,数据将按设备编号落库,machineryId 为空]", deviceCode); } @PreDestroy public void destroy() { executor.shutdownNow(); } }