From 4d33bc7f68e80a535e5d215413e034d7979d687d Mon Sep 17 00:00:00 2001 From: colaftc Date: Fri, 23 Jan 2026 21:11:13 +0800 Subject: [PATCH] feat: wecomm support image link --- .env | 2 ++ docs/2026-01-23_summary.md | 53 ++++++++++++++++++++++++++++----- printing/services.py | 29 +++++++++++++++++- printing/test_wecom_markdown.py | 23 ++++++++++++++ 4 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 printing/test_wecom_markdown.py diff --git a/.env b/.env index f926761..f2779ab 100644 --- a/.env +++ b/.env @@ -7,3 +7,5 @@ DB_NAME=flower DB_USER=postgres DB_PASSWORD=postgres PRINTING_SALES_ITEM_SOURCE_STATE_ID=2 +WECOM_WEBHOOK_KEY= +PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE=https://app.yuwen.cloud/workstation/production/batch-advance?orderId={order_id} \ No newline at end of file diff --git a/docs/2026-01-23_summary.md b/docs/2026-01-23_summary.md index 28997d7..c14ea16 100644 --- a/docs/2026-01-23_summary.md +++ b/docs/2026-01-23_summary.md @@ -10,16 +10,17 @@ - `api_v1/utils/wecom_webhook.py` - 提供 `send_wecom_webhook_message(content=..., msgtype=..., key=..., timeout_seconds=...)` - `msgtype` 支持:`text`(默认)、`markdown` -- command: +- command(通用测试发送): - `api_v1/management/commands/wecom_webhook_send_text.py` - - 用法示例: - - `uv run python manage.py wecom_webhook_send_text "hello world"` - - 发送 markdown:`uv run python manage.py wecom_webhook_send_text "**bold**" --msgtype markdown` - - 可选覆盖 key:`uv run python manage.py wecom_webhook_send_text "hello" --key xxx` + - 用法: + - text(默认): + - `uv run python manage.py wecom_webhook_send_text "hello world"` + - markdown: + - `uv run python manage.py wecom_webhook_send_text "**bold**\n> hello" --msgtype markdown` - 参数: - `text`:必填,消息内容 - `--msgtype`:可选,`text`(默认)或 `markdown` - - `--key`:可选,临时覆盖 `settings.WECOM_WEBHOOK_KEY` + - `--key`:可选,临时覆盖 `.env` 的 `WECOM_WEBHOOK_KEY` - `--timeout`:可选,HTTP 超时秒数(默认 10) #### 2) 新增:PrintingJob 每次状态推进(state_advanced)发送企业微信 markdown 通知 @@ -44,6 +45,7 @@ - 模板占位符: - `{followup_line}`(由 service 渲染:有 url 才会输出“跟进地址”行) - 模板已增加字段:`{process_params_markdown}`(工序参数,来自 `state_log.get_all_parameters_summary()`) + - 工序参数渲染增强:当参数值是 http/https URL(如图片链接)时,自动渲染为 markdown Link:`[url](url)`,便于点击查看 #### 3) 重构:提取“发送最新状态到企业微信”逻辑为可复用 service(便于后续 API 化) @@ -51,4 +53,41 @@ - `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) \ No newline at end of file + - 发送者强制为“系统自动发送”(后续 API 化可传入基于 `request.user` 的 sender_label) + +##### 3.1) command:发送某个 PrintingJob 的“最新状态”通知(可 dry-run) + +- 文件:`printing/management/commands/wecom_notify_printing_job_latest_state.py` +- 用法: + - 预览(不发请求,适合只有 http 出网的机器): + - `uv run python manage.py wecom_notify_printing_job_latest_state --dry-run` + - 实际发送(需要 https 出网 + 配置 `.env` 的 `WECOM_WEBHOOK_KEY`): + - `uv run python manage.py wecom_notify_printing_job_latest_state ` + - 可选覆盖 key / timeout: + - `uv run python manage.py wecom_notify_printing_job_latest_state --key xxx --timeout 5` +- 参数: + - `printing_job_id`:必填,PrintingJob.id + - `--dry-run`:仅输出 markdown,不调用企业微信 webhook + - `--key`:可选,临时覆盖 `.env` 的 `WECOM_WEBHOOK_KEY` + - `--timeout`:可选,HTTP 超时秒数(默认 10) + +#### 4) 排查:PrintingJob 查询 API(list / retrieve)均返回 “No PrintingJob matches the given query” + +现象: +- `api_v1` 的 PrintingJob 查询接口(列表/单体)提示 “No PrintingJob matches the given query”,但数据库中确有数据。 + +排查路径与结论(仅排查,未改动代码): +- `PrintingJobViewSet`:`api_v1/views/printing/views.py::PrintingJobViewSet` + - 继承 `CustomerVisibilityFilterMixin`,其 `get_queryset()` 会先做 **merchant 隔离**,再做 **客户可见性过滤**。 +- `CustomerVisibilityFilterMixin.filter_by_merchant()`:`api_v1/views/printing/mixins.py` + - 当前逻辑对非 superuser 强制 `queryset.filter(merchant=emp.merchant)`。 + - 当历史数据存在 `PrintingJob.merchant = NULL`(或不等于当前员工 merchant)时,会被直接过滤掉,导致 list/retrieve 都“查不到”。 +- 佐证: + - `api_v1/views/printing/test_printing_job_api.py` 中明确把测试用户设为 `superuser`,并注释说明: + “PrintingJobViewSet 默认做 merchant 隔离;本测试集不关注该隔离逻辑,设为 superuser 以避免因测试数据未设置 merchant 导致的 404/列表为空。” + - 这与线上“明明有数据但 API 查不到”的现象一致(典型是 merchant 字段为空/不一致导致隔离过滤)。 + +建议方案(未实施): +- 保持多租户隔离前提下,对 `merchant=NULL` 的历史数据做兼容: + - 若模型自身 `merchant` 为空,则可回退使用关联 customer 的 merchant(对 PrintingJob 可通过 `printing_order__customer__merchant`)判断归属。 + - 并补充非 superuser 的回归测试覆盖该历史数据场景。 \ No newline at end of file diff --git a/printing/services.py b/printing/services.py index 9729d9e..83e2c02 100644 --- a/printing/services.py +++ b/printing/services.py @@ -10,6 +10,7 @@ This module is intended for reusable business logic that may be called by: from __future__ import annotations from dataclasses import dataclass +from urllib.parse import urlparse from django.conf import settings from django.utils import timezone @@ -48,6 +49,26 @@ def _truncate_text(s: str, max_len: int) -> str: return s[: max(0, int(max_len) - 6)] + "...(截断)" +def _is_http_url(s: str) -> bool: + """ + Minimal URL check for markdown linkify. + Only treat http/https absolute URLs as linkable. + """ + s = (s or "").strip() + if not s: + return False + try: + p = urlparse(s) + except Exception: + return False + return p.scheme in ("http", "https") and bool(p.netloc) + + +def _escape_markdown_link_text(s: str) -> str: + # avoid breaking markdown link text + return (s or "").replace("[", r"\[").replace("]", r"\]") + + def render_process_params_markdown(*, params: dict, max_items: int = 60, max_value_len: int = 200) -> str: """ Render process params as markdown lines. @@ -61,7 +82,13 @@ def render_process_params_markdown(*, params: dict, max_items: int = 60, max_val 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)) + v_str = "" if v is None else str(v) + v_href = v_str.strip() + if _is_http_url(v_href): + v_disp = _escape_markdown_link_text(_truncate_text(v_href, int(max_value_len))) + vv = f"[{v_disp}]({v_href})" + else: + vv = _truncate_text(v_str, int(max_value_len)) items.append(f"- **{kk}**:{vv}") if len(items) > int(max_items): diff --git a/printing/test_wecom_markdown.py b/printing/test_wecom_markdown.py new file mode 100644 index 0000000..8311a2d --- /dev/null +++ b/printing/test_wecom_markdown.py @@ -0,0 +1,23 @@ +from django.test import SimpleTestCase + + +class WeComMarkdownRenderTest(SimpleTestCase): + def test_render_process_params_markdown_linkify_http_url(self): + from printing.services import render_process_params_markdown + + md = render_process_params_markdown( + params={ + "图片": "https://example.com/a.png", + "备注": "不是链接", + } + ) + + self.assertIn("- **图片**:[https://example.com/a.png](https://example.com/a.png)", md) + self.assertIn("- **备注**:不是链接", md) + + def test_render_process_params_markdown_linkify_strips_whitespace(self): + from printing.services import render_process_params_markdown + + md = render_process_params_markdown(params={"图片": " https://example.com/a.png "}) + self.assertIn("[https://example.com/a.png](https://example.com/a.png)", md) +