From a633a617d56c7f200935d7249aeda1f4dbfeb6e3 Mon Sep 17 00:00:00 2001 From: colaftc Date: Sat, 31 Jan 2026 23:58:40 +0800 Subject: [PATCH] feat: added custom signal for pre sales order created --- business/apps.py | 19 +++++++ business/handlers.py | 34 ++++++++++++ business/pre_order_services.py | 22 ++++++++ business/signals.py | 21 +++++++ business/tests/test_pre_sales_order.py | 77 ++++++++++++++++++++++++++ 5 files changed, 173 insertions(+) create mode 100644 business/handlers.py create mode 100644 business/signals.py diff --git a/business/apps.py b/business/apps.py index 8e6fb15..e9ed353 100644 --- a/business/apps.py +++ b/business/apps.py @@ -1,7 +1,26 @@ +import logging from django.apps import AppConfig +logger = logging.getLogger(__name__) + class BusinessConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'business' verbose_name = '业务模块' + + def ready(self): + from .models import PreSalesOrder + from . import handlers + from .signals import pre_sales_order_created + + pre_sales_order_created.connect( + handlers.on_pre_sales_order_created, + sender=PreSalesOrder, + dispatch_uid='business.on_pre_sales_order_created', + ) + + logger.info( + f'[business.apps] 已注册 pre_sales_order_created 信号处理器, ' + f'sender={PreSalesOrder}, handler={handlers.on_pre_sales_order_created}' + ) diff --git a/business/handlers.py b/business/handlers.py new file mode 100644 index 0000000..02e5db8 --- /dev/null +++ b/business/handlers.py @@ -0,0 +1,34 @@ +import logging + +logger = logging.getLogger(__name__) + + +def on_pre_sales_order_created(sender, **kwargs): + """Handle PreSalesOrder created domain event. + + For now: log only (no side effects). + """ + + order = kwargs.get('instance') + created_by = kwargs.get('created_by') + operator = kwargs.get('operator') + items_count = kwargs.get('items_count') + + if order is None: + logger.warning('[business.handlers] pre_sales_order_created 缺少 instance,已跳过') + return + + created_by_label = getattr(created_by, 'username', None) if created_by else None + operator_label = getattr(operator, 'name', None) if operator else None + + logger.info( + '[business.handlers] pre_sales_order_created: id=%s human_id=%s merchant_id=%s customer_id=%s ' + 'created_by=%s operator=%s items_count=%s', + getattr(order, 'id', None), + getattr(order, 'human_id', None), + getattr(order, 'merchant_id', None), + getattr(order, 'customer_id', None), + created_by_label or '-', + operator_label or '-', + items_count if items_count is not None else '-', + ) diff --git a/business/pre_order_services.py b/business/pre_order_services.py index 35c437f..64eccb5 100644 --- a/business/pre_order_services.py +++ b/business/pre_order_services.py @@ -211,6 +211,28 @@ def create_pre_sales_order( ] ) + # domain event: pre sales order created + # must be fired only after commit to avoid triggering on rollbacks + def _send_created_signal(): + try: + from .signals import pre_sales_order_created + + pre_sales_order_created.send( + sender=models.PreSalesOrder, + instance=pre_sales_order, + created_by=created_by, + operator=operator, + items_count=len(normalized_items), + ) + except Exception: + import logging + + logging.getLogger(__name__).exception( + '[business.pre_order_services] 触发 pre_sales_order_created signal 失败(已忽略)' + ) + + transaction.on_commit(_send_created_signal) + pre_sales_order.refresh_from_db() return pre_sales_order diff --git a/business/signals.py b/business/signals.py new file mode 100644 index 0000000..ed134a9 --- /dev/null +++ b/business/signals.py @@ -0,0 +1,21 @@ +"""business domain signals. + +We use domain-level signals (instead of model post_save) to: +- keep semantics clean (only fire on business "create" flow) +- allow passing explicit actor (created_by / operator) +- keep integrations in handlers + +NOTE: +Signals should be triggered via transaction.on_commit to avoid firing when a +transaction rolls back. +""" + +from django.dispatch import Signal + +# Fired when a PreSalesOrder (and its items) is created via service layer. +# Payload: +# - instance: PreSalesOrder +# - created_by: Django User (may be None) +# - operator: Employee (may be None) +# - items_count: int +pre_sales_order_created = Signal() diff --git a/business/tests/test_pre_sales_order.py b/business/tests/test_pre_sales_order.py index 95b2a75..c6fabb6 100644 --- a/business/tests/test_pre_sales_order.py +++ b/business/tests/test_pre_sales_order.py @@ -1,5 +1,7 @@ from django.test import TestCase from django.contrib.auth import get_user_model +from django.test import TransactionTestCase +from django.db import transaction from basic_info import models as basic_models from business import models as business_models @@ -120,3 +122,78 @@ class PreSalesOrderServiceTestCase(TestCase): pre_order_services.delete_pre_sales_order(pre_sales_order=pre_sales_order) self.assertFalse(business_models.PreSalesOrder.objects.filter(id=order_id).exists()) + + +class PreSalesOrderSignalTestCase(TransactionTestCase): + def setUp(self): + ( + self.merchant, + self.customer, + self.warehouse_strict, + self.warehouse_relaxed, + self.warehouse_strict_out, + self.product, + self.operator, + ) = create_sales_fixtures() + + self.user = get_user_model().objects.create_user(username='signal-user', password='pass') + self.operator.sys_user = self.user + self.operator.save(update_fields=['sys_user']) + + def test_pre_sales_order_created_signal_fires_on_commit(self): + from business.signals import pre_sales_order_created + + received = [] + uid = 'tests.pre_sales_order_created.on_commit' + + def _receiver(sender, **kwargs): + received.append(kwargs) + + pre_sales_order_created.connect(_receiver, dispatch_uid=uid, weak=False) + try: + pre_order_services.create_pre_sales_order( + merchant=self.merchant, + customer_id=self.customer.id, + warehouse_id=self.warehouse_relaxed.id, + operator=self.operator, + created_by=self.user, + items=[{'product_id': self.product.id, 'quantity': '1', 'unit': '米'}], + ) + + self.assertEqual(len(received), 1) + payload = received[0] + self.assertIsNotNone(payload.get('instance')) + self.assertEqual(payload.get('created_by').id, self.user.id) + self.assertEqual(payload.get('operator').id, self.operator.id) + self.assertEqual(payload.get('items_count'), 1) + finally: + pre_sales_order_created.disconnect(dispatch_uid=uid) + + def test_pre_sales_order_created_signal_not_fired_on_rollback(self): + from business.signals import pre_sales_order_created + + received = [] + uid = 'tests.pre_sales_order_created.on_rollback' + + def _receiver(sender, **kwargs): + received.append(kwargs) + + pre_sales_order_created.connect(_receiver, dispatch_uid=uid, weak=False) + try: + try: + with transaction.atomic(): + pre_order_services.create_pre_sales_order( + merchant=self.merchant, + customer_id=self.customer.id, + warehouse_id=self.warehouse_relaxed.id, + operator=self.operator, + created_by=self.user, + items=[{'product_id': self.product.id, 'quantity': '1', 'unit': '米'}], + ) + raise RuntimeError('force rollback') + except RuntimeError: + pass + + self.assertEqual(received, []) + finally: + pre_sales_order_created.disconnect(dispatch_uid=uid)