From dffb4a3e03bf95e0c0c99268b9f6e65ca2951478 Mon Sep 17 00:00:00 2001 From: colaftc Date: Tue, 27 Jan 2026 16:40:09 +0800 Subject: [PATCH] feat: notification sending for printing order created --- api_v1/views/printing/services.py | 16 ++++++ docs/2026-01-27_summary.md | 25 +++++++++ env.example | 3 ++ flower/settings.py | 18 +++++++ printing/apps.py | 14 ++++- printing/handlers.py | 85 +++++++++++++++++++++++++++++++ printing/services.py | 36 +++++++++++++ printing/signals.py | 17 +++++++ 8 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 docs/2026-01-27_summary.md create mode 100644 printing/signals.py diff --git a/api_v1/views/printing/services.py b/api_v1/views/printing/services.py index b0ee0b3..161c093 100644 --- a/api_v1/views/printing/services.py +++ b/api_v1/views/printing/services.py @@ -51,6 +51,22 @@ class PrintingOrderService: data['merchant'] = user.employee.merchant order = printing_models.PrintingOrder.objects.create(**data) + + # domain event: printing order created + # handler will ensure transaction.on_commit for external side effects (e.g. WeCom notify) + try: + from printing.signals import printing_order_created + printing_order_created.send( + sender=printing_models.PrintingOrder, + instance=order, + created_by=user, + ) + except Exception: + # signal should never break core create flow + import logging + logging.getLogger(__name__).exception( + "[api_v1.views.printing.services] 触发 printing_order_created signal 失败(已忽略)" + ) return order @staticmethod diff --git a/docs/2026-01-27_summary.md b/docs/2026-01-27_summary.md new file mode 100644 index 0000000..c9c23e0 --- /dev/null +++ b/docs/2026-01-27_summary.md @@ -0,0 +1,25 @@ +### 2026-01-27 工作记录 + +#### 1) PrintingOrder 创建通知:自定义 signal + handler(企业微信 markdown) + +目标: +- 当 `PrintingOrder` 被创建时,像“状态推进 advance”一样通过 **signal + handler** 发送企业微信通知 +- 避免使用 `post_save` 造成的语义污染/误触发 +- 外部副作用(企业微信请求)必须在事务提交后执行(`transaction.on_commit`) + +实现: +- 新增领域 signal:`printing/signals.py::printing_order_created` +- 绑定与处理: + - `printing/apps.py` 中 connect `printing_order_created` → `printing/handlers.py::on_printing_order_created` + - handler 内: + - 默认测试环境关闭(避免单测出网/刷屏) + - `transaction.on_commit` 后发送企业微信 `markdown` +- 触发点: + - `api_v1/views/printing/services.py::PrintingOrderService.create_printing_order` 在创建成功后触发 `printing_order_created` + +配置: +- `flower/settings.py` + - `PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED`(支持 `.env` 覆盖,默认非测试环境开启) + - `PRINTING_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE`(中文字段) +- `env.example` 已补充:`PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED=True` + diff --git a/env.example b/env.example index 1eb0501..f607ce2 100644 --- a/env.example +++ b/env.example @@ -75,6 +75,9 @@ WECOM_WEBHOOK_KEY= # PrintingJob 状态推进通知的“跟进地址”模板;为空则消息里省略“跟进地址”字段 PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE=https://app.yuwen.cloud/workstation/production/batch-advance?orderId={order_id} +# PrintingOrder 创建通知(企业微信):测试环境默认关闭;生产/开发默认开启 +PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED=True + ############################ # Printing 自动创建销售品(可选) ############################ diff --git a/flower/settings.py b/flower/settings.py index 290265a..623f34c 100644 --- a/flower/settings.py +++ b/flower/settings.py @@ -290,6 +290,24 @@ PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE = ( "{process_params_markdown}\n" ) +# PrintingOrder:创建时的企业微信通知(markdown) +# - 默认:测试环境关闭(避免单测出网/刷屏),非测试环境开启 +PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED = env.bool( + "PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED", + default=(not TESTING), +) +PRINTING_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE = ( + "### 印染订单创建\n" + "\n" + "- **印染订单ID**:`{printing_order_id}`\n" + "- **订单编号**:`{printing_order_human_id}`\n" + "- **创建时间**:`{created_at}`\n" + "- **发送者**:{sender}\n" + "- **客户**:{customer_name}\n" + "- **面料**:{fabric}\n" + "- **出货日期**:`{outgoing_date}`\n" +) + # Logging configuration # 目标: diff --git a/printing/apps.py b/printing/apps.py index 6927355..1a5ef6f 100644 --- a/printing/apps.py +++ b/printing/apps.py @@ -12,8 +12,9 @@ class PrintingConfig(AppConfig): def ready(self): """注册信号处理函数""" from stateflow.signals import process_completed, state_advanced - from .models import PrintingJob + from .models import PrintingJob, PrintingOrder from . import handlers + from .signals import printing_order_created # 监听 PrintingJob 流程完成信号 process_completed.connect( @@ -26,6 +27,13 @@ class PrintingConfig(AppConfig): handlers.on_printing_job_state_advanced, sender=PrintingJob ) + + # 监听 PrintingOrder 创建事件(领域 signal) + printing_order_created.connect( + handlers.on_printing_order_created, + sender=PrintingOrder, + dispatch_uid="printing.on_printing_order_created", + ) logger.info( f'[printing.apps] 已注册 process_completed 信号处理器, ' @@ -34,4 +42,8 @@ class PrintingConfig(AppConfig): logger.info( f'[printing.apps] 已注册 state_advanced 信号处理器, ' f'sender={PrintingJob}, handler={handlers.on_printing_job_state_advanced}' + ) + logger.info( + f'[printing.apps] 已注册 printing_order_created 信号处理器, ' + f'sender={PrintingOrder}, handler={handlers.on_printing_order_created}' ) \ No newline at end of file diff --git a/printing/handlers.py b/printing/handlers.py index 8f102bc..cc5e93b 100644 --- a/printing/handlers.py +++ b/printing/handlers.py @@ -242,3 +242,88 @@ def on_printing_job_state_advanced(sender, **kwargs): # 在事务提交后再发送,避免事务回滚但通知已发出 transaction.on_commit(_send_wecom) + + +def on_printing_order_created(sender, **kwargs): + """ + PrintingOrder 创建时的处理(领域 signal:printing.signals.printing_order_created)。 + + 需求: + - 在事务提交后发送企业微信机器人通知(markdown) + - 消息字段(中文): + - 印染订单ID / human_id + - 创建时间 + - 发送者(优先 created_by.employee.name / created_by.username) + - 客户、面料、出货日期(可选字段,缺省展示为 '-') + """ + # 测试环境默认关闭,避免单测出网/刷屏 + if not getattr(settings, "PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED", True): + return + if getattr(settings, "TESTING", False): + return + + order = kwargs.get("instance") + created_by = kwargs.get("created_by") + if order is None: + logger.warning("[printing.handlers] printing_order_created 信号缺少 instance,跳过通知") + return + + try: + from printing.services import render_printing_order_created_markdown + + created_at_dt = getattr(order, "created_at", None) + created_at = ( + timezone.localtime(created_at_dt).strftime("%Y-%m-%d %H:%M:%S") + if created_at_dt is not None + else timezone.localtime(timezone.now()).strftime("%Y-%m-%d %H:%M:%S") + ) + + emp = getattr(created_by, "employee", None) if created_by else None + sender_label = getattr(emp, "name", None) if emp is not None else None + sender_label = sender_label or getattr(created_by, "username", None) if created_by else None + sender_label = sender_label or "系统自动发送" + + customer_name = None + try: + customer = getattr(order, "customer", None) + customer_name = getattr(customer, "name", None) if customer else None + except Exception: + customer_name = None + + outgoing_dt = getattr(order, "outgoing_date", None) + outgoing_date = ( + timezone.localtime(outgoing_dt).strftime("%Y-%m-%d %H:%M:%S") + if outgoing_dt is not None + else "-" + ) + + message = render_printing_order_created_markdown( + printing_order_id=str(getattr(order, "id", "-") or "-"), + printing_order_human_id=str(getattr(order, "human_id", "-") or "-"), + created_at=str(created_at), + sender_label=str(sender_label), + customer_name=str(customer_name or "-"), + fabric=str(getattr(order, "fabric", "-") or "-"), + outgoing_date=str(outgoing_date), + ) + except Exception: + logger.exception("[printing.handlers] 渲染企业微信消息失败,跳过通知") + return + + def _send_wecom(): + try: + from api_v1.utils.wecom_webhook import send_wecom_webhook_message + + resp = send_wecom_webhook_message(content=message, msgtype="markdown") + if not resp.ok: + logger.warning( + "[printing.handlers] WeCom webhook 返回失败:errcode=%s, errmsg=%s, raw=%s", + resp.errcode, + resp.errmsg, + resp.raw, + ) + except Exception: + logger.exception("[printing.handlers] 发送 WeCom webhook 失败(已忽略,不影响主流程)") + + # 在事务提交后再发送,避免事务回滚但通知已发出 + transaction.on_commit(_send_wecom) diff --git a/printing/services.py b/printing/services.py index 83e2c02..c907e87 100644 --- a/printing/services.py +++ b/printing/services.py @@ -254,3 +254,39 @@ def send_printing_job_latest_state_wecom( "errcode": resp.errcode, "errmsg": resp.errmsg, } + + +def render_printing_order_created_markdown( + *, + printing_order_id: str, + printing_order_human_id: str, + created_at: str, + sender_label: str, + customer_name: str, + fabric: str, + outgoing_date: str, +) -> str: + template = getattr( + settings, + "PRINTING_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE", + ( + "### 印染订单创建\n" + "\n" + "- **印染订单ID**:`{printing_order_id}`\n" + "- **订单编号**:`{printing_order_human_id}`\n" + "- **创建时间**:`{created_at}`\n" + "- **发送者**:{sender}\n" + "- **客户**:{customer_name}\n" + "- **面料**:{fabric}\n" + "- **出货日期**:`{outgoing_date}`\n" + ), + ) + return template.format( + printing_order_id=str(printing_order_id), + printing_order_human_id=str(printing_order_human_id), + created_at=str(created_at), + sender=str(sender_label or "系统自动发送"), + customer_name=str(customer_name or "-"), + fabric=str(fabric or "-"), + outgoing_date=str(outgoing_date or "-"), + ) diff --git a/printing/signals.py b/printing/signals.py new file mode 100644 index 0000000..731da0f --- /dev/null +++ b/printing/signals.py @@ -0,0 +1,17 @@ +""" +Printing 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 / sender_label) +- keep integration (e.g. WeCom notify) in handlers +""" + +from django.dispatch import Signal + +# Fired when a PrintingOrder is created via business service. +# Payload: +# - instance: PrintingOrder +# - created_by: Django User (may be None) +printing_order_created = Signal() +