1
0
forked from erp-dev/erp

feat: sse && multi_merchant completed

This commit is contained in:
2025-11-11 10:56:20 +08:00
parent b2078dfa46
commit 2aafb93aad
43 changed files with 2445 additions and 244 deletions

View File

@@ -1,45 +1,131 @@
from django.http.response import StreamingHttpResponse
from django.http import StreamingHttpResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from contextlib import suppress
import asyncio
from rest_framework import status
from . import services
import queue
import json
_connections = set()
@api_view(['POST'])
@permission_classes([])
@csrf_exempt
@require_http_methods(["GET", "OPTIONS"])
def create_sse_event(request):
"""
创建一个简单的SSE事件流响应,用于测试和演示目的
创建一个 SSE 事件流响应。
客户端连接到此端点后会保持长连接,等待服务器推送事件。
使用方法:
- GET /sse/
- 保持连接打开以接收实时事件
注意:
- 使用纯 Django 视图,不使用 DRF避免内容协商导致的 406 错误
- SSE 需要特殊的 CORS 配置
"""
def sse_stream():
queue = asyncio.Queue()
_connections.add(queue)
# 处理 OPTIONS 预检请求
if request.method == 'OPTIONS':
response = HttpResponse()
origin = request.META.get('HTTP_ORIGIN')
if origin:
response['Access-Control-Allow-Origin'] = origin
response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
response['Access-Control-Allow-Headers'] = 'authorization, content-type, cache-control, accept'
response['Access-Control-Allow-Credentials'] = 'true'
response['Access-Control-Max-Age'] = '86400' # 24小时
return response
def event_stream():
# 创建一个同步队列用于接收消息
conn_queue = queue.Queue(maxsize=100)
services.push_connection(conn_queue)
try:
# 发送初始连接成功消息
yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established'})}\n\n"
# 持续从队列中获取消息并发送给客户端
while True:
data = queue.get()
yield f"data: {data}\n\n"
try:
# 使用同步方式等待新消息带超时30秒以便发送心跳
message = conn_queue.get(timeout=30.0)
yield f"data: {json.dumps(message)}\n\n"
except queue.Empty:
# 30秒超时发送心跳保持连接活跃
yield f": heartbeat\n\n"
except Exception as e:
print(f"Error in SSE stream: {e}")
break
finally:
_connections.remove(queue)
response = StreamingHttpResponse(sse_stream(), content_type='text/event-stream')
# 清理:从连接集合中移除此队列
services.remove_connection(conn_queue)
response = StreamingHttpResponse(
event_stream(),
content_type='text/event-stream',
)
# SSE 必需的响应头
response['Cache-Control'] = 'no-cache'
response['X-Accel-Buffering'] = 'no' # 禁用 nginx 缓冲
# CORS 响应头django-cors-headers 中间件会自动添加,但我们显式设置以确保)
# 如果请求带有 Origin 头,手动添加 CORS 响应头
origin = request.META.get('HTTP_ORIGIN')
if origin:
response['Access-Control-Allow-Origin'] = origin
response['Access-Control-Allow-Credentials'] = 'true'
return response
@api_view(['POST'])
@permission_classes([])
def push_sse_event(request):
@permission_classes([AllowAny])
def push_test_event(request):
"""
向所有连接的客户端广播一个SSE事件。
请求体应包含一个 'message' 字段,表示要发送的消息内容。
向所有连接的客户端广播一个 SSE 测试事件。
"""
message = request.data.get('message', 'Hello, SSE!')
for q in list(_connections):
with suppress(asyncio.QueueFull):
q.put_nowait(message)
return Response({'status': 'message sent'})
services.push_simple_message_with_object_id('order_paid', '订单已支付', 12345)
return Response({
'status': 'ok',
'message': 'Test event broadcasted',
'clients': len(services.get_active_connections())
})
@api_view(['GET'])
@permission_classes([AllowAny])
def get_sse_status(request):
"""
获取 SSE 连接状态信息。
返回:
- clients: 当前连接的客户端数量
- status: 服务状态
"""
active_connections = services.get_active_connections()
return Response({
'status': 'running',
'clients': len(active_connections),
'message': f'SSE server is running with {len(active_connections)} active connection(s)',
})
@api_view(['POST'])
@permission_classes([AllowAny])
def shutdown_sse(request):
"""
优雅关闭所有SSE连接的端点
"""
services.push_sse_event_to_all({
'type': 'server_shutdown',
'message': 'Server is shutting down, please reconnect later'
})
return Response({
'status': 'ok',
'message': 'Shutdown signal sent to all SSE connections',
'clients': len(services.get_active_connections())
})