1
0
forked from erp-dev/erp

added image_name to PlateOrder model

This commit is contained in:
2025-11-27 15:14:34 +08:00
parent a9c75a13fa
commit 0fa153849c
12 changed files with 108 additions and 432 deletions

View File

@@ -213,10 +213,12 @@
- **POST** `/api/v1/plate-orders/` - **POST** `/api/v1/plate-orders/`
- **描述**: 创建一个新的开版订单。 - **描述**: 创建一个新的开版订单。
- **请求体**: `PlateOrderCreateUpdateSerializer`。 - **请求体**: `PlateOrderCreateUpdateSerializer`。
- **字段补充**: `image_name`(可空字符串)用于记录上传的开版图片名称,即使未上传文件也可单独填写。
- **PUT/PATCH** `/api/v1/plate-orders/{id}/` - **PUT/PATCH** `/api/v1/plate-orders/{id}/`
- **描述**: 完全或部分更新一个开版订单。 - **描述**: 完全或部分更新一个开版订单。
- **请求体**: `PlateOrderCreateUpdateSerializer`。 - **请求体**: `PlateOrderCreateUpdateSerializer`。
- **字段补充**: 支持更新 `image_name`,便于修正或补录图片名称。
- **DELETE** `/api/v1/plate-orders/{id}/` - **DELETE** `/api/v1/plate-orders/{id}/`
- **描述**: **已禁用**。 - **描述**: **已禁用**。

View File

@@ -233,7 +233,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
fields = [ fields = [
'id', 'design_code', 'plate_type', 'plate_date', 'plate_method', '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', 'urgency_level', 'is_invalid',
'customer', 'customer_name', 'area', 'default_address', 'customer', 'customer_name', 'area', 'default_address',
'salesperson', 'salesperson_name', 'salesperson', 'salesperson_name',
@@ -297,7 +297,7 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
model = models.PlateOrder model = models.PlateOrder
fields = [ fields = [
'id', 'design_code', 'plate_type', 'plate_date', 'plate_method', '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', 'urgency_level', 'is_invalid',
'customer', 'customer_name', 'customer_phone', 'area', 'default_address', 'customer', 'customer_name', 'customer_phone', 'area', 'default_address',
'salesperson', 'salesperson_name', 'salesperson', 'salesperson_name',
@@ -352,7 +352,7 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
model = models.PlateOrder model = models.PlateOrder
fields = [ fields = [
"id", "design_code", "plate_type", "plate_date", "plate_method", "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", "urgency_level", "is_invalid",
"customer", "area", "default_address", "customer", "area", "default_address",
"salesperson", "merchandiser", "designer", "salesperson", "merchandiser", "designer",

View File

@@ -108,12 +108,14 @@ class PlateOrderAPITestCase(TestCase):
'urgency_level': '加急', 'urgency_level': '加急',
'salesperson': self.salesperson.id, 'salesperson': self.salesperson.id,
'merchandiser': self.merchandiser.id, 'merchandiser': self.merchandiser.id,
'image_name': 'sample.png',
} }
response = self.client.post('/api/v1/plate-orders/', data, format='json') response = self.client.post('/api/v1/plate-orders/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['design_code'], 'DESIGN001') self.assertEqual(response.data['design_code'], 'DESIGN001')
self.assertEqual(response.data['style_name'], '测试款式') 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()) self.assertTrue(printing_models.PlateOrder.objects.filter(design_code='DESIGN001').exists())
@@ -154,6 +156,7 @@ class PlateOrderAPITestCase(TestCase):
style_name='款式1', style_name='款式1',
fabric='棉布', fabric='棉布',
salesperson=self.salesperson, salesperson=self.salesperson,
image_name='first.png',
) )
printing_models.PlateOrder.objects.create( printing_models.PlateOrder.objects.create(
customer=self.customer, customer=self.customer,
@@ -162,6 +165,7 @@ class PlateOrderAPITestCase(TestCase):
style_name='款式2', style_name='款式2',
fabric='涤纶', fabric='涤纶',
merchandiser=self.merchandiser, merchandiser=self.merchandiser,
image_name='second.png',
) )
response = self.client.get('/api/v1/plate-orders/') response = self.client.get('/api/v1/plate-orders/')
@@ -169,8 +173,12 @@ class PlateOrderAPITestCase(TestCase):
# 根据实际返回的数据结构调整 # 根据实际返回的数据结构调整
if isinstance(response.data, dict): if isinstance(response.data, dict):
self.assertEqual(response.data['count'], 2) 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: else:
self.assertEqual(len(response.data), 2) self.assertEqual(len(response.data), 2)
self.assertIn('image_name', response.data[0])
def test_retrieve_plate_order(self): def test_retrieve_plate_order(self):
"""测试获取单个开版订单详情""" """测试获取单个开版订单详情"""
@@ -183,6 +191,7 @@ class PlateOrderAPITestCase(TestCase):
urgency_level='紧急', urgency_level='紧急',
salesperson=self.salesperson, salesperson=self.salesperson,
merchandiser=self.merchandiser, merchandiser=self.merchandiser,
image_name='detail.png',
) )
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/') 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.assertEqual(response.data['customer'], self.customer.id)
self.assertIn('customer_name', response.data) self.assertIn('customer_name', response.data)
self.assertIn('salesperson_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): def test_design_code_fallback_in_detail(self):
"""design_code 为空时返回主键ID""" """design_code 为空时返回主键ID"""

View File

@@ -52,7 +52,7 @@ class PlateOrderAdmin(admin.ModelAdmin):
}), }),
('开版信息', { ('开版信息', {
'fields': [ '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' 'is_mark_frame', 'sample_meter', 'required_sample_meters'
] ]
}), }),

View File

@@ -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='图片名称'),
),
]

View File

@@ -16,6 +16,7 @@ class PlateOrder(ModelBase):
plate_date = models.DateTimeField(null=True, blank=True, verbose_name='下版时间') plate_date = models.DateTimeField(null=True, blank=True, verbose_name='下版时间')
plate_method = models.CharField(max_length=50, blank=True, null=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='开版图') 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='打版注意事项') plate_notes = models.TextField(blank=True, null=True, verbose_name='打版注意事项')
reprint_reason = 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""" """保存时自动创建 BusinessObject"""
# 先保存以获取 ID如果是新建 # 先保存以获取 ID如果是新建
is_new = self.pk is None is_new = self.pk is None
update_fields = kwargs.get('update_fields')
super().save(*args, **kwargs) 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则自动创建 # 如果有 process 且没有 business_object则自动创建
if self.process and not self.business_object: if should_auto_create:
try: try:
from stateflow.models import Process from stateflow.models import Process
process_obj = Process.objects.get(id=self.process) process_obj = Process.objects.get(id=self.process)

View File

@@ -717,6 +717,10 @@ components:
format: binary format: binary
nullable: true nullable: true
description: 开版图片(仅 multipart/form-data 时可上传)。 description: 开版图片(仅 multipart/form-data 时可上传)。
image_name:
type: string
nullable: true
description: 开版图片的名称(可独立填写,便于标注原始文件名)。
plate_notes: plate_notes:
type: string type: string
description: 打版注意事项。 description: 打版注意事项。

View File

@@ -108,7 +108,7 @@ def validate_sse_request(request):
""" """
auth_result = authenticate_sse_request(request) auth_result = authenticate_sse_request(request)
if not auth_result: if not auth_result:
return False, HttpResponseForbidden("认证失败或用户无关联商户") return False, HttpResponseForbidden("Authentication failed or user has no associated merchant")
user, merchant_id = auth_result user, merchant_id = auth_result
request.user = user request.user = user

View File

@@ -82,6 +82,43 @@ def cleanup_all_connections():
logger.info("所有SSE连接已清理") 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): def push_sse_event_to_all(event_data: dict):
""" """
向所有连接的客户端广播一个 SSE 事件(同步队列版本) 向所有连接的客户端广播一个 SSE 事件(同步队列版本)

View File

@@ -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)

View File

@@ -72,7 +72,7 @@ class SSEAPITestCase(TestCase):
response = self.client.get('/sse/') response = self.client.get('/sse/')
self.assertEqual(response.status_code, 403) 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) self.assertEqual(len(services._connections), 0)
@@ -85,7 +85,7 @@ class SSEAPITestCase(TestCase):
) )
self.assertEqual(response.status_code, 403) 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) self.assertEqual(len(services._connections), 0)
@@ -98,7 +98,7 @@ class SSEAPITestCase(TestCase):
) )
self.assertEqual(response.status_code, 403) 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) self.assertEqual(len(services._connections), 0)
@@ -140,7 +140,7 @@ class SSEAPITestCase(TestCase):
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
data = response.json() data = response.json()
self.assertEqual(data['error'], '用户无关联商户') self.assertEqual(data['error'], 'User has no associated merchant')
def test_get_sse_status(self): def test_get_sse_status(self):
"""测试获取SSE状态""" """测试获取SSE状态"""
@@ -190,7 +190,7 @@ class SSEAPITestCase(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json() data = response.json()
self.assertEqual(data['status'], 'ok') 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['merchant_id'], self.merchant1.id)
self.assertEqual(data['clients'], 1) self.assertEqual(data['clients'], 1)
@@ -260,13 +260,18 @@ class SSEAPITestCase(TestCase):
"""测试OPTIONS预检请求""" """测试OPTIONS预检请求"""
response = self.client.options('/sse/') response = self.client.options('/sse/')
# OPTIONS请求应该成功返回CORS头 # OPTIONS请求应该成功
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 检查CORS头 # 带Origin头的OPTIONS请求应该返回CORS头
self.assertIn('Access-Control-Allow-Origin', response) response_with_origin = self.client.options(
self.assertIn('Access-Control-Allow-Methods', response) '/sse/',
self.assertIn('Access-Control-Allow-Headers', response) 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): class SSEServicesTestCase(TestCase):

View File

@@ -151,15 +151,16 @@ def shutdown_sse(request):
if not merchant_id: if not merchant_id:
return Response({'error': 'User has no associated merchant'}, status=status.HTTP_403_FORBIDDEN) 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) merchant_connections = services.get_merchant_connections(merchant_id)
client_count = len(merchant_connections)
# 实际关闭连接
services.shutdown_merchant_connections(merchant_id)
return Response({ return Response({
'status': 'ok', '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, 'merchant_id': merchant_id,
'clients': len(merchant_connections) 'clients': client_count
}) })