forked from erp-dev/erp
feat: disabled sse module and added health check to api_v2 for caddy2
This commit is contained in:
@@ -63,6 +63,16 @@ class QuickCreateEmployeeUserAPITest(TestCase):
|
|||||||
self.assertIn('商户不存在', str(response.data))
|
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):
|
class PrintingJobByCustomerAPITest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.factory = APIRequestFactory()
|
self.factory = APIRequestFactory()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from api_v2.views import (
|
from api_v2.views import (
|
||||||
|
HealthCheckView,
|
||||||
QuickCreateEmployeeUserView,
|
QuickCreateEmployeeUserView,
|
||||||
PrintingJobByCustomerView,
|
PrintingJobByCustomerView,
|
||||||
PrintingJobBatchAdvancePreviewView,
|
PrintingJobBatchAdvancePreviewView,
|
||||||
@@ -14,6 +15,7 @@ from api_v2.views import (
|
|||||||
from api_v2.views.basic_info import CustomerEmployeeBindingView
|
from api_v2.views.basic_info import CustomerEmployeeBindingView
|
||||||
|
|
||||||
urlpatterns = [
|
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('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('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'),
|
path('printing-jobs/by-customer/', PrintingJobByCustomerView.as_view(), name='api_v2_printing_job_by_customer'),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
api_v2 视图包。
|
api_v2 视图包。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .healthy import HealthCheckView
|
||||||
from .users import QuickCreateEmployeeUserView
|
from .users import QuickCreateEmployeeUserView
|
||||||
from .printing import (
|
from .printing import (
|
||||||
PrintingJobByCustomerView,
|
PrintingJobByCustomerView,
|
||||||
@@ -16,6 +17,7 @@ from .printing import (
|
|||||||
from .stateflow import BusinessObjectCloneView
|
from .stateflow import BusinessObjectCloneView
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
'HealthCheckView',
|
||||||
'QuickCreateEmployeeUserView',
|
'QuickCreateEmployeeUserView',
|
||||||
'PrintingJobByCustomerView',
|
'PrintingJobByCustomerView',
|
||||||
'PrintingJobV2Serializer',
|
'PrintingJobV2Serializer',
|
||||||
|
|||||||
39
api_v2/views/healthy.py
Normal file
39
api_v2/views/healthy.py
Normal file
@@ -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"})
|
||||||
|
|
||||||
|
|
||||||
98
sse/views.py
98
sse/views.py
@@ -29,61 +29,63 @@ def create_sse_event(request):
|
|||||||
- 需要JWT认证且用户必须有关联的商户
|
- 需要JWT认证且用户必须有关联的商户
|
||||||
"""
|
"""
|
||||||
# 处理 OPTIONS 预检请求
|
# 处理 OPTIONS 预检请求
|
||||||
if request.method == 'OPTIONS':
|
# if request.method == 'OPTIONS':
|
||||||
response = HttpResponse()
|
# response = HttpResponse()
|
||||||
origin = request.META.get('HTTP_ORIGIN')
|
# origin = request.META.get('HTTP_ORIGIN')
|
||||||
if origin:
|
# if origin:
|
||||||
response['Access-Control-Allow-Origin'] = origin
|
# response['Access-Control-Allow-Origin'] = origin
|
||||||
response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
|
# response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
|
||||||
response['Access-Control-Allow-Headers'] = 'authorization, content-type, cache-control, accept'
|
# response['Access-Control-Allow-Headers'] = 'authorization, content-type, cache-control, accept'
|
||||||
response['Access-Control-Allow-Credentials'] = 'true'
|
# response['Access-Control-Allow-Credentials'] = 'true'
|
||||||
response['Access-Control-Max-Age'] = '86400' # 24小时
|
# response['Access-Control-Max-Age'] = '86400' # 24小时
|
||||||
return response
|
# return response
|
||||||
|
|
||||||
def event_stream():
|
# def event_stream():
|
||||||
# 获取当前商户ID
|
# # 获取当前商户ID
|
||||||
merchant_id = request.merchant_id
|
# merchant_id = request.merchant_id
|
||||||
|
|
||||||
# 创建一个同步队列用于接收消息
|
# # 创建一个同步队列用于接收消息
|
||||||
conn_queue = queue.Queue(maxsize=100)
|
# conn_queue = queue.Queue(maxsize=100)
|
||||||
services.push_connection(merchant_id, conn_queue)
|
# services.push_connection(merchant_id, conn_queue)
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
# 发送初始连接成功消息
|
# # 发送初始连接成功消息
|
||||||
yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established', 'merchant_id': merchant_id})}\n\n"
|
# yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established', 'merchant_id': merchant_id})}\n\n"
|
||||||
|
|
||||||
# 持续从队列中获取消息并发送给客户端
|
# # 持续从队列中获取消息并发送给客户端
|
||||||
while True:
|
# while True:
|
||||||
try:
|
# try:
|
||||||
# 使用同步方式等待新消息,带超时(30秒)以便发送心跳
|
# # 使用同步方式等待新消息,带超时(30秒)以便发送心跳
|
||||||
message = conn_queue.get(timeout=30.0)
|
# message = conn_queue.get(timeout=30.0)
|
||||||
yield f"data: {json.dumps(message)}\n\n"
|
# yield f"data: {json.dumps(message)}\n\n"
|
||||||
except queue.Empty:
|
# except queue.Empty:
|
||||||
# 30秒超时,发送心跳保持连接活跃
|
# # 30秒超时,发送心跳保持连接活跃
|
||||||
yield f": heartbeat\n\n"
|
# yield f": heartbeat\n\n"
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
print(f"Error in SSE stream: {e}")
|
# print(f"Error in SSE stream: {e}")
|
||||||
break
|
# break
|
||||||
finally:
|
# finally:
|
||||||
# 清理:从连接集合中移除此队列
|
# # 清理:从连接集合中移除此队列
|
||||||
services.remove_connection(merchant_id, conn_queue)
|
# services.remove_connection(merchant_id, conn_queue)
|
||||||
|
|
||||||
response = StreamingHttpResponse(
|
# response = StreamingHttpResponse(
|
||||||
event_stream(),
|
# event_stream(),
|
||||||
content_type='text/event-stream',
|
# content_type='text/event-stream',
|
||||||
)
|
# )
|
||||||
|
|
||||||
# SSE 必需的响应头
|
# # SSE 必需的响应头
|
||||||
response['Cache-Control'] = 'no-cache'
|
# response['Cache-Control'] = 'no-cache'
|
||||||
response['X-Accel-Buffering'] = 'no' # 禁用 nginx 缓冲
|
# 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'
|
|
||||||
|
|
||||||
|
# # 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
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user