3 Commits

Author SHA1 Message Date
f749bcc03c feat(wst): 超时未发加「导出明细」+ 武进安能不考核 + 钉钉明细带快递;修复推送时区
① 导出明细(业务要求:超时未发要能导出核对)
  · 新增 GET /api/warehouse-ship-timeliness/export:不分页、不截断,返回本轮全部行
  · 页面「超时未发货订单」卡片右上角加「导出明细」下拉(Excel / CSV)
  · 选中某个仓时只导该仓(前端按 centerKey 过滤,与后端主仓归属同一套)
  · 导出列比页面多「快递公司 / 运单号」(追单要看有没有打单),单号一律转字符串
    避免 Excel 显示成科学计数法

② 缓存改成「存全量、出口截断」
  · 缓存键去掉 limit(原为企业级教训:WLE 核验缓存早就这么改了)
  · 页面 limit=1000、导出要全量,旧实现两边永不命中 → 每点一次导出重查一遍视图
  · 实测:首次查询 86.3s,导出命中缓存 5ms
  · 顺带修 detailTotal / heldTotal:明细被截断时页面仍显示真实总数,
    不再把「只显示前 1000 条」写成「一共就这么点」

③ 不考核快递追加「武进安能」(业务 2026-10-27)
  · 包含匹配,同时覆盖「武进安能大件」
  · ⚠️ 严格不泛化成「安能」:实测库里带「安能」的有 8 个写法,
    常州安能新 4207 单 / 安能物流 110 单等仍照旧考核(与
    warehouseLogisticsExceptionRegistry 同一口径,那里也写着这句)

④ 钉钉推送明细行在「线上单号」后加「快递」
  · 一行 = 仓|内部单号|线上单号|快递|审核 时间|已等 X 小时
  · 群里直接能判断是哪家快递、要不要走站点,不用回页面逐单点开
  · 「已剔除 N 单不考核快递」的快递清单改成读服务层,别再各写一份

⑤ 修复推送任务时区(生产容器 /etc/timezone = Etc/UTC)
  · 症状:pushShipTimelinessNotify 把裸 new Date() 传给 getShipTimeliness,
    而 service / registry 内部用 getDate()/getHours() 直读 →
    北京 13:05 = UTC 05:05,被判「未到 13:00 核验时间」→ 两轮推送永不触发
  · 改法:rawNow(真实时刻,给 Intl / toISOString)+ bjNow(平移后,
    给本地 getter 与 service 调用)双轨,与 2026-10-21 那次同源问题的修法一致
2026-10-27 16:29:08 +08:00
f766549b1a feat(wst): 发货时效两轮核验完成后各推一次钉钉群
业务原话(2026-10-27):「发货时效也需要钉钉通知:13 点核验 7 点前的超时单,
核验完成后通知;18:00 核验的订单,核验完成后也通知。」

## 做了什么

- 新增 `warehouseShipTimelinessNotifySettings`:配置与状态存取,两个轮次各一个推送时刻
  (NOON 默认 13:05 / AFTERNOON 默认 18:05),加签密钥走 resilientSecretStore。
- 新增 `warehouseShipTimelinessNotifyJob`:正文组装 + 每分钟 tick + 启动后 30 分钟补推窗口。
- 路由:`GET/PUT /api/warehouse-ship-timeliness/notify/settings`、
  `GET .../notify/state`、`POST .../notify/run`(dryRun / probe / force)。
- `index.js` 注册 `startShipTimelinessNotifyScheduler()`。
- 前端:页面右上「钉钉通知」→ 弹窗(开关、两个时刻、webhook、加签密钥、明细条数、
  @所有人、页面链接、受控预览块、上次推送)。顺带把 held 明细弹窗的 `destroyOnClose`
  改成 `destroyOnHidden`(antd 5.25 已弃用)。

## 口径要点

- **一天两推,各绑一个轮次**:13:00 轮核验「今天 07:00 前审核」、18:00 轮核验
  「截止 16:00 前审核」(都含历史未发)。两轮互不影响,两个时刻配成同一个 HH:mm 时两轮都推。
- **默认核验点 + 5 分钟**,不是整点:状态镜像 `query_time` 要略晚于核验时刻,
  整点推容易 `coversDeadline=false`(数据没覆盖核验点、超时数偏大)。
  这种时候消息里会带一行提示,仍照发(业务要的是「核验完就通知」)。
- **去重键 `<北京日期>|<轮次键>`**,一天两轮各自独立;推送成功才写状态,失败不写。
- **0 单超时也推**(群里看到「本轮 0 单」= 任务没挂)。
- **明细一行 = 仓|内部单号|线上单号|审核 时间|已等 X 小时**;
  历史单的审核时间带 MM-DD —— 13:00 轮含历史未发单,只写「审核 16:13」会被读成今天。
- 明细按仓聚集 + 每仓保底 1 条,钉钉 text 超长时从末尾砍。

## 验证

- `backend/test/warehouseShipTimelinessNotify.test.js` 新增 17 条,全过。
- 后端 `check-syntax` 通过;`node --test test/*.test.js` 491 条 / 489 过 / 2 失败,
  失败两条是既有环境问题(`cloudDatabaseConfigCache` 负缓存用例依赖数据库不可达、
  `resilientSecretStore` 的 0600 权限断言在 Windows 恒为 0o666),与本改动无关。
- 前端 `eslint src/pages/WarehouseShipTimeliness src/api/warehouseShipTimeliness.js` 0 错;
  `vite build` 通过(含 `verify-router-lazy`)。
- 本地真渲染:页面右上出现「钉钉通知」,弹窗完整渲染、点「预览正文」出正文块,无 pageerror
  (验收时把 `/notify/**` 全部 mock —— 本地系统库与生产是同一个,避免写坏生产配置)。
- 本地实数据预览(13:00 轮,冷查 46s):超时 15 单 / 4 个仓 / 不计考核 672 / 剔除 1;
  同一时刻请求 18:00 轮返回 `before_deadline`(未到 18:00 不推半成品)。

## 风险与回退

- 默认关闭(`enabled=false`)且未配 webhook 时一切接口只返回 `not_configured`,不会发消息。
- 想彻底停掉:`DISABLE_WST_NOTIFY=true` 或把推送开关关掉;两者都不影响页面本身。
- 回退:删掉 `startShipTimelinessNotifyScheduler()` 一行即可停调度,新增文件无副作用。
2026-10-27 15:43:18 +08:00
893f4d6aca feat: 新增「仓储中心24小时发货时效」侧栏(11 个主仓 · 12:00/16:00 双轮核验)
需求(yanhui 2026-10-20):在 MKH-BOM 列表左侧加一栏「仓储中心24小时发货时效」,
下列 11 个主仓(横林二仓、横林三仓、横林二仓品牌仓、崔桥工厂仓、崔桥工厂品牌仓、
崔桥工厂半成品仓、新苏北仓、睢宁仓、山东青岗集仓、山东品牌仓、河南洛宁仓),
从系统库存抓取已发货/未发货订单,按订单创建时间判定发货是否及时:
· 每天 12:00 核验「当天创建时间 7:00 之前」的订单是否已完成发货
· 每天 16:00 核验「当天创建时间 16:00 之前」的订单是否已完成发货

已确认的三条设计决策:
1. 子仓归属 → 连子仓一起算(京东云仓/自营仓/运营C专用仓/唯品仓等并入主仓)
2. 崔桥工厂半成品仓 → 系统无同名仓,先留空占位(不映射到配件仓 15718196)
3. 展示结构 → 只显示超时未发

实现要点
- 仓库归属用「关键词最长匹配」(不写死 wms_co_id),新增子仓只要名字含主仓关键词
  就自动归入,缓存 1h。实测 28 个子仓全部正确归入 11 个主仓,132 个代发仓零误并。
- 12:00 之前**不做核验**:resolveActiveRound 返回 null,服务层直接返回 notStarted,
  既不查订单表(接口 165ms)也不回填昨天的数据。前端显示「未到核验时间」+
  「12:00 开始首轮核验(核验当日 07:00 前创建的订单)」,11 个仓结构保留、数字显示「—」。
- 订单取数:两张表店铺互斥(jst_Orders = 京东/拼多多/抖音/得物/唯品会,
  jst_qmcust_orderlist = 天猫/淘宝/1688),并行分开查 + Node 侧按 o_id 去重合并,
  用 Promise.allSettled 保证单表失败时另一表数据仍可用(detailDegraded 标记)。
- 三态判定 pending / shippedLate / onTime:send_date 与出库表 io_date 取先到的发货痕迹,
  避免上游 send_date 回写滞后导致「已发的被判成没发」。
- 状态过滤:排除 Cancelled / Merged / **Split**(拆单父单永不发货,不排除会捞出一年前
  1.3 万条历史单),Question 仍计入待发。
- freshness 字段如实报告出库表最新 io_date,前端「发货数据可能滞后」告警,
  不拿一份可能滞后的数据去考核仓库。

排除的两次错误优化(均在代码注释里留了实测数据与复测脚本路径)
1. 用 created 替代 order_date 做窗口过滤(快 10 倍)—— 已证伪:2581 行里完全相等的只有 4 行,
   差 >1h 的 412 行,198 行跨日;典型形态 order_date=10-18 23:58 而 created=10-19 04:00:08
   (上游凌晨 4 点批量补录,created 是灌库时间不是下单时间)。业务口径是「订单创建时间」,
   只有 order_date 正确。
2. 「UNION ALL 一条 SQL 慢 5~8 倍」—— 被后续实测推翻(46.3s vs 并行 48.2s,基本持平)。

验证
- 新增 backend/test/warehouseShipTimeliness.test.js,10 条用例全绿,覆盖:
  最长匹配不误并、子仓命名、山东/河南高频前缀不误并、占位仓、12:00 前返回 null、
  12:00 轮窗口 00:00~07:00、16:00 轮窗口 00:00~16:00、状态排除表、三态分桶互斥、
  跨表重复 o_id 去重、round.windowEnd 必须带出(否则前端渲染成「核验 前」)。
- check-syntax 185 文件通过;全量 node --test 415 项 / 413 过 / 2 fail,
  两条失败均为已知环境因素(负缓存带 TTL 因 .env 有 CLOUD_DB_* 短路;
  resilientSecretStore 的 Windows 权限断言,基线恒失败)。
- 真渲染验收(组件级沙盒 + 真实接口数据):两态均 0 控制台错误;
  已核验态 358 单超时未发、8 个仓有数字、点仓切换选中/再点取消均正常;
  多分辨率验证 1920/1600 侧栏 208px、1440/1366 收窄 176px、列表始终竖向。
2026-10-20 11:23:08 +08:00