Compare commits

6 Commits

Author SHA1 Message Date
cd1ba55b26 优化 2026-03-13 10:30:55 +08:00
zc
687be5840e Merge branch 'refs/heads/master_lz' 2026-03-12 20:20:46 +08:00
c1d84aaf81 优化 2026-03-12 16:54:55 +08:00
zc
dec15eb913 tcp服务启动 2026-03-11 17:53:58 +08:00
18e014d9cb Merge branch 'master' into master_lz 2026-03-11 17:44:57 +08:00
2cb03b146a tcp服务 2026-03-11 17:44:06 +08:00
15 changed files with 612 additions and 6 deletions

View File

@@ -119,5 +119,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
</dependencies>
</project>

View File

@@ -155,5 +155,12 @@
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version> <!-- 使用较新稳定版本 -->
</dependency>
</dependencies>
</project>

View File

@@ -21,5 +21,11 @@
<groupId>top.wms</groupId>
<artifactId>wms-common</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -18,5 +18,12 @@
<groupId>top.wms</groupId>
<artifactId>wms-common</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.spring.EnableFileStorage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.continew.starter.core.autoconfigure.project.ProjectProperties;
@@ -29,6 +30,7 @@ import top.continew.starter.web.model.R;
@RestController
@SpringBootApplication
@RequiredArgsConstructor
@EnableScheduling
public class WmsAdminApplication {
private final ProjectProperties projectProperties;

View File

@@ -0,0 +1,95 @@
package top.wms.admin.controller.tcp.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import top.wms.admin.controller.tcp.handler.TcpServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class NettyTcpServer {
@Value("${tcp.server.port:9005}")
private int port;
@Value("${tcp.server.boss-threads:1}")
private int bossThreads;
@Value("${tcp.server.worker-threads:4}")
private int workerThreads;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private Channel serverChannel;
@PostConstruct
public void start() throws InterruptedException {
log.info("正在启动TCP服务端端口: {}", port);
bossGroup = new NioEventLoopGroup(bossThreads);
workerGroup = new NioEventLoopGroup(workerThreads);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 解决TCP粘包问题
pipeline.addLast(new LineBasedFrameDecoder(1024));
// 字符串编解码
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
// 关键修复直接new不要从Spring容器获取
pipeline.addLast(new TcpServerHandler());
log.debug("新连接接入,处理器已添加");
}
});
ChannelFuture future = bootstrap.bind(port).sync();
if (future.isSuccess()) {
serverChannel = future.channel();
log.info("TCP服务端启动成功端口: {}", port);
} else {
log.error("TCP服务端启动失败", future.cause());
}
}
@PreDestroy
public void stop() {
log.info("正在关闭TCP服务端...");
try {
if (serverChannel != null) {
serverChannel.close().sync();
}
} catch (InterruptedException e) {
log.error("关闭服务端通道时发生错误", e);
} finally {
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
log.info("TCP服务端已关闭");
}
}

View File

@@ -0,0 +1,69 @@
package top.wms.admin.controller.tcp.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class SimpleRequestMatcher {
// 用队列存储响应
private final BlockingQueue<String> responseQueue = new LinkedBlockingQueue<>(1);
// 当前等待的请求标识
private volatile boolean isWaiting = false;
/**
* 等待响应
*/
public String waitForResponse(int timeoutSeconds) {
isWaiting = true;
try {
log.info("等待响应, 超时={}秒", timeoutSeconds);
// 从队列取响应最多等待timeoutSeconds秒
String response = responseQueue.poll(timeoutSeconds, TimeUnit.SECONDS);
if (response != null) {
log.info("收到响应: {}", response);
return response;
} else {
log.error("等待响应超时");
return "TIMEOUT";
}
} catch (InterruptedException e) {
log.error("等待响应被中断: {}", e.getMessage());
return "ERROR";
} finally {
isWaiting = false;
}
}
/**
* 接收响应
*/
public void handleResponse(String response) {
try {
// 如果有请求在等待,把响应放入队列
if (isWaiting) {
responseQueue.offer(response);
log.info("响应接收成功: {}", response);
} else {
log.warn("没有正在等待的请求, 响应被丢弃: {}", response);
}
} catch (Exception e) {
log.error("处理响应失败: {}", e.getMessage());
}
}
/**
* 清空队列
*/
public void clear() {
responseQueue.clear();
isWaiting = false;
log.info("请求匹配器已清空");
}
}

View File

@@ -0,0 +1,67 @@
package top.wms.admin.controller.tcp.handler;
import top.wms.admin.controller.tcp.config.SimpleRequestMatcher;
import top.wms.admin.controller.tcp.util.SpringContextUtil;
import top.wms.admin.controller.tcp.manager.ChannelManager;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TcpServerHandler extends SimpleChannelInboundHandler<String> {
private ChannelManager channelManager;
private SimpleRequestMatcher requestMatcher;
private ChannelManager getChannelManager() {
if (channelManager == null) {
channelManager = SpringContextUtil.getBean(ChannelManager.class);
}
return channelManager;
}
private SimpleRequestMatcher getRequestMatcher() {
if (requestMatcher == null) {
requestMatcher = SpringContextUtil.getBean(SimpleRequestMatcher.class);
}
return requestMatcher;
}
private String getClientId(ChannelHandlerContext ctx) {
return ctx.channel().remoteAddress().toString();
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
String clientId = getClientId(ctx);
getChannelManager().addChannel(clientId, ctx.channel());
log.info("✅ VM客户端连接成功: {}", clientId);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
String clientId = getClientId(ctx);
getChannelManager().removeChannel(clientId);
log.info("❌ VM客户端断开连接: {}", clientId);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
String clientId = getClientId(ctx);
log.info("📥 收到VM数据 [{}]: {}", clientId, msg);
try {
// 直接把收到的消息交给匹配器(不解析,不匹配)
String cleanMsg = msg.trim();
getRequestMatcher().handleResponse(cleanMsg);
} catch (Exception e) {
log.error("处理消息失败: {}", e.getMessage());
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("连接异常: {}", cause.getMessage());
ctx.close();
}
}

View File

@@ -0,0 +1,47 @@
package top.wms.admin.controller.tcp.manager;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class ChannelManager {
// 存储所有VM客户端的连接
private final ConcurrentHashMap<String, Channel> channels = new ConcurrentHashMap<>();
// 添加连接
public void addChannel(String clientId, Channel channel) {
channels.put(clientId, channel);
log.info("VM客户端 [{}] 已连接,当前在线客户端数: {}", clientId, channels.size());
}
// 移除连接
public void removeChannel(String clientId) {
channels.remove(clientId);
log.info("VM客户端 [{}] 已断开,当前在线客户端数: {}", clientId, channels.size());
}
// 获取第一个连接的VM如果有多个VM可以根据需要修改
public Channel getFirstChannel() {
return channels.values().stream().findFirst().orElse(null);
}
// 根据ID获取连接
public Channel getChannel(String clientId) {
return channels.get(clientId);
}
// 检查是否有VM连接
public boolean hasConnection() {
return !channels.isEmpty();
}
// 获取所有连接
public ConcurrentHashMap<String, Channel> getAllChannels() {
return channels;
}
}

View File

@@ -0,0 +1,21 @@
package top.wms.admin.controller.tcp.model;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
// 指令请求
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommandRequest {
private String processId; // 要执行的流程ID如 "PROCESS_1"
private String param; // 参数,如 "10.5"
private Integer priority; // 优先级(可选)
// 构建完整的指令字符串
public String buildCommand() {
// 格式:*PROCESS_1,10.5#
return String.format("*%s,%s#", processId, param);
}
}

View File

@@ -0,0 +1,23 @@
package top.wms.admin.controller.tcp.model;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
// VM返回的结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VMResult {
private String processId; // 流程ID
private String status; // OK 或 NG
private double value; // 测量值
private long timestamp; // 时间戳
public VMResult(String processId, String status, double value) {
this.processId = processId;
this.status = status;
this.value = value;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,72 @@
package top.wms.admin.controller.tcp.service;
import io.netty.channel.Channel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import top.wms.admin.controller.tcp.manager.ChannelManager;
import top.wms.admin.controller.tcp.model.VMResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Service
public class CommandService {
// 统计
private final AtomicInteger successCount = new AtomicInteger(0);
private final AtomicInteger failCount = new AtomicInteger(0);
// 存储最近100条结果
private final ConcurrentLinkedQueue<VMResult> resultQueue = new ConcurrentLinkedQueue<>();
// 处理VM返回的结果
public void processResult(String processId, String status, double value) {
VMResult result = new VMResult(processId, status, value);
// 更新统计
if ("OK".equalsIgnoreCase(status)) {
successCount.incrementAndGet();
} else {
failCount.incrementAndGet();
}
// 存储结果
resultQueue.offer(result);
if (resultQueue.size() > 100) {
resultQueue.poll(); // 只保留最近100条
}
log.info("处理结果 - 流程: {}, 状态: {}, 数值: {}", processId, status, value);
// 这里可以添加你的业务逻辑,比如:
// 1. 存入数据库
// 2. 通过WebSocket推送给前端
// 3. 触发其他业务操作
}
// 获取统计信息
public String getStatistics() {
return String.format("成功: %d, 失败: %d, 总数: %d", successCount.get(), failCount.get(), successCount
.get() + failCount.get());
}
// 获取最新结果
public VMResult getLatestResult() {
return resultQueue.peek();
}
@Autowired
private ChannelManager channelManager;
@Scheduled(cron = "*/1 * * * * ?")
public void sendAndWait() {
// 1. 检查连接
log.info("查询时间========");
Channel channel = channelManager.getFirstChannel();
channel.writeAndFlush("001");
}
}

View File

@@ -0,0 +1,21 @@
package top.wms.admin.controller.tcp.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
}

View File

@@ -0,0 +1,165 @@
package top.wms.admin.controller.vm;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.core.FileInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.core.validation.CheckUtils;
import top.wms.admin.controller.tcp.config.SimpleRequestMatcher;
import top.wms.admin.controller.tcp.manager.ChannelManager;
import top.wms.admin.system.service.FileService;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/vm")
public class VmCommandController {
@Autowired
private ChannelManager channelManager;
@Autowired
private SimpleRequestMatcher requestMatcher;
@Autowired
private FileService fileService;
@GetMapping("/send")
public String sendAndWait(@RequestParam String msg) {
// 1. 检查连接
Channel channel = channelManager.getFirstChannel();
if (channel == null) {
return "ERROR: VM未连接";
}
String sendMsg = msg;
channel.writeAndFlush(sendMsg);
log.info("发送指令: {}", sendMsg);
// 3. 等待响应
String response = requestMatcher.waitForResponse(20);
CheckUtils.throwIf("TIMEOUT".equals(response), "响应超时,请重试");
if (StrUtil.equals(response, "success") || StrUtil.equals(response, "failed")) {
return response;
}
if (StrUtil.equals(response, msg)) {
response = "success";
} else {
response = "failed";
}
// 4. 返回结果
return response; // 直接返回VM的响应
}
// 基础路径
private static final String BASE_PATH = "C:/Users/14725/Desktop/material";
// 固定照片名称
private static final String PHOTO_NAME = "001.bmp";
/**
* 获取最新的001.bmp照片
*
* @return 图片文件
*/
/*@GetMapping("/latest-photo")
public ResponseEntity<byte[]> getLatestPhoto() {
try {
// 获取当前日期
LocalDate now = LocalDate.now();
String yearMonth = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
String day = now.format(DateTimeFormatter.ofPattern("dd"));
// 构建完整的文件路径
// 格式: C:/Users/14725/Desktop/material/202603/20260312/001.bmp
String filePath = String.format("%s/%s/%s%s/%s", BASE_PATH, yearMonth, yearMonth, day, PHOTO_NAME);
// 读取图片文件
Path imagePath = Paths.get(filePath);
// 检查文件是否存在
if (!Files.exists(imagePath)) {
return ResponseEntity.notFound().build();
}
// 读取文件字节
byte[] imageBytes = Files.readAllBytes(imagePath);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("image/bmp"));
headers.setContentLength(imageBytes.length);
// 返回图片
return new ResponseEntity<>(imageBytes, headers, HttpStatus.OK);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}*/
@GetMapping("/latest-photo")
public String getLatestPhoto() {
try {
// 获取当前日期
LocalDate now = LocalDate.now();
String yearMonth = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
String day = now.format(DateTimeFormatter.ofPattern("dd"));
// 构建完整的本地文件路径
// 格式: C:/Users/14725/Desktop/material/202603/20260312/001.bmp
String filePath = String.format("%s/%s/%s%s/%s", BASE_PATH, yearMonth, yearMonth, day, PHOTO_NAME);
// 读取图片文件
Path imagePath = Paths.get(filePath);
// 检查文件是否存在
if (!Files.exists(imagePath)) {
return null; // 或者抛出异常,根据业务需求决定
}
// 将文件转换为MultipartFile
File file = imagePath.toFile();
FileInputStream input = new FileInputStream(file);
MultipartFile multipartFile = new MockMultipartFile(
file.getName(), // 文件名
file.getName(), // 原始文件名
"image/bmp", // 内容类型
input // 文件输入流
);
// 构建MinIO存储路径
String photoStoragePath = "catch" + DateUtil.today() + "/";
// 使用现有的fileService上传到MinIO
FileInfo fileInfo = fileService.upload(multipartFile, photoStoragePath, null, true, true);
// 检查上传结果
CheckUtils.throwIfNull(fileInfo, "照片上传失败");
// 关闭输入流
input.close();
return fileInfo.getUrl();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("处理图片失败: " + e.getMessage());
}
}
}

View File

@@ -25,7 +25,7 @@ import java.util.regex.Pattern;
@Slf4j
public class AHDZCConnect {
private static final String PORT_NAME = "COM5";
private static final String PORT_NAME = "COM12";
private static final int BAUD_RATE = 9600;
private static final int DATA_BITS = 8;
private static final int STOP_BITS = 1;
@@ -52,10 +52,9 @@ public class AHDZCConnect {
@PostConstruct
public void init() {
// 项目启动时初始化并启动服务
if (false) {
ScaleService();
start();
}
}
@PreDestroy