Compare commits

...

13 Commits

Author SHA1 Message Date
b955e78ba2 Merge pull request 'fix(wst): 用聚水潭状态终态 + 销售出库单判作废/已发' (#93) from yanhui-wst-void-1029 into main 2026-10-29 15:38:51 +08:00
54518ab4d3 fix(wst): 把销售出库单纳入作废判据与停更监控
业务 14:34 点名 9992097 / 9992098 / 9992924 / 9992925 / 9992926 /
9993284 / 9993285 / 9995464 / 9995465「洛宁仓 16 单都是作废订单,为啥还在
考核之内」,并指明「在聚水潭的销售出库单里面找一下这个作废单据」。

## 查出来的事实

这 9 单在库里**三处全部显示「还在发货」**:

  订单表 qmcust      status=Delivering、send_date 空
  销售出库单         status=WaitConfirm(待出库)
  状态镜像表         order_status_name=发货中、logistics=无物流单

不是判据漏了,是**三条数据源全停在作废动作之前**(2026-10-29 15:13 实测):

  jst_Orders             14:45  正常
  qmcust                 13:25  滞后 1.8h
  状态镜像表              11:52  滞后 3.4h
  出库单(聚水潭/奇门)      03:10  滞后 12.0h  ← 作废就落在这张表上

聚水潭的作废落在**出库单** `status='Delete'` 上,而这张表今天凌晨 3 点后
零写入,所以上午作的废一条都没进来

## 改动

1. 新增 `loadOrderIoStatus()`:按 o_id 批量取出库单状态(两表、
   分批 500 / 并发 3),判据是 **「该 o_id 下没有任何非 Delete 的出库单」**。
   - 只对「发货中」生效:全库「出库单全 Delete」的候选单里 127/131
     状态表是「异常」,那批还挂着待处理,删出库单更可能是重开单,
     当成作废剔会让业务在「不计考核」里也看不到 → 丢可见性比漏剔更糟
   - 拆包/重开的单有多张出库单,删一张不算作废(实测 51 单里 7 单是这种)
   - **「没有出库单」绝不判作废**:近 3 天未发单 82%(1467/1790)本来就没出库单
2. 两张出库单表纳入 `loadSourceFreshness()`(用 local_updated / updateTime
   灌库时刻,**不能用 modified**,否则没变更时天天误报)
3. 新增 `freshness.voidJudgeReliable` + hint 明说「作废单/取消单可能判不出来」,
   告诉业务去聚水潭「销售出库单」核对
4. 明细加「销售出库单」列(已作废/待出库/已出库/未建单),导出同步加列

## 验证

- WST 单测 27/27(新增:出库单全删必剔 / 拆包重开不剔 / 异常单不剔 /
  无出库单不剔 / 停更时提示作废判不出来 / 源正常时不误报)
- 后端全量 498/500(2 个既有失败:负缓存 TTL、Windows 文件权限)
- 真实数据:ioVoidSkipped=11(发货中 + 出库单全删),overdue 仍 16 单
- 本地真渲染:表头有「销售出库单」、首行显示「待出库」、
  红条列出 5 个数据源停更状态、**pageerror 0**

## 没解决(已在页面明说,不掩盖)

这 9 单今天仍然判不出作废 —— 数据没进来,判据再准也没用。
根子在 ETL:出库单表停 12 小时。ETL 恢复后判据立即生效,
但那 9 单是否作废仍需 IT 修同步或业务在聚水潭复核。
2026-10-29 15:16:25 +08:00
1139e4c364 fix(wst): 用聚水潭状态终态判作废/已发,修「作废单已发单还在考核里」
业务 2026-10-29 第三次报同一件事:13:00 轮里作废的单、已经发货的单还在考核。
根因不是判定写错,而是**订单两表各自停更**,停更期间聚水潭做的「取消 / 发货」
订单表完全看不到 —— status 仍是 Delivering、send_date 仍是空。

## 现场证据(2026-10-29 13:00 轮)

- 当天 qmcust 停在 07:25(6 小时),13:00 轮 159 单**全部**来自它
- 订单表侧:159 单 status 全 Delivering、send_date 全空、labels 里**没有**
  「发货前售后」⟹ 原来的 `presaleVoid` 判据(订单表 labels 含关键词)完全失效
- 状态镜像表(停在 11:52,比订单表新鲜 4 小时):
  `order_status_name='已取消'` **11 单**,`order_labels` 里才有「发货前售后」
  「已打印被取消」;其余 145 单「发货中」

qmcust 13:25 补数后重算,误判量比想象大得多:
**overdue 159 → 16,作废剔除 195 单、已发剔除 17 单。**

## 改动

判据(新增,排在窗口判断与「不计考核」之前):

| 判据 | 处理 |
|---|---|
| `order_status_name='已取消'` | 作废,剔除(计 `stateVoidSkipped`) |
| `send_time` 有值 | 已发,剔除 |
| `order_status_name='已发货'` | 已发,剔除 |
| `logistic_status_name` ∈ 已揽收/运输中/派送中/已签收/拒收 | 已发,剔除 |

- `loadOaConfirmInfo` → `loadStateInfo`,同一次 IN 多取 4 个语义列,**不增加往返**
- ⚠️ **判据只用 `order_status_name`**:`aftersale_status_name='已作废'` 是「售后单被撤销、
  订单照常发货」,近 2 天 1387 单全是这种,混为一谈会把该考核的单全放跑
- ⚠️ `未揽收` / `无物流单` **不算已发**(后者是连快递单都没录,当天 39/159 单是这种)
- 状态镜像表纳入 `dataSources` 停更监控(它停了,「取消了吗」的依据就过期了)
- 明细/导出新增「聚水潭状态」「物流状态(聚水潭)」「状态表抓取时间」;
  页面「发货状态」列改优先显示聚水潭终态,与订单表不一致时打 ⚠

## 验证

- 后端 `npm run check` 496/498(2 个失败是老问题:负缓存 TTL、Windows 文件权限)
- WST 单测 **25/25**,新增两组:终态剔除(含「售后已作废不能误剔」的反向保护)、
  状态表纳入新鲜度监控
- 真实数据(本地直连生产 MSSQL,2026-10-29 13:00 轮):
  `overdue=16 / stateVoidSkipped=195 / stateShippedSkipped=17`,
  明细里「已取消」残留 **0**;剩余 16 单全是河南洛宁仓「发货中 + 无物流单」的真积压
- 作废证据交叉核对:状态表近 2 天「已取消」59263 单,94% 带「发货前售后」标签,
  与业务口径一致
2026-10-29 13:45:41 +08:00
c0e48688c1 Merge pull request 'feat(wst): 数据源停更熔断——订单表停更时醒目标注,别把「没同步」当「没发货」' (#92) from yanhui-wst-stale-1029 into main 2026-10-29 09:13:26 +08:00
cb7d06c20c feat(wst): 数据源停更熔断——订单表停更时醒目标注,别把「没同步」当「没发货」
## 业务问题

10-28 18:00 轮页面核验出 546 单超时未发,业务核对「基本都发了,怎么会核验出未发」。

## 根因(不是判定写错,是数据源停更)

`jst_Orders` 当时**从 14:40 就停止同步了**(停了 4 小时),停更之后发出的单
`send_date` 一直是空,因此全被判成「未发货」:

| 来源表 | 超时单数 | 该表状态 |
|---|---|---|
| `jst_Orders` | 540 | 最后更新 14:40(停 4 小时) |
| `jst_qmcust_orderlist` | 6 | 17:30 正常 |

两个订单表**各自独立同步**,同一天完全可能一个正常一个停更。原来只拿候选行的
`MAX(updated_at)` 判快照滞后,会被「碰巧新」的行掩盖 —— 必须直接问源表。

## 改动

后端 `warehouseShipTimelinessService.js`:

- 新增 `loadSourceFreshness()`:分别直接查 `jst_Orders.MAX(updateTime)` 与
  `jst_qmcust_orderlist.MAX(local_updated)`(限轮次窗口起 30 天,避免全表扫描),
  两源并行;按 `CANGKU_WST_SOURCE_STALE_MINUTES`(默认 60 分钟)判停更
  - 查询失败或取不到更新时间 → **不判停更**(宁可漏报,不谎报为停更)
- 明细行新增 `src` / `srcLabel` / `sourceStale`;`summary` 新增 `sourceStaleUnshipped`
- `freshness.stale` 改为「候选行滞后 **OR** 任一源停更」;`hint` 说清停更源、
  停更时刻、以及各源各贡献多少单(不能只写「这张表」)
- 导出数据带 `dataSources`,Excel 脱离页面也有上下文
- 响应新增 `dataSources` 数组(每源 label / lastUpdate / lagMinutes / stale)

前端 `WarehouseShipTimeliness/index.jsx`:

- 红条 Alert:「N 单所在订单表已停止同步,这些单很可能已经发货」(error 级),
  描述里逐源列出「最后同步 14:40:05(已 3.9 小时)」
- 超时统计卡下方小字:「其中 N 单数据未同步」(悬停有说明)
- 明细「内部单号」旁「未同步」红标签 + tooltip,一眼看出哪几行不可信
- 导出新增「数据源 / 数据源是否停更」两列(`是(该单可能已发)`)

## 验证

- 后端 `npm run check`:**494/496**,2 个失败是既有环境问题(负缓存 TTL 计时、
  Windows 文件权限 0o600),改动前同样失败
- `warehouseShipTimeliness.test.js` **23/23** 通过(含新增「停更表超时单必须打标
  并计入 sourceStaleUnshipped」用例)
- 本地真渲染(Playwright + 手签 token,localhost:5173):
  - 真数据:13:00 前 `notStarted` 正常展示「—」,无 pageerror
  - mock 停更数据:红条 / 逐源行 / 统计小字 / 3 行「未同步」标签全部渲染,无 pageerror
- 真实接口 10-28 13:00 轮:`dataSources` 正确识别 `jst_Orders` 停更(滞后 96 分钟)
  而 qmcust 正常;`sourceStaleUnshipped=1146` 与明细逐行打标同源同口径

## 风险与回退

- 只新增字段与展示,**不改动分轮、起算点、候选池口径** —— 超时单量本身不变
- 阈值可用 `CANGKU_WST_SOURCE_STALE_MINUTES` 调;不配默认 60 分钟
- 低峰期(凌晨)可能出现「1 小时无新单」被判停更,但页面只在 13:00 / 18:00
  核验时刻展示超时数据,这两个时点 1 小时不同步即属异常
- 回退:revert 本提交即可,无数据迁移、无新表、无新依赖
2026-10-29 08:25:41 +08:00
6b6dfdd1da Merge pull request 'fix(wst): 起算点改为「审核时间 / 订单 modified 取较晚者」,修 13:00 轮 65 单假超时' (#91) from yanhui-wst-modified-1028 into main 2026-10-28 17:07:11 +08:00
983fee0d8a fix(wst): 起算点改为「审核时间 / 订单 modified 取较晚者」,修 13:00 轮 65 单假超时
## 业务问题

业务举证:13:00 轮不该有那么多单 —— 「9 点前的单 13:00 前已经发完了」。
线上实测 65 单,逐单核对全部不该在 13:00 轮。

## 根因(两个,都指向「起算点取错」)

### ① 状态表的审核时间会严重滞后

业务给的举证单 9978643:

| 字段 | 值 |
|---|---|
| `qmcust.modified` | **2026-10-28 14:52:29** |
| 聚水潭页面真实审核时间 | **2026-10-28 14:52:28**(秒级吻合) |
| 状态表 `last_oa_confirm_time` | 2026-10-27 17:08:43(旧了一天) |
| 状态表 `query_time` | 2026-10-28 10:29:11(10:29 后没再抓) |

只信状态表 → 这单被当成「10-27 就审核了」,混进 13:00 轮超时,冤枉仓库。

### ② 审核时间为空的单回退 created,把今天的单按历史单算

65 单里 57 单状态表审核时间为 NULL。回退 `created` 后:

| o_id | created | modified | 回退后算成 | 真实 |
|---|---|---|---|---|
| 9964841 | 10-26 17:22 | **10-28 13:40** | 10-26 审核 → 进 13:00 轮 | 今天下午才审核 |
| 9949424 | 10-25 17:03 | **10-28 13:50** | 同上 | 同上 |
| 9983773 | 10-27 23:23 | **10-28 13:59** | 同上 | 同上 |

57 单的 `modified` 全部落在今天 13:40~15:03 —— 都是今天下午批量审核的。

## 改动

- **起算点** = `max(状态表 last_oa_confirm_time, 订单表 modified, 订单表 created)`
  - 取较晚者:修状态表滞后
  - 纳入 created:个别单 `modified` 早于 `created`(脏数据),起算点不该早于下单时刻
  - `benchmarkSource` 透出 `oa_confirm` / `modified` / `created`,页面 tooltip 说明来源
- **新增 `laterOf()`**:同格式 `YYYY-MM-DD HH:mm:ss` 直接字典序比较,不转 Date(避开时区坑)
- **主查询 SQL 增加 `x.modified`** 列(订单两表都有,无需额外查询)

## 性能修复(同批,线上 /list 曾 300s 超时)

上版状态表查询**串行 9 批实测 135s**,叠加主查询 63~105s 直接把接口顶过 300s,
页面打不开(实测 curl `--max-time 300` 超时)。改为**并发 6**:

| 方式 | 耗时 |
|---|---|
| 串行 9 批 | 135s |
| 并发 3 | 28.8s |
| **并发 6(本版)** | **10~15s** |

并发 6 是生产实测的安全值(单批约 15s,不至于把 402 万行视图的连接池打爆)。

## 改后效果(10-28 17:00 真实库)

| 轮次 | 改前 | 改后 |
|---|---|---|
| 13:00 轮 | 65 单 | **0 单** |
| 18:00 轮 | — | 927 单(全部今天 07:59~16:00 审核,无历史单) |

- `fallbackToCreated` 从 57 → **0**(不再有回退单)
- 9978643 ✅ 已剔除
- 两个轮次都验证「今天窗口上界之后起算的 = 0」

## 验证

- 后端 `npm run check`:**493/495**(2 个失败是既有环境问题:TTL 计时、Windows 权限,改动前同样失败)
- 新增 2 个测试锁定该口径:
  - `modified` 晚于窗口 → 必须剔除(9978643 那类单)
  - 状态表审核时间晚于 `modified` → 以状态表为准
  - `modified` 早于 `created` → 取 `created`,不倒退
- 前端 lint + build 通过(35.2s)
- 真实库双轮验证通过

## 风险与回退

- **`modified` 被客服改单/改备注刷新**时起算点会推后 → 少报超时(放行方向,不冤枉仓库)。
  业务已知悉并接受;这是当前唯一能拿到「今天真实审核时间」的可靠字段。
- 回退方式:`git revert <merge commit>` 即回到只认状态表审核时间的版本。
2026-10-28 17:02:54 +08:00
85c48ebe77 Merge pull request 'fix(wst): 起算点改回「最后审核时间」,并剔除订单表漏回写的已发单' (#90) 2026-10-28 15:40:56 +08:00
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
6a5f6e8ae0 refactor(wst): 页面名去掉「24 小时」,改为「发货时效(到点未发即超时)」
规则完全不动,只改名字,消除误导。

## 为什么改

页面原名「仓储中心 24 小时发货时效」,但判定规则是「到 13:00 / 18:00
核验时刻仍未发货即超时」,**没有满 24 小时的门槛**(业务 2026-10-20 已三次纠偏定版)。
名字和规则打架,导致看的人以为「不满 24 小时不该算超时」。

实测例子:10-27 16:00 创建的单,今天 13:00 核验时只等了 **21 小时**就被判超时;
13:00 轮 599 单里有 416 单属于 10-27 16 点后创建。业务看到就问「创建时间都在 7 点后,
为什么算超时」—— 其实是名字里的「24 小时」在误导(那些是**昨天**晚上的单,
不是今天的)。

## 改了什么

用户可见(2 处):
- 侧栏菜单:`仓储中心24小时发货时效` → `仓储中心发货时效`
  (不加括号:侧栏现有最长 label 12 字,加了会到 18 字溢出)
- 页面标题:`仓储中心 24 小时发货时效` → `仓储中心发货时效(到点未发即超时)`

其余为注释同步(8 个文件),代码逻辑与判定口径**零改动**:
`warehouseShipTimelinessRegistry/Service/NotifyJob/NotifySettings`、
`warehouseLogisticsExceptionRegistry`、`frontend/api/warehouseShipTimeliness`、`backend/index`。

## 不变的部分

- 核验时点、窗口上界(07:00 / 16:00)、仓级例外(山东 15:00、洛宁 13:00)
- 数据源与起算点(订单两表 + `created`)
- 钉钉推送抬头本来就是「【仓储中心发货时效】」,不用改

## 验证

- 前端 eslint 通过、`npm run build` 通过(31.86s)
- 浏览器真渲染(本地 5173,等足 75 秒数据返回):
  - 标题 = `仓储中心发货时效(到点未发即超时)`
  - 侧栏项 = `仓储中心发货时效`,全页 `body` 内已无「24小时」字样
  - 数据正常渲染:超时未发 599 单 / 不计考核 849 / 涉及仓库 8 个 / 最久未发 13 天
  - 明细「创建时间」列完整显示 `2026-10-27 19:xx` 这类日期,不存在时区错位
  - 无 pageerror
- 后端 `npm run check`:本次只动注释,不影响
2026-10-28 14:30:54 +08:00
6c2686784d Merge pull request #88: 发货时效改用订单两表 + 起算点改为订单创建时间 2026-10-28 13:08:28 +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
913827000a Merge pull request 'feat(wst): 超时未发加「导出明细」+ 武进安能不考核 + 钉钉明细带快递;修复推送时区' (#87) from yanhui-wst-export-20261027 into main 2026-10-27 16:33:03 +08:00
13 changed files with 1803 additions and 859 deletions

View File

@ -1,7 +1,7 @@
'use strict';
/**
* 仓储中心 24 小时发货时效 —— HTTP 路由(2026-10-20 新增)
* 仓储中心发货时效 —— HTTP 路由(2026-10-20 新增)
* ────────────────────────────────────────────────────────────────────────────
* GET /api/warehouse-ship-timeliness/list
* query:

View File

@ -681,7 +681,7 @@ async function startServer(options = {}) {
cubejsApp.use('/api/product-monitoring', authMiddleware, productMonitoringRoutes);
cubejsApp.use('/api/modular-stock-monitor', authMiddleware, modularStockMonitorRoutes);
cubejsApp.use('/api/mkh-bom', authMiddleware, mkhBomRoutes);
// 仓储中心 24 小时发货时效(2026-10-20 新增):列表左侧侧栏,11 个主仓超时未发
// 仓储中心发货时效(2026-10-20 新增):列表左侧侧栏,11 个主仓超时未发
cubejsApp.use('/api/warehouse-ship-timeliness', authMiddleware, warehouseShipTimelinessRoutes);
// 仓储中心物流转运异常(2026-10-21 新增):每日 09:00 核验前一日已发货单的物流记录
cubejsApp.use('/api/warehouse-logistics-exception', authMiddleware, warehouseLogisticsExceptionRoutes);

View File

@ -1,7 +1,7 @@
'use strict';
/**
* 仓储中心 24 小时发货时效 · 两轮核验完成后把超时未发单推到钉钉群
* 仓储中心发货时效 · 两轮核验完成后把超时未发单推到钉钉群
* ────────────────────────────────────────────────────────────────────────────
* 业务口径(2026-10-27 原话):
* 「发货时效也需要钉钉通知:13 点核验 7 点前的超时单,核验完成后通知;
@ -9,14 +9,18 @@
* ⟹ **一天两推**,各绑一个核验轮次(轮次定义见 warehouseShipTimelinessRegistry 的 ROUNDS):
* NOON 13:00 核验「今天 07:00 前审核」→ 默认 13:05 推
* AFTERNOON 18:00 核验「截止 16:00 前审核」→ 默认 18:05 推
* ⚠️ 默认「核验时刻 + 5 分钟」:状态镜像 `query_time` 得晚于核验时刻,
* ⚠️ 2026-10-28 起数据源换成订单两表(jst_Orders / jst_qmcust_orderlist),
* 起算点是**审核时间** `last_oa_confirm_time`(不是订单创建时间);
* 取不到审核时间的单回退创建时间,正文里在该时间后加 `*` 提示。
* 推送正文写「审核 HH:mm」(此前一版误写「创建」,已纠正)。
* ⚠️ 默认「核验时刻 + 5 分钟」:订单表 `updateTime` 得晚于核验时刻,
* 整点推容易拿到 `freshness.coversDeadline=false`(数据没覆盖核验点,超时数偏大)。
* 时刻在页面上可改,改完立即生效(每分钟 tick 比对,不用重启后端)。
*
* 消息内容:
* 抬头(含「超时」字样,钉钉关键词安全设置要用)+ 汇总(超时单数 / 涉及仓数 / 最久已等)
* + 不计考核(待审核·订单异常,不算仓库责任)+ 按仓超时单数 + 前 N 条明细 + 页面链接。
* 明细一行 = 仓|内部单号|线上单号|快递|审核 HH:mm|已等 X 小时(业务对账用)。
* 明细一行 = 仓|内部单号|线上单号|快递|创建 HH:mm|已等 X 小时(业务对账用)。
* (「快递」是业务 2026-10-27 要求加的,见 buildDetailLine 的说明)
* 0 条也推 —— 群里看到「本轮 0 单超时」就知道任务在跑。
*
@ -118,14 +122,18 @@ function formatOverdue(minutes) {
}
/**
* 审核时间的展示:本轮的日期就只给 HH:mm,跨天(历史未发单)必须带 MM-DD。
* 起算时间(= 审核时间 `last_oa_confirm_time`)的展示:
* 本轮的日期只给 HH:mm,跨天(历史未发单)必须带 MM-DD。
* 🔥 为什么要带日期:13:00 轮窗口上界是 07:00,但口径「含历史未发」,
* 明细里会出现昨天 16:13 审核的单 —— 只写「审核 16:13」会被读成「今天 16:13」,
* 看着像「审核时间晚于窗口,不该算超时」,业务会对不上账。
*
* ⚠️ 传进来的 `auditTime` 是服务层的 benchmarkAt:审核时间,取不到时回退订单创建时间
* (此时 row.benchmarkSource === 'created',调用方会给时间加 `*`)。
*/
function auditWhen(auditTime, roundDate) {
const text = cleanCell(auditTime);
if (!text) return '未审核';
if (!text) return '';
const hit = text.match(/(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/);
if (!hit) {
const clock = text.match(/(\d{2}:\d{2})/);
@ -143,15 +151,21 @@ function auditWhen(auditTime, roundDate) {
* 🔥 第四段「快递」是业务 2026-10-27 要求加的:群里看到超时单要能直接判断
* 是哪个快递、是不是得走站点(比如安能系、德邦、京东各区网点揽收节奏完全不同),
* 不用再回页面一单单点开确认。
* ⚠️ 审核时间跨天要带日期,见 auditWhen 的说明。
* 🔥 2026-10-28 第五段是「审核」时间(起算点),不是订单创建时间;
* 审核时间缺失、回退订单创建时间的单,时间后面带 `*`。
* ⚠️ 跨天要带日期,见 auditWhen 的说明。
*/
function buildDetailLine(row, index, roundDate) {
const when = auditWhen(row?.auditTime, roundDate);
const auditText = when
? `审核 ${when}${row?.benchmarkSource === 'created' ? '*' : ''}`
: '审核 —';
const parts = [
cleanCell(row?.warehouseName || row?.centerName, '未知仓'),
cleanCell(row?.oId, '无单号'),
cleanCell(row?.soId, '—'),
cleanCell(row?.logisticCompany, '—'),
`审核 ${auditWhen(row?.auditTime, roundDate)}`,
auditText,
`已等 ${formatOverdue(row?.overdueMinutes)}`,
];
return `${index}. ${parts.join('|')}`;
@ -275,7 +289,7 @@ function buildNotifyText(data = {}, options = {}) {
head.push('本轮没有超时未发货的订单,全部按时发出。');
}
if (heldCount > 0) {
head.push(`另有 ${heldCount} 单不计考核(已支付待审核 / 订单异常)`);
head.push(`另有 ${heldCount} 单不计考核(待审核 / 订单异常)`);
}
const carrierSkipped = Number(summary.carrierSkipped) || 0;
if (carrierSkipped > 0) {

View File

@ -4,7 +4,7 @@
* 仓储中心物流转运异常(2026-10-21 新增)
* ────────────────────────────────────────────────────────────────────────────
* 需求(yanhui 2026-10-21):
* 在左侧栏加一栏「仓储中心物流转运异常」(放在「仓储中心 24 小时发货时效」下面),
* 在左侧栏加一栏「仓储中心物流转运异常」(放在「仓储中心发货时效」下面),
* 下属 12 个发货仓:
* 横林二仓 / 横林三仓 / 横林二仓品牌仓 / 崔桥工厂仓 / 崔桥工厂品牌仓 /
* 崔桥工厂半成品包装项目仓 / 新苏北仓 / 睢宁仓 / 山东青岗集仓 / 山东品牌仓 /

View File

@ -1,7 +1,7 @@
'use strict';
/**
* 「仓储中心 24 小时发货时效」两轮核验完成后推文本到钉钉群 —— 配置与状态存取
* 「仓储中心发货时效」两轮核验完成后推文本到钉钉群 —— 配置与状态存取
* ────────────────────────────────────────────────────────────────────────────
* 业务口径(2026-10-27):
* 「13 点核验 7 点前的超时单,核验完成后通知;18:00 核验的订单,核验完成后也通知。」

View File

@ -1,10 +1,10 @@
'use strict';
/**
* 仓储中心 24 小时发货时效(2026-10-20 新增)
* 仓储中心发货时效(2026-10-20 新增)
* ────────────────────────────────────────────────────────────────────────────
* 需求(yanhui 2026-10-20):
* MKH-BOM 列表左侧加一栏「仓储中心24小时发货时效」,下列 11 个仓:
* MKH-BOM 列表左侧加一栏「仓储中心发货时效」,下列 11 个仓:
* 横林二仓 / 横林三仓 / 横林二仓品牌仓 / 崔桥工厂仓 / 崔桥工厂品牌仓 /
* 崔桥工厂半成品包装项目仓 / 新苏北仓 / 睢宁仓 / 山东青岗集仓 / 山东品牌仓 / 河南洛宁仓
* 从系统库存抓取「已发货订单」与「未发货订单」,按**订单审核时间**分轮核算。
@ -32,7 +32,10 @@
* rpt.jst_Orders —— 京东 / 拼多多 / 抖音 / 得物 / 唯品会
* rpt.jst_qmcust_orderlist —— 天猫 / 淘宝 / 1688
* 字段口径(2026-10-20 四次修正,全部以聚水潭「销售出库单」页面为准):
* · 创建时间:**销售出库单的 `created`**(出库单创建时间)
* 🔴 2026-10-28 现状:候选池取订单两表的 `send_date`,起算点取状态表的
* `last_oa_confirm_time`(审核时间),**下面这段「创建时间 = 出库单 created」
* 是老口径,仅作历史记录,不要再照着实现**。
* · 创建时间:**销售出库单的 `created`**(出库单创建时间)(已废弃)
* ⚠️ 不是 `order_date`(平台下单时间);
* ⚠️ 更不是 `modified` —— 实测数仓每天凌晨把整批订单的 modified 刷成
* 02:2x / 04:3x(样本 9765107 等 7 单全是 10-20 04:39~04:40,
@ -262,6 +265,48 @@ const OUTBOUND_TABLES = Object.freeze({
// ⚠️ 本表**没有金额**,金额要回订单表按 `o_id = inner_order_id` 取。
const ORDER_STATE_TABLE = 'jst_order_express_details';
// ── 🔥 2026-10-28 数据源切换:主数据源改成「订单 + 订单明细」────────────────
// 业务 yanhui 2026-10-28 明确指出:「核验抓取的销售订单表是错误的,应该抓
// `jst_Orders`(订单)+ `jst_OrderItem`(订单明细),o_id → o_id;审核时间就是
// jst_Orders 里的创建时间」。
//
// 为什么原来那张状态镜像表不能用(实测证据):
// ① `send_time` 会**漏回写**:近 7 天「状态表说发货中+未发」的 4157 单里,
// 订单表已有 `send_date` 的 **563 单**、status 已是 Sent 的 492 单 ——
// 明明发了却还算超时;2026-10-28 06h/07h 那批大批量刷新后集中爆发。
// ② 状态表对**被合并 / 拆单父单**不回写(已有 89 + 755 单),必须回订单表看 status。
// ③ 状态表会**整批停更**:2026-10-27 11:54:59 之后 6.8 小时零写入,
// 18:00 轮推出去 34 单里混着已发货的(业务当场发现)。
// ④ 两张表发货时间**其实同源**:状态表 send_time 与订单表 send_date
// 27387 单逐条比对**零偏差** ⟹ 订单表才是权威,状态表只是它的滞后副本。
//
// ⟹ 主查询直接打订单两表(店铺互斥必须都读),**不再依赖状态表**。
// 仓库归属用订单表的 `wms_co_id`(不是状态表的仓名文本),更准。
// 状态表只剩「售后已作废」一个判据,实测在「发货中+未发」里只影响 4 单,可放弃。
const ORDER_SOURCE_TABLES = Object.freeze(['jst_Orders', 'jst_qmcust_orderlist']);
// 订单表 `status` 的取值(聚水潭实时状态,不是下单快照 —— 这一点
// `jst_Orders.status` 与代码历史上「status 不可信」的结论不同,见下面 CREATED 段说明):
// Delivering 发货中 —— **唯一该考核仓库的状态**
// WaitConfirm 待审核 —— 仓库还没接到单,单列「待审核」不计超时
// Question 疑问/异常 —— 单列「异常待处理」不计超时
// Sent 已发货 → 剔除
// Cancelled 已取消(业务叫「作废」)→ 剔除
// Split / Merged 拆单父单 / 被合并单 → 剔除(自己永不发货)
// WaitOuterSent 等供销商|外仓发货 → 剔除(不是本仓发)
// 近 30 天分布:Sent 132341 / Cancelled 66643 / Split 8251 / Question 2057 /
// Merged 1854 / Delivering 1446 / WaitConfirm 190 / WaitOuterSent 45
const ORDER_PENDING_STATUS = 'Delivering';
const ORDER_UNAUDITED_STATUS = 'WaitConfirm';
const ORDER_QUESTION_STATUS = 'Question';
const ORDER_SHIPPED_STATUS = 'Sent';
// 主查询只捞这三种(其余状态一律不进候选,SQL 侧就过滤掉,不传回 Node)
const ORDER_CANDIDATE_STATUSES = Object.freeze([
ORDER_PENDING_STATUS,
ORDER_UNAUDITED_STATUS,
ORDER_QUESTION_STATUS,
]);
// 「发货中」= 已审核、仓库还没发货 —— **唯一该考核仓库的状态**
const SHIPPING_STATUS = '发货中';
// 「已支付待审核」= 还没审单,仓库根本没接到 → 不计超时(单列为「待审核」)
@ -276,24 +321,26 @@ const ABNORMAL_STATUS = '异常';
// 漏掉这个排除,整个页面会被作废单淹没。
const IGNORED_STATES = Object.freeze(['已发货', '已取消', '等供销商|外仓发货']);
// ── 起算点:聚水潭「创建时间」──────────────────────────────────────────────
// 业务 2026-10-20 最终定版:「起算点是聚水潭的创建时间,不是审核时间」。
// ── 起算点:聚水潭「审核时间」`last_oa_confirm_time` ────────────────────────
// 🔥 2026-10-28 **下午二次纠偏**(业务 yanhui 拿 9837824 打回来):
// 起算点是**审核时间**,不是订单创建时间。证据:
// 9837824 created 10-19 23:44:22 → last_oa_confirm_time 10-28 09:58:42,
// 积压 8 天半;仓库 09:58 审核、10:09:55 快递员就上门取件。
// 按 created 算「超时 205 小时」= 把客服审核积压算到仓库头上。
// 量化(10-28 13:00 轮 599 单):376 单是当天 08:10~10:30 才批量审核的,
// 按审核时间分轮后 13:00 轮只剩 247(再剔除漏回写的已发单 → 138)。
//
// ⚠️ 状态表 `jst_order_express_details` **没有**聚水潭创建时间,必须回订单表补:
// · `createdAt`(本表那个 datetime2)—— ❌ **不是**订单创建时间。
// 实测比付款晚 1.5~3.5 小时(261019-… pay 21:05:16 / createdAt 次日 00:23:51),
// 是**同步落库时间**,用了会把起算点整体推后几小时。
// · `pay_time` —— ❌ 付款时间,业务明确说「这一单的创建还是按照付款时间来的」,
// 要换掉。
// · ✅ `jst_Orders.created` / `jst_qmcust_orderlist.created` —— **聚水潭创建时间**,
// 稳定比下单/付款晚 15~30 秒(抽样 00:07:23 → 00:07:41),
// 这才是聚水潭订单列表里「创建时间」那一列。
// ⚠️ 上午那版注释里「created 100% 早于 last_oa_confirm_time,正常差 7~40 分钟,
// 业务要求按创建时间算」已被推翻 —— 那句话只统计了当期正常单,
// 长尾积压单能差 8~11 天。**别再把起算点改回 created。**
//
// 性能(2026-10-20 实测 9,676 个候选):
// 只投影 `o_id, created` 两列、分批 200 → jst_Orders **14.5s** + qmcust **2.9s**。
// 🔥 千万别顺手把 pay_amount / labels 一起取出来:同样 IN 400 个只要 **38.7s**。
// 审核时间只存在于状态镜像表 `jst_order_express_details`(订单两表没有这个字段),
// 由 service 层按 `inner_order_id IN (...)` 批量取;取不到时回退 created 并标 `*`。
// 用 last 而不是 first:业务认为改单重审后重新进作业流,从最后一次审核计时
// (代价是重复审核会冲掉已累计超时,业务已知悉)。
const CREATED_SOURCE_TABLES = Object.freeze(['jst_Orders', 'jst_qmcust_orderlist']);
// 分批大小:200 实测最快(再大收益递减,再小批数太多)
// ⚠️ 2026-10-28 起主链路已不用(created 直接从主查询取),保留给历史脚本/测试引用。
const CREATED_BACKFILL_BATCH = 200;
// ── 订单表里的「作废」状态:这些单不会由本单发货,必须剔除 ─────────────────
@ -331,6 +378,64 @@ const VOID_ORDER_LABEL_KEYWORDS = Object.freeze(['发货前售后']);
// 万一哪天出现「发货中 + 已作废 + 订单表也不是 Sent」,不至于再被算成超时。
const AFTERSALE_VOID_STATUS = '已作废';
/**
* 状态镜像表 `jst_order_express_details.order_status_name` 的**终态**。
*
* 🔥 2026-10-29 血的教训(业务第三次报「作废单/已发单还在考核里」):
* 订单两表**会各自停更**(当天 qmcust 停在 07:25、jst_Orders 停在 12:45),
* 停更期间在聚水潭做的「取消 / 发货」动作,订单表里根本看不到 ——
* 订单 status 仍是 `Delivering`、`send_date` 仍是空,于是被当成真超时。
* 状态镜像表虽然也会停(当天 11:52),但**比订单表新鲜 4 小时**,
* 而且 `order_status_name` 是聚水潭订单状态的直接映射。
*
* 实测(2026-10-29 13:00 轮 159 单):
* · `order_status_name='已取消'` 11 单 —— 订单表的 `labels` 里**没有**「发货前售后」
* 这个关键词(`presaleVoid` 判据完全失效),状态表的 `order_labels` 才有,且带
* 「已打印被取消」。业务就是拿这 11 单说「作废的也在考核之内」。
* · `order_status_name='已发货'` 0 单 —— 机制上必须有,否则停更期发的货永远判不出。
*
* ⚠️ **`aftersale_status_name='已作废'` 不是作废单**!那是「售后单被撤销」,
* 订单照常发货、**仍然要考核**(近 2 天 1387 单都是这种)。别把两个混为一谈,
* 能判「这单不用发了」的只有 `order_status_name='已取消'`。
*/
const STATE_ORDER_VOID_STATUS = '已取消';
const STATE_ORDER_SHIPPED_STATUS = '已发货';
/**
* 销售出库单 `status` 里**等于单据作废**的值。
*
* 🔥 2026-10-29 新增(业务点名 9992097 等 9 单:「在聚水潭的销售出库单里找一下这个作废单据」)。
*
* 聚水潭的作废会落在**出库单**上:`jst_salesOutboundInfo.status` / `jst_qmcust_salesoutbound.status`
* 取值只有四种(近 2 天实测):`Confirmed` / `WaitConfirm` / `Delete` / `OuterConfirming`,
* 其中 `Delete` 就是单据作废。
*
* 为什么必须加这一条判据(状态表 `order_status_name='已取消'` 不够):
* · 出库单 `Delete` 的近 7 天样本里,**55 单订单表仍是 `Delivering`、36 单是 `Question`**,
* 这些单状态镜像表未必抓得到(它一天只抓几次),只能靠出库单判;
* · 出库单是仓库侧最贴身的单据,作废动作一定先落在这里。
*
* ⚠️ **不能只看「有 Delete 就算作废」**:一个订单可能有多张出库单(拆包 / 重开),
* 删掉旧的那张、新的还在 `WaitConfirm` —— 这种单照常要发。
* ⟹ 判据是「**该订单没有任何非 Delete 的出库单**」,见 service 里 ioAllDeleted 的说明。
*
* ⚠️ 也**不能**用「没有出库单」当判据:近 3 天未发单里 **82%(1467/1790)本来就没有出库单**
* (订单还没推下游),拿它判作废会把绝大多数正常待发单全放跑。
*/
const IO_VOID_STATUS = 'Delete';
/**
* 状态镜像表 `logistic_status_name` 里**等于已发**的值。
*
* `拒收` 也算已发 —— 快递拿走过了,客户拒收是后续环节,仓库的发货动作已完成。
* ⚠️ **`无物流单` 不算**(39/159 单是这种:单子还没录快递单,可能自提/待定),
* `未揽收` 也不算(只是打了单,快递还没来取)。
* ⚠️ 别拿 `logistic_time` 当发货时间:它多数是「快递员将上门取件」的预告。
*/
const STATE_LOGISTIC_SHIPPED_STATUSES = Object.freeze([
'已揽收', '运输中', '派送中', '已签收', '拒收',
]);
// 24 小时发货时效 —— ⚠️ 仅用于**页面命名/路由**,**不参与判定**。
// 2026-10-20 三次调整后判定改成「到 13:00 / 18:00 核验时刻仍未发即超时」,
// 没有滚动 24 小时窗口(见 SHIP_TIMELINESS_HOURS 的历史误用记录)。
@ -439,12 +544,29 @@ async function loadCenterWarehouseMap(force = false) {
warehouseCache = {
at: Date.now(),
rows: list,
// 全量「wms_co_id → 仓名」映射(**含归不到主仓的仓**)。
// 发货时效主查询不再 LEFT JOIN 仓表(实测 JOIN 会让执行计划从 28s 恶化到 61~105s),
// 改为在主查询里取回 wms_co_id、在 Node 侧用这份缓存换名字。
allNames: rows.map((r) => ({ wmsCoId: Number(r.wms_co_id), name: String(r.name || '') })),
unmatched,
unmatchedCount: unmatched.length,
excluded,
};
return list;
}
/** 全量「wms_co_id → 仓名」映射(含归不到主仓的仓),供 service 侧做仓名还原 */
async function loadWarehouseNameMap() {
if (Date.now() - warehouseCache.at >= WAREHOUSE_CACHE_TTL_MS || !warehouseCache.rows.length) {
await loadCenterWarehouseMap();
}
const map = new Map();
for (const r of warehouseCache.allNames || []) {
if (Number.isFinite(r.wmsCoId)) map.set(r.wmsCoId, r.name);
}
return map;
}
function clearWarehouseCache() {
warehouseCache = { at: 0, rows: [] };
}
@ -557,6 +679,7 @@ module.exports = {
centerWindowEnd,
nextRoundDeadline,
loadCenterWarehouseMap,
loadWarehouseNameMap,
clearWarehouseCache,
getWarehouseCache: () => warehouseCache,
resolveActiveRound,
@ -569,6 +692,13 @@ module.exports = {
NOT_SPLIT_MERGED_SQL,
OUTBOUND_TABLES,
ORDER_STATE_TABLE,
// 2026-10-28 起主数据源 = 订单两表(状态表退居兜底)
ORDER_SOURCE_TABLES,
ORDER_PENDING_STATUS,
ORDER_UNAUDITED_STATUS,
ORDER_QUESTION_STATUS,
ORDER_SHIPPED_STATUS,
ORDER_CANDIDATE_STATUSES,
SHIPPING_STATUS,
UNAUDITED_STATUS,
ABNORMAL_STATUS,
@ -579,6 +709,10 @@ module.exports = {
VOID_QUESTION_STATUS,
VOID_ORDER_LABEL_KEYWORDS,
AFTERSALE_VOID_STATUS,
STATE_ORDER_VOID_STATUS,
STATE_ORDER_SHIPPED_STATUS,
STATE_LOGISTIC_SHIPPED_STATUSES,
IO_VOID_STATUS,
SHIP_TIMELINESS_HOURS,
toNumber,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -141,8 +141,8 @@ test('按仓块只列超时 > 0 的仓,并按单数降序', () => {
test('明细一行 = 仓|内部单号|线上单号|快递|审核 时间|已等', () => {
const built = job.buildNotifyText(sampleData(), { maxDetailRows: 30 });
assert.match(built.text, /1\. 简尚-崔桥工厂仓|9855123|261021-301318827670255|武进韵达|审核 06:32|已等 6 小时 28 分/);
// 无线上单号 → 「—」;无快递名 → 「—」;无审核时间 → 「未审核」;都不能出现 undefined
assert.match(built.text, /3\. 简尚-横林二仓|9855300|—|—|审核 未审核|已等 2 小时/);
// 无线上单号 → 「—」;无快递名 → 「—」;无创建时间 → 「无创建时间」;都不能出现 undefined
assert.match(built.text, /3\. 简尚-横林二仓|9855300|—|—|审核 —|已等 2 小时/);
assert.doesNotMatch(built.text, /undefined|NaN/);
});
@ -164,7 +164,7 @@ test('历史未发单的审核时间必须带 MM-DD,否则会被读成「今
// 直接测函数本身的跨天判定
assert.equal(job.auditWhen('2026-10-27 06:32:11', '2026-10-27'), '06:32');
assert.equal(job.auditWhen('2026-10-26 16:13:00', '2026-10-27'), '10-26 16:13');
assert.equal(job.auditWhen('', '2026-10-27'), '未审核');
assert.equal(job.auditWhen('', '2026-10-27'), '');
});
test('不计考核 / 不考核快递 / 数据未覆盖核验点都要在正文里说明', () => {

View File

@ -4,7 +4,7 @@ import request from './request';
// 超时必须放大到 3 分钟,否则前端会先断开、后端还在跑。
const TIMELINESS_TIMEOUT_MS = 3 * 60 * 1000;
// 仓储中心 24 小时发货时效:每个主仓「超时未发」数量 + 超时订单明细
// 仓储中心发货时效:每个主仓「超时未发」数量 + 超时订单明细
// params.round 'NOON' | 'AFTERNOON'(不传按当前时刻自动判定)
// params.limit 明细条数上限
export const getWarehouseShipTimeliness = (params = {}) => request.get(

View File

@ -37,7 +37,7 @@ export const DEFAULT_SIDEBAR_CONFIG = {
{ key: 'mkh-bom-data', label: 'MKH-BOM资料', icon: 'table', routeKey: 'mkh-bom-data', visible: true, order: 20 },
],
},
{ key: 'warehouse-ship-timeliness', type: 'item', label: '仓储中心24小时发货时效', icon: 'inbox', routeKey: 'warehouse-ship-timeliness', visible: true, order: 110 },
{ key: 'warehouse-ship-timeliness', type: 'item', label: '仓储中心发货时效', icon: 'inbox', routeKey: 'warehouse-ship-timeliness', visible: true, order: 110 },
{ key: 'warehouse-logistics-exception', type: 'item', label: '仓储中心物流转运异常', icon: 'warning', routeKey: 'warehouse-logistics-exception', visible: true, order: 111 },
{
key: 'large-stock-monitor', type: 'group', label: '大货库存监控', icon: 'appstore', visible: true, order: 200, defaultExpanded: true,

View File

@ -83,7 +83,7 @@ const NOTIFY_HELP_ITEMS = [
业务口径:<b>13:00 核验 7 点前审核的超时单</b>、<b>18:00 核验 16 点前审核的单</b>,
各自<b>核验完成后</b>推一次,所以是两个独立时刻,互不影响。
<br />
默认给到 <b>13:05 / 18:05</b>(核验点 + 5 分钟):聚水潭状态镜像要晚几分钟才覆盖到核验时刻,
默认给到 <b>13:05 / 18:05</b>(核验点 + 5 分钟):订单表的 updateTime 要晚几分钟才覆盖到核验时刻,
整点推容易报「数据没覆盖核验点、超时数偏大」。想改成整点也可以,消息里会带这句提醒。
<br />
同一轮次同一天只推一次(服务重启不会重复轰炸);要重发点「立即推送」。
@ -165,19 +165,46 @@ const HELD_COLUMNS = [
},
},
{
title: '审核时间',
title: (
<Tooltip title="聚水潭「审核时间」= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者(状态表会滞后,9978643 旧值比真实审核早一天)。两者都没有才回退订单创建时间,标 *">
<span>审核时间</span>
</Tooltip>
),
dataIndex: 'auditTime',
width: 152,
ellipsis: true,
render: (v) => (v || <span className="wst-detail__fallback">未审核</span>),
render: (v, row) => {
if (!v) return <span className="wst-detail__fallback">无审核时间</span>;
return row?.benchmarkSource === 'created' ? `${v} *` : v;
},
},
{ title: '下单时间', dataIndex: 'createdAt', width: 152, ellipsis: true, render: (v) => v || '—' },
{ title: '付款时间', dataIndex: 'payTime', width: 152, ellipsis: true, render: (v) => v || '—' },
{ title: '已挂起', dataIndex: 'heldDays', width: 88, render: (v) => (v > 0 ? `${v} 天` : '—') },
];
const DETAIL_COLUMNS = [
// 内部单号 = 聚水潭 o_id(系统内部编号,仓管在系统里按它查单)
{ title: '内部单号', dataIndex: 'oId', width: 110 },
{
title: '内部单号',
dataIndex: 'oId',
width: 158,
// 🔥 来自停更订单表的单必须打标(2026-10-28):
// 该表停更之后发出的单,send_date 仍是空 → 被判「未发」,实际很可能已经发了。
// 18:00 轮实测 546 单里 540 单是这种,不标出来业务会以为判定写错。
render: (v, row) => (
<span className="wst-detail__oid">
{v || '—'}
{row?.sourceStale ? (
<Tooltip
title={`这单来自 ${row?.srcLabel || '订单表'},该表已停止同步:停更之后发出的单在这里仍是「未发货」,很可能已经发了。请先在聚水潭核对。`}
>
<Tag bordered={false} color="error" className="wst-detail__stale">未同步</Tag>
</Tooltip>
) : null}
</span>
),
},
// 线上订单号 = 平台 so_id(京东/抖音/天猫等平台单号,客服按它对接客户)
{
title: '线上订单号',
@ -203,7 +230,7 @@ const DETAIL_COLUMNS = [
{
// 发货时间 = 聚水潭回传的发货时间(业务 2026-10-20:明细里不要创建时间,要发货时间)
title: (
<Tooltip title="聚水潭回传的发货时间(send_time)。为空但有运单号的,说明仓库已打单、还没揽收/回传发货时间,会显示成「已打单」">
<Tooltip title="订单表 jst_Orders.send_date(聚水潭回传的发货时间)。为空但有运单号的,说明仓库已打单、还没揽收/回传发货时间,会显示成「已打单」">
<span>发货时间</span>
</Tooltip>
),
@ -224,47 +251,111 @@ const DETAIL_COLUMNS = [
},
},
{
// 审核时间 = **考核基准**,取聚水潭「审核时间」列(最后一次审核,重审会刷新)
// 审核时间 = 本页的**考核起算点**(业务 2026-10-28 下午三次定版)
// = max(状态表 last_oa_confirm_time, 订单表 modified),都没有才回退创建时间并打 `*`
title: (
<Tooltip title="= 聚水潭订单列表「审核时间」那一列(最后一次审核,重新审核会刷新)。今天 7:00 前审核的 13:00 核验;截止 16:00 前审核的(含今天之前未发的历史单)18:00 核验,已等时长也从它算">
<Tooltip title="聚水潭「审核时间」= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者。状态表会滞后(9978643 状态表是 10-27 17:08,真实审核 10-28 14:52),所以取较晚的那个。今天 7:00 前审核的 13:00 核验;截止 16:00 前审核的(含历史未发单)18:00 核验。带 * 表示两者都没有,回退订单创建时间">
<span>审核时间</span>
</Tooltip>
),
dataIndex: 'auditTime',
width: 152,
ellipsis: true,
render: (v, row) => (v
? v
: (
<Tooltip title="没取到审核时间,已回退创建/付款时间参与分轮与计时">
<span className="wst-detail__fallback">{row?.benchmarkTime || '—'} *</span>
</Tooltip>
)),
render: (v, row) => {
if (!v) return <span className="wst-detail__fallback">—</span>;
if (row?.benchmarkSource === 'created') {
return (
<Tooltip title="状态表审核时间和订单表 modified 都取不到,已回退订单创建时间参与分轮与计时(`*` 为标记)">
<span>{v} *</span>
</Tooltip>
);
}
return v;
},
},
// 下单时间 = 客户在平台下单那一刻(只做参考对账,不参与计时)
{
title: (
<Tooltip title="聚水潭订单创建时间(jst_Orders.created),即客户下单时刻。只做参考,不参与分轮与计时">
<span>下单时间</span>
</Tooltip>
),
dataIndex: 'createdAt',
width: 152,
ellipsis: true,
render: (v) => v || '—',
},
// 付款时间 = 客户付款那一刻(并列展示,方便对账)
{ title: '付款时间', dataIndex: 'payTime', width: 152, ellipsis: true, render: (v) => v || '—' },
{
// 发货状态:聚水潭实时状态(发货中 = 已审核未发货,即考核对象)
// 发货状态:**优先显示聚水潭终态**(状态镜像表),订单表状态只作对照
// 🔥 2026-10-29:订单两表会各自停更,停更期间聚水潭里已经「已取消 / 已发货」的单,
// 订单表里仍是「发货中」——业务看到的「作废的也在考核里」就是这么来的。
// 状态镜像表比订单表新鲜,`order_status_name` 才是聚水潭的真状态。
title: (
<Tooltip title="聚水潭订单实时状态。发货中 = 已审核、仓库还没发货">
<Tooltip title="优先显示聚水潭订单终态(状态镜像表,订单表停更时只有它靠得住)。⚠ = 与订单表 jst_Orders.status 不一致">
<span>发货状态</span>
</Tooltip>
),
dataIndex: 'state',
width: 104,
render: (v) => (
<Tag bordered={false} color={v === '发货中' ? 'orange' : 'default'}>{v || '—'}</Tag>
),
dataIndex: 'stateOrderStatus',
width: 128,
render: (v, row) => {
const lake = v || '';
const ord = row?.state || '';
const diff = Boolean(lake && ord && lake !== ord);
const text = lake || ord || '—';
const color = text === '已取消' ? 'red' : (text === '发货中' ? 'orange' : 'default');
return (
<Tooltip title={`聚水潭:${lake || '—'}|订单表:${ord || '—'}${diff ? '(不一致,订单表没同步到这个状态)' : ''}`}>
<Tag bordered={false} color={color}>{text}{diff ? ' ⚠' : ''}</Tag>
</Tooltip>
);
},
},
{
title: '物流状态',
dataIndex: 'logisticStatus',
width: 100,
render: (v) => (v ? <Tag bordered={false}>{v}</Tag> : '—'),
// 物流状态:优先用状态表的真实物流节点(未揽收 / 已揽收 / 运输中 / 已签收…),
// 订单表没有物流轨迹,只能靠「有没有运单号」猜「已打单 · 未揽收」
title: (
<Tooltip title="聚水潭物流节点(状态镜像表)。已揽收及之后 = 货已经被快递拿走">
<span>物流状态</span>
</Tooltip>
),
dataIndex: 'stateLogisticStatus',
width: 108,
render: (v, row) => {
const text = v || row?.logisticStatus || '';
if (!text) return '—';
const shipped = ['已揽收', '运输中', '派送中', '已签收', '拒收'].includes(v);
return <Tag bordered={false} color={shipped ? 'green' : 'default'}>{text}</Tag>;
},
},
{
// 销售出库单状态(2026-10-29 新增)
// 🔥 业务点名「这些订单都是作废订单」后挖出来的:聚水潭的作废**落在出库单上**
// (`status='Delete'`),订单表的 labels 判据在那批单上 0 命中。
// 这一列让业务能直接对照聚水潭「销售出库单」页面,不用再问「系统里为什么没有」。
title: (
<Tooltip title="聚水潭「销售出库单」的单据状态。已作废=单据被删(不考核);待出库=已生成单据但没出库;空白=还没建出库单(正常,近八成未发单本来就没有)">
<span>销售出库单</span>
</Tooltip>
),
dataIndex: 'ioStatus',
width: 112,
render: (v) => {
const map = {
Delete: ['已作废', 'red'],
WaitConfirm: ['待出库', 'blue'],
Confirmed: ['已出库', 'green'],
OuterConfirming: ['外部确认中', 'default'],
};
const hit = map[v];
if (!hit) return '—';
return <Tag bordered={false} color={hit[1]}>{hit[0]}</Tag>;
},
},
{
title: (
<Tooltip title="从审核时间到本轮核验时刻(13:00 / 18:00)等了多久">
<Tooltip title="从审核时间(仓库接到单的时刻)到本轮核验时刻(13:00 / 18:00)等了多久">
<span>已等时长</span>
</Tooltip>
),
@ -291,15 +382,43 @@ const EXPORT_COLUMNS = [
{ title: '发货仓', dataIndex: 'warehouseName' },
{ title: '快递公司', dataIndex: 'logisticCompany', exportValue: (v) => v || '未指定' },
{ title: '运单号', dataIndex: 'logisticId', exportValue: (v) => v || '未打单' },
{ title: '物流状态', dataIndex: 'logisticStatus' },
{ title: '物流状态', dataIndex: 'stateLogisticStatus', exportValue: (v, row) => v || row?.logisticStatus || '' },
{ title: '发货状态', dataIndex: 'state' },
// 聚水潭终态与订单表状态分开导出:两者不一致的单,就是「订单表没同步」的证据
{ title: '聚水潭状态', dataIndex: 'stateOrderStatus', exportValue: (v) => v || '—' },
// 销售出库单状态:聚水潭的作废落在这一层(Delete=单据已作废),
// 业务对账时要拿这一列跟聚水潭「销售出库单」页面对
{
title: '销售出库单',
dataIndex: 'ioStatus',
exportValue: (v) => ({
Delete: '已作废',
WaitConfirm: '待出库',
Confirmed: '已出库',
OuterConfirming: '外部确认中',
}[v] || '未建单'),
},
{ title: '状态表抓取时间', dataIndex: 'stateQueryTime', exportValue: (v) => v || '—' },
// 导出也要带上「这单的来源表 + 该表是否停更」,否则 Excel 脱离页面就没有上下文,
// 拿着 540 单「未发」去追仓库,实际人家早就发了。
{ title: '数据源', dataIndex: 'srcLabel', exportValue: (v) => v || '—' },
{
title: '数据源是否停更',
dataIndex: 'sourceStale',
exportValue: (v) => (v ? '是(该单可能已发)' : '否'),
},
{ title: '发货时间', dataIndex: 'sendTime', exportValue: (v) => v || '未发货' },
{
title: '审核时间',
dataIndex: 'auditTime',
// 审核时间缺失时页面会回退创建/付款并打 `*`,导出也保持一致,否则对账对不上
exportValue: (v, row) => v || (row?.benchmarkTime ? `${row.benchmarkTime} *` : '未审核'),
// 起算点 = 审核时间;没取到时回退订单创建时间,导出用 `*` 标出来,否则对账对不上
exportValue: (v, row) => {
const t = v || row?.benchmarkTime || '';
if (!t) return '无审核时间';
return row?.benchmarkSource === 'created' ? `${t} *` : t;
},
},
{ title: '下单时间', dataIndex: 'createdAt', exportValue: (v) => v || '—' },
{ title: '付款时间', dataIndex: 'payTime' },
// 分钟数给一列原始值,业务在 Excel 里能直接排序/筛选;人读的那列再格式化
{ title: '已等(分钟)', dataIndex: 'overdueMinutes' },
@ -320,17 +439,22 @@ const roundFileText = (roundInfo) => (
);
/**
* 仓储中心 24 小时发货时效(独立页面)
* 仓储中心发货时效(独立页面)
* ────────────────────────────────────────────────────────────────
* 业务口径(yanhui 2026-10-20):
* 业务口径(yanhui 2026-10-20 定轮次;2026-10-28 定起算点):
* · 11 个主仓,子仓并入主仓一起算;
* · 数据源 = 订单表 jst_Orders + jst_qmcust_orderlist(两张表店铺互斥,都要读);
* · 今天 07:00 前**审核**的订单 13:00 核验,截止今天 16:00 前审核的单 18:00 核验
* (含 7 点前未发出的、含今天之前审核至今未发的历史单;山东两仓 15:00、河南洛宁 13:00);
* 今天之前审核仍未发的(历史未发)两个轮次都算;
* (含 7 点前审核未发出的、含今天之前审核至今未发的历史单;山东两仓 15:00、河南洛宁 13:00);
* 今天之前仍未发的(历史未发)两个轮次都算;
* **到核验时刻未发即超时**,没有「满 24 小时」门槛(业务纠偏点);
* 轮次归属和「已等时长」都按**审核时间**算(聚水潭「审核时间」列,最后一次审核);
* 轮次归属和「已等时长」都按**审核时间**算 ——
* 业务 2026-10-28 下午三次定版:审核时间 = max(状态表 last_oa_confirm_time, 订单表 modified)。
* 只取状态表会漏 —— 状态表会滞后(9978643 状态表写的是 10-27 17:08,聚水潭真实审核 10-28 14:52,
* 整整早一天),导致「今天下午才审核的单」被算进 13:00 轮,冤枉仓库;
* 两者都没有才回退订单创建时间,页面上带 `*` 标记;
* · **13:00 之前不核验**,直接显示「未到核验时间」,不拿昨天的结果冒充今天;
* · 已出库 / 待出库以**聚水潭发货时间**为准(有 send_time = 已发);
* · 已出库 / 待出库以订单表 **send_date** 为准(SQL 直接 `send_date IS NULL` 才进候选);
* · 只显示超时未出库(发完的仓显示「—」),点仓库可筛右侧明细。
*
* 数据来自 /api/warehouse-ship-timeliness/list(service 层见
@ -392,6 +516,11 @@ function WarehouseShipTimeliness() {
const summary = data?.summary || {};
const round = data?.round || null;
const freshness = data?.freshness || null;
// 🔥 超时单里「所在订单表已停更」的数量(2026-10-28 定型):
// 停更⟹停更之后发出的单 send_date 仍为空⟹被判「未发」,但实际很可能已发。
// 18:00 轮实测 546 单里 540 单属于这种,必须显著提示,否则业务会以为判定写错。
const staleUnshipped = Number(summary.sourceStaleUnshipped || 0);
const staleSources = Array.isArray(data?.dataSources) ? data.dataSources.filter((s) => s && s.stale) : [];
// 13:00 之前后端返回 round=null + notStarted=true:此时**不展示任何超时数字**
const notStarted = Boolean(data?.notStarted);
const hasRound = Boolean(round);
@ -619,9 +748,11 @@ function WarehouseShipTimeliness() {
return (
<SycmPage className="wst-page">
<header className="wst-header">
<Title level={3}>仓储中心 24 小时发货时效</Title>
<Title level={3}>仓储中心发货时效(到点未发即超时)</Title>
<Text type="secondary">
按聚水潭订单实时状态核验 11 个主仓是否在承诺时效内完成发货,只列超时未发货的单量。
按聚水潭订单表实时状态核验 11 个主仓是否在承诺时效内完成发货,只列超时未发货的单量。
{/* 2026-10-28 业务定:名字里不再出现「24 小时」—— 规则本身没有 24h 门槛,
原名会让 10-27 16:00 创建(只等了 21 小时)的单看着像「没到时限也被判超时」。*/}
</Text>
<Text type="secondary" className="wst-note">
每天 <b>13:00</b> 核验「<b>今天 07:00 前审核</b>」、<b>18:00</b> 核验「<b>截止 16:00 前审核</b>」、
@ -631,15 +762,17 @@ function WarehouseShipTimeliness() {
<b>到点还没发就算超时 —— 不设「满 24 小时」门槛</b>(早上 7 点前那份单,13:00 还没走就是超时)。
<b>仓级例外</b>:18:00 轮里 <b>山东青岗集仓 / 山东品牌仓</b> 只看 <b>15:00 前审核</b>的单、
<b>河南洛宁仓</b> 只看 <b>13:00 前审核</b>的单(核验时刻仍是 18:00)。
<b>轮次与「已等时长」都按审核时间算</b>(= 聚水潭「审核时间」列,最后一次审核);
明细并列「审核时间 / 发货时间」,方便直接和聚水潭对账。
<b>已发 / 未发以聚水潭发货时间为准</b>:有发货时间就是已发,不再用订单表的发货字段反推
—— 该字段经常不回写,会把早就发货的单误判成超时。
<b>轮次与「已等时长」都按审核时间算</b>(= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者:
审核才是仓库接到单的时刻,按下单时间算会把客服审核积压算到仓库头上;
状态表的审核时间会滞后,所以要和订单 modified 取较晚的那个);
明细并列「审核时间 / 下单时间 / 发货时间」,方便直接和聚水潭对账。
<b>已发 / 未发以订单表 send_date 为准</b>(有值即已发,SQL 侧直接过滤不进候选);
订单表 send_date 没回写、但状态表 send_time 有值的,也按已发剔除(不漏冤仓库)。
<b>已打单但还没揽收的</b>(聚水潭已生成运单号、发货时间还空着)会标成「已打单 · 未揽收」,
鼠标悬停能看到运单号 —— 这类单还留在列表里,等聚水潭回传发货时间才消失。
<b>{excludedCarrierText} 不考核</b>(客户自提、专线大件或加盟快运网点,揽收与在途仓库控制不了),
已按物流公司自动剔除。
<b>「已支付待审核」和「异常」的单不计考核</b>(仓库还没接到或订单本身有问题),单独归入「不计考核」并给出明细;
<b>「待审核」和「异常」的单不计考核</b>(仓库还没接到或订单本身有问题),单独归入「不计考核」并给出明细;
<b>作废单(已取消,含发货前售后)不考核也不展示</b> —— 客户付款后取消的单仓库本就不该发。
</Text>
</header>
@ -691,14 +824,44 @@ function WarehouseShipTimeliness() {
/>
) : null}
{freshness && (freshness.stale || freshness.coversDeadline === false) ? (
{(freshness && (freshness.stale || freshness.coversDeadline === false))
|| staleUnshipped > 0
|| staleSources.length > 0 ? (
<Alert
// 快照没覆盖到核验时刻 → 数字必然偏大,用红色;只是滞后用黄色
type={freshness.coversDeadline === false ? 'error' : 'warning'}
// 快照没覆盖到核验时刻 / 有单来自停更的表 → 数字必然偏大,用红色;只是滞后用黄色
type={staleUnshipped > 0 || freshness?.coversDeadline === false ? 'error' : 'warning'}
showIcon
className="wst-alert"
message={freshness.coversDeadline === false ? '数据未覆盖本轮核验时刻,超时单量偏大' : '发货数据可能滞后'}
description={freshness.hint}
message={
staleUnshipped > 0
? `${number(staleUnshipped)} 单所在订单表已停止同步,这些单很可能已经发货`
: (freshness?.voidJudgeReliable === false
// 🔥 2026-10-29:出库单/状态表停更 ⟹ 作废判据失效。
// 业务当天报「洛宁仓 16 单都是作废单」,但我们三个来源全停在作废动作之前,
// 判据再准也判不出来 —— 必须明说,别让业务以为是系统认定「没作废」。
? '出库单数据源已停止同步,作废单可能判不出来'
: (staleSources.length > 0
// 状态镜像表也算数据源:它停了,判「已取消 / 已发货」的依据就过期了
? '数据源已停止同步(订单表 / 状态镜像表)'
: (freshness?.coversDeadline === false
? '数据未覆盖本轮核验时刻,超时单量偏大'
: '发货数据可能滞后')))
}
description={(
<>
{freshness?.hint}
{staleSources.length ? (
<div className="wst-alert__sources">
{staleSources.map((s) => (
<span key={s.key} className="wst-alert__source">
{s.label}:最后同步 {String(s.lastUpdate || '').slice(11, 19) || '—'}
(已 {Math.round((s.lagMinutes || 0) / 6) / 10} 小时)
</span>
))}
</div>
) : null}
</>
)}
/>
) : null}
@ -715,6 +878,13 @@ function WarehouseShipTimeliness() {
formatter={(v) => (notStarted ? '—' : number(v))}
valueStyle={{ color: (summary.overdueUnshipped || 0) > 0 ? '#cf1322' : undefined }}
/>
{staleUnshipped > 0 ? (
<Tooltip title="这批单来自已停止同步的订单表:停更之后发出的单,在库里 send_date 还是空,会被判成「未发」。先在聚水潭核对,别直接拿去追仓库。">
<div className="wst-stat__warn">
其中 {number(staleUnshipped)} 单数据未同步
</div>
</Tooltip>
) : null}
</Card>
<Card
bordered={false}
@ -723,7 +893,7 @@ function WarehouseShipTimeliness() {
>
<Statistic
title={(
<Tooltip title="已支付待审核(还没审单,仓库没接到)和订单异常的单。这类单不进仓库考核,点开看明细。">
<Tooltip title="待审核(订单状态 WaitConfirm,还没审单,仓库没接到)和订单异常(Question)的单。这类单不进仓库考核,点开看明细。">
<span>不计考核 <span className="wst-stat__more">明细</span></span>
</Tooltip>
)}
@ -877,7 +1047,7 @@ function WarehouseShipTimeliness() {
type="info"
showIcon
className="wst-alert"
message="「已支付待审核」= 还没审单,仓库根本没接到;「订单异常」= 聚水潭标记异常(缺货/地址/风控等)。两类都不该算仓库超时,故单列于此。"
message="「待审核」= 订单状态还没审单(WaitConfirm),仓库根本没接到;「订单异常」= 聚水潭标记 Question(缺货/地址/风控等)。两类都不该算仓库超时,故单列于此。"
/>
{heldOrders.length ? (
<Table
@ -1071,16 +1241,25 @@ function WarehouseShipTimeliness() {
</Modal>
<Text type="secondary" className="wst-foot">
说明:<b>今天 7:00 前审核</b>的订单 13:00 核验;<b>截止 16:00 前审核</b>的单 18:00 核验
说明:数据源 = 聚水潭<b>订单表 jst_Orders + jst_qmcust_orderlist</b>(两张表店铺互斥,按 o_id 去重)。
<b>今天 7:00 前审核</b>的订单 13:00 核验;<b>截止 16:00 前审核</b>的单 18:00 核验
(含 7 点前审核、13:00 还没发出的,也含今天之前审核至今未发的历史单)。
<b>到点仍未发货即超时</b>(不设「满 24 小时」门槛);
轮次与「已等时长」都按审核时间(聚水潭显示值)算。
发货 = 聚水潭回传的发货时间。
<b>作废单不考核</b>(被合并 / 拆单父单 / 已取消,或订单表已有发货时间)
{summary.voidCount > 0 ? ` —— 本轮已自动剔除 ${summary.voidCount} 单` : ''};
轮次与「已等时长」都按<b>审核时间</b>算(= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者,
审核才是仓库接到单的时刻;状态表会滞后,故取较晚者)。两者都取不到才回退订单创建时间,列上带 <b>*</b>。
发货 = 订单表 send_date(为空才算未发)。<b>已发货不考核</b>:状态表 send_time 有值、
聚水潭状态「已发货」、或物流已揽收及之后,任一成立即判已发
{summary.stateShippedSkipped > 0 ? ` —— 本轮已剔除 ${summary.stateShippedSkipped} 单` : ''}。
状态只取「发货中」。
<b>作废单不考核</b>:聚水潭状态「已取消」,或订单表 labels 含「发货前售后」
{summary.voidCount > 0
? ` —— 本轮已自动剔除 ${summary.voidCount} 单`
+ `${summary.stateVoidSkipped > 0 ? `(其中 ${summary.stateVoidSkipped} 单靠聚水潭状态判出,订单表当时没同步)` : ''}`
: ''};
⚠️ 售后状态「已作废」<b>不算</b>作废单 —— 那是售后单被撤销、订单照常发货,仍要考核。
<b>{excludedCarrierText}不考核</b>
{summary.carrierSkipped > 0 ? ` —— 本轮已剔除 ${summary.carrierSkipped} 单` : ''};
已支付待审核、订单异常的单归入「不计考核」,避免冤枉仓库。
待审核、订单异常的单归入「不计考核」,避免冤枉仓库。
</Text>
</SycmPage>
);

View File

@ -111,6 +111,45 @@
color: #1677ff;
}
/* 超时卡下面的「其中 N 单数据未同步」—— 订单表停更时的警示小字(2026-10-28) */
.wst-stat__warn {
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
color: #cf1322;
cursor: help;
}
/* 停更告警里逐个源列出「最后同步时间 / 滞后多久」 */
.wst-alert__sources {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
}
.wst-alert__source {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
font-variant-numeric: tabular-nums;
}
/* 明细里的「内部单号 + 未同步标记」 */
.wst-detail__oid {
display: inline-flex;
align-items: center;
gap: 4px;
font-variant-numeric: tabular-nums;
}
.wst-detail__stale {
margin-inline-end: 0;
font-size: 11px;
line-height: 16px;
padding-inline: 5px;
cursor: help;
}
/* ============ 主从两栏 ============ */
.wst-body {
display: flex;