fix: 暂存

This commit is contained in:
2026-06-23 14:32:19 +08:00
parent 9be461539c
commit 2a311ca25f
4 changed files with 40 additions and 4 deletions

View File

@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException;
/** /**
@@ -49,6 +50,16 @@ public class GlobalExceptionHandler {
log.warn("SaToken 上下文未初始化(异步分派场景): {}", ex.getMessage()); log.warn("SaToken 上下文未初始化(异步分派场景): {}", ex.getMessage());
} }
/**
* 异步请求响应已不可用SSE 连接断开后心跳试图写入)
*
* 返回 void 避免 Spring 尝试以 JSON 写入已损坏的 text/event-stream 响应。
*/
@ExceptionHandler(AsyncRequestNotUsableException.class)
public void handleAsyncRequestNotUsable(AsyncRequestNotUsableException ex) {
// SSE 连接已断开后心跳的残留异常,无需处理
}
// ==================== 业务异常 ==================== // ==================== 业务异常 ====================
/** /**

View File

@@ -57,6 +57,9 @@ public class SseConnectionManager {
/** /**
* 移除连接 * 移除连接
* *
* 先移除映射再尝试关闭,避免回调递归。
* 响应已损坏时 complete() 可能抛出 RuntimeException静默忽略。
*
* @param userId 用户 accountId * @param userId 用户 accountId
*/ */
public void remove(String userId) { public void remove(String userId) {
@@ -64,7 +67,8 @@ public class SseConnectionManager {
if (removed != null) { if (removed != null) {
try { try {
removed.complete(); removed.complete();
} catch (Exception ignored) { } catch (Throwable ignored) {
// 响应已损坏AsyncRequestNotUsableException 等),忽略
} }
log.info("[SSE] 用户连接已移除, userId={}, 当前在线={}", userId, emitters.size()); log.info("[SSE] 用户连接已移除, userId={}, 当前在线={}", userId, emitters.size());
} }

View File

@@ -69,8 +69,8 @@ public class SseController {
emitter.send(SseEmitter.event() emitter.send(SseEmitter.event()
.name("heartbeat") .name("heartbeat")
.data("")); .data(""));
} catch (IOException e) { } catch (Exception e) {
// 客户端已断开,清理 // 客户端已断开IOException或响应已不可用AsyncRequestNotUsableException
connectionManager.remove(userId); connectionManager.remove(userId);
heartbeat.shutdown(); heartbeat.shutdown();
} }

View File

@@ -158,14 +158,35 @@ public class RoomWebSocketHandler extends TextWebSocketHandler {
return null; return null;
} }
/** 聊天消息最大长度 */
private static final int CHAT_MAX_LENGTH = 500;
/** /**
* 聊天消息处理(暂不持久化,直接广播) * 聊天消息处理(暂不持久化,直接广播)
*
* 校验规则:
* - content 不能为 null 或空字符串
* - content 长度不能超过 CHAT_MAX_LENGTH500 字符)
* - 移除 HTML 标签防止 XSS
*/ */
private void handleChat(String roomId, String userId, String content) { private void handleChat(String roomId, String userId, String content) {
// 内容为空或超长时直接丢弃
if (content == null || content.isBlank()) {
return;
}
if (content.length() > CHAT_MAX_LENGTH) {
content = content.substring(0, CHAT_MAX_LENGTH);
}
// 简易 XSS 防护:移除 HTML 标签
String sanitized = content
.replace("<", "")
.replace(">", "");
// TODO: 后续可增加消息持久化 // TODO: 后续可增加消息持久化
broadcastToRoom(roomId, new WsMessage(WsOutboundMessage.CHAT_BROADCAST, Map.of( broadcastToRoom(roomId, new WsMessage(WsOutboundMessage.CHAT_BROADCAST, Map.of(
"userId", userId, "userId", userId,
"content", content, "content", sanitized,
"timestamp", System.currentTimeMillis() "timestamp", System.currentTimeMillis()
))); )));
} }