From 69c3daa919d581a661d1e8607a21aa1f33836db4 Mon Sep 17 00:00:00 2001 From: colaftc Date: Sat, 27 Dec 2025 09:04:12 +0800 Subject: [PATCH] feat: disabled sse module and added health check to api_v2 for caddy2 --- api_v2/tests.py | 10 ++++ api_v2/urls.py | 2 + api_v2/views/__init__.py | 2 + api_v2/views/healthy.py | 39 ++++++++++++++++ sse/views.py | 98 ++++++++++++++++++++-------------------- 5 files changed, 103 insertions(+), 48 deletions(-) create mode 100644 api_v2/views/healthy.py diff --git a/api_v2/tests.py b/api_v2/tests.py index c1000c4..702fc5a 100644 --- a/api_v2/tests.py +++ b/api_v2/tests.py @@ -63,6 +63,16 @@ class QuickCreateEmployeeUserAPITest(TestCase): self.assertIn('商户不存在', str(response.data)) +class HealthCheckV2APITest(TestCase): + def setUp(self): + self.client = APIClient() + + def test_health_check_returns_200(self): + resp = self.client.get('/api/v2/health/') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data.get('status'), 'ok') + + class PrintingJobByCustomerAPITest(TestCase): def setUp(self): self.factory = APIRequestFactory() diff --git a/api_v2/urls.py b/api_v2/urls.py index cac2df5..09a837a 100644 --- a/api_v2/urls.py +++ b/api_v2/urls.py @@ -1,6 +1,7 @@ from django.urls import path from api_v2.views import ( + HealthCheckView, QuickCreateEmployeeUserView, PrintingJobByCustomerView, PrintingJobBatchAdvancePreviewView, @@ -14,6 +15,7 @@ from api_v2.views import ( from api_v2.views.basic_info import CustomerEmployeeBindingView urlpatterns = [ + path('health/', HealthCheckView.as_view(), name='api_v2_health_check'), path('users/quick-create/', QuickCreateEmployeeUserView.as_view(), name='api_v2_user_quick_create'), path('customers/bind-employee/', CustomerEmployeeBindingView.as_view(), name='api_v2_customer_bind_employee'), path('printing-jobs/by-customer/', PrintingJobByCustomerView.as_view(), name='api_v2_printing_job_by_customer'), diff --git a/api_v2/views/__init__.py b/api_v2/views/__init__.py index c81efea..e43bacf 100644 --- a/api_v2/views/__init__.py +++ b/api_v2/views/__init__.py @@ -2,6 +2,7 @@ api_v2 视图包。 """ +from .healthy import HealthCheckView from .users import QuickCreateEmployeeUserView from .printing import ( PrintingJobByCustomerView, @@ -16,6 +17,7 @@ from .printing import ( from .stateflow import BusinessObjectCloneView __all__ = [ + 'HealthCheckView', 'QuickCreateEmployeeUserView', 'PrintingJobByCustomerView', 'PrintingJobV2Serializer', diff --git a/api_v2/views/healthy.py b/api_v2/views/healthy.py new file mode 100644 index 0000000..180b743 --- /dev/null +++ b/api_v2/views/healthy.py @@ -0,0 +1,39 @@ +import logging +import os + +from django.db import connections, transaction +from rest_framework.response import Response +from rest_framework.views import APIView + +logger = logging.getLogger(__name__) + + +class HealthCheckView(APIView): + """ + GET /api/v2/health/ + + 仅做最轻量的 DB 查询(SELECT 1)。 + 若失败(含超时):直接退出当前进程,让容器重启。 + """ + + authentication_classes: list = [] + permission_classes: list = [] + + # 单条查询超时(毫秒)。避免 health check 卡死占用 worker。 + STATEMENT_TIMEOUT_MS = 2000 + + def get(self, request): + try: + # 用事务包住 SET LOCAL,确保 statement_timeout 只对本次请求生效 + with transaction.atomic(using="default"): + with connections["default"].cursor() as cursor: + cursor.execute("SET LOCAL statement_timeout = %s;", [self.STATEMENT_TIMEOUT_MS]) + cursor.execute("SELECT 1;") + cursor.fetchone() + except Exception: + logger.exception("api_v2 health check failed; exiting process") + os._exit(1) # 等价于 Go 的 os.Exit(1):立即退出进程 + + return Response({"status": "ok"}) + + diff --git a/sse/views.py b/sse/views.py index dbdbf5e..257f074 100644 --- a/sse/views.py +++ b/sse/views.py @@ -29,61 +29,63 @@ def create_sse_event(request): - 需要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 + # 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 + # def event_stream(): + # # 获取当前商户ID + # merchant_id = request.merchant_id - # 创建一个同步队列用于接收消息 - conn_queue = queue.Queue(maxsize=100) - services.push_connection(merchant_id, conn_queue) + # # 创建一个同步队列用于接收消息 + # 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" + # 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) + # # 持续从队列中获取消息并发送给客户端 + # 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', - ) + # 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' + # # 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