diff --git a/src/main/java/com/qyft/ms/system/config/DeviceTcpConfig.java b/src/main/java/com/qyft/ms/system/config/DeviceTcpConfig.java new file mode 100644 index 0000000..2f47c85 --- /dev/null +++ b/src/main/java/com/qyft/ms/system/config/DeviceTcpConfig.java @@ -0,0 +1,41 @@ +package com.qyft.ms.system.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "tcp") +public class DeviceTcpConfig { + /** + * 是否启用 TCP 连接 + */ + private boolean enable; + /** + * 是否开启本地tcp服务器 + */ + private boolean serverEnable; + /** + * TCP 链接地址 + */ + private String host; + /** + * TCP 端口 + */ + private int port; + /** + * 断线重连间隔(毫秒) + */ + private int reconnect; + /** + * 连接超时时间(毫秒) + */ + private int timeout; + /** + * 指令反馈超时时间 + */ + private int feedbackTimeout; + +} + diff --git a/src/main/java/com/qyft/ms/system/config/WebSocketConfig.java b/src/main/java/com/qyft/ms/system/config/WebSocketConfig.java new file mode 100644 index 0000000..78ce2b9 --- /dev/null +++ b/src/main/java/com/qyft/ms/system/config/WebSocketConfig.java @@ -0,0 +1,14 @@ +package com.qyft.ms.system.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/src/main/java/com/qyft/ms/system/core/client/DeviceTcpClient.java b/src/main/java/com/qyft/ms/system/core/client/DeviceTcpClient.java new file mode 100644 index 0000000..82b9793 --- /dev/null +++ b/src/main/java/com/qyft/ms/system/core/client/DeviceTcpClient.java @@ -0,0 +1,112 @@ +package com.qyft.ms.system.core.client; + +import cn.hutool.json.JSONUtil; +import com.qyft.ms.system.config.DeviceTcpConfig; +import com.qyft.ms.system.core.handler.DeviceMessageHandler; +import com.qyft.ms.system.core.handler.TcpConnectionHandler; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.json.JsonObjectDecoder; +import io.netty.util.CharsetUtil; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DeviceTcpClient { + + private final DeviceTcpConfig tcpConfig; + + private final EventLoopGroup group = new NioEventLoopGroup(); + private Channel channel; + private Bootstrap bootstrap; + + @PostConstruct + private void init() { + if (tcpConfig.isEnable()) { + connect(); + } + } + + // 连接到TCP服务器 + public void connect() { + try { + if (bootstrap == null) { + bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, tcpConfig.getTimeout()) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel ch) { + // 添加JSON解码器 + ch.pipeline().addLast(new JsonObjectDecoder()); + // 添加设备消息处理器 + ch.pipeline().addLast(new DeviceMessageHandler()); + // 添加外部定义的TCP连接处理器,并传入TcpClient实例 + ch.pipeline().addLast(new TcpConnectionHandler(DeviceTcpClient.this)); + } + }); + } + + log.info("尝试连接到TCP服务 {}:{}", tcpConfig.getHost(), tcpConfig.getPort()); + ChannelFuture future = bootstrap.connect(new InetSocketAddress(tcpConfig.getHost(), tcpConfig.getPort())); + + future.addListener((ChannelFutureListener) f -> { + if (f.isSuccess()) { + channel = f.channel(); + log.info("已链接到TCP服务"); + } else { + log.error("无法连接到TCP服务. {}ms后重试...", tcpConfig.getReconnect()); + f.channel().eventLoop().schedule(this::connect, tcpConfig.getReconnect(), TimeUnit.MILLISECONDS); + } + }); + + } catch (Exception e) { + log.error("尝试连接到TCP服务发生意外错误: {}", e.getMessage(), e); + } + } + + /** + * 定时检查TCP连接 + */ + @Scheduled(fixedRateString = "${tcp.reconnect}") + private void checkConnection() { + if (channel == null || !channel.isActive()) { + log.error("TCP服务链接丢失"); + connect(); + } + } + + /** + * 将obj转换成json后发送 + */ + public boolean sendToJSON(Object request) { + String msg = JSONUtil.toJsonStr(request); + return this.send(msg); + } + + /** + * 发送字符串请求到TCP服务器 + */ + public boolean send(String request) { + if (channel != null && channel.isActive()) { + log.info("发送TCP指令:{}", request); + channel.writeAndFlush(Unpooled.copiedBuffer(request, CharsetUtil.UTF_8)); + return true; + } else { + log.error("TCP服务未连接,无法发送请求: {}", request); + return false; + } + } +} diff --git a/src/main/java/com/qyft/ms/system/core/handler/DeviceMessageHandler.java b/src/main/java/com/qyft/ms/system/core/handler/DeviceMessageHandler.java new file mode 100644 index 0000000..62a3de4 --- /dev/null +++ b/src/main/java/com/qyft/ms/system/core/handler/DeviceMessageHandler.java @@ -0,0 +1,31 @@ +package com.qyft.ms.system.core.handler; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DeviceMessageHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buf = (ByteBuf) msg; + String serverMsg = buf.toString(CharsetUtil.UTF_8); + log.info("serverMsg:{}", serverMsg); + try { + JSONObject deviceResult = JSONUtil.parseObj(serverMsg); + JSONObject payload = deviceResult.getJSONObject("payload"); + String tag = deviceResult.get("tag").toString(); + Integer cmdId = payload.getInt("cmdId"); + // 根据 tag 和 cmdId 进行处理 + } catch (Exception e) { + log.error("TCP服务消息处理错误: {}, error: {}", serverMsg, e.getMessage(), e); + } finally { + buf.release(); + } + } +} diff --git a/src/main/java/com/qyft/ms/system/core/handler/TcpConnectionHandler.java b/src/main/java/com/qyft/ms/system/core/handler/TcpConnectionHandler.java new file mode 100644 index 0000000..4145930 --- /dev/null +++ b/src/main/java/com/qyft/ms/system/core/handler/TcpConnectionHandler.java @@ -0,0 +1,34 @@ +package com.qyft.ms.system.core.handler; + +import com.qyft.ms.system.core.client.DeviceTcpClient; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TcpConnectionHandler extends ChannelInboundHandlerAdapter { + + private final DeviceTcpClient tcpClient; + + // 通过构造方法注入 TcpClient 引用 + public TcpConnectionHandler(DeviceTcpClient tcpClient) { + this.tcpClient = tcpClient; + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.error("TCP连接丢失,准备重新连接..."); + if (ctx.channel() != null && ctx.channel().isOpen()) { + ctx.channel().close(); + } + // 调用 TcpClient 的连接方法 + tcpClient.connect(); + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("TCP连接发生异常: {}", cause.getMessage(), cause); + ctx.close(); + } +} diff --git a/src/main/java/com/qyft/ms/system/core/server/WebSocketServer.java b/src/main/java/com/qyft/ms/system/core/server/WebSocketServer.java new file mode 100644 index 0000000..0bd7f10 --- /dev/null +++ b/src/main/java/com/qyft/ms/system/core/server/WebSocketServer.java @@ -0,0 +1,79 @@ +package com.qyft.ms.system.core.server; + +import jakarta.websocket.*; +import jakarta.websocket.server.ServerEndpoint; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +@Slf4j +@ServerEndpoint("/ws") +@Component +public class WebSocketServer { + + private static final Set sessions = new CopyOnWriteArraySet<>(); + // 消息按顺序发送,加锁 + private static final Object sendLock = new Object(); + + /** + * 群发消息给所有客户端 + */ + public static void sendMessageToClients(String message) { + synchronized (sendLock) { + for (Session session : sessions) { + if (session.isOpen()) { + try { + session.getBasicRemote().sendText(message); + log.info("发送消息给客户端(sessionId={}):{}", session.getId(), message); + } catch (Exception e) { + log.error("发送消息失败,sessionId={}", session.getId(), e); + } + } else { + log.warn("跳过关闭状态的 session,sessionId={}", session.getId()); + } + } + } + } + + /** + * 当有新的 WebSocket 连接建立时调用 + */ + @OnOpen + private void onOpen(Session session) { + sessions.add(session); + log.info("WebSocket 客户端已连接,sessionId={},当前在线人数={}", session.getId(), sessions.size()); + } + + /** + * 当接收到来自客户端的消息时调用 + */ + @OnMessage + public void onMessage(String message, Session session) { + log.info("接收到来自客户端(sessionId={})的消息:{}", session.getId(), message); + // 根据业务需求处理消息,此处可添加进一步的处理逻辑 + } + + /** + * 当 WebSocket 连接关闭时调用 + */ + @OnClose + public void onClose(Session session, CloseReason reason) { + sessions.remove(session); + log.info("WebSocket 客户端断开连接,sessionId={},原因={},当前在线人数={}", + session.getId(), reason.getReasonPhrase(), sessions.size()); + } + + /** + * 当 WebSocket 连接发生错误时调用 + */ + @OnError + public void onError(Session session, Throwable error) { + if (session != null) { + log.error("WebSocket 发生错误,sessionId={}", session.getId(), error); + } else { + log.error("WebSocket 发生错误,session 未初始化", error); + } + } +} diff --git a/src/main/java/com/qyft/ms/system/service/RoleService.java b/src/main/java/com/qyft/ms/system/service/RoleService.java index bc907cc..0f32360 100644 --- a/src/main/java/com/qyft/ms/system/service/RoleService.java +++ b/src/main/java/com/qyft/ms/system/service/RoleService.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service; */ @Service @RequiredArgsConstructor -public class RoleService extends ServiceImpl{ +public class RoleService extends ServiceImpl { public Role findByCode(String code) { return this.baseMapper.findByCode(code); diff --git a/src/main/java/com/qyft/ms/system/service/UserService.java b/src/main/java/com/qyft/ms/system/service/UserService.java index ee65790..8c24bd3 100644 --- a/src/main/java/com/qyft/ms/system/service/UserService.java +++ b/src/main/java/com/qyft/ms/system/service/UserService.java @@ -18,7 +18,7 @@ import java.util.stream.Collectors; */ @Service @RequiredArgsConstructor -public class UserService extends ServiceImpl{ +public class UserService extends ServiceImpl { private final HttpServletRequest request; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b270f02..c26e0ec 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,7 +18,7 @@ spring: mybatis-plus: configuration: # 开启 SQL 日志输出(可选) -# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 如果需要加载 XML 文件(自定义 SQL),可配置 mapper-locations: mapper-locations: classpath*:mapper/*.xml @@ -39,8 +39,8 @@ tcp: enable: false # 是否开启 TCP 连接 server-enable: true # 是否开启 TCP 连接 host: 127.0.0.1 -# host: 192.168.1.168 -# host: 192.168.1.168 + # host: 192.168.1.168 + # host: 192.168.1.168 port: 9080 reconnect: 5000 # 断线重连间隔(单位:毫秒) timeout: 10000 # 连接超时时间(单位:毫秒) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 30d4f25..5bc8be5 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,31 +1,31 @@ - + - + - - - - ${log.pattern} - - - - - - ${log.path}/sys-info.log + + + + ${log.pattern} + + + + + + ${log.path}/sys-info.log - + - ${log.path}/sys-info.%d{yyyy-MM-dd}.log - - 60 - - - ${log.pattern} - - + ${log.path}/sys-info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + INFO @@ -33,16 +33,16 @@ DENY - - - - ${log.path}/sys-error.log + + + + ${log.path}/sys-error.log ${log.path}/sys-error.%d{yyyy-MM-dd}.log - - 60 + + 60 ${log.pattern} @@ -50,26 +50,26 @@ ERROR - + ACCEPT - + DENY - - - - + + + + + + + + - - - - - + - - + + \ No newline at end of file diff --git a/src/main/resources/mapper/system/RoleMapper.xml b/src/main/resources/mapper/system/RoleMapper.xml index 106dc76..77cf19d 100644 --- a/src/main/resources/mapper/system/RoleMapper.xml +++ b/src/main/resources/mapper/system/RoleMapper.xml @@ -6,6 +6,8 @@ diff --git a/src/main/resources/mapper/system/UserMapper.xml b/src/main/resources/mapper/system/UserMapper.xml index 947de6e..367f495 100644 --- a/src/main/resources/mapper/system/UserMapper.xml +++ b/src/main/resources/mapper/system/UserMapper.xml @@ -6,6 +6,8 @@ diff --git a/src/main/resources/sql/init.sql b/src/main/resources/sql/init.sql index c0f18cc..9d81c8c 100644 --- a/src/main/resources/sql/init.sql +++ b/src/main/resources/sql/init.sql @@ -1,14 +1,35 @@ -- 创建 sys_user 表,用于存储用户信息 CREATE TABLE IF NOT EXISTS sys_user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 用户ID,自增长主键 - username TEXT NOT NULL, -- 用户名 - nickname TEXT, -- 用户昵称 - password TEXT NOT NULL, -- 密码 - role_id INTEGER, -- 角色ID,关联sys_role表 - is_deleted TINYINT DEFAULT 0, -- 删除标记(0:未删除,1:已删除) - create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 更新时间 + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, -- 用户ID,自增长主键 + username + TEXT + NOT + NULL, -- 用户名 + nickname + TEXT, -- 用户昵称 + password + TEXT + NOT + NULL, -- 密码 + role_id + INTEGER, -- 角色ID,关联sys_role表 + is_deleted + TINYINT + DEFAULT + 0, -- 删除标记(0:未删除,1:已删除) + create_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, -- 创建时间 + update_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP -- 更新时间 ); -- 插入测试数据到 sys_user 表 @@ -20,11 +41,27 @@ VALUES ('admin', 'Admin', '12345', 1, 0), -- 创建 sys_role 表,用于存储角色信息 CREATE TABLE IF NOT EXISTS sys_role ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 角色ID,自增长主键 - name TEXT NOT NULL, -- 角色名称 - code TEXT NOT NULL, -- 角色代码,如 ADMIN, USER等 - create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 更新时间 + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, -- 角色ID,自增长主键 + name + TEXT + NOT + NULL, -- 角色名称 + code + TEXT + NOT + NULL, -- 角色代码,如 ADMIN, USER等 + create_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, -- 创建时间 + update_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP -- 更新时间 ); -- 插入角色数据到 sys_role 表 @@ -37,66 +74,155 @@ VALUES ('管理员', 'ADMIN'), -- 创建 matrix 表,用于存储基质类型信息 CREATE TABLE IF NOT EXISTS matrix ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 基质类型ID,自增长主键 - name TEXT NOT NULL, -- 基质名称 - create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 更新时间 + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, -- 基质类型ID,自增长主键 + name + TEXT + NOT + NULL, -- 基质名称 + create_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, -- 创建时间 + update_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP -- 更新时间 ); -- 创建 matrix_craft 表,用于存储基质工艺信息 CREATE TABLE IF NOT EXISTS matrix_craft ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 工艺ID,自增长主键 - name TEXT NOT NULL, -- 工艺名称 - matrix_id INTEGER NOT NULL, -- 关联的基质类型ID(外键) - matrix_path_type TEXT NOT NULL, -- 基质路径类型 - motor_z_height INTEGER, -- 电机Z轴高度 - gas_pressure INTEGER, -- 气压 - volume INTEGER, -- 容积 - matrix_flow_velocity INTEGER, -- 基质流速 - high_voltage BOOLEAN, -- 是否采用高压(TRUE 或 FALSE) - high_voltage_value INTEGER, -- 高压值 - spacing INTEGER, -- 间距 - moving_speed INTEGER, -- 移动速度 - times INTEGER, -- 次数 - create_user INTEGER, -- 创建用户ID - create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 更新时间 + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, -- 工艺ID,自增长主键 + name + TEXT + NOT + NULL, -- 工艺名称 + matrix_id + INTEGER + NOT + NULL, -- 关联的基质类型ID(外键) + matrix_path_type + TEXT + NOT + NULL, -- 基质路径类型 + motor_z_height + INTEGER, -- 电机Z轴高度 + gas_pressure + INTEGER, -- 气压 + volume + INTEGER, -- 容积 + matrix_flow_velocity + INTEGER, -- 基质流速 + high_voltage + BOOLEAN, -- 是否采用高压(TRUE 或 FALSE) + high_voltage_value + INTEGER, -- 高压值 + spacing + INTEGER, -- 间距 + moving_speed + INTEGER, -- 移动速度 + times + INTEGER, -- 次数 + create_user + INTEGER, -- 创建用户ID + create_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, -- 创建时间 + update_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP -- 更新时间 ); -- 创建 operation_log 表,用于存储操作记录信息 CREATE TABLE IF NOT EXISTS operation_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 日志记录ID,自增长主键 - matrix_id INTEGER, -- 关联的基质ID - matrix_info TEXT, -- 基质信息详情 - status INTEGER, -- 状态标识(如操作结果状态码) - create_user INTEGER, -- 创建该日志的用户ID - create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 日志创建时间 - update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 日志更新时间 + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, -- 日志记录ID,自增长主键 + matrix_id + INTEGER, -- 关联的基质ID + matrix_info + TEXT, -- 基质信息详情 + status + INTEGER, -- 状态标识(如操作结果状态码) + create_user + INTEGER, -- 创建该日志的用户ID + create_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, -- 日志创建时间 + update_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP -- 日志更新时间 ); -- 创建 sys_settings 表,用于存储系统配置参数 CREATE TABLE IF NOT EXISTS sys_settings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 配置ID,自增长主键 - parent_id INTEGER, -- 父级配置ID(用于层级配置) - name TEXT NOT NULL, -- 配置名称 - code TEXT, -- 配置代码 - value TEXT -- 配置值 + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, -- 配置ID,自增长主键 + parent_id + INTEGER, -- 父级配置ID(用于层级配置) + name + TEXT + NOT + NULL, -- 配置名称 + code + TEXT, -- 配置代码 + value + TEXT -- 配置值 ); -- 创建 position 表,用于存储设备固定点位信息 -CREATE TABLE IF NOT EXISTS position ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 点位ID,自增长主键 - point_name TEXT NOT NULL, -- 点位名称 - x REAL NOT NULL, -- X坐标 - y REAL NOT NULL, -- Y坐标 - z REAL NOT NULL, -- Z坐标 - create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间 - update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 更新时间 +CREATE TABLE IF NOT EXISTS position +( + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, -- 点位ID,自增长主键 + point_name + TEXT + NOT + NULL, -- 点位名称 + x + REAL + NOT + NULL, -- X坐标 + y + REAL + NOT + NULL, -- Y坐标 + z + REAL + NOT + NULL, -- Z坐标 + create_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, -- 创建时间 + update_time + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP -- 更新时间 );