forked from erp-dev/erp
feat: wecomm notification support command
This commit is contained in:
@@ -31,6 +31,16 @@
|
|||||||
- `PrintingJob.id`
|
- `PrintingJob.id`
|
||||||
- `state.name`
|
- `state.name`
|
||||||
- 推进时间(取 `state_log.completed_at`)
|
- 推进时间(取 `state_log.completed_at`)
|
||||||
|
- 发送者(默认取推进人 `completed_by` 的员工姓名/username;兜底为“系统自动发送”)
|
||||||
- 模板位置(便于后续改文案):`flower/settings.py`
|
- 模板位置(便于后续改文案):`flower/settings.py`
|
||||||
- `PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE`
|
- `PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE`
|
||||||
- 开关:`PRINTING_JOB_STATE_ADVANCED_WECOM_NOTIFY_ENABLED`(默认测试环境关闭,非测试环境开启)
|
- 开关:`PRINTING_JOB_STATE_ADVANCED_WECOM_NOTIFY_ENABLED`(默认测试环境关闭,非测试环境开启)
|
||||||
|
- 模板已增加字段:`{sender}`(发送者)
|
||||||
|
|
||||||
|
#### 3) 重构:提取“发送最新状态到企业微信”逻辑为可复用 service(便于后续 API 化)
|
||||||
|
|
||||||
|
- 新增:`printing/services.py`
|
||||||
|
- `send_printing_job_latest_state_wecom(printing_job_id, sender_label, ...)`
|
||||||
|
- `render_printing_job_state_markdown(...)`
|
||||||
|
- command:`printing/management/commands/wecom_notify_printing_job_latest_state.py`
|
||||||
|
- 发送者强制为“系统自动发送”(后续 API 化可传入基于 `request.user` 的 sender_label)
|
||||||
@@ -271,12 +271,13 @@ else:
|
|||||||
# - 默认:测试环境关闭(避免单测出网/刷屏),非测试环境开启
|
# - 默认:测试环境关闭(避免单测出网/刷屏),非测试环境开启
|
||||||
PRINTING_JOB_STATE_ADVANCED_WECOM_NOTIFY_ENABLED = (not TESTING)
|
PRINTING_JOB_STATE_ADVANCED_WECOM_NOTIFY_ENABLED = (not TESTING)
|
||||||
PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE = (
|
PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE = (
|
||||||
"### PrintingJob 状态推进\n"
|
"### 印染任务状态推进\n"
|
||||||
"\n"
|
"\n"
|
||||||
"- **PrintingOrder.id**: `{printing_order_id}`\n"
|
"- **印染订单ID**:`{printing_order_id}`\n"
|
||||||
"- **PrintingJob.id**: `{printing_job_id}`\n"
|
"- **印染任务ID**:`{printing_job_id}`\n"
|
||||||
"- **State**: **{state_name}**\n"
|
"- **推进状态**:**{state_name}**\n"
|
||||||
"- **AdvancedAt**: `{advanced_at}`\n"
|
"- **推进时间**:`{advanced_at}`\n"
|
||||||
|
"- **发送者**:{sender}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -203,11 +203,21 @@ def on_printing_job_state_advanced(sender, **kwargs):
|
|||||||
"- **AdvancedAt**: `{advanced_at}`\n"
|
"- **AdvancedAt**: `{advanced_at}`\n"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
sender_label = None
|
||||||
|
completed_by = kwargs.get('completed_by')
|
||||||
|
if completed_by:
|
||||||
|
emp = getattr(completed_by, 'employee', None)
|
||||||
|
sender_label = getattr(emp, 'name', None) if emp is not None else None
|
||||||
|
sender_label = sender_label or getattr(completed_by, 'username', None) or '系统自动发送'
|
||||||
|
else:
|
||||||
|
sender_label = '系统自动发送'
|
||||||
|
|
||||||
message = template.format(
|
message = template.format(
|
||||||
printing_order_id=str(printing_order_id) if printing_order_id is not None else '-',
|
printing_order_id=str(printing_order_id) if printing_order_id is not None else '-',
|
||||||
printing_job_id=str(printing_job_id) if printing_job_id is not None else '-',
|
printing_job_id=str(printing_job_id) if printing_job_id is not None else '-',
|
||||||
state_name=str(state_name),
|
state_name=str(state_name),
|
||||||
advanced_at=str(advanced_at),
|
advanced_at=str(advanced_at),
|
||||||
|
sender=str(sender_label),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _send_wecom():
|
def _send_wecom():
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
from printing.services import send_printing_job_latest_state_wecom
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "发送指定 PrintingJob 的“最新状态”到企业微信机器人(markdown)"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("printing_job_id", type=int, help="PrintingJob.id")
|
||||||
|
parser.add_argument("--key", type=str, default=None, help="可选:临时覆盖 settings.WECOM_WEBHOOK_KEY")
|
||||||
|
parser.add_argument("--timeout", type=float, default=10.0, help="HTTP 超时(秒)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="仅输出将要发送的 markdown 内容,不实际调用企业微信 webhook(适用于无 https 出网环境)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
printing_job_id = int(options["printing_job_id"])
|
||||||
|
key = options.get("key")
|
||||||
|
timeout = float(options.get("timeout") or 10.0)
|
||||||
|
dry_run = bool(options.get("dry_run"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = send_printing_job_latest_state_wecom(
|
||||||
|
printing_job_id=printing_job_id,
|
||||||
|
sender_label="系统自动发送",
|
||||||
|
key=key,
|
||||||
|
timeout_seconds=timeout,
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
raise CommandError(str(exc)) from exc
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(result["message"])
|
||||||
|
return
|
||||||
|
|
||||||
|
if not bool(result.get("ok")):
|
||||||
|
raise CommandError(
|
||||||
|
f"WeCom webhook 返回失败:errcode={result.get('errcode')}, errmsg={result.get('errmsg')}, raw={result.get('wecom')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"OK: {result.get('wecom')}"))
|
||||||
|
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""
|
||||||
|
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 _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,
|
||||||
|
) -> 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"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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 "系统自动发送"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 = get_printing_job_latest_state(job=job)
|
||||||
|
msg = render_printing_job_state_markdown(
|
||||||
|
printing_order_id=str(job.printing_order_id),
|
||||||
|
printing_job_id=str(job.id),
|
||||||
|
state_name=latest.state_name,
|
||||||
|
advanced_at=latest.advanced_at,
|
||||||
|
sender_label=sender_label,
|
||||||
|
)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
return {"dry_run": True, "message": msg, "state_name": latest.state_name, "advanced_at": latest.advanced_at}
|
||||||
|
|
||||||
|
resp = send_wecom_webhook_message(
|
||||||
|
content=msg,
|
||||||
|
msgtype="markdown",
|
||||||
|
key=key,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"dry_run": False,
|
||||||
|
"message": msg,
|
||||||
|
"state_name": latest.state_name,
|
||||||
|
"advanced_at": latest.advanced_at,
|
||||||
|
"wecom": resp.raw,
|
||||||
|
"ok": resp.ok,
|
||||||
|
"errcode": resp.errcode,
|
||||||
|
"errmsg": resp.errmsg,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user