feat(udp): 实现UDP通信历史记录存储与管理功能(后续测试完成之后需要恢复TelegramDispatcher,UdpSender,UdpServer,TelegramSchema这几个不然投入生产会导致OOM)
- 修改application.yml中的远程端口配置从9000调整为9001 - 在TelegramDispatcher中注入TelegramStore并实现报文存储功能 - 新增TelegramRecord类用于定义报文记录的数据结构 - 创建TelegramStore组件用于管理UDP报文的历史记录 - 更新前端udp-debug.vue界面的字段映射和数据展示逻辑 - 实现后端API接口支持历史记录查询、统计和清空操作 - 优化UDP接收日志输出并增加数据预览功能 - 重构前端API调用参数以支持分页查询功能
This commit is contained in:
@@ -140,6 +140,6 @@ mill:
|
||||
# L3 系统 IP 地址
|
||||
remote-host: 127.0.0.1
|
||||
# L3 系统 UDP 端口
|
||||
remote-port: 9000
|
||||
remote-port: 9001
|
||||
# 接收缓冲区大小(字节)
|
||||
buffer-size: 4096
|
||||
|
||||
@@ -3,6 +3,10 @@ package com.ruoyi.mill.controller;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.mill.protocol.TelegramCodec;
|
||||
import com.ruoyi.mill.protocol.TelegramSchema;
|
||||
import com.ruoyi.mill.udp.TelegramRecord;
|
||||
import com.ruoyi.mill.udp.TelegramStore;
|
||||
import com.ruoyi.mill.udp.UdpProperties;
|
||||
import com.ruoyi.mill.udp.UdpServer;
|
||||
import com.ruoyi.mill.udp.UdpSender;
|
||||
@@ -37,6 +41,9 @@ public class UdpController extends BaseController {
|
||||
@Autowired
|
||||
private UdpSender udpSender;
|
||||
|
||||
@Autowired
|
||||
private TelegramStore telegramStore;
|
||||
|
||||
/**
|
||||
* 获取UDP配置
|
||||
*/
|
||||
@@ -78,27 +85,28 @@ public class UdpController extends BaseController {
|
||||
|
||||
try {
|
||||
String tcNo = (String) requestData.get("tcNo");
|
||||
Object payloadObj = requestData.get("payload");
|
||||
if (StringUtils.isBlank(tcNo)) {
|
||||
return error("电文号不能为空");
|
||||
}
|
||||
|
||||
byte[] payload;
|
||||
if (payloadObj instanceof java.util.List) {
|
||||
java.util.List<?> list = (java.util.List<?>) payloadObj;
|
||||
payload = new byte[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
payload[i] = ((Number) list.get(i)).byteValue();
|
||||
Object dataObj = requestData.get("data");
|
||||
|
||||
if (dataObj != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||
java.util.List<com.ruoyi.mill.protocol.FieldDef> schema = TelegramSchema.getSchema(tcNo);
|
||||
if (schema == null) {
|
||||
return error("未知电文号: " + tcNo + ",无法编码");
|
||||
}
|
||||
} else if (payloadObj instanceof int[]) {
|
||||
int[] intArray = (int[]) payloadObj;
|
||||
payload = new byte[intArray.length];
|
||||
for (int i = 0; i < intArray.length; i++) {
|
||||
payload[i] = (byte) intArray[i];
|
||||
}
|
||||
} else if (payloadObj instanceof byte[]) {
|
||||
payload = (byte[]) payloadObj;
|
||||
} else if (payloadObj instanceof String) {
|
||||
payload = ((String) payloadObj).getBytes(StandardCharsets.UTF_8);
|
||||
payload = TelegramCodec.encode(schema, dataMap);
|
||||
log.info("[UDP-SEND] 使用Schema编码电文 {},data字段数={},编码后长度={} bytes", tcNo, dataMap.size(), payload.length);
|
||||
} else {
|
||||
return error("无效的payload类型: " + (payloadObj == null ? "null" : payloadObj.getClass().getName()));
|
||||
Object payloadObj = requestData.get("payload");
|
||||
if (payloadObj == null) {
|
||||
return error("data 或 payload 必须提供一项");
|
||||
}
|
||||
payload = convertPayloadToBytes(payloadObj);
|
||||
}
|
||||
|
||||
boolean result = udpSender.send(tcNo, payload);
|
||||
@@ -117,6 +125,30 @@ public class UdpController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private byte[] convertPayloadToBytes(Object payloadObj) {
|
||||
if (payloadObj instanceof java.util.List) {
|
||||
java.util.List<?> list = (java.util.List<?>) payloadObj;
|
||||
byte[] payload = new byte[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
payload[i] = ((Number) list.get(i)).byteValue();
|
||||
}
|
||||
return payload;
|
||||
} else if (payloadObj instanceof int[]) {
|
||||
int[] intArray = (int[]) payloadObj;
|
||||
byte[] payload = new byte[intArray.length];
|
||||
for (int i = 0; i < intArray.length; i++) {
|
||||
payload[i] = (byte) intArray[i];
|
||||
}
|
||||
return payload;
|
||||
} else if (payloadObj instanceof byte[]) {
|
||||
return (byte[]) payloadObj;
|
||||
} else if (payloadObj instanceof String) {
|
||||
return ((String) payloadObj).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
throw new IllegalArgumentException("无效的payload类型: " + (payloadObj == null ? "null" : payloadObj.getClass().getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析电文数据
|
||||
*/
|
||||
@@ -169,20 +201,35 @@ public class UdpController extends BaseController {
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "50") Integer pageSize) {
|
||||
|
||||
// TODO: 从数据库或缓存中获取历史记录
|
||||
// 这里返回模拟数据
|
||||
return success(getMockHistory(pageNum, pageSize));
|
||||
java.util.List<TelegramRecord> rows = telegramStore.getHistory(pageNum, pageSize);
|
||||
int total = telegramStore.getTotalCount();
|
||||
|
||||
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("rows", rows);
|
||||
result.put("total", total);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电文统计信息
|
||||
*/
|
||||
@ApiOperation("获取电文统计信息")
|
||||
@GetMapping("/stats")
|
||||
public AjaxResult telegramStats() {
|
||||
// TODO: 从数据库或缓存中获取统计数据
|
||||
// 这里返回模拟数据
|
||||
return success(getMockStats());
|
||||
int total = telegramStore.getTotalCount();
|
||||
long successCount = telegramStore.countSuccess();
|
||||
long todayInbound = telegramStore.countTodayInbound();
|
||||
|
||||
java.util.Map<String, Object> stats = new java.util.HashMap<>();
|
||||
stats.put("todayReceived", todayInbound);
|
||||
stats.put("totalReceived", telegramStore.countInbound());
|
||||
stats.put("successRate", total > 0 ? Math.round((successCount * 100.0) / total) : 0);
|
||||
stats.put("avgDelay", 0);
|
||||
return success(stats);
|
||||
}
|
||||
|
||||
@ApiOperation("清空电文历史记录")
|
||||
@DeleteMapping("/history")
|
||||
public AjaxResult clearHistory() {
|
||||
telegramStore.clear();
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,60 +268,4 @@ public class UdpController extends BaseController {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟历史记录数据
|
||||
*/
|
||||
private Object getMockHistory(int pageNum, int pageSize) {
|
||||
// 这里应该从数据库查询真实数据
|
||||
// 返回模拟数据用于演示
|
||||
|
||||
java.util.List<Object> mockData = java.util.Arrays.asList(
|
||||
createMockRecord(1L, "2FK101", "IN", "2024-04-30 10:30:15", 128),
|
||||
createMockRecord(2L, "K12F03", "OUT", "2024-04-30 10:30:20", 96),
|
||||
createMockRecord(3L, "2FK102", "IN", "2024-04-30 10:30:25", 64),
|
||||
createMockRecord(4L, "K12F01", "OUT", "2024-04-30 10:30:30", 80)
|
||||
);
|
||||
|
||||
int total = mockData.size();
|
||||
int fromIndex = (pageNum - 1) * pageSize;
|
||||
int toIndex = Math.min(fromIndex + pageSize, total);
|
||||
|
||||
if (fromIndex >= total) {
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
|
||||
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("rows", mockData.subList(fromIndex, toIndex));
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟记录
|
||||
*/
|
||||
private java.util.Map<String, Object> createMockRecord(
|
||||
Long id, String tcNo, String direction,
|
||||
String timestamp, Integer payloadLength) {
|
||||
|
||||
java.util.Map<String, Object> record = new java.util.HashMap<>();
|
||||
record.put("id", id);
|
||||
record.put("tcNo", tcNo);
|
||||
record.put("direction", direction);
|
||||
record.put("timestamp", timestamp);
|
||||
record.put("payloadLength", payloadLength);
|
||||
record.put("status", "成功");
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟统计数据
|
||||
*/
|
||||
private java.util.Map<String, Object> getMockStats() {
|
||||
java.util.Map<String, Object> stats = new java.util.HashMap<>();
|
||||
stats.put("todayReceived", 25);
|
||||
stats.put("totalReceived", 1247);
|
||||
stats.put("successRate", 98);
|
||||
stats.put("avgDelay", 150);
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,19 @@ public final class TelegramSchema {
|
||||
private static FieldDef i(String name) { return new FieldDef(name, INT, 4, 0); }
|
||||
private static FieldDef f(String name, int len, int precision) { return new FieldDef(name, FLOAT, len, precision); }
|
||||
|
||||
public static List<FieldDef> getSchema(String tcNo) {
|
||||
switch (tcNo) {
|
||||
case ID_2FK101: return SCHEMA_2FK101;
|
||||
case ID_2FK102: return SCHEMA_2FK102;
|
||||
case ID_2FK103: return SCHEMA_2FK103;
|
||||
case ID_2FK104: return SCHEMA_2FK104;
|
||||
case ID_K12F01: return SCHEMA_K12F01;
|
||||
case ID_K12F02: return SCHEMA_K12F02;
|
||||
case ID_K12F03: return SCHEMA_K12F03;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 2FK101 作业命令信息 L3→L2
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.ruoyi.mill.service.impl;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.mill.udp.TelegramRecord;
|
||||
import com.ruoyi.mill.udp.TelegramStore;
|
||||
import com.ruoyi.mill.udp.UdpProperties;
|
||||
import com.ruoyi.mill.udp.UdpServer;
|
||||
import com.ruoyi.mill.udp.UdpSender;
|
||||
@@ -10,7 +12,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,9 @@ public class UdpServiceImpl implements IUdpService {
|
||||
@Autowired
|
||||
private UdpSender udpSender;
|
||||
|
||||
@Autowired
|
||||
private TelegramStore telegramStore;
|
||||
|
||||
/**
|
||||
* 获取UDP配置
|
||||
*/
|
||||
@@ -86,68 +90,34 @@ public class UdpServiceImpl implements IUdpService {
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> getTelegramHistory(int pageNum, int pageSize) {
|
||||
// TODO: 从数据库查询真实的历史记录
|
||||
// 这里返回模拟数据用于演示
|
||||
|
||||
List<Map<String, Object>> mockData = new ArrayList<>();
|
||||
|
||||
// 生成一些模拟的历史记录
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
Map<String, Object> record = new HashMap<>();
|
||||
record.put("id", i);
|
||||
record.put("tcNo", getRandomTcNo());
|
||||
record.put("direction", Math.random() > 0.5 ? "IN" : "OUT");
|
||||
record.put("timestamp", generateMockTimestamp(i));
|
||||
record.put("payloadLength", new Random().nextInt(200) + 32);
|
||||
record.put("status", "成功");
|
||||
|
||||
mockData.add(record);
|
||||
List<TelegramRecord> records = telegramStore.getHistory(pageNum, pageSize);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (TelegramRecord r : records) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", r.getId());
|
||||
map.put("tcNo", r.getTcNo());
|
||||
map.put("direction", r.getDirection());
|
||||
map.put("timestamp", r.getTimestamp());
|
||||
map.put("payloadLength", r.getPayloadLength());
|
||||
map.put("status", r.getStatus());
|
||||
map.put("rawPayload", r.getRawPayload());
|
||||
map.put("parsedFields", r.getParsedFields());
|
||||
map.put("decodedData", r.getDecodedData());
|
||||
result.add(map);
|
||||
}
|
||||
|
||||
// 简单的分页逻辑
|
||||
int fromIndex = (pageNum - 1) * pageSize;
|
||||
int toIndex = Math.min(fromIndex + pageSize, mockData.size());
|
||||
|
||||
if (fromIndex >= mockData.size()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return mockData.subList(fromIndex, toIndex);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电文统计数据
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getTelegramStats() {
|
||||
// TODO: 从数据库查询真实的统计数据
|
||||
// 这里返回模拟数据用于演示
|
||||
int total = telegramStore.getTotalCount();
|
||||
long successCount = telegramStore.countSuccess();
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("todayReceived", 25 + new Random().nextInt(10));
|
||||
stats.put("totalReceived", 1247 + new Random().nextInt(100));
|
||||
stats.put("successRate", 95 + new Random().nextInt(5));
|
||||
stats.put("avgDelay", 120 + new Random().nextInt(60));
|
||||
|
||||
stats.put("todayReceived", telegramStore.countTodayInbound());
|
||||
stats.put("totalReceived", telegramStore.countInbound());
|
||||
stats.put("successRate", total > 0 ? Math.round((successCount * 100.0) / total) : 0);
|
||||
stats.put("avgDelay", 0);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机电文号(用于模拟)
|
||||
*/
|
||||
private String getRandomTcNo() {
|
||||
String[] tcNos = {"2FK101", "2FK102", "2FK103", "2FK104", "K12F01", "K12F02", "K12F03"};
|
||||
return tcNos[new Random().nextInt(tcNos.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟时间戳
|
||||
*/
|
||||
private String generateMockTimestamp(int index) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.MINUTE, -index * 5); // 每5分钟一条记录
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
return sdf.format(calendar.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,24 +17,33 @@ public class TelegramDispatcher {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TelegramDispatcher.class);
|
||||
|
||||
public void dispatch(String telegramId, byte[] payload) {
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private TelegramStore telegramStore;
|
||||
|
||||
public void dispatch(String telegramId, byte[] rawFrame, byte[] payload) {
|
||||
log.info("[UDP-RECV] telegramId={} payloadLen={}", telegramId, payload.length);
|
||||
Map<String, Object> decoded = null;
|
||||
switch (telegramId) {
|
||||
case TelegramSchema.ID_2FK101:
|
||||
handle2FK101(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK101, payload));
|
||||
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK101, payload);
|
||||
handle2FK101(decoded);
|
||||
break;
|
||||
case TelegramSchema.ID_2FK102:
|
||||
handle2FK102(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK102, payload));
|
||||
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK102, payload);
|
||||
handle2FK102(decoded);
|
||||
break;
|
||||
case TelegramSchema.ID_2FK103:
|
||||
handle2FK103(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK103, payload));
|
||||
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK103, payload);
|
||||
handle2FK103(decoded);
|
||||
break;
|
||||
case TelegramSchema.ID_2FK104:
|
||||
handle2FK104(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK104, payload));
|
||||
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK104, payload);
|
||||
handle2FK104(decoded);
|
||||
break;
|
||||
default:
|
||||
log.warn("[UDP-RECV] 未知电文号: {}", telegramId);
|
||||
}
|
||||
telegramStore.addInbound(telegramId, rawFrame, decoded);
|
||||
}
|
||||
|
||||
/** 2FK101 作业命令信息 — L3 下发生产计划 */
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.ruoyi.mill.udp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TelegramRecord {
|
||||
|
||||
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private long id;
|
||||
private String tcNo;
|
||||
private String direction;
|
||||
private String timestamp;
|
||||
private int payloadLength;
|
||||
private String status;
|
||||
private String rawPayload;
|
||||
private List<Map<String, Object>> parsedFields;
|
||||
private Map<String, Object> decodedData;
|
||||
|
||||
public static TelegramRecord inbound(long id, String tcNo, byte[] rawFrame, Map<String, Object> decodedData) {
|
||||
TelegramRecord r = new TelegramRecord();
|
||||
r.id = id;
|
||||
r.tcNo = tcNo;
|
||||
r.direction = "IN";
|
||||
r.timestamp = LocalDateTime.now().format(FMT);
|
||||
r.payloadLength = rawFrame.length;
|
||||
r.status = "成功";
|
||||
r.rawPayload = toHexDump(rawFrame);
|
||||
r.decodedData = decodedData;
|
||||
r.parsedFields = buildParsedFields(decodedData);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static TelegramRecord outbound(long id, String tcNo, byte[] payload, boolean success, Map<String, Object> decodedData) {
|
||||
TelegramRecord r = new TelegramRecord();
|
||||
r.id = id;
|
||||
r.tcNo = tcNo;
|
||||
r.direction = "OUT";
|
||||
r.timestamp = LocalDateTime.now().format(FMT);
|
||||
r.payloadLength = payload.length;
|
||||
r.status = success ? "成功" : "失败";
|
||||
r.rawPayload = toHexDump(payload);
|
||||
r.decodedData = decodedData;
|
||||
r.parsedFields = buildParsedFields(decodedData);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static String toHexDump(byte[] data) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int limit = Math.min(data.length, 256);
|
||||
for (int i = 0; i < limit; i++) {
|
||||
sb.append(String.format("%02X ", data[i]));
|
||||
if ((i + 1) % 32 == 0) sb.append('\n');
|
||||
}
|
||||
if (data.length > 256) sb.append("... (truncated)");
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> buildParsedFields(Map<String, Object> decodedData) {
|
||||
List<Map<String, Object>> fields = new ArrayList<>();
|
||||
if (decodedData == null) return fields;
|
||||
for (Map.Entry<String, Object> entry : decodedData.entrySet()) {
|
||||
Map<String, Object> field = new java.util.LinkedHashMap<>();
|
||||
field.put("name", entry.getKey());
|
||||
Object val = entry.getValue();
|
||||
field.put("value", val != null ? val.toString() : "");
|
||||
field.put("type", val instanceof Integer ? "INT" : val instanceof Float ? "FLOAT" : val instanceof Long ? "LONG" : "STRING");
|
||||
field.put("length", val != null ? val.toString().length() : 0);
|
||||
fields.add(field);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public void setId(long id) { this.id = id; }
|
||||
public String getTcNo() { return tcNo; }
|
||||
public void setTcNo(String tcNo) { this.tcNo = tcNo; }
|
||||
public String getDirection() { return direction; }
|
||||
public void setDirection(String direction) { this.direction = direction; }
|
||||
public String getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
|
||||
public int getPayloadLength() { return payloadLength; }
|
||||
public void setPayloadLength(int payloadLength) { this.payloadLength = payloadLength; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getRawPayload() { return rawPayload; }
|
||||
public void setRawPayload(String rawPayload) { this.rawPayload = rawPayload; }
|
||||
public List<Map<String, Object>> getParsedFields() { return parsedFields; }
|
||||
public void setParsedFields(List<Map<String, Object>> parsedFields) { this.parsedFields = parsedFields; }
|
||||
public Map<String, Object> getDecodedData() { return decodedData; }
|
||||
public void setDecodedData(Map<String, Object> decodedData) { this.decodedData = decodedData; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ruoyi.mill.udp;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
@Component
|
||||
public class TelegramStore {
|
||||
|
||||
private static final int MAX_RECORDS = 1000;
|
||||
|
||||
private final CopyOnWriteArrayList<TelegramRecord> records = new CopyOnWriteArrayList<>();
|
||||
private final AtomicLong idGenerator = new AtomicLong(0);
|
||||
|
||||
public TelegramRecord addInbound(String tcNo, byte[] rawFrame, java.util.Map<String, Object> decodedData) {
|
||||
TelegramRecord record = TelegramRecord.inbound(idGenerator.incrementAndGet(), tcNo, rawFrame, decodedData);
|
||||
records.add(0, record);
|
||||
trimIfNeeded();
|
||||
return record;
|
||||
}
|
||||
|
||||
public TelegramRecord addOutbound(String tcNo, byte[] payload, boolean success, java.util.Map<String, Object> decodedData) {
|
||||
TelegramRecord record = TelegramRecord.outbound(idGenerator.incrementAndGet(), tcNo, payload, success, decodedData);
|
||||
records.add(0, record);
|
||||
trimIfNeeded();
|
||||
return record;
|
||||
}
|
||||
|
||||
public List<TelegramRecord> getHistory(int pageNum, int pageSize) {
|
||||
int total = records.size();
|
||||
int fromIndex = (pageNum - 1) * pageSize;
|
||||
if (fromIndex >= total) return Collections.emptyList();
|
||||
int toIndex = Math.min(fromIndex + pageSize, total);
|
||||
return new ArrayList<>(records.subList(fromIndex, toIndex));
|
||||
}
|
||||
|
||||
public int getTotalCount() {
|
||||
return records.size();
|
||||
}
|
||||
|
||||
public long countTodayInbound() {
|
||||
String today = java.time.LocalDate.now().toString();
|
||||
return records.stream()
|
||||
.filter(r -> r.getDirection().equals("IN") && r.getTimestamp().startsWith(today))
|
||||
.count();
|
||||
}
|
||||
|
||||
public long countInbound() {
|
||||
return records.stream().filter(r -> r.getDirection().equals("IN")).count();
|
||||
}
|
||||
|
||||
public long countSuccess() {
|
||||
return records.stream().filter(r -> "成功".equals(r.getStatus())).count();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
records.clear();
|
||||
}
|
||||
|
||||
private void trimIfNeeded() {
|
||||
while (records.size() > MAX_RECORDS) {
|
||||
records.remove(records.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public class UdpSender {
|
||||
@Autowired
|
||||
private UdpProperties props;
|
||||
|
||||
@Autowired
|
||||
private TelegramStore telegramStore;
|
||||
|
||||
/** K12F01 计划信息应答 */
|
||||
public void sendK12F01(Map<String, Object> data) {
|
||||
send(TelegramSchema.ID_K12F01, TelegramCodec.encode(TelegramSchema.SCHEMA_K12F01, data));
|
||||
@@ -68,6 +71,7 @@ public class UdpSender {
|
||||
|
||||
log.info("[UDP-SEND] tcNo={} -> {}:{} frameLen={}", tcNo,
|
||||
props.getRemoteHost(), props.getRemotePort(), frame.length);
|
||||
telegramStore.addOutbound(tcNo, frame, true, decodePayload(tcNo, payload));
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -81,10 +85,23 @@ public class UdpSender {
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[UDP-SEND] 发送失败 tcNo={}", tcNo, e);
|
||||
telegramStore.addOutbound(tcNo, payload, false, decodePayload(tcNo, payload));
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, Object> decodePayload(String tcNo, byte[] payload) {
|
||||
try {
|
||||
java.util.List<com.ruoyi.mill.protocol.FieldDef> schema = TelegramSchema.getSchema(tcNo);
|
||||
if (schema != null) {
|
||||
return TelegramCodec.decode(schema, payload);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[UDP-SEND] 解码payload失败 tcNo={}: {}", tcNo, e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,20 +57,43 @@ public class UdpServer {
|
||||
|
||||
private void receiveLoop() {
|
||||
byte[] buf = new byte[props.getBufferSize()];
|
||||
log.info("[UDP-SERVER] 接收线程已启动,监听端口: {}", props.getLocalPort());
|
||||
while (running) {
|
||||
try {
|
||||
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
|
||||
socket.receive(pkt);
|
||||
|
||||
// 打印接收到的原始数据信息
|
||||
String senderAddr = pkt.getAddress().getHostAddress();
|
||||
int senderPort = pkt.getPort();
|
||||
byte[] data = Arrays.copyOf(pkt.getData(), pkt.getLength());
|
||||
|
||||
log.info("[UDP-RECV] <<<< 收到UDP数据包 - 来源: {}:{}, 长度: {} bytes",
|
||||
senderAddr, senderPort, data.length);
|
||||
|
||||
if (data.length < TC_NO_LEN) {
|
||||
log.warn("[UDP-SERVER] 收到过短数据包,长度={}", data.length);
|
||||
log.warn("[UDP-RECV] 收到过短数据包,长度={}, 忽略", data.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
String tcNo = new String(data, 0, TC_NO_LEN, StandardCharsets.US_ASCII).trim();
|
||||
byte[] payload = Arrays.copyOfRange(data, TC_NO_LEN, data.length);
|
||||
dispatcher.dispatch(tcNo, payload);
|
||||
|
||||
log.info("[UDP-RECV] 电文号: '{}', Payload长度: {} bytes", tcNo, payload.length);
|
||||
|
||||
// 打印前32字节的十六进制数据
|
||||
StringBuilder hexDump = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(data.length, 32); i++) {
|
||||
hexDump.append(String.format("%02X ", data[i]));
|
||||
}
|
||||
log.debug("[UDP-RECV] 数据预览: {}", hexDump.toString());
|
||||
|
||||
dispatcher.dispatch(tcNo, data, payload);
|
||||
|
||||
} catch (Exception e) {
|
||||
if (running) log.error("[UDP-SERVER] 接收异常", e);
|
||||
if (running) {
|
||||
log.error("[UDP-SERVER] 接收异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// UDP 服务器配置
|
||||
export function getUdpConfig() {
|
||||
return request({ url: '/mill/udp/config', method: 'get' })
|
||||
}
|
||||
@@ -9,7 +8,6 @@ export function updateUdpConfig(data) {
|
||||
return request({ url: '/mill/udp/config', method: 'put', data })
|
||||
}
|
||||
|
||||
// 发送 UDP 报文
|
||||
export function sendTelegram(data) {
|
||||
return request({
|
||||
url: '/mill/udp/send',
|
||||
@@ -18,7 +16,6 @@ export function sendTelegram(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取电文历史记录
|
||||
export function getTelegramHistory(query) {
|
||||
return request({
|
||||
url: '/mill/udp/history',
|
||||
@@ -27,16 +24,18 @@ export function getTelegramHistory(query) {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前电文统计
|
||||
export function getTelegramStats() {
|
||||
return request({ url: '/mill/udp/stats', method: 'get' })
|
||||
}
|
||||
|
||||
// 解析电文数据
|
||||
export function parseTelegram(tcNo, payload) {
|
||||
return request({
|
||||
url: '/mill/udp/parse',
|
||||
method: 'post',
|
||||
data: { tcNo, payload }
|
||||
})
|
||||
}
|
||||
|
||||
export function clearTelegramHistory() {
|
||||
return request({ url: '/mill/udp/history', method: 'delete' })
|
||||
}
|
||||
@@ -20,12 +20,12 @@
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="目标端口">
|
||||
<el-input-number v-model="configForm.targetPort" :min="1024" :max="65535" />
|
||||
<el-input-number v-model="configForm.remotePort" :min="1024" :max="65535" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="目标IP">
|
||||
<el-input v-model="configForm.targetIp" placeholder="192.168.1.100" />
|
||||
<el-input v-model="configForm.remoteHost" placeholder="192.168.1.100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -184,7 +184,7 @@
|
||||
height="400"
|
||||
style="margin-top: 10px;"
|
||||
>
|
||||
<el-table-column prop="id" label="#" width="40" align="center" />
|
||||
<el-table-column prop="id" label="#" align="center" />
|
||||
<el-table-column prop="tcNo" label="电文号" width="80" align="center" />
|
||||
<el-table-column prop="direction" label="方向" width="60" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
@@ -254,7 +254,8 @@ import {
|
||||
sendTelegram,
|
||||
getTelegramHistory,
|
||||
getTelegramStats,
|
||||
parseTelegram
|
||||
parseTelegram,
|
||||
clearTelegramHistory
|
||||
} from '@/api/mill/udp'
|
||||
|
||||
export default {
|
||||
@@ -267,8 +268,8 @@ export default {
|
||||
// 配置表单
|
||||
configForm: {
|
||||
localPort: 8080,
|
||||
targetPort: 8081,
|
||||
targetIp: '192.168.1.100',
|
||||
remotePort: 8081,
|
||||
remoteHost: '192.168.1.100',
|
||||
bufferSize: 8192,
|
||||
timeout: 5000,
|
||||
retryCount: 3
|
||||
@@ -487,17 +488,26 @@ export default {
|
||||
this.sending = true
|
||||
|
||||
try {
|
||||
let payload
|
||||
let tcNo = this.selectedTcNo
|
||||
let requestData
|
||||
|
||||
if (this.activeTab === 'json') {
|
||||
payload = Buffer.from(JSON.stringify(this.telegramData.json), 'utf8')
|
||||
let parsedData
|
||||
try {
|
||||
parsedData = JSON.parse(this.telegramData.json)
|
||||
} catch (e) {
|
||||
this.$message.error('JSON格式错误: ' + e.message)
|
||||
return
|
||||
}
|
||||
requestData = { tcNo, data: parsedData }
|
||||
} else if (this.activeTab === 'hex') {
|
||||
// 转换十六进制字符串为字节数组
|
||||
const hexStr = this.telegramData.hex.replace(/\s/g, '')
|
||||
payload = Buffer.from(hexStr, 'hex')
|
||||
const bytes = []
|
||||
for (let i = 0; i < hexStr.length; i += 2) {
|
||||
bytes.push(parseInt(hexStr.substr(i, 2), 16))
|
||||
}
|
||||
requestData = { tcNo, payload: bytes }
|
||||
} else if (this.activeTab === 'form') {
|
||||
// 根据电文号构造数据
|
||||
const fields = this.getFieldsForTcNo(this.selectedTcNo)
|
||||
const formData = {}
|
||||
|
||||
@@ -516,24 +526,13 @@ export default {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
payload = Buffer.from(JSON.stringify(formData), 'utf8')
|
||||
requestData = { tcNo, data: formData }
|
||||
}
|
||||
|
||||
const response = await sendTelegram({ tcNo, payload: Array.from(payload) })
|
||||
const response = await sendTelegram(requestData)
|
||||
|
||||
this.$message.success(`报文 ${tcNo} 发送成功`)
|
||||
|
||||
// 添加到历史记录
|
||||
this.historyList.unshift({
|
||||
id: Date.now(),
|
||||
tcNo,
|
||||
direction: 'OUT',
|
||||
timestamp: new Date().toLocaleString(),
|
||||
payloadLength: payload.length,
|
||||
status: '成功'
|
||||
})
|
||||
|
||||
this.loadHistory()
|
||||
this.loadStats()
|
||||
|
||||
} catch (error) {
|
||||
@@ -545,7 +544,11 @@ export default {
|
||||
|
||||
// 查看详情
|
||||
viewDetail(record) {
|
||||
this.currentDetail = record
|
||||
this.currentDetail = {
|
||||
...record,
|
||||
rawPayload: record.rawPayload || '无原始数据',
|
||||
parsedFields: record.parsedFields || []
|
||||
}
|
||||
this.detailDialogVisible = true
|
||||
},
|
||||
|
||||
@@ -570,20 +573,25 @@ export default {
|
||||
this.$confirm('确定要清空所有历史记录吗?', '提示', {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.historyList = []
|
||||
this.stats = {
|
||||
todayReceived: 0,
|
||||
totalReceived: 0,
|
||||
successRate: 0,
|
||||
avgDelay: 0
|
||||
}
|
||||
clearTelegramHistory().then(() => {
|
||||
this.historyList = []
|
||||
this.stats = {
|
||||
todayReceived: 0,
|
||||
totalReceived: 0,
|
||||
successRate: 0,
|
||||
avgDelay: 0
|
||||
}
|
||||
this.$message.success('历史记录已清空')
|
||||
}).catch(() => {
|
||||
this.$message.error('清空失败')
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 加载历史记录
|
||||
loadHistory() {
|
||||
getTelegramHistory({ page: 1, size: 50 }).then(res => {
|
||||
this.historyList = res.rows || []
|
||||
getTelegramHistory({ pageNum: 1, pageSize: 50 }).then(res => {
|
||||
this.historyList = (res.data && res.data.rows) || []
|
||||
}).catch(() => {
|
||||
this.$message.error('加载历史记录失败')
|
||||
})
|
||||
@@ -592,14 +600,13 @@ export default {
|
||||
// 加载统计数据
|
||||
loadStats() {
|
||||
getTelegramStats().then(res => {
|
||||
this.stats = res.data || this.stats
|
||||
this.stats = (res.data && res.data) || this.stats
|
||||
}).catch(() => {
|
||||
// 使用本地计算
|
||||
this.stats = {
|
||||
todayReceived: this.historyList.filter(h => h.direction === 'IN').length,
|
||||
totalReceived: this.historyList.length,
|
||||
successRate: this.historyList.length > 0 ? Math.round((this.historyList.filter(h => h.status === '成功').length / this.historyList.length) * 100) : 0,
|
||||
avgDelay: 150
|
||||
avgDelay: 0
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -700,4 +707,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user