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

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

View File

@@ -2,6 +2,10 @@ package com.ecep.contract.ds.project.service;
import java.util.List;
import com.ecep.contract.QueryService;
import com.ecep.contract.model.CompanyBankAccount;
import com.ecep.contract.util.SpecificationUtils;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
@@ -21,7 +25,7 @@ import com.ecep.contract.model.ProjectSaleType;
@Lazy
@Service
@CacheConfig(cacheNames = "sale-type")
public class SaleTypeService implements IEntityService<ProjectSaleType> {
public class SaleTypeService implements IEntityService<ProjectSaleType>, QueryService<ProjectSaleType> {
@Lazy
@Autowired
private ProjectSaleTypeRepository saleTypeRepository;
@@ -49,6 +53,17 @@ public class SaleTypeService implements IEntityService<ProjectSaleType> {
return saleTypeRepository.findAll(spec, pageable);
}
@Override
public Page<ProjectSaleType> findAll(JsonNode paramsNode, Pageable pageable) {
Specification<ProjectSaleType> spec = null;
if (paramsNode.has("searchText")) {
spec = getSpecification(paramsNode.get("searchText").asText());
}
// field
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "active");
return findAll(spec, pageable);
}
@Override
public Specification<ProjectSaleType> getSpecification(String searchText) {
if (!StringUtils.hasText(searchText)) {

View File

@@ -1,6 +1,29 @@
package com.ecep.contract.handler;
import com.ecep.contract.*;
import com.ecep.contract.constant.WebSocketConstant;
import com.ecep.contract.ds.other.service.EmployeeLoginHistoryService;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.model.Employee;
import com.ecep.contract.model.EmployeeLoginHistory;
import com.ecep.contract.service.WebSocketServerTaskManager;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -14,37 +37,6 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PingMessage;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.ecep.contract.IEntityService;
import com.ecep.contract.PageArgument;
import com.ecep.contract.PageContent;
import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.other.service.EmployeeLoginHistoryService;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.model.Employee;
import com.ecep.contract.model.EmployeeLoginHistory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
/**
* WebSocket处理器
* 处理与客户端的WebSocket连接消息传递和断开连接
@@ -64,6 +56,8 @@ public class WebSocketHandler extends TextWebSocketHandler {
private EmployeeService employeeService;
@Autowired
private ScheduledExecutorService scheduledExecutorService;
@Autowired
private WebSocketServerTaskManager taskManager;
// 存储所有活跃的WebSocket会话
private final Map<Integer, SessionInfo> activeSessions = Collections.synchronizedMap(new HashMap<>());
@@ -101,21 +95,18 @@ public class WebSocketHandler extends TextWebSocketHandler {
sessionInfo.setEmployeeId((Integer) session.getAttributes().get("employeeId"));
if (sessionInfo.getEmployeeId() == null) {
logger.error("会话未绑定用户: " + session.getId());
logger.error("会话未绑定用户: {}", session.getId());
sendError(session, 401, "会话未绑定用户");
session.close();
return;
}
activeSessions.put(sessionInfo.getEmployeeId(), sessionInfo);
System.out.println(sessionInfo.getLoginHistoryId());
System.out.println(sessionInfo.getEmployeeId());
logger.info("WebSocket连接已建立: " + session.getId());
logger.info("WebSocket连接已建立: {}", session.getId());
Employee employee = employeeService.findById(sessionInfo.getEmployeeId());
if (employee == null) {
logger.error("未找到用户: #" + sessionInfo.getEmployeeId());
logger.error("未找到用户: #{}", sessionInfo.getEmployeeId());
return;
}
@@ -123,30 +114,6 @@ public class WebSocketHandler extends TextWebSocketHandler {
sessionInfo.setSchedule(schedule);
}
private void sendError(WebSocketSession session, int errorCode, String message) {
if (session == null || !session.isOpen()) {
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message);
return;
}
try {
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("errorCode", errorCode);
objectNode.put("success", false);
objectNode.put("message", message);
String errorMessage = objectMapper.writeValueAsString(objectNode);
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
session.sendMessage(new TextMessage(errorMessage));
} else {
logger.warn("会话已关闭,无法发送错误消息: {}", message);
}
} catch (Exception e) {
// 捕获所有可能的异常防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
}
/**
* 接收文本消息时调用
@@ -154,8 +121,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
logger.info("收到来自客户端的消息: " + payload + " (会话ID: " + session.getId() + ")");
if (logger.isInfoEnabled()) {
logger.info("收到来自客户端的消息: {} (会话ID: {})", payload, session.getId());
}
// 处理文本格式的ping消息
if ("ping".equals(payload)) {
// 回复文本格式的pong消息
@@ -168,7 +136,8 @@ public class WebSocketHandler extends TextWebSocketHandler {
return;
}
if (handleAsMessageCallback(session, payload)) {
// 尝试将消息作为JSON处理
if (handleAsJson(session, payload)) {
return;
}
@@ -176,10 +145,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
logger.info("处理普通消息: " + payload);
}
private boolean handleAsMessageCallback(WebSocketSession session, String payload) {
if (session == null || !session.isOpen()) {
private boolean handleAsJson(WebSocketSession session, String payload) {
if (!session.isOpen()) {
logger.warn("尝试在已关闭的WebSocket会话上处理消息回调");
return false;
return true;
}
JsonNode jsonNode = null;
@@ -189,73 +158,98 @@ public class WebSocketHandler extends TextWebSocketHandler {
logger.warn("解析消息回调JSON失败: {}", payload, e);
return false;
}
if (!jsonNode.has("messageId")) {
// 没有 messageId 的消息不处理
return false;
}
String messageId = jsonNode.get("messageId").asText();
if (!jsonNode.has("service")) {
sendError(session, messageId, "缺失 service 参数");
if (jsonNode.has(WebSocketConstant.MESSAGE_ID_FIELD_NAME)) {
// 处理 messageId 的消息
String messageId = jsonNode.get(WebSocketConstant.MESSAGE_ID_FIELD_NAME).asText();
try {
handleAsMessageCallback(session, messageId, jsonNode);
} catch (Exception e) {
sendError(session, messageId, e.getMessage());
logger.warn("处理消息回调失败 (消息ID: {}): {}", messageId, e.getMessage(), e);
}
return true;
}
if (jsonNode.has(WebSocketConstant.SESSION_ID_FIELD_NAME)) {
taskManager.onMessage(session, jsonNode);
return true;
}
return false;
}
String serviceName = jsonNode.get("service").asText();
private void handleAsMessageCallback(WebSocketSession session, String messageId, JsonNode jsonNode) throws Exception {
if (!jsonNode.has(WebSocketConstant.SERVICE_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 service 参数");
}
String serviceName = jsonNode.get(WebSocketConstant.SERVICE_FIELD_NAME).asText();
Object service = null;
try {
service = SpringApp.getBean(serviceName);
} catch (Exception e) {
sendError(session, messageId, "未找到服务: " + serviceName);
return true;
throw new IllegalArgumentException("未找到服务: " + serviceName);
}
if (!jsonNode.has("method")) {
sendError(session, messageId, "缺失 method 参数");
return true;
if (!jsonNode.has(WebSocketConstant.METHOD_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 method 参数");
}
String methodName = jsonNode.get("method").asText();
String methodName = jsonNode.get(WebSocketConstant.METHOD_FIELD_NAME).asText();
JsonNode argumentsNode = jsonNode.get("arguments");
Object result = null;
if (methodName.equals("findAll")) {
result = invokerFindAllMethod(service, argumentsNode);
} else if (methodName.equals("findById")) {
result = invokerFindByIdMethod(service, argumentsNode);
} else if (methodName.equals("save")) {
result = invokerSaveMethod(service, argumentsNode);
} else if (methodName.equals("delete")) {
result = invokerDeleteMethod(service, argumentsNode);
} else {
result = invokerOtherMethod(service, methodName, argumentsNode);
}
String response = objectMapper.writeValueAsString(Map.of(
WebSocketConstant.MESSAGE_ID_FIELD_NAME, messageId,
WebSocketConstant.SUCCESS_FIELD_VALUE, true,
"data", result
));
session.sendMessage(new TextMessage(response));
}
private Object invokerOtherMethod(Object service, String methodName, JsonNode argumentsNode)
throws NoSuchMethodException, SecurityException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, JsonProcessingException {
int size = argumentsNode.size();
if (size == 0) {
Method method = service.getClass().getMethod(methodName);
return method.invoke(service);
}
// 参数值
JsonNode paramsNode = argumentsNode.get(0);
// 参数类型
JsonNode typesNode = argumentsNode.get(1);
Class<?>[] parameterTypes = new Class<?>[typesNode.size()];
Object[] args = new Object[paramsNode.size()];
for (int i = 0; i < typesNode.size(); i++) {
String type = typesNode.get(i).asText();
parameterTypes[i] = Class.forName(type);
args[i] = objectMapper.treeToValue(paramsNode.get(i), parameterTypes[i]);
}
Class<?> targetClass = getTargetClass(service.getClass());
try {
Object result = null;
if (methodName.equals("findAll")) {
result = invokerFindAllMethod(service, jsonNode.get("arguments"));
} else if (methodName.equals("findById")) {
result = invokerFindByIdMethod(service, jsonNode.get("arguments"));
} else if (methodName.equals("save")) {
result = invokerSaveMethod(service, jsonNode.get("arguments"));
} else if (methodName.equals("delete")) {
result = invokerDeleteMethod(service, jsonNode.get("arguments"));
} else {
sendError(session, messageId, "未实现的方法: " + methodName);
return true;
}
// 再次检查会话状态
if (!session.isOpen()) {
logger.warn("会话已关闭,无法发送处理结果 (消息ID: {})");
return true;
}
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("messageId", messageId);
objectNode.set("data", objectMapper.valueToTree(result));
String response = objectMapper.writeValueAsString(objectNode);
session.sendMessage(new TextMessage(response));
return true;
} catch (Exception e) {
// 防止重复发送消息导致的TEXT_PARTIAL_WRITING异常
if (!session.isOpen()) {
logger.warn("会话已关闭,无法发送错误消息 (消息ID: {})");
} else {
sendError(session, messageId, e.getMessage());
}
logger.error("处理消息回调失败 (消息ID: {})", messageId, e);
return true;
Method method = targetClass.getMethod(methodName, parameterTypes);
return method.invoke(service, args);
} catch (NoSuchMethodException e) {
logger.error("targetClass: {}, Methods:{}", targetClass, targetClass.getMethods());
throw e;
}
}
private Object invokerDeleteMethod(Object service, JsonNode argumentsNode) {
JsonNode paramsNode = argumentsNode.get(0);
if (!paramsNode.has("id")) {
@@ -445,6 +439,7 @@ public class WebSocketHandler extends TextWebSocketHandler {
}
try {
JsonNode typesNode = argumentsNode.get(1);
if (paramsNode.isInt()) {
Method method = service.getClass().getMethod("findById", Integer.class);
return method.invoke(service, paramsNode.asInt());
@@ -469,17 +464,22 @@ public class WebSocketHandler extends TextWebSocketHandler {
}
private void sendError(WebSocketSession session, String messageId, String message) {
_sendError(session, WebSocketConstant.MESSAGE_ID_FIELD_NAME, messageId, message);
}
private void _sendError(WebSocketSession session, String fieldName, String messageId, String message) {
if (session == null || !session.isOpen()) {
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message);
return;
}
try {
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("messageId", messageId);
objectNode.put("success", false);
objectNode.put("message", message);
String errorMessage = objectMapper.writeValueAsString(objectNode);
String errorMessage = objectMapper.writeValueAsString(Map.of(
fieldName, messageId,
WebSocketConstant.SUCCESS_FIELD_VALUE, false,
WebSocketConstant.MESSAGE_FIELD_NAME, message
));
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
@@ -505,6 +505,19 @@ public class WebSocketHandler extends TextWebSocketHandler {
@Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
logger.info("收到来自客户端的Pong消息: " + message.getPayload() + " (会话ID: " + session.getId() + ")");
// 从活跃会话集合中移除会话
SessionInfo sessionInfo = activeSessions.get((Integer) session.getAttributes().get("employeeId"));
if (sessionInfo == null) {
logger.warn("收到来自客户端的Pong消息但会话不存在: " + session.getId());
return;
}
Integer loginHistoryId = sessionInfo.getLoginHistoryId();
if (loginHistoryId != null) {
EmployeeLoginHistory history = employeeLoginHistoryService.findById(loginHistoryId);
history.setActiveTime(LocalDateTime.now());
employeeLoginHistoryService.save(history);
}
}
/**
@@ -516,7 +529,7 @@ public class WebSocketHandler extends TextWebSocketHandler {
logger.info(
"WebSocket连接已关闭: " + session.getId() + ", 状态码: " + status.getCode() + ", 原因: " + status.getReason());
// 从活跃会话集合中移除会话
SessionInfo sessionInfo = activeSessions.remove(session.getAttributes().get("employeeId"));
SessionInfo sessionInfo = activeSessions.remove((Integer) session.getAttributes().get("employeeId"));
if (sessionInfo == null) {
return;
}
@@ -572,4 +585,29 @@ public class WebSocketHandler extends TextWebSocketHandler {
public int getActiveSessionCount() {
return activeSessions.size();
}
private void sendError(WebSocketSession session, int errorCode, String message) {
if (session == null || !session.isOpen()) {
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message);
return;
}
try {
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put(WebSocketConstant.ERROR_CODE_FIELD_NAME, errorCode);
objectNode.put(WebSocketConstant.SUCCESS_FIELD_VALUE, false);
objectNode.put(WebSocketConstant.MESSAGE_FIELD_NAME, message);
String errorMessage = objectMapper.writeValueAsString(objectNode);
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
session.sendMessage(new TextMessage(errorMessage));
} else {
logger.warn("会话已关闭,无法发送错误消息: {}", message);
}
} catch (Exception e) {
// 捕获所有可能的异常防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
}
}

View File

@@ -0,0 +1,130 @@
package com.ecep.contract.service;
import com.ecep.contract.Message;
import com.ecep.contract.constant.WebSocketConstant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
@Service
public class WebSocketTaskManager implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(WebSocketTaskManager.class);
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ScheduledExecutorService scheduledExecutorService;
private Map<String, String> taskClzMap = Map.of();
@Override
public void afterPropertiesSet() throws Exception {
taskClzMap = Map.of(
"ContractSyncTask", "com.ecep.contract.cloud.u8.ContractSyncTask",
"ContractRepairTask", "com.ecep.contract.ds.contract.tasker.ContractRepairTask"
);
}
public void onMessage(WebSocketSession session, JsonNode jsonNode) {
// 处理 sessionId 的消息
String sessionId = jsonNode.get(WebSocketConstant.SESSION_ID_FIELD_NAME).asText();
try {
handleAsSessionCallback(session, sessionId, jsonNode);
} catch (Exception e) {
sendError(session, sessionId, e.getMessage());
logger.warn("处理会话回调失败 (会话ID: {}): {}", sessionId, e.getMessage(), e);
}
}
private void handleAsSessionCallback(WebSocketSession session, String sessionId, JsonNode jsonNode) {
if (!jsonNode.has("type")) {
throw new IllegalArgumentException("缺失 type 参数");
}
String type = jsonNode.get("type").asText();
if (type.equals("createTask")) {
createTask(session, sessionId, jsonNode);
}
}
private void createTask(WebSocketSession session, String sessionId, JsonNode jsonNode) {
if (!jsonNode.has("taskName")) {
throw new IllegalArgumentException("缺失 taskName 参数");
}
String taskName = jsonNode.get("taskName").asText();
String clzName = taskClzMap.get(taskName);
if (clzName == null) {
throw new IllegalArgumentException("未知的任务类型: " + taskName);
}
Object tasker = null;
try {
Class<?> clz = Class.forName(clzName);
tasker = clz.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("未知的任务类型: " + taskName + ", class: " + clzName);
} catch (Exception e) {
throw new IllegalArgumentException("任务类型: " + taskName + ", class: " + clzName + " 实例化失败");
}
if (tasker instanceof WebSocketServerTasker t) {
t.setTitleHandler(title -> sendToSession(session, sessionId, "title", title));
t.setMessageHandler(msg -> sendMessageToSession(session, sessionId, msg));
t.init(jsonNode);
scheduledExecutorService.submit(t);
}
}
private boolean sendMessageToSession(WebSocketSession session, String sessionId, Message msg) {
return sendToSession(session, sessionId, "message", msg.getLevel().getName(), msg.getMessage());
}
private boolean sendToSession(WebSocketSession session, String sessionId, String type, Object... args) {
try {
String text = objectMapper.writeValueAsString(Map.of(
WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId,
"type", type,
"args", args
));
session.sendMessage(new TextMessage(text));
} catch (IOException e) {
// 捕获所有可能的异常,防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
return true;
}
private void sendError(WebSocketSession session, String sessionId, String message) {
if (session == null || !session.isOpen()) {
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message);
return;
}
try {
String errorMessage = objectMapper.writeValueAsString(Map.of(
WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId,
WebSocketConstant.SUCCESS_FIELD_VALUE, false,
WebSocketConstant.MESSAGE_FIELD_NAME, message
));
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
session.sendMessage(new TextMessage(errorMessage));
} else {
logger.warn("会话已关闭,无法发送错误消息: {}", message);
}
} catch (Exception e) {
// 捕获所有可能的异常,防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
}
}

View File

@@ -0,0 +1,15 @@
package com.ecep.contract.service;
import com.ecep.contract.Message;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.concurrent.Callable;
import java.util.function.Predicate;
public interface WebSocketTasker extends Callable<Object> {
void setMessageHandler(Predicate<Message> messageHandler);
void setTitleHandler(Predicate<String> titleHandler);
void init(JsonNode jsonNode);
}

View File

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