feat(wle): 仓储转运异常每天定点推文本到钉钉群 #73

Merged
yanhui merged 1 commits from codex/yanhui-wle-notify-text-20261022 into main 2026-10-22 11:01:32 +08:00
Collaborator

目的与范围

「仓储中心物流转运异常」页在「导出」右侧新增 异常通知:每天 09:10(页面可改)把当天核验批次的异常情况以文本消息推到钉钉群,0 条也推(用来确认任务没挂)。

为什么是 text 而不是 Excel 附件:钉钉群自定义机器人 webhook 只支持 text / markdown / link / actionCard,发文件必须走企业内部应用(AppKey/AppSecret/robotCode/openConversationId 四项凭据 + 应用权限 + 机器人进群)。业务确认「群里看文本就够,要明细点链接去页面导出」,于是走最轻的 webhook —— 凭据从 4 项降到 1 项。

改动文件(9 个,+1912)

类型 文件 说明
新增 backend/src/services/dingtalkRobotWebhookSender.js webhook 发 text:加签、按字节截断、errcode 透传
新增 backend/src/services/warehouseLogisticsExceptionNotifySettings.js 配置/状态存取(webhook 明文回显、加签密钥只回掩码)
新增 backend/src/jobs/warehouseLogisticsExceptionNotifyJob.js 正文组装 + 每分钟 tick 调度
新增 backend/test/warehouseLogisticsExceptionNotify.test.js 15 项回归
改 backend/src/api/warehouseLogisticsException.js notify/{settings,state,run} 路由(run 支持 dryRun/probe/force/date)
改 backend/src/index.js +4 行:注册 scheduler
改 frontend/src/api/warehouseLogisticsException.js 3 个接口封装
改 frontend/src/pages/WarehouseLogisticsException/{index.jsx,style.css} 「异常通知」弹窗 + 内联预览

群消息长什么样

【仓储物流转运异常】10-22 09:00 核验 · 10-21 发货
已发货 9886 单 / 9886 个包裹,异常 144 个包裹(1.5%)
类型:已揽收未中转 137 · 未揽收 7
已剔除 1805 个包裹(承运商 / 已终止 / 刷单·信誉单,不计考核)

按仓(异常包裹):
1. 山东青岗集仓 122 个(发货 1649 单)
…

明细前 30 条:
1. 简尚-睢宁仓|9844248|睢宁德邦|已揽收未中转|10-21 08:24|24.6h
…
…其余 114 条见页面。
查看全部 / 导出 Excel:https://ck.yydd.ai:8443/planning/operation/warehouse-logistics-exception?date=2026-10-22
  • 明细条数可配 0~50(默认 30);truncated 只表示被 20000 字节上限砍过,不表示「按配置只列前 N 条」。
  • 抬头固定含「异常」,命中钉钉「自定义关键词」安全设置的常见关键词。

同步与兼容性

  • 新配置首次读取为空 → ready=false,弹窗提示「Webhook 地址还没配」,不影响页面取数与导出。
  • 定时任务只在 enabled=true 时生效;同一推送日只推一次(lastSentDate 去重),手动「立即推送」才强制重发。
  • 本地 ENABLE_BACKGROUND_JOBS=false 不跑定时任务,用页面按钮验证(生产生效)。
  • 加签密钥走 resilientSecretStore,表单留空 = 沿用已存;接口永不明文返回。

验证

项 结果
后端新增测试 15/15 通过(正文结构/数字、0 条平安消息、条数上限、字节精简不切半字、Webhook 地址校验、加签 crypto 手算对齐、errcode 310000 透传)
后端全量 465 项 / 463 过;2 项既有环境失败(Windows 权限位、负缓存需隧道故障态),与本改动无关
前端 eslint 0 error;vite build 成功(26s)
真数据干跑 144 条异常 / 列 30 条 / 3768 字节 / 20s;按仓包裹数 122+5+5+4+4+3+1=144 对得上
真渲染(playwright) 弹窗字段 Webhook / 加签密钥 / 明细条数 / @所有人 / 页面链接,旧字段 AppKey / AppSecret / robotCode / 群会话 ID 零残留;点「预览正文」内联渲染出正文

风险与回退

  • 钉钉侧失败只写 lastState.lastError 并在弹窗展示(带 errcode),不影响页面取数。
  • 已知:本项目 React 19 + antd v5,Modal.info 静态方法静默不渲染(只留一行 context warning)→ 预览改为弹窗内受控区块,已在代码注释里写明。
  • 回退:删掉 index.js 里 startWarehouseLogisticsExceptionNotifyScheduler() 一行即可停推送;前端不点「异常通知」不产生任何副作用。

上线后需要业务做的一步

在弹窗里填钉钉群机器人 Webhook 地址(加签密钥按安全设置决定填不填)→ 点「测试通道」→ 收到测试消息后打开「每天推送」开关并保存。

## 目的与范围 「仓储中心物流转运异常」页在「导出」右侧新增 **异常通知**:每天 09:10(页面可改)把当天核验批次的异常情况以**文本消息**推到钉钉群,**0 条也推**(用来确认任务没挂)。 **为什么是 text 而不是 Excel 附件**:钉钉群自定义机器人 webhook 只支持 text / markdown / link / actionCard,发文件必须走企业内部应用(AppKey/AppSecret/robotCode/openConversationId 四项凭据 + 应用权限 + 机器人进群)。业务确认「群里看文本就够,要明细点链接去页面导出」,于是走最轻的 webhook —— 凭据从 4 项降到 1 项。 ### 改动文件(9 个,+1912) | 类型 | 文件 | 说明 | |---|---|---| | 新增 | `backend/src/services/dingtalkRobotWebhookSender.js` | webhook 发 text:加签、按字节截断、errcode 透传 | | 新增 | `backend/src/services/warehouseLogisticsExceptionNotifySettings.js` | 配置/状态存取(webhook 明文回显、加签密钥只回掩码) | | 新增 | `backend/src/jobs/warehouseLogisticsExceptionNotifyJob.js` | 正文组装 + 每分钟 tick 调度 | | 新增 | `backend/test/warehouseLogisticsExceptionNotify.test.js` | 15 项回归 | | 改 | `backend/src/api/warehouseLogisticsException.js` | `notify/{settings,state,run}` 路由(run 支持 dryRun/probe/force/date) | | 改 | `backend/src/index.js` | +4 行:注册 scheduler | | 改 | `frontend/src/api/warehouseLogisticsException.js` | 3 个接口封装 | | 改 | `frontend/src/pages/WarehouseLogisticsException/{index.jsx,style.css}` | 「异常通知」弹窗 + 内联预览 | ### 群消息长什么样 ``` 【仓储物流转运异常】10-22 09:00 核验 · 10-21 发货 已发货 9886 单 / 9886 个包裹,异常 144 个包裹(1.5%) 类型:已揽收未中转 137 · 未揽收 7 已剔除 1805 个包裹(承运商 / 已终止 / 刷单·信誉单,不计考核) 按仓(异常包裹): 1. 山东青岗集仓 122 个(发货 1649 单) … 明细前 30 条: 1. 简尚-睢宁仓|9844248|睢宁德邦|已揽收未中转|10-21 08:24|24.6h … …其余 114 条见页面。 查看全部 / 导出 Excel:https://ck.yydd.ai:8443/planning/operation/warehouse-logistics-exception?date=2026-10-22 ``` - 明细条数可配 0~50(默认 30);`truncated` 只表示被 20000 字节上限砍过,不表示「按配置只列前 N 条」。 - 抬头固定含「异常」,命中钉钉「自定义关键词」安全设置的常见关键词。 ## 同步与兼容性 - 新配置首次读取为空 → `ready=false`,弹窗提示「Webhook 地址还没配」,不影响页面取数与导出。 - 定时任务只在 `enabled=true` 时生效;同一推送日只推一次(`lastSentDate` 去重),手动「立即推送」才强制重发。 - 本地 `ENABLE_BACKGROUND_JOBS=false` 不跑定时任务,用页面按钮验证(生产生效)。 - 加签密钥走 `resilientSecretStore`,表单留空 = 沿用已存;接口永不明文返回。 ## 验证 | 项 | 结果 | |---|---| | 后端新增测试 | **15/15 通过**(正文结构/数字、0 条平安消息、条数上限、字节精简不切半字、Webhook 地址校验、加签 crypto 手算对齐、errcode 310000 透传) | | 后端全量 | 465 项 / 463 过;2 项既有环境失败(Windows 权限位、负缓存需隧道故障态),与本改动无关 | | 前端 | eslint 0 error;vite build 成功(26s) | | 真数据干跑 | 144 条异常 / 列 30 条 / **3768 字节** / 20s;按仓包裹数 122+5+5+4+4+3+1=144 对得上 | | 真渲染(playwright) | 弹窗字段 `Webhook / 加签密钥 / 明细条数 / @所有人 / 页面链接`,旧字段 `AppKey / AppSecret / robotCode / 群会话 ID` **零残留**;点「预览正文」内联渲染出正文 | ## 风险与回退 - 钉钉侧失败只写 `lastState.lastError` 并在弹窗展示(带 errcode),不影响页面取数。 - 已知:本项目 React 19 + antd v5,`Modal.info` 静态方法**静默不渲染**(只留一行 context warning)→ 预览改为弹窗内受控区块,已在代码注释里写明。 - 回退:删掉 `index.js` 里 `startWarehouseLogisticsExceptionNotifyScheduler()` 一行即可停推送;前端不点「异常通知」不产生任何副作用。 ## 上线后需要业务做的一步 在弹窗里填钉钉群机器人 **Webhook 地址**(加签密钥按安全设置决定填不填)→ 点「测试通道」→ 收到测试消息后打开「每天推送」开关并保存。
yanhui added 1 commit 2026-10-22 11:00:54 +08:00
在「仓储中心物流转运异常」页新增「异常通知」:每天 09:10(可改)把当天核验批次
的异常情况以**文本消息**推到钉钉群,0 条也推(用来确认任务没挂)。

为什么是 text 而不是 Excel 附件:钉钉群自定义机器人 webhook 只支持
text/markdown/link/actionCard,发文件必须走企业内部应用(AppKey/AppSecret/
robotCode/openConversationId 四项凭据 + 应用权限 + 机器人进群)。业务确认
「群里看文本就够,要明细点链接去页面导出」,于是走最轻的 webhook。

后端
- 新增 services/dingtalkRobotWebhookSender.js:POST /robot/send + msgtype=text
  · 加签 urlEncode(base64(HmacSHA256(secret, "timestamp\nsecret"))),timestamp 毫秒
  · text.content 按 UTF-8 **字节**截断到 20000(钉钉硬上限),不切半个汉字
  · webhook 严格匹配 ^/robot/send$ 且必须有 access_token;errcode/errmsg 原样抛
- 新增 services/warehouseLogisticsExceptionNotifySettings.js:
  · webhookUrl 存在 system_config(完整回显,业务要核对);加签密钥走
    resilientSecretStore,接口只回 configured + 掩码,留空=沿用已存
  · sendTime 非法退回 09:10;maxDetailRows 0~50 默认 30
  · 状态 lastSentDate(按推送日去重)等存 system_config
- 新增 jobs/warehouseLogisticsExceptionNotifyJob.js:
  · buildNotifyText:抬头【仓储物流转运异常】→ 发货/包裹/异常/异常率 → 分类型
    → 剔除数 → 按仓(只列有异常的仓)→ 明细前 N 条 → 页面链接(带 date)
  · 明细行「仓|内部单号|快递|异常类型|发货时间|待处理」,字段内换行压平
  · 超长从明细末尾逐条砍并标记 truncated(truncated 只表示被字节上限砍过,
    不表示「按配置只列前 N 条」)
  · 每分钟 tick 比对北京时间 HH:mm == 配置 sendTime,改时间不用重启
- api:GET/PUT /notify/settings、GET /notify/state、POST /notify/run
  (dryRun 预览不校验凭据;probe 只发一句测试文本、不查业务数据)

前端
- 「导出」右侧新增「异常通知」(铃铛)弹窗:开关、推送时间、Webhook、加签密钥、
  明细条数、@所有人、页面链接、上次推送结果、三段帮助说明
- 「测试通道」「预览正文」「保存」「立即推送到群」
- 预览改成弹窗内受控区块:本项目跑 React 19 + antd v5,Modal.info 静态方法
  拿不到 context 会**静默不渲染**(只留一行 warning,看着像后端没返回)

同步与兼容
- 新配置首次读取为空 → ready=false,页面提示「Webhook 地址还没配」,不影响页面其它功能
- 定时任务只在 enabled=true 时生效;本地 ENABLE_BACKGROUND_JOBS=false 不跑,用页面按钮验

验证
- backend/test/warehouseLogisticsExceptionNotify.test.js 15/15:正文结构与数字、
  0 条平安消息、条数上限、字节精简、Webhook 校验、加签 crypto 手算对齐、errcode 透传
- 后端全量 465 项 / 463 过(2 项既有环境失败:Windows 权限位、负缓存需隧道故障态)
- 前端 eslint 0、build 成功
- 真数据干跑:144 条异常 / 列 30 条 / 3768 字节 / 20s,按仓包裹数合计 144 对得上
- playwright 真渲染:弹窗字段 Webhook/加签密钥/明细条数/@所有人/页面链接,
  旧字段 AppKey/AppSecret/robotCode/群会话 ID 零残留;预览内联渲染出正文

风险与回退
- 钉钉侧失败只会记 lastState.lastError 并在页面展示,不影响页面取数
- 回退:删除 index.js 里的 startWarehouseLogisticsExceptionNotifyScheduler 注册即可停推送
Author
Collaborator

合并授权记录(规范 6.1 路径二)

项 内容
授权人 yanhui(仓库所有者)
时间 2026-10-22(北京时间)
覆盖 SHA bdc2cf9fd656022d365c61817aae1b0cb1f18612
审核记录 reviews = 0、comments = 0(建 PR 后核对,无独立审核者)
授权原话 询问下一步时选择「直接走 PR + 部署」,该选项明确写明流程为「建分支 → 提 PR → 合并 → 部署生产 → 验收」

本地留档:.workbuddy/pr73-merge-authorization.md

## 合并授权记录(规范 6.1 路径二) | 项 | 内容 | |---|---| | 授权人 | yanhui(仓库所有者) | | 时间 | 2026-10-22(北京时间) | | 覆盖 SHA | `bdc2cf9fd656022d365c61817aae1b0cb1f18612` | | 审核记录 | `reviews = 0`、`comments = 0`(建 PR 后核对,无独立审核者) | | 授权原话 | 询问下一步时选择「直接走 PR + 部署」,该选项明确写明流程为「建分支 → 提 PR → 合并 → 部署生产 → 验收」 | 本地留档:`.workbuddy/pr73-merge-authorization.md`
yanhui merged commit ac61802b07 into main 2026-10-22 11:01:32 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lunanting/cangku#73
No description provided.