feat: 实现WebSocket通信框架及任务管理功能

新增WebSocket客户端和服务端通信框架,包括会话管理、心跳检测和自动重连机制
添加任务管理器用于处理WebSocket任务创建和执行
实现消息回调处理和错误处理机制
重构销售类型服务并添加缓存支持
移除旧的销售类型服务实现
This commit is contained in:
2025-09-17 11:44:39 +08:00
parent ada539bebf
commit 30deb0a280
19 changed files with 495 additions and 160 deletions

View File

@@ -1,6 +1,8 @@
package com.ecep.contract;
import com.ecep.contract.constant.WebSocketConstant;
import com.ecep.contract.msg.SimpleMessage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import javafx.application.Platform;
@@ -24,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* WebSocket消息服务
@@ -49,6 +52,9 @@ public class WebSocketService {
private SimpleBooleanProperty online = new SimpleBooleanProperty(false);
private SimpleStringProperty message = new SimpleStringProperty("");
// 存储所有活跃的WebSocket会话
private final Map<String, WebSocketClientSession> sessions = Collections.synchronizedMap(new HashMap<>());
// 存储所有活跃的WebSocket会话
private final Map<String, CompletableFuture<JsonNode>> callbacks = Collections.synchronizedMap(new HashMap<>());
@@ -75,29 +81,23 @@ public class WebSocketService {
try {
JsonNode node = objectMapper.readTree(text);
if (node.has("messageId")) {
String messageId = node.get("messageId").asText();
if (node.has(WebSocketConstant.MESSAGE_ID_FIELD_NAME)) {
String messageId = node.get(WebSocketConstant.MESSAGE_ID_FIELD_NAME).asText();
CompletableFuture<JsonNode> future = callbacks.remove(messageId);
if (future != null) {
if (node.has("success")) {
if (!node.get("success").asBoolean()) {
future.completeExceptionally(
new RuntimeException("请求失败:来自服务器的消息=" + node.get("message").asText()));
return;
}
}
// 使用具体类型后这里不会再出现类型不匹配的错误
if (node.has("data")) {
future.complete(node.get("data"));
} else {
future.complete(node);
}
onCallbackMessage(future, node);
} else {
logger.error("未找到对应的回调future: {}", messageId);
}
} else if (node.has("errorCode")) {
int errorCode = node.get("errorCode").asInt();
String errorMsg = node.get("message").asText();
} else if (node.has(WebSocketConstant.SESSION_ID_FIELD_NAME)) {
String sessionId = node.get(WebSocketConstant.SESSION_ID_FIELD_NAME).asText();
WebSocketClientSession session = sessions.get(sessionId);
if (session != null) {
session.onMessage(node);
}
} else if (node.has(WebSocketConstant.ERROR_CODE_FIELD_NAME)) {
int errorCode = node.get(WebSocketConstant.ERROR_CODE_FIELD_NAME).asInt();
String errorMsg = node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText();
// TODO 需要重新登录
logger.error("收到错误消息: 错误码={}, 错误信息={}", errorCode, errorMsg);
}
@@ -139,6 +139,22 @@ public class WebSocketService {
}
};
private void onCallbackMessage(CompletableFuture<JsonNode> future, JsonNode node) {
if (node.has(WebSocketConstant.SUCCESS_FIELD_VALUE)) {
if (!node.get(WebSocketConstant.SUCCESS_FIELD_VALUE).asBoolean()) {
future.completeExceptionally(
new RuntimeException("请求失败:来自服务器的消息=" + node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText()));
return;
}
}
// 使用具体类型后这里不会再出现类型不匹配的错误
if (node.has("data")) {
future.complete(node.get("data"));
} else {
future.complete(node);
}
}
public void send(String string) {
if (webSocket != null && webSocket.send(string)) {
logger.debug("send message success:{}", string);
@@ -147,6 +163,10 @@ public class WebSocketService {
}
}
public void send(Object message) throws JsonProcessingException {
send(objectMapper.writeValueAsString(message));
}
public CompletableFuture<JsonNode> send(SimpleMessage msg) {
CompletableFuture<JsonNode> future = new CompletableFuture<>();
try {
@@ -168,6 +188,14 @@ public class WebSocketService {
return future;
}
public CompletableFuture<JsonNode> invoke(String service, String method, Object... params) {
SimpleMessage msg = new SimpleMessage();
msg.setService(service);
msg.setMethod(method);
msg.setArguments(params);
return send(msg).orTimeout(getReadTimeout(), TimeUnit.MILLISECONDS);
}
public void initWebSocket() {
isActive = true;
OkHttpClient httpClient = Desktop.instance.getHttpClient();
@@ -278,4 +306,25 @@ public class WebSocketService {
return online;
}
public void withSession(Consumer<WebSocketClientSession> sessionConsumer) {
WebSocketClientSession session = createSession();
try {
sessionConsumer.accept(session);
} finally {
// closeSession(session);
}
}
private void closeSession(WebSocketClientSession session) {
if (session != null) {
sessions.remove(session.getSessionId());
session.close();
}
}
private WebSocketClientSession createSession() {
WebSocketClientSession session = new WebSocketClientSession(this);
sessions.put(session.getSessionId(), session);
return session;
}
}

View File

@@ -0,0 +1,56 @@
package com.ecep.contract;
import com.ecep.contract.constant.WebSocketConstant;
import com.ecep.contract.task.Tasker;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import java.util.Map;
import java.util.UUID;
public class WebSocketSession {
@Getter
private String sessionId = UUID.randomUUID().toString();
private WebSocketClientTasker tasker;
private final WebSocketService webSocketService;
public WebSocketSession(WebSocketService webSocketService) {
this.webSocketService = webSocketService;
}
public void close() {
}
public void submitTask(WebSocketClientTasker tasker, Object... args) throws JsonProcessingException {
Map<String, Object> argments = Map.of(
WebSocketConstant.SESSION_ID_FIELD_NAME, getSessionId(),
"type", "createTask",
"taskName", tasker.getTaskName(),
"args", args);
webSocketService.send(argments);
}
public void onMessage(JsonNode node) {
if (node.has("type")) {
String type = node.get("type").asText();
if (type.equals("message")) {
JsonNode args = node.get("args");
String message = args.get(1).asText();
String level = args.get(0).asText();
if (tasker instanceof Tasker<?> t) {
t.updateMessage(java.util.logging.Level.parse(level), message);
}
} else if (type.equals("title")) {
JsonNode args = node.get("args");
String message = args.get(0).asText();
if (tasker instanceof Tasker<?> t) {
t.updateTitle(message);
}
}
}
}
}

View File

@@ -0,0 +1,4 @@
package com.ecep.contract;
public interface WebSocketClientTasker {
}

View File

@@ -0,0 +1,4 @@
package com.ecep.contract.service;
public class CompanyCustomerFileTypeService {
}

View File

@@ -4,10 +4,14 @@ import java.util.List;
import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyInvoiceInfo;
import com.ecep.contract.service.QueryService;
import com.ecep.contract.service.ViewModelService;
import com.ecep.contract.vm.CompanyInvoiceInfoViewModel;
import org.springframework.stereotype.Service;
public class CompanyInvoiceInfoService implements ViewModelService<CompanyInvoiceInfo, CompanyInvoiceInfoViewModel> {
@Service
public class CompanyInvoiceInfoService extends QueryService<CompanyInvoiceInfo, CompanyInvoiceInfoViewModel> {
public List<CompanyInvoiceInfo> searchByCompany(Company company, String searchText) {
throw new UnsupportedOperationException("未实现");

View File

@@ -0,0 +1,4 @@
package com.ecep.contract.service;
public class CompanyVendorFileTypeService {
}

View File

@@ -0,0 +1,4 @@
package com.ecep.contract.service;
public class HolidayService {
}

View File

@@ -1,16 +0,0 @@
package com.ecep.contract.service;
import org.springframework.stereotype.Service;
import com.ecep.contract.model.ProjectSaleType;
import com.ecep.contract.vm.ProjectSaleTypeViewModel;
@Service
public class SaleTypeService extends QueryService<ProjectSaleType, ProjectSaleTypeViewModel> {
public ProjectSaleType findByName(String name) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'findByName'");
}
}

View File

@@ -0,0 +1,4 @@
package com.ecep.contract.vm;
public class CompanyCustomerFileTypeLocalViewModel {
}

View File

@@ -0,0 +1,4 @@
package com.ecep.contract.vm;
public class CompanyVendorFileTypeLocalViewModel {
}

View File

@@ -0,0 +1,4 @@
package com.ecep.contract.vm;
public class EnumViewModel {
}