From 32343b271b7f86d158d0841a66a75564184470e4 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Wed, 5 Aug 2026 18:00:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=80=9A=E7=9F=A5=20SSE=20=E9=95=BF?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E8=A2=AB=20Nginx=2060s=20=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=8E=90=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/nginx.conf | 16 ++++++++++ .../admin/src/components/NotificationBell.tsx | 3 +- .../notifications/notifications.controller.ts | 29 +++++++++++-------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/apps/admin/nginx.conf b/apps/admin/nginx.conf index ce1d6dd..ebaef43 100644 --- a/apps/admin/nginx.conf +++ b/apps/admin/nginx.conf @@ -25,6 +25,22 @@ server { add_header Cache-Control "no-cache"; } + # SSE 长连接:禁用代理缓冲并放宽读写超时,避免 60s 空闲被掐断 + location ~ ^/api/(notifications/stream|attendance-records/import/dingtalk/stream|ai/chat/.*/stream)$ { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + add_header X-Accel-Buffering no; + } + location /api/ { proxy_pass http://backend:3000/api/; proxy_set_header Host $host; diff --git a/apps/admin/src/components/NotificationBell.tsx b/apps/admin/src/components/NotificationBell.tsx index 3f194bc..3dd59f3 100644 --- a/apps/admin/src/components/NotificationBell.tsx +++ b/apps/admin/src/components/NotificationBell.tsx @@ -58,6 +58,7 @@ const NotificationBell: React.FC = () => { const token = useUserStore.getState().token; if (!token) return; const es = new EventSource(`/api/notifications/stream?token=${encodeURIComponent(token)}`); + es.onopen = () => setSseDown(false); es.onmessage = (event) => { try { JSON.parse(event.data); @@ -68,7 +69,7 @@ const NotificationBell: React.FC = () => { } }; es.onerror = () => { - es.close(); + // 不主动关闭:EventSource 会自动重连,主动关闭会导致一次超时后实时通知永久断流 setSseDown(true); }; return () => { diff --git a/apps/server/src/notifications/notifications.controller.ts b/apps/server/src/notifications/notifications.controller.ts index c1189a4..d285482 100644 --- a/apps/server/src/notifications/notifications.controller.ts +++ b/apps/server/src/notifications/notifications.controller.ts @@ -9,7 +9,7 @@ import { UseGuards, } from '@nestjs/common'; import { Request } from 'express'; -import { Observable, map } from 'rxjs'; +import { Observable, interval, map, merge } from 'rxjs'; import { NotificationsService } from './notifications.service'; import { NotificationQueryDto } from './dto/notification.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @@ -53,17 +53,22 @@ export class NotificationsController { req.on('close', () => { this.service.unsubscribe(userId); }); - return this.service.subscribe(userId).pipe( - map((notification) => ({ - data: JSON.stringify({ - id: notification.id, - type: notification.type, - title: notification.title, - content: notification.content, - link: notification.link, - createdAt: notification.createdAt, - }), - } as MessageEvent)), + // 每 25s 发送一次空消息作为心跳,避免空闲连接被 Nginx 等中间层超时掐断。 + // 空 data 会被前端 EventSource 收到并忽略(JSON.parse 失败)。 + return merge( + this.service.subscribe(userId).pipe( + map((notification) => ({ + data: JSON.stringify({ + id: notification.id, + type: notification.type, + title: notification.title, + content: notification.content, + link: notification.link, + createdAt: notification.createdAt, + }), + } as MessageEvent)), + ), + interval(25_000).pipe(map(() => ({ data: '' } as MessageEvent))), ); }