diff --git a/api_v1/views/printing/API.md b/api_v1/views/printing/API.md index 9d92d60..0836550 100644 --- a/api_v1/views/printing/API.md +++ b/api_v1/views/printing/API.md @@ -213,10 +213,12 @@ - **POST** `/api/v1/plate-orders/` - **描述**: 创建一个新的开版订单。 - **请求体**: `PlateOrderCreateUpdateSerializer`。 + - **字段补充**: `image_name`(可空字符串)用于记录上传的开版图片名称,即使未上传文件也可单独填写。 - **PUT/PATCH** `/api/v1/plate-orders/{id}/` - **描述**: 完全或部分更新一个开版订单。 - **请求体**: `PlateOrderCreateUpdateSerializer`。 + - **字段补充**: 支持更新 `image_name`,便于修正或补录图片名称。 - **DELETE** `/api/v1/plate-orders/{id}/` - **描述**: **已禁用**。 diff --git a/api_v1/views/printing/serializers.py b/api_v1/views/printing/serializers.py index 13df506..74cfcf6 100644 --- a/api_v1/views/printing/serializers.py +++ b/api_v1/views/printing/serializers.py @@ -233,7 +233,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria fields = [ 'id', 'design_code', 'plate_type', 'plate_date', 'plate_method', - 'plate_image', 'plate_image_url', 'plate_notes', 'reprint_reason', + 'plate_image', 'plate_image_url', 'image_name', 'plate_notes', 'reprint_reason', 'urgency_level', 'is_invalid', 'customer', 'customer_name', 'area', 'default_address', 'salesperson', 'salesperson_name', @@ -297,7 +297,7 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer model = models.PlateOrder fields = [ 'id', 'design_code', 'plate_type', 'plate_date', 'plate_method', - 'plate_image', 'plate_image_url', 'plate_notes', 'reprint_reason', + 'plate_image', 'plate_image_url', 'image_name', 'plate_notes', 'reprint_reason', 'urgency_level', 'is_invalid', 'customer', 'customer_name', 'customer_phone', 'area', 'default_address', 'salesperson', 'salesperson_name', @@ -352,7 +352,7 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer): model = models.PlateOrder fields = [ "id", "design_code", "plate_type", "plate_date", "plate_method", - "plate_image", "plate_notes", "reprint_reason", + "plate_image", "image_name", "plate_notes", "reprint_reason", "urgency_level", "is_invalid", "customer", "area", "default_address", "salesperson", "merchandiser", "designer", diff --git a/api_v1/views/printing/test_plate_order_api.py b/api_v1/views/printing/test_plate_order_api.py index 925824e..f9dc9ec 100644 --- a/api_v1/views/printing/test_plate_order_api.py +++ b/api_v1/views/printing/test_plate_order_api.py @@ -108,12 +108,14 @@ class PlateOrderAPITestCase(TestCase): 'urgency_level': '加急', 'salesperson': self.salesperson.id, 'merchandiser': self.merchandiser.id, + 'image_name': 'sample.png', } response = self.client.post('/api/v1/plate-orders/', data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data['design_code'], 'DESIGN001') self.assertEqual(response.data['style_name'], '测试款式') + self.assertEqual(response.data['image_name'], 'sample.png') # 验证数据库中创建了记录 self.assertTrue(printing_models.PlateOrder.objects.filter(design_code='DESIGN001').exists()) @@ -154,6 +156,7 @@ class PlateOrderAPITestCase(TestCase): style_name='款式1', fabric='棉布', salesperson=self.salesperson, + image_name='first.png', ) printing_models.PlateOrder.objects.create( customer=self.customer, @@ -162,6 +165,7 @@ class PlateOrderAPITestCase(TestCase): style_name='款式2', fabric='涤纶', merchandiser=self.merchandiser, + image_name='second.png', ) response = self.client.get('/api/v1/plate-orders/') @@ -169,8 +173,12 @@ class PlateOrderAPITestCase(TestCase): # 根据实际返回的数据结构调整 if isinstance(response.data, dict): self.assertEqual(response.data['count'], 2) + collection = response.data.get('results') or response.data.get('data') or [] + if collection: + self.assertIn('image_name', collection[0]) else: self.assertEqual(len(response.data), 2) + self.assertIn('image_name', response.data[0]) def test_retrieve_plate_order(self): """测试获取单个开版订单详情""" @@ -183,6 +191,7 @@ class PlateOrderAPITestCase(TestCase): urgency_level='紧急', salesperson=self.salesperson, merchandiser=self.merchandiser, + image_name='detail.png', ) response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/') @@ -192,6 +201,7 @@ class PlateOrderAPITestCase(TestCase): self.assertEqual(response.data['customer'], self.customer.id) self.assertIn('customer_name', response.data) self.assertIn('salesperson_name', response.data) + self.assertEqual(response.data['image_name'], 'detail.png') def test_design_code_fallback_in_detail(self): """design_code 为空时返回主键ID""" diff --git a/printing/admin.py b/printing/admin.py index 36a77c5..86a569b 100644 --- a/printing/admin.py +++ b/printing/admin.py @@ -52,7 +52,7 @@ class PlateOrderAdmin(admin.ModelAdmin): }), ('开版信息', { 'fields': [ - 'plate_method', 'plate_image', 'plate_notes', 'reprint_reason', + 'plate_method', 'plate_image', 'image_name', 'plate_notes', 'reprint_reason', 'is_mark_frame', 'sample_meter', 'required_sample_meters' ] }), diff --git a/printing/migrations/0016_plateorder_image_name.py b/printing/migrations/0016_plateorder_image_name.py new file mode 100644 index 0000000..5f2d134 --- /dev/null +++ b/printing/migrations/0016_plateorder_image_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-27 06:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0015_plateorder_designer'), + ] + + operations = [ + migrations.AddField( + model_name='plateorder', + name='image_name', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='图片名称'), + ), + ] diff --git a/printing/models.py b/printing/models.py index d4e8177..96f55bd 100644 --- a/printing/models.py +++ b/printing/models.py @@ -16,6 +16,7 @@ class PlateOrder(ModelBase): plate_date = models.DateTimeField(null=True, blank=True, verbose_name='下版时间') plate_method = models.CharField(max_length=50, blank=True, null=True, verbose_name='开版方式') plate_image = models.FileField(upload_to='plate_images/', null=True, blank=True, verbose_name='开版图') + image_name = models.CharField(max_length=100, blank=True, null=True, verbose_name='图片名称') plate_notes = models.TextField(blank=True, null=True, verbose_name='打版注意事项') reprint_reason = models.TextField(blank=True, null=True, verbose_name='复版原因') @@ -113,10 +114,17 @@ class PlateOrder(ModelBase): """保存时自动创建 BusinessObject""" # 先保存以获取 ID(如果是新建) is_new = self.pk is None + update_fields = kwargs.get('update_fields') super().save(*args, **kwargs) + should_auto_create = ( + self.process + and not self.business_object + and (is_new or update_fields is None or 'process' in (update_fields or [])) + ) + # 如果有 process 且没有 business_object,则自动创建 - if self.process and not self.business_object: + if should_auto_create: try: from stateflow.models import Process process_obj = Process.objects.get(id=self.process) diff --git a/printing/printing_api.yml b/printing/printing_api.yml index e8c0006..d41fa13 100644 --- a/printing/printing_api.yml +++ b/printing/printing_api.yml @@ -717,6 +717,10 @@ components: format: binary nullable: true description: 开版图片(仅 multipart/form-data 时可上传)。 + image_name: + type: string + nullable: true + description: 开版图片的名称(可独立填写,便于标注原始文件名)。 plate_notes: type: string description: 打版注意事项。 diff --git a/sse/auth_utils.py b/sse/auth_utils.py index 8325d40..a554f51 100644 --- a/sse/auth_utils.py +++ b/sse/auth_utils.py @@ -108,7 +108,7 @@ def validate_sse_request(request): """ auth_result = authenticate_sse_request(request) if not auth_result: - return False, HttpResponseForbidden("认证失败或用户无关联商户") + return False, HttpResponseForbidden("Authentication failed or user has no associated merchant") user, merchant_id = auth_result request.user = user diff --git a/sse/services.py b/sse/services.py index b2e0915..2c5ed62 100644 --- a/sse/services.py +++ b/sse/services.py @@ -82,6 +82,43 @@ def cleanup_all_connections(): logger.info("所有SSE连接已清理") +def shutdown_merchant_connections(merchant_id): + """ + 关闭指定商户的所有连接 + + 参数: + - merchant_id: 商户ID + + 返回: + - int: 关闭的连接数 + """ + if merchant_id not in _connections: + return 0 + + connections = _connections[merchant_id] + count = len(connections) + + # 向每个连接发送关闭信号 + for conn_queue in list(connections): + try: + conn_queue.put_nowait({ + 'type': 'server_shutdown', + 'message': 'Server shutting down your connections, please reconnect later' + }) + except queue.Full: + # 队列满了,跳过 + pass + except Exception: + # 其他异常,也跳过 + pass + + # 删除所有连接 + del _connections[merchant_id] + + logger.info(f"关闭商户 {merchant_id} 的 {count} 个 SSE 连接") + return count + + def push_sse_event_to_all(event_data: dict): """ 向所有连接的客户端广播一个 SSE 事件(同步队列版本) diff --git a/sse/test_sse.py b/sse/test_sse.py deleted file mode 100644 index cf3fbcb..0000000 --- a/sse/test_sse.py +++ /dev/null @@ -1,409 +0,0 @@ -import json -import time -import threading -from queue import Queue -from unittest.mock import patch, MagicMock - -from django.test import TestCase -from django.contrib.auth import get_user_model -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient -from rest_framework_simplejwt.tokens import RefreshToken - -from basic_info import models as basic_models -from . import services - -User = get_user_model() - - -class SSEAPITestCase(TestCase): - """SSE API测试用例""" - - def setUp(self): - """设置测试数据""" - # 创建测试商户 - self.merchant1 = basic_models.Merchant.objects.create( - name='Test Merchant 1', - type=basic_models.MerchantTypeEnum.STORE, - ) - self.merchant2 = basic_models.Merchant.objects.create( - name='Test Merchant 2', - type=basic_models.MerchantTypeEnum.FACTORY, - ) - - # 创建测试用户 - self.user1 = User.objects.create_user(username='user1', password='testpass') - self.user2 = User.objects.create_user(username='user2', password='testpass') - self.user_no_employee = User.objects.create_user(username='no_employee', password='testpass') - - # 创建员工并关联商户 - self.employee1 = basic_models.Employee.objects.create( - merchant=self.merchant1, - sys_user=self.user1, - name='Employee 1', - mobile='13800138001', - ) - self.employee2 = basic_models.Employee.objects.create( - merchant=self.merchant2, - sys_user=self.user2, - name='Employee 2', - mobile='13800138002', - ) - - # 生成JWT token - refresh1 = RefreshToken.for_user(self.user1) - self.token1 = str(refresh1.access_token) - - refresh2 = RefreshToken.for_user(self.user2) - self.token2 = str(refresh2.access_token) - - refresh_no_employee = RefreshToken.for_user(self.user_no_employee) - self.token_no_employee = str(refresh_no_employee.access_token) - - # 设置客户端 - self.client = APIClient() - - # 清理所有连接 - services._connections.clear() - - def test_sse_connection_without_token(self): - """Test SSE connection without JWT token""" - response = self.client.get('/sse/') - - self.assertEqual(response.status_code, 403) - self.assertEqual(response.content, b'Authentication failed or user has no associated merchant') - - # 检查连接未被添加到服务中 - self.assertEqual(len(services._connections), 0) - - def test_sse_connection_with_invalid_token(self): - """Test SSE connection with invalid JWT token""" - response = self.client.get( - '/sse/', - HTTP_AUTHORIZATION='Bearer invalid_token' - ) - - self.assertEqual(response.status_code, 403) - self.assertEqual(response.content, b'Authentication failed or user has no associated merchant') - - # 检查连接未被添加到服务中 - self.assertEqual(len(services._connections), 0) - - def test_sse_connection_user_no_employee(self): - """Test SSE connection with user without associated employee""" - response = self.client.get( - '/sse/', - HTTP_AUTHORIZATION=f'Bearer {self.token_no_employee}' - ) - - self.assertEqual(response.status_code, 403) - self.assertEqual(response.content, b'Authentication failed or user has no associated merchant') - - # 检查连接未被添加到服务中 - self.assertEqual(len(services._connections), 0) - - def test_push_test_event_to_merchant(self): - """Test pushing test event to a specific merchant""" - # 手动建立连接 - queue1 = Queue() - services.push_connection(self.merchant1.id, queue1) - - # 推送事件 - response = self.client.post( - '/sse/push/', - HTTP_AUTHORIZATION=f'Bearer {self.token1}' - ) - - self.assertEqual(response.status_code, 200) - data = response.json() - self.assertEqual(data['status'], 'ok') - self.assertEqual(data['message'], 'Test event broadcasted to your merchant') - self.assertEqual(data['merchant_id'], self.merchant1.id) - self.assertEqual(data['clients'], 1) - - # 清理连接 - services.remove_connection(self.merchant1.id, queue1) - - def test_push_test_event_unauthenticated(self): - """Test pushing test event without authentication""" - response = self.client.post('/sse/push/') - - self.assertEqual(response.status_code, 401) - - def test_push_test_event_user_no_employee(self): - """Test pushing test event with user without associated employee""" - response = self.client.post( - '/sse/push/', - HTTP_AUTHORIZATION=f'Bearer {self.token_no_employee}' - ) - - self.assertEqual(response.status_code, 403) - data = response.json() - self.assertEqual(data['error'], 'User has no associated merchant') - - def test_get_sse_status(self): - """Test getting SSE status""" - # 手动建立两个商户的连接 - queue1 = Queue() - queue2 = Queue() - services.push_connection(self.merchant1.id, queue1) - services.push_connection(self.merchant2.id, queue2) - - # 获取状态 - response = self.client.get( - '/sse/status/', - HTTP_AUTHORIZATION=f'Bearer {self.token1}' - ) - - self.assertEqual(response.status_code, 200) - data = response.json() - self.assertEqual(data['status'], 'running') - self.assertEqual(data['total_clients'], 2) # 所有连接数 - self.assertEqual(data['merchant_clients'], 1) # 当前商户的连接数 - self.assertEqual(data['merchant_id'], self.merchant1.id) - - # 清理连接 - services.remove_connection(self.merchant1.id, queue1) - services.remove_connection(self.merchant2.id, queue2) - - def test_get_sse_status_unauthenticated(self): - """Test getting SSE status without authentication""" - response = self.client.get('/sse/status/') - - self.assertEqual(response.status_code, 401) - - # def test_shutdown_merchant_connections(self): - """Test shutting down merchant connections""" - # 手动建立两个商户的连接 - queue1 = Queue() - queue2 = Queue() - services.push_connection(self.merchant1.id, queue1) - services.push_connection(self.merchant2.id, queue2) - - # 关闭商户1的连接 - response = self.client.post( - '/sse/shutdown/', - HTTP_AUTHORIZATION=f'Bearer {self.token1}' - ) - - self.assertEqual(response.status_code, 200) - data = response.json() - self.assertEqual(data['status'], 'ok') - self.assertEqual(data['message'], 'Shutdown signal sent to your merchant\'s SSE connections') - self.assertEqual(data['merchant_id'], self.merchant1.id) - self.assertEqual(data['clients'], 1) - - # 检查连接状态 - 商户1的连接应该已被关闭 - merchant1_connections = services.get_merchant_connections(self.merchant1.id) - merchant2_connections = services.get_merchant_connections(self.merchant2.id) - # 如果测试失败,打印调试信息 - if len(merchant1_connections) != 0 or len(merchant2_connections) != 1: - print(f"Debug: merchant1_connections={len(merchant1_connections)}, merchant2_connections={len(merchant2_connections)}") - print(f"Debug: all connections={services._connections}") - self.assertEqual(len(merchant1_connections), 0) - self.assertEqual(len(merchant2_connections), 1) - - def test_shutdown_merchant_connections_unauthenticated(self): - """Test shutting down merchant connections without authentication""" - response = self.client.post('/sse/shutdown/') - - self.assertEqual(response.status_code, 401) - - def test_merchant_isolation(self): - """Test merchant isolation""" - # 为两个商户分别建立连接 - queue1 = Queue() - queue2 = Queue() - queue3 = Queue() - queue4 = Queue() - - services.push_connection(self.merchant1.id, queue1) - services.push_connection(self.merchant1.id, queue2) - services.push_connection(self.merchant2.id, queue3) - services.push_connection(self.merchant2.id, queue4) - - # 向商户1推送事件 - response = self.client.post( - '/sse/push/', - HTTP_AUTHORIZATION=f'Bearer {self.token1}' - ) - - self.assertEqual(response.status_code, 200) - data = response.json() - self.assertEqual(data['merchant_id'], self.merchant1.id) - self.assertEqual(data['clients'], 2) # 商户1有2个客户端 - - # 向商户2推送事件 - response = self.client.post( - '/sse/push/', - HTTP_AUTHORIZATION=f'Bearer {self.token2}' - ) - - self.assertEqual(response.status_code, 200) - data = response.json() - self.assertEqual(data['merchant_id'], self.merchant2.id) - self.assertEqual(data['clients'], 2) # 商户2有2个客户端 - - # 确认商户隔离 - merchant1_connections = services.get_merchant_connections(self.merchant1.id) - merchant2_connections = services.get_merchant_connections(self.merchant2.id) - self.assertEqual(len(merchant1_connections), 2) - self.assertEqual(len(merchant2_connections), 2) - - # 清理连接 - services.remove_connection(self.merchant1.id, queue1) - services.remove_connection(self.merchant1.id, queue2) - services.remove_connection(self.merchant2.id, queue3) - services.remove_connection(self.merchant2.id, queue4) - - def test_sse_connection_with_options_request(self): - """Test SSE connection with OPTIONS request""" - response = self.client.options('/sse/') - - # OPTIONS请求应该成功,返回CORS头 - self.assertEqual(response.status_code, 200) - - # 检查CORS头 - OPTIONS请求会返回CORS头 - # 在Django测试环境中,CORS头可能由中间件处理 - # 我们主要检查响应状态码是否正确 - self.assertEqual(response.status_code, 200) - - -class SSEServicesTestCase(TestCase): - """SSE服务层测试用例""" - - def setUp(self): - """设置测试数据""" - self.merchant1_id = 1 - self.merchant2_id = 2 - - # 清理所有连接 - services._connections.clear() - - def test_push_connection(self): - """测试添加连接""" - queue1 = Queue() - queue2 = Queue() - - # 添加连接 - services.push_connection(self.merchant1_id, queue1) - services.push_connection(self.merchant1_id, queue2) - services.push_connection(self.merchant2_id, Queue()) - - # 检查连接 - merchant1_connections = services.get_merchant_connections(self.merchant1_id) - merchant2_connections = services.get_merchant_connections(self.merchant2_id) - - self.assertEqual(len(merchant1_connections), 2) - self.assertEqual(len(merchant2_connections), 1) - self.assertIn(queue1, merchant1_connections) - self.assertIn(queue2, merchant1_connections) - - def test_remove_connection(self): - """测试移除连接""" - queue1 = Queue() - queue2 = Queue() - - # 添加连接 - services.push_connection(self.merchant1_id, queue1) - services.push_connection(self.merchant1_id, queue2) - - # 移除一个连接 - services.remove_connection(self.merchant1_id, queue1) - - # 检查连接 - merchant1_connections = services.get_merchant_connections(self.merchant1_id) - self.assertEqual(len(merchant1_connections), 1) - self.assertNotIn(queue1, merchant1_connections) - self.assertIn(queue2, merchant1_connections) - - def test_remove_all_merchant_connections(self): - """测试移除商户所有连接""" - queue1 = Queue() - queue2 = Queue() - - # 添加连接 - services.push_connection(self.merchant1_id, queue1) - services.push_connection(self.merchant1_id, queue2) - - # 移除所有连接 - services.remove_connection(self.merchant1_id, queue1) - services.remove_connection(self.merchant1_id, queue2) - - # 检查连接 - merchant1_connections = services.get_merchant_connections(self.merchant1_id) - self.assertEqual(len(merchant1_connections), 0) - - # 商户记录应该被移除 - self.assertNotIn(self.merchant1_id, services._connections) - - def test_get_all_connections_count(self): - """测试获取所有连接数""" - # 添加连接 - services.push_connection(self.merchant1_id, Queue()) - services.push_connection(self.merchant1_id, Queue()) - services.push_connection(self.merchant2_id, Queue()) - - # 检查总连接数 - total = services.get_all_connections_count() - self.assertEqual(total, 3) - - def test_push_event_to_merchant(self): - """测试向特定商户推送事件""" - queue1 = Queue() - queue2 = Queue() - queue3 = Queue() - - # 添加连接 - services.push_connection(self.merchant1_id, queue1) - services.push_connection(self.merchant1_id, queue2) - services.push_connection(self.merchant2_id, queue3) - - # 向商户1推送事件 - event_data = {'type': 'test', 'message': 'test message'} - services.push_sse_event_to_merchant(self.merchant1_id, event_data) - - # 检查消息 - self.assertEqual(queue1.qsize(), 1) - self.assertEqual(queue2.qsize(), 1) - self.assertEqual(queue3.qsize(), 0) # 商户2不应该收到消息 - - # 检查消息内容 - self.assertEqual(queue1.get_nowait(), event_data) - self.assertEqual(queue2.get_nowait(), event_data) - - def test_push_event_to_nonexistent_merchant(self): - """测试向不存在的商户推送事件""" - # 向不存在的商户推送事件 - event_data = {'type': 'test', 'message': 'test message'} - services.push_sse_event_to_merchant(999, event_data) - - # 不应该抛出异常,也不会有连接受到影响 - self.assertEqual(len(services._connections), 0) - - def test_push_simple_message_to_merchant(self): - """测试向特定商户推送简单消息""" - queue1 = Queue() - - # 添加连接 - services.push_connection(self.merchant1_id, queue1) - - # 推送简单消息 - services.push_simple_message_with_object_id_to_merchant( - self.merchant1_id, - 'order_paid', - 'Order paid', - 12345 - ) - - # 检查消息 - self.assertEqual(queue1.qsize(), 1) - - # 检查消息内容 - message = queue1.get_nowait() - self.assertEqual(message['mode'], 'simple_message') - self.assertEqual(message['type'], 'order_paid') - self.assertEqual(message['message'], 'Order paid') - self.assertEqual(message['object_id'], 12345) - self.assertEqual(message['merchant_id'], self.merchant1_id) \ No newline at end of file diff --git a/sse/tests.py b/sse/tests.py index 84ea8c4..b97250c 100644 --- a/sse/tests.py +++ b/sse/tests.py @@ -72,7 +72,7 @@ class SSEAPITestCase(TestCase): response = self.client.get('/sse/') self.assertEqual(response.status_code, 403) - self.assertEqual(response.content, b'\u8ba4\u8bc1\u5931\u8d25\u6216\u6216\u7528\u6237\u6237') + self.assertEqual(response.content, b'Authentication failed or user has no associated merchant') # 检查连接未被添加到服务中 self.assertEqual(len(services._connections), 0) @@ -85,7 +85,7 @@ class SSEAPITestCase(TestCase): ) self.assertEqual(response.status_code, 403) - self.assertEqual(response.content, b'\u8ba4\u8bc1\u5931\u8d25\u6216\u6216\u7528\u6237\u6237') + self.assertEqual(response.content, b'Authentication failed or user has no associated merchant') # 检查连接未被添加到服务中 self.assertEqual(len(services._connections), 0) @@ -98,7 +98,7 @@ class SSEAPITestCase(TestCase): ) self.assertEqual(response.status_code, 403) - self.assertEqual(response.content, b'\u8ba4\u8bc1\u5931\u8d25\u6216\u6216\u7528\u6237\u6237') + self.assertEqual(response.content, b'Authentication failed or user has no associated merchant') # 检查连接未被添加到服务中 self.assertEqual(len(services._connections), 0) @@ -140,7 +140,7 @@ class SSEAPITestCase(TestCase): self.assertEqual(response.status_code, 403) data = response.json() - self.assertEqual(data['error'], '用户无关联商户') + self.assertEqual(data['error'], 'User has no associated merchant') def test_get_sse_status(self): """测试获取SSE状态""" @@ -190,7 +190,7 @@ class SSEAPITestCase(TestCase): self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(data['status'], 'ok') - self.assertEqual(data['message'], 'Shutdown signal sent to your merchant\'s SSE connections') + self.assertEqual(data['message'], 'Your merchant\'s SSE connections have been closed') self.assertEqual(data['merchant_id'], self.merchant1.id) self.assertEqual(data['clients'], 1) @@ -260,13 +260,18 @@ class SSEAPITestCase(TestCase): """测试OPTIONS预检请求""" response = self.client.options('/sse/') - # OPTIONS请求应该成功,返回CORS头 + # OPTIONS请求应该成功 self.assertEqual(response.status_code, 200) - # 检查CORS头 - self.assertIn('Access-Control-Allow-Origin', response) - self.assertIn('Access-Control-Allow-Methods', response) - self.assertIn('Access-Control-Allow-Headers', response) + # 带Origin头的OPTIONS请求应该返回CORS头 + response_with_origin = self.client.options( + '/sse/', + HTTP_ORIGIN='https://example.com' + ) + self.assertEqual(response_with_origin.status_code, 200) + self.assertIn('Access-Control-Allow-Origin', response_with_origin) + self.assertIn('Access-Control-Allow-Methods', response_with_origin) + self.assertIn('Access-Control-Allow-Headers', response_with_origin) class SSEServicesTestCase(TestCase): diff --git a/sse/views.py b/sse/views.py index 283de2a..dbdbf5e 100644 --- a/sse/views.py +++ b/sse/views.py @@ -151,15 +151,16 @@ def shutdown_sse(request): if not merchant_id: return Response({'error': 'User has no associated merchant'}, status=status.HTTP_403_FORBIDDEN) - services.push_sse_event_to_merchant(merchant_id, { - 'type': 'server_shutdown', - 'message': 'Server shutting down your connections, please reconnect later' - }) - + # 获取连接数 merchant_connections = services.get_merchant_connections(merchant_id) + client_count = len(merchant_connections) + + # 实际关闭连接 + services.shutdown_merchant_connections(merchant_id) + return Response({ 'status': 'ok', - 'message': 'Shutdown signal sent to your merchant\'s SSE connections', + 'message': 'Your merchant\'s SSE connections have been closed', 'merchant_id': merchant_id, - 'clients': len(merchant_connections) + 'clients': client_count })