数字孪生后台

This commit is contained in:
猫猫guowen
2025-12-19 14:16:47 +08:00
parent ccd7d95206
commit 7b816d10f0
13 changed files with 1144 additions and 8 deletions

View File

@@ -0,0 +1,27 @@
package com.ruoyi.api.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.tcp.TcpServerManager;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/tcp")
public class TcpServerController {
private final TcpServerManager manager;
public TcpServerController(TcpServerManager manager) {
this.manager = manager;
}
@GetMapping("/status")
public AjaxResult status() {
return AjaxResult.success(manager.status());
}
@PostMapping("/send/{channel}")
public AjaxResult send(@PathVariable String channel, @RequestBody String message) {
boolean ok = manager.broadcast(channel, message);
if (ok) return AjaxResult.success();
return AjaxResult.error("channel not found");
}
}

View File

@@ -107,7 +107,7 @@ public class ScheduleThread {
ssjList.put(deviceSsj.getDeviceCode(), deviceSsj);
}
//plc输送机读取线程
PlcSsjRead plcSsjRead = new PlcSsjRead(ssjList, new PlcConnectInfo("127.0.0.1", 1502));
PlcSsjRead plcSsjRead = new PlcSsjRead(ssjList, new PlcConnectInfo("172.16.100.9", 1502));
Thread plcSsjReadThread = new Thread(plcSsjRead);
plcSsjReadCustomThread = new CustomThread<PlcSsjRead>(plcSsjReadThread, plcSsjRead);
@@ -118,7 +118,7 @@ public class ScheduleThread {
tsjList.put(t.getName(), t);
}
//plc提升机读取线程
PlcTsjRead plcTsjRead = new PlcTsjRead(tsjList, new PlcConnectInfo("127.0.0.1", 1502));
PlcTsjRead plcTsjRead = new PlcTsjRead(tsjList, new PlcConnectInfo("172.16.100.9", 1502));
Thread plcTsjReadThread = new Thread(plcTsjRead);
plcTsjReadCustomThread = new CustomThread<PlcTsjRead>(plcTsjReadThread, plcTsjRead);

View File

@@ -0,0 +1,147 @@
package com.ruoyi.tcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.device.srgv.domain.DeviceSrgv;
import com.ruoyi.device.ssj.domain.DeviceSsj;
import com.ruoyi.device.tsj.domain.DeviceTsj;
import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead;
import com.ruoyi.schedule.plcRead.ssj.PlcSsjRead;
import com.ruoyi.schedule.plcRead.tsj.PlcTsjRead;
import lombok.Data;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class GetDeviceAlarm {
private final TcpServerManager manager;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 构造函数:注入 TCP 管理器,用于管理 5555 端口客户端与广播
*/
public GetDeviceAlarm(TcpServerManager manager) {
this.manager = manager;
}
/**
* 初始化:注册 5555 端口的客户端连接监听,客户端第一次连接时立即发送报警数据
*/
@PostConstruct
public void init() {
manager.setClientListener("port5555", this::handleConnect);
}
/**
* 客户端连接处理:构建报警数据并写入到 Socket 输出流
*/
private void handleConnect(Socket socket) {
try {
String payload = buildAlarmPayload();
OutputStream os = socket.getOutputStream();
os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (Exception ignored) {
}
}
/**
* 周期发送:每 2 秒检查 5555 端口是否有连接,有则广播报警数据,无连接不发送
*/
@Scheduled(fixedRate = 2000)
public void send() {
Integer count = manager.status().getOrDefault("port5555", 0);
if (count != null && count > 0) {
String payload = buildAlarmPayload();
manager.broadcast("port5555", payload == null ? "" : payload);
}
}
/**
* 构建报警数据:汇总四向车、输送机、提升机报警
* - 四向车:报警代码 > 1,字典类型 srgv_alarm
* - 输送机:报警代码 > 2,字典类型 ssj_ex
* - 提升机:报警代码 > 1000,字典类型 tsj_alarm
* 返回 JSON 字符串格式:{"alarm":[{id,name,alarm,alarmMsg}]}
*/
private String buildAlarmPayload() {
List<AlarmDto> alarms = new ArrayList<>();
Map<String, DeviceSrgv> srgvMap = PlcSrgvRead.deviceSrgvMap;
if (srgvMap != null) {
for (DeviceSrgv d : srgvMap.values()) {
Integer code = d.getAlarm();
if (code != null && code > 1) {
AlarmDto dto = new AlarmDto();
dto.setId(d.getDeviceId() == null ? "" : String.valueOf(d.getDeviceId()));
dto.setName("四向车");
dto.setAlarm(code == null ? "" : String.valueOf(code));
dto.setAlarmMsg(DictUtils.getDictLabel("srgv_alarm", code == null ? "" : String.valueOf(code)));
alarms.add(dto);
}
}
}
Map<String, DeviceSsj> ssjMap = PlcSsjRead.deviceSsjMap;
if (ssjMap != null) {
for (DeviceSsj d : ssjMap.values()) {
Integer code = d.getException();
if (code != null && code > 2) {
AlarmDto dto = new AlarmDto();
dto.setId(d.getDeviceCode() == null ? "" : d.getDeviceCode());
dto.setName("输送机");
dto.setAlarm(String.valueOf(code));
dto.setAlarmMsg(DictUtils.getDictLabel("ssj_ex", String.valueOf(code)));
alarms.add(dto);
}
}
}
Map<String, DeviceTsj> tsjMap = PlcTsjRead.deviceTsjMap;
if (tsjMap != null) {
for (Map.Entry<String, DeviceTsj> entry : tsjMap.entrySet()) {
String codeId = entry.getKey();
DeviceTsj d = entry.getValue();
Integer code = d.getSbzt();
if (code != null && code > 1000) {
AlarmDto dto = new AlarmDto();
dto.setId(codeId == null ? "" : codeId);
dto.setName("提升机");
dto.setAlarm(String.valueOf(code));
dto.setAlarmMsg(DictUtils.getDictLabel("tsj_alarm", String.valueOf(code)));
alarms.add(dto);
}
}
}
try {
Wrapper w = new Wrapper();
w.setAlarm(alarms);
return objectMapper.writeValueAsString(w);
} catch (Exception e) {
return "{\"alarm\":[]}";
}
}
@Data
private static class Wrapper {
private List<AlarmDto> alarm;
}
@Data
private static class AlarmDto {
private String id;
private String name;
private String alarm;
private String alarmMsg;
}
}

View File

@@ -0,0 +1,140 @@
package com.ruoyi.tcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.device.srgv.domain.DeviceSrgv;
import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead;
import com.ruoyi.job.service.IMydoJobService;
import com.ruoyi.job.domain.MydoJob;
import com.ruoyi.common.utils.DictUtils;
import lombok.Data;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class GetDeviceSrgvMap {
private final TcpServerManager manager;
private final ObjectMapper objectMapper = new ObjectMapper();
private final IMydoJobService mydoJobService;
/**
* 构造函数:注入 TCP 管理器与作业服务,用于管理 2222 端口广播并根据任务号补全物料信息
*/
public GetDeviceSrgvMap(TcpServerManager manager, IMydoJobService mydoJobService) {
this.manager = manager;
this.mydoJobService = mydoJobService;
}
/**
* 初始化:注册 2222 端口的客户端连接监听,客户端第一次连接时立即发送 AGV 数据
*/
@PostConstruct
public void init() {
manager.setClientListener("port2222", this::handleConnect);
}
/**
* 客户端连接处理:构建 AGV 数据并写入到 Socket 输出流
*/
private void handleConnect(Socket socket) {
try {
String payload = buildAgvPayload();
OutputStream os = socket.getOutputStream();
os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (Exception ignored) {
}
}
/**
* 周期发送:每 2 秒检查 2222 端口是否有连接,有则广播 AGV 数据,无连接不发送
*/
@Scheduled(fixedRate = 2000)
public void send() {
Integer count = manager.status().getOrDefault("port2222", 0);
if (count != null && count > 0) {
String payload = buildAgvPayload();
manager.broadcast("port2222", payload == null ? "" : payload);
}
}
/**
* 构建 AGV 数据:从 PLC 缓存读取四向车状态并组装 JSON
* - 计算在位与状态(基于执行阶段 workPhase)
* - 报警码与文案:使用字典 srgv_alarm
* - 当任务号有效时,查询 MydoJob 补齐托盘与物料信息
* 返回 JSON 字符串格式:{"AGV":[{...}]}
*/
private String buildAgvPayload() {
Map<String, DeviceSrgv> map = PlcSrgvRead.deviceSrgvMap;
List<AgvDto> list = new ArrayList<>();
if (map != null) {
for (DeviceSrgv d : map.values()) {
AgvDto dto = new AgvDto();
dto.setId(d.getDeviceId() == null ? "" : String.format("%04d", d.getDeviceId()));
dto.setTaskNo(d.getTaskNo() == null ? "" : String.valueOf(d.getTaskNo()));
dto.setIsThere((d.getWorkPhase() >= 3 && d.getWorkPhase() < 6 ? "1" : "0"));
Integer wp = d.getWorkPhase();
String status = "0";
if (wp != null) {
if (wp == 3) status = "1";
else if (wp == 6) status = "2";
else if (wp == 24) status = "3";
}
dto.setStatus(status);
dto.setLocation(d.getCurrSite() == null ? "" : d.getCurrSite());
dto.setAlarm(d.getAlarm() == null ? "" : String.valueOf(d.getAlarm()));
dto.setAlarmMsg(DictUtils.getDictLabel("srgv_alarm", d.getAlarm() == null ? "" : String.valueOf(d.getAlarm())));
if (d.getTaskNo() != null && d.getTaskNo() > 0) {
try {
MydoJob job = mydoJobService.selectMydoJobByTaskNo(Long.valueOf(d.getTaskNo()));
dto.setTrayCode(job == null || job.getTrayCode() == null ? "" : job.getTrayCode());
dto.setProductCode(job == null || job.getProductId() == null ? "" : job.getProductId());
dto.setProductName(job == null || job.getProductName() == null ? "" : job.getProductName());
} catch (Exception e) {
dto.setTrayCode("");
dto.setProductCode("");
dto.setProductName("");
}
} else {
dto.setTrayCode("");
dto.setProductCode("");
dto.setProductName("");
}
list.add(dto);
}
}
try {
Wrapper w = new Wrapper();
w.setAGV(list);
return objectMapper.writeValueAsString(w);
} catch (Exception e) {
return "{\"AGV\":[]}";
}
}
@Data
private static class Wrapper {
private List<AgvDto> AGV;
}
@Data
private static class AgvDto {
private String id;
private String taskNo;
private String isThere;
private String status;
private String location;
private String alarm;
private String alarmMsg;
private String trayCode;
private String productCode;
private String productName;
}
}

View File

@@ -0,0 +1,188 @@
package com.ruoyi.tcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.device.ssj.domain.DeviceSsj;
import com.ruoyi.schedule.plcRead.ssj.PlcSsjRead;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.ruoyi.job.service.IMydoJobService;
import com.ruoyi.job.domain.MydoJob;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class GetDeviceSsjMap {
private final TcpServerManager manager;
private final ObjectMapper objectMapper = new ObjectMapper();
private final IMydoJobService mydoJobService;
/**
* 构造函数:注入 TCP 管理器与作业服务,用于管理 3333 端口广播并根据任务号补全物料信息
*/
public GetDeviceSsjMap(TcpServerManager manager, IMydoJobService mydoJobService) {
this.manager = manager;
this.mydoJobService = mydoJobService;
}
/**
* 初始化:注册 3333 端口的客户端连接监听,客户端第一次连接时立即发送输送机数据
*/
@PostConstruct
public void init() {
manager.setClientListener("port3333", this::handleConnect);
}
/**
* 客户端连接处理:构建输送机数据并写入到 Socket 输出流
*/
private void handleConnect(Socket socket) {
try {
String payload = readConveyorsFromCache();
OutputStream os = socket.getOutputStream();
os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (Exception ignored) {
}
}
/**
* 周期发送:每 2 秒检查 3333 端口是否有连接,有则广播输送机数据,无连接不发送
*/
@Scheduled(fixedRate = 2000)
public void send() {
Integer count = manager.status().getOrDefault("port3333", 0);
if (count != null && count > 0) {
String payload = readConveyorsFromCache();
manager.broadcast("port3333", payload == null ? "" : payload);
}
}
/**
* 从 PLC 缓存读取输送机状态并组装 JSON
* - 工作模式转换为中文标签
* - 报警码与文案:使用字典 ssj_ex
* - 当任务号有效时,查询 MydoJob 补齐托盘与物料信息
* 返回 JSON 字符串格式:{"ssj":[{...}]}
*/
private String readConveyorsFromCache() {
Map<String, DeviceSsj> map = PlcSsjRead.deviceSsjMap;
List<SsjDto> list = new ArrayList<>();
if (map == null || map.isEmpty()) {
return toJson(list);
}
for (DeviceSsj d : map.values()) {
SsjDto dto = new SsjDto();
dto.setId(d.getDeviceCode());
Integer cf = d.getModel();
String mode = "0";
if (cf != null) {
if (cf == 1) {
mode = "手动";
}else if (cf == 2) {
mode = "自动";
}else {
mode = String.valueOf(cf);
}
}
dto.setWorkingMode(mode);
dto.setTaskNo(String.valueOf(d.getTaskNo()));
dto.setRotationDirection(calcRotationDirection(d.getTaskNo()));
dto.setAlarm(String.valueOf(d.getException()));
dto.setAlarmMsg(DictUtils.getDictLabel("ssj_ex", String.valueOf(d.getException())));
dto.setDW(String.valueOf(d.getDw()));
dto.setKX(String.valueOf(d.getKx()));
if (d.getTaskNo() > 0) {
try {
MydoJob job = mydoJobService.selectMydoJobByTaskNo(Long.valueOf(d.getTaskNo()));
dto.setTrayCode(job == null || job.getTrayCode() == null ? "" : job.getTrayCode());
dto.setProductCode(job == null || job.getProductId() == null ? "" : job.getProductId());
dto.setProductName(job == null || job.getProductName() == null ? "" : job.getProductName());
} catch (Exception e) {
dto.setTrayCode("");
dto.setProductCode("");
dto.setProductName("");
}
} else {
dto.setTrayCode("");
dto.setProductCode("");
dto.setProductName("");
}
dto.setRunning("");
list.add(dto);
}
return toJson(list);
}
/**
* 计算输送机旋转方向:依据作业 inOrOut 字段,>=20 返回 "2",<20 返回 "1"
*/
private String calcRotationDirection(int taskNo) {
try {
if (taskNo <= 0) return "0";
MydoJob job = mydoJobService.selectMydoJobByTaskNo((long) taskNo);
if (job == null || job.getInOrOut() == null) return "0";
int val;
try {
val = Integer.parseInt(job.getInOrOut());
} catch (NumberFormatException e) {
return "0";
}
return val >= 20 ? "2" : "1";
} catch (Exception e) {
return "0";
}
}
/**
* 列表对象序列化为 JSON 字符串
*/
private String toJson(List<SsjDto> list) {
try {
Wrapper w = new Wrapper();
w.setSsj(list);
return objectMapper.writeValueAsString(w);
} catch (Exception e) {
return "{\"ssj\":[]}";
}
}
public static class Wrapper {
private List<SsjDto> ssj;
/**
* 获取输送机列表
*/
public List<SsjDto> getSsj() { return ssj; }
/**
* 设置输送机列表
*/
public void setSsj(List<SsjDto> ssj) { this.ssj = ssj; }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class SsjDto {
private String id;
private String workingMode;
private String taskNo;
private String rotationDirection;
private String alarm;
private String alarmMsg;
private String DW;
private String KX;
private String running;
private String trayCode;
private String productCode;
private String productName;
}
}

View File

@@ -0,0 +1,151 @@
package com.ruoyi.tcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.device.tsj.domain.DeviceTsj;
import com.ruoyi.job.domain.MydoJob;
import com.ruoyi.job.service.IMydoJobService;
import com.ruoyi.schedule.plcRead.tsj.PlcTsjRead;
import lombok.Data;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class GetDeviceTsjMap {
private final TcpServerManager manager;
private final ObjectMapper objectMapper = new ObjectMapper();
private final IMydoJobService mydoJobService;
/**
* 构造函数:注入 TCP 管理器与作业服务,用于管理 4444 端口广播并根据任务号补全物料信息
*/
public GetDeviceTsjMap(TcpServerManager manager, IMydoJobService mydoJobService) {
this.manager = manager;
this.mydoJobService = mydoJobService;
}
/**
* 初始化:注册 4444 端口的客户端连接监听,客户端第一次连接时立即发送提升机数据
*/
@PostConstruct
public void init() {
manager.setClientListener("port4444", this::handleConnect);
}
/**
* 客户端连接处理:构建提升机数据并写入到 Socket 输出流
*/
private void handleConnect(Socket socket) {
try {
String payload = buildTsjPayload();
OutputStream os = socket.getOutputStream();
os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (Exception ignored) {
}
}
/**
* 周期发送:每 2 秒检查 4444 端口是否有连接,有则广播提升机数据,无连接不发送
*/
@Scheduled(fixedRate = 2000)
public void send() {
Integer count = manager.status().getOrDefault("port4444", 0);
if (count != null && count > 0) {
String payload = buildTsjPayload();
manager.broadcast("port4444", payload == null ? "" : payload);
}
}
/**
* 构建提升机数据:从 PLC 缓存读取提升机状态并组装 JSON
* - 工作模式转换为中文标签
* - 报警码与文案:使用字典 tsj_alarm
* - 当任务号有效时,查询 MydoJob 补齐托盘与物料信息
* 返回 JSON 字符串格式:{"tsj":[{...}]}
*/
private String buildTsjPayload() {
Map<String, DeviceTsj> map = PlcTsjRead.deviceTsjMap;
List<TsjDto> list = new ArrayList<>();
if (map != null) {
for (Map.Entry<String, DeviceTsj> entry : map.entrySet()) {
String code = entry.getKey();
DeviceTsj d = entry.getValue();
TsjDto dto = new TsjDto();
dto.setId(String.valueOf(d.getId()));
dto.setLayer(d.getDqcs() == null ? "" : String.valueOf(d.getDqcs()));
dto.setTaskNo(d.getRwh() == null ? "" : String.valueOf(d.getRwh()));
dto.setZxjd(d.getJdbz() == null ? "" : String.valueOf(d.getJdbz()));
Integer cf = d.getCzfs();
String mode = "0";
if (cf != null) {
if (cf == 1) {
mode = "手动";
}else if (cf == 2) {
mode = "自动";
}else {
mode = String.valueOf(cf);
}
}
dto.setWorkingMode(mode);
dto.setSourceLayer(d.getYch() == null ? "" : String.valueOf(d.getYch()));
dto.setTargetLayer(d.getMbch() == null ? "" : String.valueOf(d.getMbch()));
dto.setAlarm(d.getSbzt() == null ? "" : String.valueOf(d.getSbzt()));
dto.setAlarmMsg(DictUtils.getDictLabel("tsj_alarm", d.getSbzt() == null ? "" : String.valueOf(d.getSbzt())));
if (d.getRwh() != null && d.getRwh() > 0) {
try {
MydoJob job = mydoJobService.selectMydoJobByTaskNo(Long.valueOf(d.getRwh()));
dto.setTrayCode(job == null || job.getTrayCode() == null ? "" : job.getTrayCode());
dto.setProductCode(job == null || job.getProductId() == null ? "" : job.getProductId());
dto.setProductName(job == null || job.getProductName() == null ? "" : job.getProductName());
} catch (Exception e) {
dto.setTrayCode("");
dto.setProductCode("");
dto.setProductName("");
}
} else {
dto.setTrayCode("");
dto.setProductCode("");
dto.setProductName("");
}
list.add(dto);
}
}
try {
Wrapper w = new Wrapper();
w.setTsj(list);
return objectMapper.writeValueAsString(w);
} catch (Exception e) {
return "{\"tsj\":[]}";
}
}
@Data
private static class Wrapper {
private List<TsjDto> tsj;
}
@Data
private static class TsjDto {
private String id;
private String taskNo;
private String zxjd;
private String workingMode;
private String sourceLayer;
private String targetLayer;
private String alarm;
private String alarmMsg;
private String trayCode;
private String productCode;
private String productName;
private String layer;
}
}

View File

@@ -0,0 +1,149 @@
package com.ruoyi.tcp;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class TcpServer implements Runnable {
@Getter
private final String name;
@Getter
private final int port;
private final AtomicBoolean running = new AtomicBoolean(false);
private ServerSocket serverSocket;
private final Set<Socket> clients = ConcurrentHashMap.newKeySet();
private final ExecutorService clientPool = Executors.newCachedThreadPool();
@lombok.Setter
private volatile java.util.function.Consumer<Socket> clientListener;
public TcpServer(String name, int port) {
this.name = name;
this.port = port;
}
/**
* 启动服务器:在守护线程中运行 ServerSocket 接受客户端连接
*/
public void start() {
if (running.compareAndSet(false, true)) {
Thread t = new Thread(this, "TcpServer-" + name + "-" + port);
t.setDaemon(true);
t.start();
}
}
/**
* 停止服务器:关闭 ServerSocket 与所有客户端连接,终止客户端线程池
*/
public void stop() {
running.set(false);
try {
if (serverSocket != null) serverSocket.close();
} catch (IOException ignored) {
}
for (Socket s : clients) {
try {
s.close();
} catch (IOException ignored) {
}
}
clients.clear();
clientPool.shutdownNow();
}
/**
* 获取当前已连接的客户端数量
*/
public int clientCount() {
return clients.size();
}
/**
* 广播消息到所有已连接客户端;写入失败的客户端将被移除
*/
public void broadcast(String message) {
byte[] bytes = (message + "\n").getBytes(StandardCharsets.UTF_8);
for (Socket s : clients) {
try {
OutputStream os = s.getOutputStream();
os.write(bytes);
os.flush();
} catch (IOException e) {
try {
s.close();
} catch (IOException ignored) {
}
clients.remove(s);
}
}
}
@Override
/**
* 服务器主循环:绑定端口,阻塞等待新连接并交由客户端处理线程执行
*/
public void run() {
try {
serverSocket = new ServerSocket(port);
log.info("TCP server {} started on {}", name, port);
while (running.get()) {
Socket socket = serverSocket.accept();
clients.add(socket);
clientPool.execute(() -> handleClient(socket));
}
} catch (IOException e) {
if (running.get()) {
log.error("TCP server {} failed on {} {}", name, port, e.getMessage());
}
} finally {
stop();
}
}
/**
* 单个客户端处理:
* - 调用已注册的客户端监听器进行初次处理
* - 读取客户端输入,支持 ping/pong 心跳应答
* - 连接结束时关闭资源并移除客户端
*/
private void handleClient(Socket socket) {
if (clientListener != null) {
try {
clientListener.accept(socket);
} catch (Exception e) {
log.error("Error in client listener for {} on port {}", name, port, e);
}
}
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String line;
while ((line = br.readLine()) != null && running.get()) {
if ("ping".equalsIgnoreCase(line.trim())) {
OutputStream os = socket.getOutputStream();
os.write("pong\n".getBytes(StandardCharsets.UTF_8));
os.flush();
}
}
} catch (IOException ignored) {
} finally {
try {
socket.close();
} catch (IOException ignored) {
}
clients.remove(socket);
}
}
}

View File

@@ -0,0 +1,87 @@
package com.ruoyi.tcp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class TcpServerManager {
private final TcpServerProperties properties;
private final Map<String, TcpServer> servers = new HashMap<>();
/**
* 构造函数:注入 TCP 服务器配置(启用标志与端口映射)
*/
public TcpServerManager(TcpServerProperties properties) {
this.properties = properties;
}
/**
* 初始化:按配置创建并启动各端口的 TCP 服务器
*/
@PostConstruct
public void init() {
if (!properties.isEnabled()) return;
for (Map.Entry<String, Integer> e : properties.getChannels().entrySet()) {
TcpServer s = new TcpServer(e.getKey(), e.getValue());
servers.put(e.getKey(), s);
s.start();
}
log.info("TCP servers initialized {}", properties.getChannels());
}
/**
* 销毁:停止所有 TCP 服务器并清理缓存
*/
@PreDestroy
public void destroy() {
for (TcpServer s : servers.values()) {
s.stop();
}
servers.clear();
}
/**
* 状态查询:返回每个通道当前已连接客户端数量
*/
public Map<String, Integer> status() {
Map<String, Integer> map = new HashMap<>();
for (Map.Entry<String, TcpServer> e : servers.entrySet()) {
map.put(e.getKey(), e.getValue().clientCount());
}
return map;
}
/**
* 广播消息:向指定通道的所有客户端发送文本消息
*/
public boolean broadcast(String channel, String message) {
TcpServer s = servers.get(channel);
if (s == null) return false;
s.broadcast(message);
return true;
}
/**
* 设置客户端监听器:为指定通道注册客户端连接回调
*/
public void setClientListener(String channel, java.util.function.Consumer<java.net.Socket> listener) {
TcpServer s = servers.get(channel);
if (s != null) {
s.setClientListener(listener);
}
}
/**
* 获取服务器实例集合(只读视图)
*/
public Map<String, TcpServer> getServers() {
return Collections.unmodifiableMap(servers);
}
}

View File

@@ -0,0 +1,42 @@
package com.ruoyi.tcp;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
@ConfigurationProperties(prefix = "tcpserver")
public class TcpServerProperties {
private boolean enabled = false;
private Map<String, Integer> channels = new LinkedHashMap<>();
/**
* 获取是否启用 TCP 服务器功能
*/
public boolean isEnabled() {
return enabled;
}
/**
* 设置是否启用 TCP 服务器功能
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* 获取通道端口映射(通道名 -> 端口号)
*/
public Map<String, Integer> getChannels() {
return channels;
}
/**
* 设置通道端口映射(通道名 -> 端口号)
*/
public void setChannels(Map<String, Integer> channels) {
this.channels = channels;
}
}

View File

@@ -0,0 +1,129 @@
package com.ruoyi.tcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.Constant;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Component
@Slf4j
public class Wcs2wmsgetEmptyCargoSpace {
private final TcpServerManager manager;
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestTemplate restTemplate = new RestTemplate();
/**
* 构造函数:注入 TCP 管理器,用于在 1111 端口管理客户端与数据发送
*/
public Wcs2wmsgetEmptyCargoSpace(TcpServerManager manager) {
this.manager = manager;
}
/**
* 初始化:注册 1111 端口客户端监听,客户端首次连接时触发数据发送
*/
@PostConstruct
public void init() {
manager.setClientListener("port1111", this::handleConnect);
}
/**
* 客户端连接处理:调用 WMS 接口获取空货位数据,转换为统一 JSON 并写入到 Socket
*/
private void handleConnect(Socket socket) {
try {
String url = Constant.WMS_BASE_URL + "twin/getEmptyCargoSpace";
log.info("Fetching WMS data from {}", url);
String wmsResponse = restTemplate.getForObject(url, String.class);
JsonNode root = objectMapper.readTree(wmsResponse);
List<CargoDto> cargoList = new ArrayList<>();
// Handle if root is array or wrapped in "data" or "rows"
JsonNode arrayNode = null;
if (root.isArray()) {
arrayNode = root;
} else if (root.has("data") && root.get("data").isArray()) {
arrayNode = root.get("data");
} else if (root.has("rows") && root.get("rows").isArray()) {
arrayNode = root.get("rows");
}
if (arrayNode != null) {
for (JsonNode node : arrayNode) {
cargoList.add(mapToDto(node));
}
} else {
// If it's a single object, maybe treat as one item?
// Or maybe the format is different. For now, log warning if empty.
log.warn("WMS response is not an array: {}", wmsResponse);
}
CargoResponse response = new CargoResponse();
response.setCargo(cargoList);
String json = objectMapper.writeValueAsString(response);
OutputStream os = socket.getOutputStream();
os.write((json + "\n").getBytes(StandardCharsets.UTF_8));
os.flush();
log.info("Sent WMS data to client on port 1111");
} catch (Exception e) {
log.error("Failed to fetch WMS data or send to client", e);
}
}
/**
* 映射单条 WMS 返回记录为 DTO:优先取目标字段名,兼容 BasCargoSpace 字段名
*/
private CargoDto mapToDto(JsonNode node) {
CargoDto dto = new CargoDto();
// Mapping strategy: try user's target keys first, then BasCargoSpace keys
dto.setRow(getText(node, "row", "thePlatoon"));
dto.setColumn(getText(node, "column", "theColumn"));
dto.setLayer(getText(node, "layer", "theFloor"));
dto.setStatus(getText(node, "status"));
dto.setTrayCode(getText(node, "trayCode"));
dto.setProductCode(getText(node, "productCode"));
dto.setProductName(getText(node, "productName"));
return dto;
}
/**
* 读取节点文本:按给定字段名顺序查找首个存在且非空的值
*/
private String getText(JsonNode node, String... keys) {
for (String key : keys) {
if (node.has(key) && !node.get(key).isNull()) {
return node.get(key).asText();
}
}
return "";
}
@Data
private static class CargoResponse {
private List<CargoDto> cargo;
}
@Data
private static class CargoDto {
private String row;
private String column;
private String layer;
private String status;
private String trayCode;
private String productCode;
private String productName;
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.tcp;
import com.ruoyi.common.Constant;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
@Component
public class Wcs2wmsgetStockList {
private final TcpServerManager manager;
private final RestTemplate restTemplate = new RestTemplate();
/**
* 构造函数:注入 TCP 管理器,用于在 6666 端口管理客户端与数据发送
*/
public Wcs2wmsgetStockList(TcpServerManager manager) {
this.manager = manager;
}
/**
* 初始化:注册 6666 端口客户端监听,客户端首次连接时触发库存数据发送
*/
@PostConstruct
public void init() {
manager.setClientListener("port6666", this::handleConnect);
}
/**
* 客户端连接处理:调用 WMS 接口获取库存列表并写入到 Socket 输出流
*/
private void handleConnect(Socket socket) {
try {
String url = Constant.WMS_BASE_URL + "twin/getStockList";
String payload = restTemplate.getForObject(url, String.class);
OutputStream os = socket.getOutputStream();
os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (Exception ignored) {
}
}
/**
* 周期发送:每 10 秒检查 6666 端口是否有连接,有则广播库存数据,无连接不发送
*/
@Scheduled(fixedRate = 10000)
public void send() {
Integer count = manager.status().getOrDefault("port6666", 0);
if (count != null && count > 0) {
String url = Constant.WMS_BASE_URL + "twin/getStockList";
String payload = restTemplate.getForObject(url, String.class);
manager.broadcast("port6666", payload == null ? "" : payload);
}
}
}

View File

@@ -127,3 +127,25 @@ xss:
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# TCP服务器配置
tcpserver:
# 是否开启TCP服务器
enabled: true
# 通道配置
channels:
status: 9000
events: 9001
alarms: 9002
# 货位与库存查询端口
port1111: 1111
# 数据分析端口
port6666: 6666
# Srgv端口
port2222: 2222
# ssj端口
port3333: 3333
# tsj端口
port4444: 4444
# 报警端口
port5555: 5555