1
0
forked from erp-dev/erp
Files
erpnew/sse/views.py

169 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from . import services
from .auth_utils import require_sse_authentication, get_user_merchant_id
import queue
import json
@csrf_exempt
@require_http_methods(["GET", "OPTIONS"])
@require_sse_authentication
def create_sse_event(request):
"""
创建一个 SSE 事件流响应。
客户端连接到此端点后会保持长连接,等待服务器推送事件。
使用方法:
- GET /sse/
- 保持连接打开以接收实时事件
注意:
- 使用纯 Django 视图,不使用 DRF避免内容协商导致的 406 错误
- SSE 需要特殊的 CORS 配置
- 需要JWT认证且用户必须有关联的商户
"""
# 处理 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():
# # 获取当前商户ID
# merchant_id = request.merchant_id
# # 创建一个同步队列用于接收消息
# conn_queue = queue.Queue(maxsize=100)
# services.push_connection(merchant_id, conn_queue)
# try:
# # 发送初始连接成功消息
# yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established', 'merchant_id': merchant_id})}\n\n"
# # 持续从队列中获取消息并发送给客户端
# while True:
# 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:
# # 清理:从连接集合中移除此队列
# services.remove_connection(merchant_id, 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'
response = HttpResponse()
response.content = 'not allowed'
response.status_code = 405
return response
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def push_test_event(request):
"""
向当前用户所属商户的所有连接客户端广播一个 SSE 测试事件。
"""
# 获取当前用户的商户ID
merchant_id = get_user_merchant_id(request)
if not merchant_id:
return Response({'error': 'User has no associated merchant'}, status=status.HTTP_403_FORBIDDEN)
services.push_simple_message_with_object_id_to_merchant(
merchant_id, 'order_paid', '订单已支付', 12345
)
merchant_connections = services.get_merchant_connections(merchant_id)
return Response({
'status': 'ok',
'message': 'Test event broadcasted to your merchant',
'merchant_id': merchant_id,
'clients': len(merchant_connections)
})
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_sse_status(request):
"""
获取 SSE 连接状态信息。
返回:
- total_clients: 所有商户的客户端总数
- merchant_clients: 当前商户的客户端数量
- status: 服务状态
"""
merchant_id = get_user_merchant_id(request)
if not merchant_id:
return Response({'error': 'User has no associated merchant'}, status=status.HTTP_403_FORBIDDEN)
# 获取所有连接数
all_connections = services.get_all_connections_count()
# 获取当前商户的连接数
merchant_connections = services.get_merchant_connections(merchant_id)
return Response({
'status': 'running',
'total_clients': all_connections,
'merchant_clients': len(merchant_connections),
'merchant_id': merchant_id,
'message': f'SSE server is running with {all_connections} total connections, {len(merchant_connections)} for your merchant',
})
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def shutdown_sse(request):
"""
关闭当前用户所属商户的所有SSE连接
"""
merchant_id = get_user_merchant_id(request)
if not merchant_id:
return Response({'error': 'User has no associated merchant'}, status=status.HTTP_403_FORBIDDEN)
# 获取连接数
merchant_connections = services.get_merchant_connections(merchant_id)
client_count = len(merchant_connections)
# 实际关闭连接
services.shutdown_merchant_connections(merchant_id)
return Response({
'status': 'ok',
'message': 'Your merchant\'s SSE connections have been closed',
'merchant_id': merchant_id,
'clients': client_count
})