From 399c5f5bd86707e95cda783bb8ecba1afd8ac2fc Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Mon, 22 Jun 2026 13:43:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20SseController=20SS?= =?UTF-8?q?E=20=E8=AE=A2=E9=98=85=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webgamebackend/sse/SseController.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/main/java/com/webgame/webgamebackend/sse/SseController.java diff --git a/src/main/java/com/webgame/webgamebackend/sse/SseController.java b/src/main/java/com/webgame/webgamebackend/sse/SseController.java new file mode 100644 index 0000000..7d07401 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/sse/SseController.java @@ -0,0 +1,79 @@ +package com.webgame.webgamebackend.sse; + +import cn.dev33.satoken.stp.StpUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * SSE 连接端点 + * + * 提供 GET /sse/subscribe 端点,建立持久化 SSE 连接。 + * 连接成功后立即发送 connected 事件确认握手,之后每 30 秒发送心跳保活。 + */ +@Slf4j +@RestController +@RequestMapping("/sse") +@RequiredArgsConstructor +public class SseController { + + private final SseConnectionManager connectionManager; + + /** + * 建立 SSE 订阅连接 + * + * 需要登录鉴权(Sa-Token 自动校验)。 + * EventSource API 不支持自定义请求头,token 通过 URL 查询参数 ?token=xxx 传递。 + * + * @return SseEmitter 实例(30 分钟超时) + */ + @GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribe() { + String userId = StpUtil.getLoginIdAsString(); + SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30 分钟超时 + + connectionManager.register(userId, emitter); + + // 发送初始连接确认 + try { + emitter.send(SseEmitter.event() + .name("connected") + .data("{\"message\":\"连接成功\"}")); + } catch (IOException e) { + log.warn("[SSE] 发送 connected 事件失败, userId={}", userId); + } + + // 启动心跳定时任务 + ScheduledExecutorService heartbeat = Executors.newSingleThreadScheduledExecutor( + r -> new Thread(r, "sse-heartbeat-" + userId.substring(0, Math.min(8, userId.length()))) + ); + heartbeat.scheduleAtFixedRate(() -> { + try { + emitter.send(SseEmitter.event() + .name("heartbeat") + .data("")); + } catch (IOException e) { + // 客户端已断开,清理 + connectionManager.remove(userId); + heartbeat.shutdown(); + } + }, 30, 30, TimeUnit.SECONDS); + + // 连接关闭时停止心跳 + emitter.onCompletion(heartbeat::shutdown); + emitter.onTimeout(heartbeat::shutdown); + emitter.onError(e -> heartbeat.shutdown()); + + log.info("[SSE] 新连接建立, userId={}, 当前在线={}", userId, connectionManager.getOnlineCount()); + return emitter; + } +}