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 地址
|
# L3 系统 IP 地址
|
||||||
remote-host: 127.0.0.1
|
remote-host: 127.0.0.1
|
||||||
# L3 系统 UDP 端口
|
# L3 系统 UDP 端口
|
||||||
remote-port: 9000
|
remote-port: 9001
|
||||||
# 接收缓冲区大小(字节)
|
# 接收缓冲区大小(字节)
|
||||||
buffer-size: 4096
|
buffer-size: 4096
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ package com.ruoyi.mill.controller;
|
|||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
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.UdpProperties;
|
||||||
import com.ruoyi.mill.udp.UdpServer;
|
import com.ruoyi.mill.udp.UdpServer;
|
||||||
import com.ruoyi.mill.udp.UdpSender;
|
import com.ruoyi.mill.udp.UdpSender;
|
||||||
@@ -37,6 +41,9 @@ public class UdpController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UdpSender udpSender;
|
private UdpSender udpSender;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TelegramStore telegramStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取UDP配置
|
* 获取UDP配置
|
||||||
*/
|
*/
|
||||||
@@ -78,27 +85,28 @@ public class UdpController extends BaseController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
String tcNo = (String) requestData.get("tcNo");
|
String tcNo = (String) requestData.get("tcNo");
|
||||||
Object payloadObj = requestData.get("payload");
|
if (StringUtils.isBlank(tcNo)) {
|
||||||
|
return error("电文号不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
byte[] payload;
|
byte[] payload;
|
||||||
if (payloadObj instanceof java.util.List) {
|
Object dataObj = requestData.get("data");
|
||||||
java.util.List<?> list = (java.util.List<?>) payloadObj;
|
|
||||||
payload = new byte[list.size()];
|
if (dataObj != null) {
|
||||||
for (int i = 0; i < list.size(); i++) {
|
@SuppressWarnings("unchecked")
|
||||||
payload[i] = ((Number) list.get(i)).byteValue();
|
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[]) {
|
payload = TelegramCodec.encode(schema, dataMap);
|
||||||
int[] intArray = (int[]) payloadObj;
|
log.info("[UDP-SEND] 使用Schema编码电文 {},data字段数={},编码后长度={} bytes", tcNo, dataMap.size(), payload.length);
|
||||||
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);
|
|
||||||
} else {
|
} 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);
|
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 = "1") Integer pageNum,
|
||||||
@RequestParam(defaultValue = "50") Integer pageSize) {
|
@RequestParam(defaultValue = "50") Integer pageSize) {
|
||||||
|
|
||||||
// TODO: 从数据库或缓存中获取历史记录
|
java.util.List<TelegramRecord> rows = telegramStore.getHistory(pageNum, pageSize);
|
||||||
// 这里返回模拟数据
|
int total = telegramStore.getTotalCount();
|
||||||
return success(getMockHistory(pageNum, pageSize));
|
|
||||||
|
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||||
|
result.put("rows", rows);
|
||||||
|
result.put("total", total);
|
||||||
|
return success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取电文统计信息
|
|
||||||
*/
|
|
||||||
@ApiOperation("获取电文统计信息")
|
@ApiOperation("获取电文统计信息")
|
||||||
@GetMapping("/stats")
|
@GetMapping("/stats")
|
||||||
public AjaxResult telegramStats() {
|
public AjaxResult telegramStats() {
|
||||||
// TODO: 从数据库或缓存中获取统计数据
|
int total = telegramStore.getTotalCount();
|
||||||
// 这里返回模拟数据
|
long successCount = telegramStore.countSuccess();
|
||||||
return success(getMockStats());
|
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();
|
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 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); }
|
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
|
// 2FK101 作业命令信息 L3→L2
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.ruoyi.mill.service.impl;
|
package com.ruoyi.mill.service.impl;
|
||||||
|
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
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.UdpProperties;
|
||||||
import com.ruoyi.mill.udp.UdpServer;
|
import com.ruoyi.mill.udp.UdpServer;
|
||||||
import com.ruoyi.mill.udp.UdpSender;
|
import com.ruoyi.mill.udp.UdpSender;
|
||||||
@@ -10,7 +12,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +32,9 @@ public class UdpServiceImpl implements IUdpService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UdpSender udpSender;
|
private UdpSender udpSender;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TelegramStore telegramStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取UDP配置
|
* 获取UDP配置
|
||||||
*/
|
*/
|
||||||
@@ -86,68 +90,34 @@ public class UdpServiceImpl implements IUdpService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<Map<String, Object>> getTelegramHistory(int pageNum, int pageSize) {
|
public List<Map<String, Object>> getTelegramHistory(int pageNum, int pageSize) {
|
||||||
// TODO: 从数据库查询真实的历史记录
|
List<TelegramRecord> records = telegramStore.getHistory(pageNum, pageSize);
|
||||||
// 这里返回模拟数据用于演示
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (TelegramRecord r : records) {
|
||||||
List<Map<String, Object>> mockData = new ArrayList<>();
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", r.getId());
|
||||||
// 生成一些模拟的历史记录
|
map.put("tcNo", r.getTcNo());
|
||||||
for (int i = 1; i <= 20; i++) {
|
map.put("direction", r.getDirection());
|
||||||
Map<String, Object> record = new HashMap<>();
|
map.put("timestamp", r.getTimestamp());
|
||||||
record.put("id", i);
|
map.put("payloadLength", r.getPayloadLength());
|
||||||
record.put("tcNo", getRandomTcNo());
|
map.put("status", r.getStatus());
|
||||||
record.put("direction", Math.random() > 0.5 ? "IN" : "OUT");
|
map.put("rawPayload", r.getRawPayload());
|
||||||
record.put("timestamp", generateMockTimestamp(i));
|
map.put("parsedFields", r.getParsedFields());
|
||||||
record.put("payloadLength", new Random().nextInt(200) + 32);
|
map.put("decodedData", r.getDecodedData());
|
||||||
record.put("status", "成功");
|
result.add(map);
|
||||||
|
|
||||||
mockData.add(record);
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
// 简单的分页逻辑
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取电文统计数据
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getTelegramStats() {
|
public Map<String, Object> getTelegramStats() {
|
||||||
// TODO: 从数据库查询真实的统计数据
|
int total = telegramStore.getTotalCount();
|
||||||
// 这里返回模拟数据用于演示
|
long successCount = telegramStore.countSuccess();
|
||||||
|
|
||||||
Map<String, Object> stats = new HashMap<>();
|
Map<String, Object> stats = new HashMap<>();
|
||||||
stats.put("todayReceived", 25 + new Random().nextInt(10));
|
stats.put("todayReceived", telegramStore.countTodayInbound());
|
||||||
stats.put("totalReceived", 1247 + new Random().nextInt(100));
|
stats.put("totalReceived", telegramStore.countInbound());
|
||||||
stats.put("successRate", 95 + new Random().nextInt(5));
|
stats.put("successRate", total > 0 ? Math.round((successCount * 100.0) / total) : 0);
|
||||||
stats.put("avgDelay", 120 + new Random().nextInt(60));
|
stats.put("avgDelay", 0);
|
||||||
|
|
||||||
return stats;
|
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);
|
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);
|
log.info("[UDP-RECV] telegramId={} payloadLen={}", telegramId, payload.length);
|
||||||
|
Map<String, Object> decoded = null;
|
||||||
switch (telegramId) {
|
switch (telegramId) {
|
||||||
case TelegramSchema.ID_2FK101:
|
case TelegramSchema.ID_2FK101:
|
||||||
handle2FK101(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK101, payload));
|
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK101, payload);
|
||||||
|
handle2FK101(decoded);
|
||||||
break;
|
break;
|
||||||
case TelegramSchema.ID_2FK102:
|
case TelegramSchema.ID_2FK102:
|
||||||
handle2FK102(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK102, payload));
|
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK102, payload);
|
||||||
|
handle2FK102(decoded);
|
||||||
break;
|
break;
|
||||||
case TelegramSchema.ID_2FK103:
|
case TelegramSchema.ID_2FK103:
|
||||||
handle2FK103(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK103, payload));
|
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK103, payload);
|
||||||
|
handle2FK103(decoded);
|
||||||
break;
|
break;
|
||||||
case TelegramSchema.ID_2FK104:
|
case TelegramSchema.ID_2FK104:
|
||||||
handle2FK104(TelegramCodec.decode(TelegramSchema.SCHEMA_2FK104, payload));
|
decoded = TelegramCodec.decode(TelegramSchema.SCHEMA_2FK104, payload);
|
||||||
|
handle2FK104(decoded);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
log.warn("[UDP-RECV] 未知电文号: {}", telegramId);
|
log.warn("[UDP-RECV] 未知电文号: {}", telegramId);
|
||||||
}
|
}
|
||||||
|
telegramStore.addInbound(telegramId, rawFrame, decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 2FK101 作业命令信息 — L3 下发生产计划 */
|
/** 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
|
@Autowired
|
||||||
private UdpProperties props;
|
private UdpProperties props;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TelegramStore telegramStore;
|
||||||
|
|
||||||
/** K12F01 计划信息应答 */
|
/** K12F01 计划信息应答 */
|
||||||
public void sendK12F01(Map<String, Object> data) {
|
public void sendK12F01(Map<String, Object> data) {
|
||||||
send(TelegramSchema.ID_K12F01, TelegramCodec.encode(TelegramSchema.SCHEMA_K12F01, 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,
|
log.info("[UDP-SEND] tcNo={} -> {}:{} frameLen={}", tcNo,
|
||||||
props.getRemoteHost(), props.getRemotePort(), frame.length);
|
props.getRemoteHost(), props.getRemotePort(), frame.length);
|
||||||
|
telegramStore.addOutbound(tcNo, frame, true, decodePayload(tcNo, payload));
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -81,10 +85,23 @@ public class UdpSender {
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[UDP-SEND] 发送失败 tcNo={}", tcNo, e);
|
log.error("[UDP-SEND] 发送失败 tcNo={}", tcNo, e);
|
||||||
|
telegramStore.addOutbound(tcNo, payload, false, decodePayload(tcNo, payload));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
private void receiveLoop() {
|
||||||
byte[] buf = new byte[props.getBufferSize()];
|
byte[] buf = new byte[props.getBufferSize()];
|
||||||
|
log.info("[UDP-SERVER] 接收线程已启动,监听端口: {}", props.getLocalPort());
|
||||||
while (running) {
|
while (running) {
|
||||||
try {
|
try {
|
||||||
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
|
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
|
||||||
socket.receive(pkt);
|
socket.receive(pkt);
|
||||||
|
|
||||||
|
// 打印接收到的原始数据信息
|
||||||
|
String senderAddr = pkt.getAddress().getHostAddress();
|
||||||
|
int senderPort = pkt.getPort();
|
||||||
byte[] data = Arrays.copyOf(pkt.getData(), pkt.getLength());
|
byte[] data = Arrays.copyOf(pkt.getData(), pkt.getLength());
|
||||||
|
|
||||||
|
log.info("[UDP-RECV] <<<< 收到UDP数据包 - 来源: {}:{}, 长度: {} bytes",
|
||||||
|
senderAddr, senderPort, data.length);
|
||||||
|
|
||||||
if (data.length < TC_NO_LEN) {
|
if (data.length < TC_NO_LEN) {
|
||||||
log.warn("[UDP-SERVER] 收到过短数据包,长度={}", data.length);
|
log.warn("[UDP-RECV] 收到过短数据包,长度={}, 忽略", data.length);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String tcNo = new String(data, 0, TC_NO_LEN, StandardCharsets.US_ASCII).trim();
|
String tcNo = new String(data, 0, TC_NO_LEN, StandardCharsets.US_ASCII).trim();
|
||||||
byte[] payload = Arrays.copyOfRange(data, TC_NO_LEN, data.length);
|
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) {
|
} 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'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// UDP 服务器配置
|
|
||||||
export function getUdpConfig() {
|
export function getUdpConfig() {
|
||||||
return request({ url: '/mill/udp/config', method: 'get' })
|
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 })
|
return request({ url: '/mill/udp/config', method: 'put', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送 UDP 报文
|
|
||||||
export function sendTelegram(data) {
|
export function sendTelegram(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/mill/udp/send',
|
url: '/mill/udp/send',
|
||||||
@@ -18,7 +16,6 @@ export function sendTelegram(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取电文历史记录
|
|
||||||
export function getTelegramHistory(query) {
|
export function getTelegramHistory(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/mill/udp/history',
|
url: '/mill/udp/history',
|
||||||
@@ -27,12 +24,10 @@ export function getTelegramHistory(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前电文统计
|
|
||||||
export function getTelegramStats() {
|
export function getTelegramStats() {
|
||||||
return request({ url: '/mill/udp/stats', method: 'get' })
|
return request({ url: '/mill/udp/stats', method: 'get' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析电文数据
|
|
||||||
export function parseTelegram(tcNo, payload) {
|
export function parseTelegram(tcNo, payload) {
|
||||||
return request({
|
return request({
|
||||||
url: '/mill/udp/parse',
|
url: '/mill/udp/parse',
|
||||||
@@ -40,3 +35,7 @@ export function parseTelegram(tcNo, payload) {
|
|||||||
data: { tcNo, payload }
|
data: { tcNo, payload }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearTelegramHistory() {
|
||||||
|
return request({ url: '/mill/udp/history', method: 'delete' })
|
||||||
|
}
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="目标端口">
|
<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-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="目标IP">
|
<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-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
height="400"
|
height="400"
|
||||||
style="margin-top: 10px;"
|
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="tcNo" label="电文号" width="80" align="center" />
|
||||||
<el-table-column prop="direction" label="方向" width="60" align="center">
|
<el-table-column prop="direction" label="方向" width="60" align="center">
|
||||||
<template slot-scope="{ row }">
|
<template slot-scope="{ row }">
|
||||||
@@ -254,7 +254,8 @@ import {
|
|||||||
sendTelegram,
|
sendTelegram,
|
||||||
getTelegramHistory,
|
getTelegramHistory,
|
||||||
getTelegramStats,
|
getTelegramStats,
|
||||||
parseTelegram
|
parseTelegram,
|
||||||
|
clearTelegramHistory
|
||||||
} from '@/api/mill/udp'
|
} from '@/api/mill/udp'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -267,8 +268,8 @@ export default {
|
|||||||
// 配置表单
|
// 配置表单
|
||||||
configForm: {
|
configForm: {
|
||||||
localPort: 8080,
|
localPort: 8080,
|
||||||
targetPort: 8081,
|
remotePort: 8081,
|
||||||
targetIp: '192.168.1.100',
|
remoteHost: '192.168.1.100',
|
||||||
bufferSize: 8192,
|
bufferSize: 8192,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
retryCount: 3
|
retryCount: 3
|
||||||
@@ -487,17 +488,26 @@ export default {
|
|||||||
this.sending = true
|
this.sending = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let payload
|
|
||||||
let tcNo = this.selectedTcNo
|
let tcNo = this.selectedTcNo
|
||||||
|
let requestData
|
||||||
|
|
||||||
if (this.activeTab === 'json') {
|
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') {
|
} else if (this.activeTab === 'hex') {
|
||||||
// 转换十六进制字符串为字节数组
|
|
||||||
const hexStr = this.telegramData.hex.replace(/\s/g, '')
|
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') {
|
} else if (this.activeTab === 'form') {
|
||||||
// 根据电文号构造数据
|
|
||||||
const fields = this.getFieldsForTcNo(this.selectedTcNo)
|
const fields = this.getFieldsForTcNo(this.selectedTcNo)
|
||||||
const formData = {}
|
const formData = {}
|
||||||
|
|
||||||
@@ -516,24 +526,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
requestData = { tcNo, data: formData }
|
||||||
payload = Buffer.from(JSON.stringify(formData), 'utf8')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await sendTelegram({ tcNo, payload: Array.from(payload) })
|
const response = await sendTelegram(requestData)
|
||||||
|
|
||||||
this.$message.success(`报文 ${tcNo} 发送成功`)
|
this.$message.success(`报文 ${tcNo} 发送成功`)
|
||||||
|
this.loadHistory()
|
||||||
// 添加到历史记录
|
|
||||||
this.historyList.unshift({
|
|
||||||
id: Date.now(),
|
|
||||||
tcNo,
|
|
||||||
direction: 'OUT',
|
|
||||||
timestamp: new Date().toLocaleString(),
|
|
||||||
payloadLength: payload.length,
|
|
||||||
status: '成功'
|
|
||||||
})
|
|
||||||
|
|
||||||
this.loadStats()
|
this.loadStats()
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -545,7 +544,11 @@ export default {
|
|||||||
|
|
||||||
// 查看详情
|
// 查看详情
|
||||||
viewDetail(record) {
|
viewDetail(record) {
|
||||||
this.currentDetail = record
|
this.currentDetail = {
|
||||||
|
...record,
|
||||||
|
rawPayload: record.rawPayload || '无原始数据',
|
||||||
|
parsedFields: record.parsedFields || []
|
||||||
|
}
|
||||||
this.detailDialogVisible = true
|
this.detailDialogVisible = true
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -570,20 +573,25 @@ export default {
|
|||||||
this.$confirm('确定要清空所有历史记录吗?', '提示', {
|
this.$confirm('确定要清空所有历史记录吗?', '提示', {
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.historyList = []
|
clearTelegramHistory().then(() => {
|
||||||
this.stats = {
|
this.historyList = []
|
||||||
todayReceived: 0,
|
this.stats = {
|
||||||
totalReceived: 0,
|
todayReceived: 0,
|
||||||
successRate: 0,
|
totalReceived: 0,
|
||||||
avgDelay: 0
|
successRate: 0,
|
||||||
}
|
avgDelay: 0
|
||||||
|
}
|
||||||
|
this.$message.success('历史记录已清空')
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.error('清空失败')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 加载历史记录
|
// 加载历史记录
|
||||||
loadHistory() {
|
loadHistory() {
|
||||||
getTelegramHistory({ page: 1, size: 50 }).then(res => {
|
getTelegramHistory({ pageNum: 1, pageSize: 50 }).then(res => {
|
||||||
this.historyList = res.rows || []
|
this.historyList = (res.data && res.data.rows) || []
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message.error('加载历史记录失败')
|
this.$message.error('加载历史记录失败')
|
||||||
})
|
})
|
||||||
@@ -592,14 +600,13 @@ export default {
|
|||||||
// 加载统计数据
|
// 加载统计数据
|
||||||
loadStats() {
|
loadStats() {
|
||||||
getTelegramStats().then(res => {
|
getTelegramStats().then(res => {
|
||||||
this.stats = res.data || this.stats
|
this.stats = (res.data && res.data) || this.stats
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// 使用本地计算
|
|
||||||
this.stats = {
|
this.stats = {
|
||||||
todayReceived: this.historyList.filter(h => h.direction === 'IN').length,
|
todayReceived: this.historyList.filter(h => h.direction === 'IN').length,
|
||||||
totalReceived: this.historyList.length,
|
totalReceived: this.historyList.length,
|
||||||
successRate: this.historyList.length > 0 ? Math.round((this.historyList.filter(h => h.status === '成功').length / this.historyList.length) * 100) : 0,
|
successRate: this.historyList.length > 0 ? Math.round((this.historyList.filter(h => h.status === '成功').length / this.historyList.length) * 100) : 0,
|
||||||
avgDelay: 150
|
avgDelay: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user