""" Printing domain services. This module is intended for reusable business logic that may be called by: - management commands - API views - signal handlers """ from __future__ import annotations from dataclasses import dataclass from django.conf import settings from django.utils import timezone from api_v1.utils.wecom_webhook import send_wecom_webhook_message from printing.models import PrintingJob @dataclass(frozen=True) class PrintingJobLatestState: state_name: str advanced_at: str def build_printing_job_followup_url(*, printing_order_id: int | None) -> str: tmpl = getattr( settings, "PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE", "", ) tmpl = (str(tmpl or "")).strip() if not tmpl: return "" oid = printing_order_id if printing_order_id is not None else "" try: return str(tmpl).format(order_id=str(oid)) except Exception: # template mis-config should not crash critical flows return f"https://app.yuwen.cloud/workstation/production/batch-advance?orderId={oid}" def _truncate_text(s: str, max_len: int) -> str: s = s or "" if len(s) <= max_len: return s return s[: max(0, int(max_len) - 6)] + "...(截断)" def render_process_params_markdown(*, params: dict, max_items: int = 60, max_value_len: int = 200) -> str: """ Render process params as markdown lines. Example: - **温度**:25 """ if not params: return "- (无)" items = [] for k in sorted(params.keys(), key=lambda x: str(x)): v = params.get(k) kk = _truncate_text(str(k), 80) vv = _truncate_text(str(v), int(max_value_len)) items.append(f"- **{kk}**:{vv}") if len(items) > int(max_items): remain = len(items) - int(max_items) items = items[: int(max_items)] + [f"- ……(其余 {remain} 项已省略)"] return "\n".join(items) def _user_label(user) -> str: """ Format a human-readable sender label from Django User. Prefer employee.name if available. """ if not user: return "系统自动发送" emp = getattr(user, "employee", None) name = getattr(emp, "name", None) if emp is not None else None if name: return str(name) username = getattr(user, "username", None) if username: return str(username) return "系统自动发送" def get_printing_job_latest_state(*, job: PrintingJob) -> PrintingJobLatestState: """ Latest state is defined as the latest non-cancelled StateFlowRecord on job.business_object. """ if not job.business_object: raise ValueError(f"PrintingJob 未绑定 business_object,无法获取状态:id={job.id}") state_log = ( job.business_object.state_logs.filter(is_cancelled=False) .select_related("state") .order_by("-completed_at", "-id") .first() ) if state_log is None: return PrintingJobLatestState(state_name="未开始", advanced_at="-") state_name = getattr(state_log.state, "name", None) or str(state_log.state) advanced_at_dt = state_log.completed_at advanced_at = timezone.localtime(advanced_at_dt).strftime("%Y-%m-%d %H:%M:%S") return PrintingJobLatestState(state_name=str(state_name), advanced_at=str(advanced_at)) def render_printing_job_state_markdown( *, printing_order_id: str, printing_job_id: str, state_name: str, advanced_at: str, sender_label: str, followup_url: str, process_params_markdown: str, ) -> str: template = getattr( settings, "PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE", ( "### 印染任务状态推进\n" "\n" "- **印染订单ID**:`{printing_order_id}`\n" "- **印染任务ID**:`{printing_job_id}`\n" "- **推进状态**:**{state_name}**\n" "- **推进时间**:`{advanced_at}`\n" "- **发送者**:{sender}\n" "{followup_line}" "\n" "**工序参数**:\n" "{process_params_markdown}\n" ), ) followup_url = (followup_url or "").strip() followup_line = f"- **跟进地址**:[{followup_url}]({followup_url})\n" if followup_url else "" return template.format( printing_order_id=str(printing_order_id), printing_job_id=str(printing_job_id), state_name=str(state_name), advanced_at=str(advanced_at), sender=str(sender_label or "系统自动发送"), followup_line=str(followup_line), process_params_markdown=str(process_params_markdown), ) def send_printing_job_latest_state_wecom( *, printing_job_id: int, sender_label: str, key: str | None = None, timeout_seconds: float = 10.0, dry_run: bool = False, ) -> dict: """ Reusable entrypoint for command / future API. """ job = ( PrintingJob.objects.select_related("printing_order", "business_object") .filter(id=int(printing_job_id)) .first() ) if not job: raise ValueError(f"PrintingJob 不存在:id={printing_job_id}") # latest state log for parameters state_log = ( job.business_object.state_logs.filter(is_cancelled=False) .select_related("state") .order_by("-completed_at", "-id") .first() if job.business_object else None ) if state_log is None: state_name = "未开始" advanced_at = "-" params = {} else: state_name = getattr(state_log.state, "name", None) or str(state_log.state) advanced_at = timezone.localtime(state_log.completed_at).strftime("%Y-%m-%d %H:%M:%S") params = state_log.get_all_parameters_summary(include_cancelled=False) followup_url = build_printing_job_followup_url(printing_order_id=job.printing_order_id) process_params_markdown = render_process_params_markdown(params=params) msg = render_printing_job_state_markdown( printing_order_id=str(job.printing_order_id), printing_job_id=str(job.id), state_name=str(state_name), advanced_at=str(advanced_at), sender_label=sender_label, followup_url=followup_url, process_params_markdown=process_params_markdown, ) if dry_run: return { "dry_run": True, "message": msg, "state_name": str(state_name), "advanced_at": str(advanced_at), "followup_url": str(followup_url), "process_params": params, } resp = send_wecom_webhook_message( content=msg, msgtype="markdown", key=key, timeout_seconds=timeout_seconds, ) return { "dry_run": False, "message": msg, "state_name": str(state_name), "advanced_at": str(advanced_at), "followup_url": str(followup_url), "process_params": params, "wecom": resp.raw, "ok": resp.ok, "errcode": resp.errcode, "errmsg": resp.errmsg, }