4 Commits

Author SHA1 Message Date
e7e0b96b46 fix(wst): 起算点改回「最后审核时间」,并剔除订单表漏回写的已发单
业务 2026-10-28 下午拿 9837824 指出:页面「创建时间」显示 10-19 23:44,
而该单实际是 10-28 09:58:42 才审核。逐字段核对确认起算点口径错了 ——
上午那版按 jst_Orders.created 算,等于把客服审核积压算到仓库头上。

- 起算点 = 状态表 last_oa_confirm_time(业务选「最后审核时间」);
  取不到审核时间的单回退 created,页面 / 导出 / 钉钉该列打 *
- 已发判定补一条兜底:订单表 send_date 空、但状态表 send_time 有值的按已发剔除
  (实测 144 单里 6 单属此类,且有揽收轨迹)
- 明细与导出列改为「审核时间 / 下单时间 / 发货时间」并列,去掉旧「创建时间」列
- 钉钉明细第五段由「创建」改回「审核」;抬头「核验 XX 前创建」改「前审核」
- 回归测试同步改断言;stub 增加审核时间查询分支(默认空 = 全量回退 created)

实测(10-28 13:00 轮):599 → 144(按审核时间分轮)→ 138(剔除漏回写已发单)。
599 单里 376 单是当天 08:10~10:30 才批量审核的积压单,491 单其实已有运单号。

验收:后端 check 491/493(2 个失败为既有 Windows 权限 / TTL 用例,改动前同样失败);
前端 eslint 通过、build 23.92s 通过;本地真渲染 138 单、表头含「审核时间 / 下单时间」、
回退单带 *、无 pageerror。
2026-10-28 15:34:29 +08:00
776a2cbde2 feat(wst): 发货时效改用订单两表 + 起算点改为订单创建时间
业务 2026-10-28 定版:「核验抓取的销售订单表是错误的,应该在数据库抓
jst_Orders(订单)+ jst_OrderItem(明细),审核时间就是 jst_Orders 里的创建时间」。

## 为什么换数据源(状态镜像表的硬伤,均有实测)

1. `send_time` 漏回写:近 7 天「状态表说发货中+未发」的 4157 单里,
   订单表已有 `send_date` 的 563 单、status 已是 Sent 的 492 单 —— 发了还算超时。
2. 被合并 / 拆单父单不回写:89 + 755 单被冤枉。
3. 会整批停更:2026-10-27 11:54:59 之后 6.8 小时零写入,18:00 轮推出的
   34 单里混着已发货的。
4. 两张表发货时间其实同源:27387 单逐条比对**零偏差** ⟹ 订单表才是权威。

## 改了什么

后端
- 主查询改打 `rpt.jst_Orders` + `rpt.jst_qmcust_orderlist`(店铺互斥,UNION ALL,
  按 o_id 去重),SQL 侧过滤 `send_date IS NULL` + `status IN (Delivering/WaitConfirm/Question)`。
- 起算点由状态表 `last_oa_confirm_time` 改为订单表 `created`;
  实测 created 100% 早于审核时间,正常单差 7~40 分钟,夜间下单次日审的单差 6~9 小时
  —— 换口径后单量与「已等时长」都会变大,业务已知悉。
- 仓库归属:主查询只回 `wms_co_id`,Node 侧用仓表缓存换仓名再走 `resolveCenter()`
  (实测 SQL 里 LEFT JOIN 仓表会把查询从 28s 拖到 61~105s)。
- `OPTION (RECOMPILE)` + 时间字面量:qmcust 不加 RECOMPILE 实测 33~50s,加上 0.4s。
- 删除 `backfillOrderFacts()` 与 `mapLimit()`(起算点/状态/仓名已由主查询一次取全)。
- 不再按 `is_split / is_merge` 过滤 —— 实测 `is_split=1` 恰是拆单**子单**
  (同一 so_id 多行、各自要发货,近 30 天 1096 行),老逻辑 `is_split=0` 会把它们全排掉。
- 钉钉明细第 5 段「审核 HH:mm」→「创建 HH:mm」,无值文案「未审核」→「无创建时间」。

前端
- 明细/挂起清单/导出列「审核时间」→「创建时间」,Tooltip 与说明文案同步改写;
- 顶部说明补「数据源 = 订单两表」「已发未发取 send_date」;
- 「已支付待审核」统一为「待审核」(新数据源里就是 WaitConfirm)。

测试
- 两个测试文件的桩改成「订单两表 UNION」的返回形状,断言改为
  created 起算、候选状态白名单、SQL 必须带 RECOMPILE / 不含状态镜像表 / 不 JOIN 仓表。

## 验证

- `npm --prefix backend run check`:491/493 通过。2 项失败是既有环境问题:
  `resilientSecretStore`(Windows 文件权限 0o666≠0o600)、
  `cloudDatabaseConfigCache` 负缓存 TTL(依赖隧道状态),均与本改动无关,单独跑同样失败。
- 前端 eslint + build 通过。
- 真实库:NOON 1553 单 / AFTERNOON 1866 单(本地 2026-10-28),
  数据质量:无仓名 0 / 时间异常 0 / 含 undefined|null|NaN 0;
  单次查询耗时 35~38s(30 天护栏,冷缓存)。
- 浏览器真渲染(playwright,强制 round=NOON):
  表头 `…发货时间 / 创建时间 / 付款时间…`、轮次提示「核验今天 07:00 前创建的订单」、
  「导出明细」导出 CSV 表头含「创建时间」共 1551 行数据 + 1 行表头,无 pageerror。

## 风险与回退

- 口径变化是可感知的:单量上升,夜间下单/次日审核的单「已等时长」增加 6~9 小时,
  属业务确认后的预期结果。
- 状态表「售后已作废」判据随换源放弃(实测在「发货中+未发」里只命中 4 单),
  这类单现在按正常单算超时;如业务反馈,可单独补查。
- 回退:单文件回滚本提交即可,无数据迁移、无配置变更。
2026-10-28 11:34:45 +08:00
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