在「仓储中心物流转运异常」页新增「异常通知」:每天 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 注册即可停推送
346 lines
6.1 KiB
CSS
346 lines
6.1 KiB
CSS
.wle-page {
|
|
padding: 16px 20px 24px;
|
|
}
|
|
|
|
.wle-header {
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.wle-header .ant-typography {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
/* 口径说明:页面顶部是深色渐变,直接放浅色小字看不清,给一块浅底 */
|
|
.wle-note {
|
|
display: block;
|
|
max-width: 1180px;
|
|
margin-top: 8px;
|
|
padding: 10px 14px;
|
|
background: var(--sycm-bg-container);
|
|
border: 1px solid var(--sycm-border-color);
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
line-height: 1.75;
|
|
}
|
|
|
|
/* ============ 批次状态条 ============ */
|
|
.wle-status {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.wle-status__row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.wle-status__batch {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.wle-status__badge {
|
|
padding: 1px 8px;
|
|
border-radius: 4px;
|
|
background: rgba(22, 119, 255, 0.12);
|
|
color: #0958d9;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
line-height: 20px;
|
|
}
|
|
|
|
.wle-status__badge--idle {
|
|
background: rgba(136, 135, 128, 0.14);
|
|
color: var(--sycm-text-tertiary);
|
|
}
|
|
|
|
.wle-status__label {
|
|
font-size: 13px;
|
|
color: var(--sycm-text-primary);
|
|
}
|
|
|
|
.wle-status__window,
|
|
.wle-status__time {
|
|
font-size: 12px;
|
|
color: var(--sycm-text-tertiary);
|
|
}
|
|
|
|
.wle-alert {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* ============ 统计卡 ============ */
|
|
.wle-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.wle-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
}
|
|
|
|
.wle-stat {
|
|
height: 100%;
|
|
}
|
|
|
|
.wle-stat__sub {
|
|
display: block;
|
|
margin-top: 4px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ============ 异常类型筛选 ============ */
|
|
.wle-types {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.wle-type {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 10px 2px 2px;
|
|
border: 1px solid var(--sycm-border-color);
|
|
border-radius: 16px;
|
|
background: var(--sycm-bg-container);
|
|
cursor: pointer;
|
|
transition: border-color 0.15s, background 0.15s;
|
|
}
|
|
|
|
.wle-type:hover {
|
|
border-color: rgba(22, 119, 255, 0.45);
|
|
}
|
|
|
|
.wle-type.is-active {
|
|
border-color: rgba(22, 119, 255, 0.6);
|
|
background: rgba(22, 119, 255, 0.08);
|
|
}
|
|
|
|
.wle-type.is-empty {
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.wle-type__count {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--sycm-text-primary);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.wle-type__clear {
|
|
padding: 0;
|
|
border: none;
|
|
background: transparent;
|
|
font-size: 12px;
|
|
color: #1677ff;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* ============ 主从两栏 ============ */
|
|
.wle-body {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 14px;
|
|
}
|
|
|
|
.wle-centers {
|
|
flex: none;
|
|
width: 272px;
|
|
}
|
|
|
|
.wle-centers__hint {
|
|
font-size: 11px;
|
|
}
|
|
|
|
.wle-centers__list {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.wle-center {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
width: 100%;
|
|
padding: 6px 8px;
|
|
margin-bottom: 2px;
|
|
border: 1px solid transparent;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
font-size: 13px;
|
|
text-align: left;
|
|
color: var(--sycm-text-primary);
|
|
cursor: pointer;
|
|
transition: background 0.12s ease;
|
|
}
|
|
|
|
.wle-center:hover {
|
|
background: var(--sycm-bg-subtle);
|
|
}
|
|
|
|
.wle-center.is-active {
|
|
border-color: rgba(22, 119, 255, 0.45);
|
|
background: rgba(22, 119, 255, 0.08);
|
|
}
|
|
|
|
.wle-center.is-clean .wle-center__name {
|
|
color: var(--sycm-text-tertiary);
|
|
}
|
|
|
|
.wle-center__name {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.wle-center__badge {
|
|
flex: none;
|
|
min-width: 24px;
|
|
padding: 0 6px;
|
|
border-radius: 9px;
|
|
background: rgba(136, 135, 128, 0.12);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
line-height: 17px;
|
|
text-align: center;
|
|
color: var(--sycm-text-tertiary);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.wle-center__badge.is-warn {
|
|
background: rgba(255, 77, 79, 0.16);
|
|
color: #cf1322;
|
|
}
|
|
|
|
.wle-detail {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wle-detail__scope {
|
|
margin-left: 8px;
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
color: var(--sycm-text-tertiary);
|
|
}
|
|
|
|
.wle-detail__clear {
|
|
padding: 0;
|
|
border: none;
|
|
background: transparent;
|
|
font-size: 12px;
|
|
color: #1677ff;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wle-detail__hours {
|
|
color: #cf1322;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.wle-detail__soid {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.wle-foot {
|
|
display: block;
|
|
margin-top: 12px;
|
|
font-size: 12px;
|
|
color: var(--sycm-text-tertiary);
|
|
}
|
|
|
|
/* 窄屏:仓库列表改两列网格,明细整行下移 */
|
|
@media (max-width: 1200px) {
|
|
.wle-body {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.wle-centers {
|
|
width: 100%;
|
|
}
|
|
|
|
.wle-centers__list {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 2px 10px;
|
|
}
|
|
}
|
|
|
|
/* ── 异常通知(每天推 Excel 到钉钉群)───────────────────────────── */
|
|
.wle-notify__row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.wle-notify__label {
|
|
flex: 0 0 88px;
|
|
font-size: 13px;
|
|
color: var(--sycm-text-secondary);
|
|
}
|
|
|
|
.wle-notify__last {
|
|
padding: 8px 10px;
|
|
border-radius: 6px;
|
|
background: var(--sycm-fill-quaternary, rgba(0, 0, 0, 0.02));
|
|
font-size: 12px;
|
|
}
|
|
|
|
.wle-notify__error {
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.wle-notify__help {
|
|
font-size: 12px;
|
|
line-height: 1.8;
|
|
color: var(--sycm-text-secondary);
|
|
}
|
|
|
|
.wle-notify__help code {
|
|
padding: 0 4px;
|
|
border-radius: 3px;
|
|
background: var(--sycm-fill-quaternary, rgba(0, 0, 0, 0.04));
|
|
}
|
|
|
|
.wle-notify__preview {
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--sycm-border, rgba(0, 0, 0, 0.08));
|
|
border-radius: 8px;
|
|
background: var(--sycm-fill-quaternary, rgba(0, 0, 0, 0.02));
|
|
font-size: 12px;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.wle-notify__preview-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
|
|
.wle-notify__preview pre {
|
|
margin: 8px 0 0;
|
|
padding: 8px 10px;
|
|
border-radius: 6px;
|
|
background: var(--sycm-bg-container, #fff);
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
font-size: 12px;
|
|
line-height: 1.7;
|
|
/* 30 条明细大概 40 行,别把整个弹窗撑爆;内部滚动看全 */
|
|
max-height: 280px;
|
|
overflow: auto;
|
|
}
|