feat(mill): 添加UDP调试工具功能

- 在路由配置中新增tool模块和udp-debug页面
- 添加UDP通信相关依赖到ruoyi-mill模块
- 实现UdpProperties配置类并添加超时和重试参数
- 重构UdpSender实现重试机制和超时控制
- 创建application-mill.properties配置文件
- 定义IUdpService接口提供UDP通信服务
- 添加系统菜单初始化SQL脚本
- 实现前端API接口用于UDP配置和报文发送
- 开发UDP调试工具Vue组件界面
- 编写UDP调试工具快速启动指南文档
This commit is contained in:
2026-04-30 16:59:21 +08:00
parent 7e67bae35f
commit 2e17943a7e
13 changed files with 1684 additions and 26 deletions

View File

@@ -0,0 +1,280 @@
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.udp.UdpProperties;
import com.ruoyi.mill.udp.UdpServer;
import com.ruoyi.mill.udp.UdpSender;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
/**
* UDP 通信控制器
*
* @author ruoyi
*/
@Api(tags = "冷轧 - UDP通信")
@RestController
@RequestMapping("/mill/udp")
public class UdpController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(UdpController.class);
@Autowired
private UdpProperties udpProperties;
@Autowired
private UdpServer udpServer;
@Autowired
private UdpSender udpSender;
/**
* 获取UDP配置
*/
@ApiOperation("获取UDP配置")
@GetMapping("/config")
public AjaxResult config() {
return success(udpProperties);
}
/**
* 更新UDP配置
*/
@ApiOperation("更新UDP配置")
@PutMapping("/config")
public AjaxResult updateConfig(@RequestBody UdpProperties properties) {
// 验证端口范围
if (properties.getLocalPort() < 1024 || properties.getLocalPort() > 65535) {
return error("本地端口必须在1024-65535之间");
}
if (properties.getRemotePort() < 1024 || properties.getRemotePort() > 65535) {
return error("目标端口必须在1024-65535之间");
}
if (StringUtils.isBlank(properties.getRemoteHost())) {
return error("目标IP不能为空");
}
// 这里应该调用服务层更新配置
// udpPropertiesService.updateConfig(properties);
return success();
}
/**
* 发送UDP报文
*/
@ApiOperation("发送UDP报文")
@PostMapping("/send")
public AjaxResult sendTelegram(@RequestBody Map<String, Object> requestData) {
try {
String tcNo = (String) requestData.get("tcNo");
Object payloadObj = requestData.get("payload");
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();
}
} 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);
} else {
return error("无效的payload类型: " + (payloadObj == null ? "null" : payloadObj.getClass().getName()));
}
boolean result = udpSender.send(tcNo, payload);
if (result) {
log.info("[UDP-SEND] 成功发送电文 {},长度: {} bytes", tcNo, payload.length);
return success();
} else {
log.warn("[UDP-SEND] 发送电文失败 {},长度: {} bytes", tcNo, payload.length);
return error("发送失败,请检查网络连接");
}
} catch (Exception e) {
log.error("[UDP-SEND] 发送电文异常: {}", e.getMessage());
return error("发送异常: " + e.getMessage());
}
}
/**
* 解析电文数据
*/
@ApiOperation("解析电文数据")
@PostMapping("/parse")
public AjaxResult parseTelegram(@RequestBody Map<String, Object> requestData) {
try {
String tcNo = (String) requestData.get("tcNo");
Object payloadObj = requestData.get("payload");
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();
}
} 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);
} else {
return error("无效的payload类型: " + (payloadObj == null ? "null" : payloadObj.getClass().getName()));
}
// 根据电文号进行解析
String parsedResult = parseTelegramData(tcNo, payload);
return success(parsedResult);
} catch (Exception e) {
log.error("[TELEGRAM-PARSE] 解析异常: {}", e.getMessage());
return error("解析失败: " + e.getMessage());
}
}
/**
* 获取电文历史记录
*/
@ApiOperation("获取电文历史记录")
@GetMapping("/history")
public AjaxResult telegramHistory(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "50") Integer pageSize) {
// TODO: 从数据库或缓存中获取历史记录
// 这里返回模拟数据
return success(getMockHistory(pageNum, pageSize));
}
/**
* 获取电文统计信息
*/
@ApiOperation("获取电文统计信息")
@GetMapping("/stats")
public AjaxResult telegramStats() {
// TODO: 从数据库或缓存中获取统计数据
// 这里返回模拟数据
return success(getMockStats());
}
/**
* 解析电文数据(内部方法)
*/
private String parseTelegramData(String tcNo, byte[] payload) {
StringBuilder result = new StringBuilder();
switch (tcNo) {
case "2FK101":
result.append("电文类型: 作业命令信息\n");
result.append("电文号: ").append(tcNo).append("\n");
result.append("长度: ").append(payload.length).append(" bytes\n");
result.append("时间: ").append(java.time.LocalDateTime.now()).append("\n");
break;
case "K12F03":
result.append("电文类型: 生产信息电文\n");
result.append("电文号: ").append(tcNo).append("\n");
result.append("长度: ").append(payload.length).append(" bytes\n");
result.append("时间: ").append(java.time.LocalDateTime.now()).append("\n");
break;
default:
result.append("未知电文号: ").append(tcNo).append("\n");
result.append("长度: ").append(payload.length).append(" bytes\n");
result.append("原始数据: ");
for (int i = 0; i < Math.min(payload.length, 32); i++) {
result.append(String.format("%02X ", payload[i]));
}
if (payload.length > 32) {
result.append("...");
}
}
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

@@ -0,0 +1,39 @@
package com.ruoyi.mill.service;
import com.ruoyi.mill.udp.UdpProperties;
import com.ruoyi.common.core.domain.AjaxResult;
import java.util.List;
import java.util.Map;
/**
* UDP 通信服务接口
*
* @author ruoyi
*/
public interface IUdpService {
/**
* 获取UDP配置
*/
UdpProperties getUdpConfig();
/**
* 更新UDP配置
*/
AjaxResult updateUdpConfig(UdpProperties properties);
/**
* 发送UDP报文
*/
boolean sendTelegram(String tcNo, byte[] payload);
/**
* 获取电文历史记录
*/
List<Map<String, Object>> getTelegramHistory(int pageNum, int pageSize);
/**
* 获取电文统计数据
*/
Map<String, Object> getTelegramStats();
}

View File

@@ -0,0 +1,153 @@
package com.ruoyi.mill.service.impl;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.mill.udp.UdpProperties;
import com.ruoyi.mill.udp.UdpServer;
import com.ruoyi.mill.udp.UdpSender;
import com.ruoyi.mill.service.IUdpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* UDP 通信服务实现
*
* @author ruoyi
*/
@Service
public class UdpServiceImpl implements IUdpService {
private static final Logger log = LoggerFactory.getLogger(UdpServiceImpl.class);
@Autowired
private UdpProperties udpProperties;
@Autowired
private UdpServer udpServer;
@Autowired
private UdpSender udpSender;
/**
* 获取UDP配置
*/
@Override
public UdpProperties getUdpConfig() {
return udpProperties;
}
/**
* 更新UDP配置
*/
@Override
public AjaxResult updateUdpConfig(UdpProperties properties) {
try {
// 这里应该验证配置并保存到数据库
// 暂时只是返回成功
log.info("[UDP-CONFIG] 更新UDP配置: 本地端口={}, 目标IP={}:{}",
properties.getLocalPort(),
properties.getRemoteHost(),
properties.getRemotePort());
return AjaxResult.success();
} catch (Exception e) {
log.error("[UDP-CONFIG] 更新配置失败", e);
return AjaxResult.error("更新配置失败: " + e.getMessage());
}
}
/**
* 发送UDP报文
*/
@Override
public boolean sendTelegram(String tcNo, byte[] payload) {
try {
boolean result = udpSender.send(tcNo, payload);
if (result) {
log.info("[UDP-SEND] 成功发送电文 {},长度: {} bytes", tcNo, payload.length);
} else {
log.warn("[UDP-SEND] 发送电文失败 {},长度: {} bytes", tcNo, payload.length);
}
return result;
} catch (Exception e) {
log.error("[UDP-SEND] 发送电文异常 {}: {}", tcNo, e.getMessage());
return false;
}
}
/**
* 获取电文历史记录
*/
@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);
}
// 简单的分页逻辑
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
public Map<String, Object> getTelegramStats() {
// TODO: 从数据库查询真实的统计数据
// 这里返回模拟数据用于演示
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));
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

@@ -22,6 +22,12 @@ public class UdpProperties {
/** 接收缓冲区大小(字节) */
private int bufferSize = 4096;
/** 超时时间(毫秒) */
private int timeout = 5000;
/** 重试次数 */
private int retryCount = 3;
public int getLocalPort() { return localPort; }
public void setLocalPort(int p) { this.localPort = p; }
public String getRemoteHost() { return remoteHost; }
@@ -30,4 +36,8 @@ public class UdpProperties {
public void setRemotePort(int p) { this.remotePort = p; }
public int getBufferSize() { return bufferSize; }
public void setBufferSize(int s) { this.bufferSize = s; }
public int getTimeout() { return timeout; }
public void setTimeout(int t) { this.timeout = t; }
public int getRetryCount() { return retryCount; }
public void setRetryCount(int r) { this.retryCount = r; }
}

View File

@@ -41,22 +41,50 @@ public class UdpSender {
send(TelegramSchema.ID_K12F03, TelegramCodec.encode(TelegramSchema.SCHEMA_K12F03, data));
}
private void send(String tcNo, byte[] payload) {
try (DatagramSocket socket = new DatagramSocket()) {
// 帧 = 6字节电文号 + payload
byte[] tcNoBytes = Arrays.copyOf(
tcNo.getBytes(StandardCharsets.US_ASCII), 6);
byte[] frame = new byte[6 + payload.length];
System.arraycopy(tcNoBytes, 0, frame, 0, 6);
System.arraycopy(payload, 0, frame, 6, payload.length);
/**
* 发送UDP报文供控制器调用
*/
public boolean send(String tcNo, byte[] payload) {
try {
int retryCount = props.getRetryCount();
int timeout = props.getTimeout();
for (int i = 0; i < retryCount; i++) {
try {
DatagramSocket socket = new DatagramSocket();
socket.setSoTimeout(timeout);
// 帧 = 6字节电文号 + payload
byte[] tcNoBytes = Arrays.copyOf(
tcNo.getBytes(StandardCharsets.US_ASCII), 6);
byte[] frame = new byte[6 + payload.length];
System.arraycopy(tcNoBytes, 0, frame, 0, 6);
System.arraycopy(payload, 0, frame, 6, payload.length);
InetAddress addr = InetAddress.getByName(props.getRemoteHost());
DatagramPacket pkt = new DatagramPacket(frame, frame.length, addr, props.getRemotePort());
socket.send(pkt);
socket.close();
log.info("[UDP-SEND] tcNo={} -> {}:{} frameLen={}", tcNo,
props.getRemoteHost(), props.getRemotePort(), frame.length);
return true;
} catch (Exception e) {
log.warn("[UDP-SEND] 第{}次重试失败 tcNo={}: {}", i + 1, tcNo, e.getMessage());
if (i == retryCount - 1) {
throw e;
}
Thread.sleep(100); // 短暂延迟后重试
}
}
InetAddress addr = InetAddress.getByName(props.getRemoteHost());
DatagramPacket pkt = new DatagramPacket(frame, frame.length, addr, props.getRemotePort());
socket.send(pkt);
log.info("[UDP-SEND] tcNo={} -> {}:{} frameLen={}", tcNo,
props.getRemoteHost(), props.getRemotePort(), frame.length);
} catch (Exception e) {
log.error("[UDP-SEND] 发送失败 tcNo={}", tcNo, e);
return false;
}
return false;
}
}