package cn.iocoder.yudao.module.mes.service.collect; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.mes.enums.collect.MesCollectMsgTypeEnum; import tools.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; /** * MES 数采报文解析工具 * * @author 超级管理员 */ @Component @Slf4j public class MesCollectDataParser { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]"); public JsonNode parsePayload(String payload) { return JsonUtils.parseTree(payload); } public JsonNode parseData(JsonNode root) { JsonNode data = root.get("Data"); if (data != null && data.isTextual()) { try { data = JsonUtils.parseTree(data.asText()); } catch (Exception e) { log.warn("[parseData][Data 字段不是合法 JSON,按原文处理:{}]", StrUtil.maxLength(data.asText(), 200)); return root; } } return data != null && data.isObject() ? data : root; } public String getText(JsonNode node, String field) { JsonNode value = node.get(field); return value == null || value.isNull() ? null : value.asText(); } public Integer getInteger(JsonNode node, String field) { JsonNode value = node.get(field); if (value == null || value.isNull()) { return null; } if (value.isNumber()) { return value.intValue(); } try { return Integer.valueOf(value.asText().trim()); } catch (NumberFormatException e) { return null; } } public Long getLong(JsonNode node, String field) { JsonNode value = node.get(field); if (value == null || value.isNull()) { return null; } if (value.isNumber()) { return value.longValue(); } try { return Long.valueOf(value.asText().trim()); } catch (NumberFormatException e) { return null; } } public BigDecimal getDecimal(JsonNode node, String field) { JsonNode value = node.get(field); if (value == null || value.isNull()) { return null; } if (value.isNumber()) { return value.decimalValue(); } try { return new BigDecimal(value.asText().trim()); } catch (NumberFormatException e) { return null; } } /** * 提取指定前缀 + 数字的字段,如 T1..Tn、ET1..ETn */ public String extractNumberedFields(JsonNode data, String prefix) { Map result = new LinkedHashMap<>(); data.properties().forEach(entry -> { String name = entry.getKey(); if (!name.startsWith(prefix) || name.length() == prefix.length()) { return; } String suffix = name.substring(prefix.length()); if (!suffix.chars().allMatch(Character::isDigit)) { return; } JsonNode value = entry.getValue(); result.put(name, value != null && value.isNumber() ? value.decimalValue() : value == null ? null : value.asText()); }); if (result.isEmpty()) { return null; } return JsonUtils.toJsonString(result); } public LocalDateTime parseEventTime(JsonNode root) { LocalDateTime time = parseTimeText(getText(root, "time")); if (time != null) { return time; } Long timestamp = getLong(root, "timestamp"); if (timestamp == null) { timestamp = getLong(root, "sendStamp"); } if (timestamp != null && timestamp > 0) { Instant instant = timestamp > 1_000_000_000_000L ? Instant.ofEpochMilli(timestamp) : Instant.ofEpochSecond(timestamp); return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); } return LocalDateTime.now(); } public LocalDateTime parseTimeText(String text) { if (StrUtil.isBlank(text)) { return null; } try { return LocalDateTime.parse(text.trim(), DATE_TIME_FORMATTER); } catch (Exception e) { return null; } } public String valueToString(JsonNode value) { if (value == null || value.isNull()) { return null; } return value.isValueNode() ? value.asText() : value.toString(); } public MesCollectMsgTypeEnum resolveMsgType(String topicField, String mqttTopic, JsonNode data, String alarmTopicSuffix) { MesCollectMsgTypeEnum type = MesCollectMsgTypeEnum.getByType(topicField); if (type != MesCollectMsgTypeEnum.UNKNOWN) { return type; } if (StrUtil.isNotBlank(topicField) && StrUtil.equalsIgnoreCase(topicField, alarmTopicSuffix)) { return MesCollectMsgTypeEnum.ALARM; } String suffix = extractTopicSuffix(mqttTopic); if (StrUtil.isNotBlank(suffix)) { type = MesCollectMsgTypeEnum.getByType(suffix); if (type != MesCollectMsgTypeEnum.UNKNOWN) { return type; } if (StrUtil.equalsIgnoreCase(suffix, alarmTopicSuffix)) { return MesCollectMsgTypeEnum.ALARM; } } if (data == null) { return MesCollectMsgTypeEnum.UNKNOWN; } if (data.has("wmId")) { return MesCollectMsgTypeEnum.ALARM; } if (data.has("varId")) { return MesCollectMsgTypeEnum.OP_LOG; } if (data.has("CYCN")) { return MesCollectMsgTypeEnum.SPC; } if (data.has("STS") || data.has("OPM")) { return MesCollectMsgTypeEnum.REALTIME; } if (data.has("TS1") || data.has("CP1M")) { return MesCollectMsgTypeEnum.TECH; } return MesCollectMsgTypeEnum.UNKNOWN; } public String resolveDeviceId(JsonNode root, String mqttTopic) { String deviceId = getText(root, "devId"); if (StrUtil.isNotBlank(deviceId)) { return deviceId.trim(); } String[] parts = mqttTopic == null ? new String[0] : mqttTopic.split("/"); return parts.length >= 2 ? parts[parts.length - 2] : null; } /** * 网关格式报文拆包:报文首层 key 是设备名,value 是该设备的属性字典。 * *

例如 {"630框绞机2":{"计米":268.3499}} 拆成 {630框绞机2 -> {"计米":268.3499}}; * 连接事件 {"device":"大拉"} 这类扁平报文没有嵌套对象,返回空 Map 由调用方另行处理。

*/ public Map splitGatewayDevices(JsonNode root) { Map devices = new LinkedHashMap<>(); if (root == null || !root.isObject()) { return devices; } root.properties().forEach(entry -> { String deviceCode = StrUtil.trim(entry.getKey()); JsonNode data = entry.getValue(); if (StrUtil.isBlank(deviceCode) || data == null || !data.isObject()) { return; } devices.put(deviceCode, data); }); return devices; } /** * 取主题末段作为报文类型,如 gateway/attributes -> attributes、gateway/disconnect -> disconnect */ public String extractTopicSuffix(String mqttTopic) { if (StrUtil.isBlank(mqttTopic)) { return null; } String[] parts = mqttTopic.split("/"); return parts.length == 0 ? null : parts[parts.length - 1]; } }