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:
2026-05-05 14:43:38 +08:00
parent 2e17943a7e
commit 4fea237b33
11 changed files with 384 additions and 191 deletions

View File

@@ -140,6 +140,6 @@ mill:
# L3 系统 IP 地址
remote-host: 127.0.0.1
# L3 系统 UDP 端口
remote-port: 9000
remote-port: 9001
# 接收缓冲区大小(字节)
buffer-size: 4096

View File

@@ -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;
}
}

View File

@@ -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
// ─────────────────────────────────────────────────────────────

View File

@@ -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());
}
}

View File

@@ -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 下发生产计划 */

View File

@@ -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; }
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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' })
}

View File

@@ -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>