### 2026-01-23 工作记录 #### 1) 新增:企业微信机器人 Webhook(text 消息)util 与测试命令 - settings: - `flower/settings.py` 新增 `WECOM_WEBHOOK_BASE_URL` / `WECOM_WEBHOOK_KEY` - `WECOM_WEBHOOK_KEY` 已改为从 `.env` 读取(默认空);若为空,发送时会报错(不会静默吞掉) - 仓库内提供 `env.example`(等价于常见的 `.env.example`),可用 `cp env.example .env` 生成本地 `.env` - util: - `api_v1/utils/wecom_webhook.py` - 提供 `send_wecom_webhook_message(content=..., msgtype=..., key=..., timeout_seconds=...)` - `msgtype` 支持:`text`(默认)、`markdown` - command(通用测试发送): - `api_v1/management/commands/wecom_webhook_send_text.py` - 用法: - 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`:可选,临时覆盖 `.env` 的 `WECOM_WEBHOOK_KEY` - `--timeout`:可选,HTTP 超时秒数(默认 10) #### 2) 新增:PrintingJob 每次状态推进(state_advanced)发送企业微信 markdown 通知 - 信号来源:`stateflow.signals.state_advanced`(每次推进都会触发,发送点在 `stateflow.services.advance_to_next_state()` 内) - 监听注册:`printing/apps.py` 里 `state_advanced.connect(..., sender=PrintingJob)` - handler:`printing/handlers.py::on_printing_job_state_advanced` - 使用 `transaction.on_commit(...)`,确保事务提交后再发送企业微信(避免回滚导致通知不一致) - 发送内容(markdown)包含: - `PrintingOrder.id` - `PrintingJob.id` - `state.name` - 推进时间(取 `state_log.completed_at`) - 发送者(默认取推进人 `completed_by` 的员工姓名/username;兜底为“系统自动发送”) - 模板位置(便于后续改文案):`flower/settings.py` - `PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE` - 开关:`PRINTING_JOB_STATE_ADVANCED_WECOM_NOTIFY_ENABLED`(默认测试环境关闭,非测试环境开启) - 模板已增加字段:`{sender}`(发送者) - 跟进地址: - `.env`:`PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE`(默认空) - 若为空则**消息中省略“跟进地址”字段**,但消息仍然发送 - 模板占位符: - `{followup_line}`(由 service 渲染:有 url 才会输出“跟进地址”行) - 模板已增加字段:`{process_params_markdown}`(工序参数,来自 `state_log.get_all_parameters_summary()`) - 工序参数渲染增强:当参数值是 http/https URL(如图片链接)时,自动渲染为 markdown Link:`[url](url)`,便于点击查看 #### 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) ##### 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 的回归测试覆盖该历史数据场景。