业务 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 单), 这类单现在按正常单算超时;如业务反馈,可单独补查。 - 回退:单文件回滚本提交即可,无数据迁移、无配置变更。
649 lines
39 KiB
JavaScript
649 lines
39 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 仓储中心 24 小时发货时效(2026-10-20 新增)
|
||
* ────────────────────────────────────────────────────────────────────────────
|
||
* 需求(yanhui 2026-10-20):
|
||
* MKH-BOM 列表左侧加一栏「仓储中心24小时发货时效」,下列 11 个仓:
|
||
* 横林二仓 / 横林三仓 / 横林二仓品牌仓 / 崔桥工厂仓 / 崔桥工厂品牌仓 /
|
||
* 崔桥工厂半成品包装项目仓 / 新苏北仓 / 睢宁仓 / 山东青岗集仓 / 山东品牌仓 / 河南洛宁仓
|
||
* 从系统库存抓取「已发货订单」与「未发货订单」,按**订单审核时间**分轮核算。
|
||
* 今天 07:00 前审核的订单 → 13:00 核验;**截止今天 16:00 前审核**的单(含今天之前未发的)→ 18:00 核验;
|
||
* 昨天及更早审核、至今未发的(历史未发单)两个轮次都算。
|
||
* (2026-10-20 三次调整:
|
||
* 二次 —— 核验时刻由 12:00/16:00 改为 13:00/18:00;
|
||
* 三次 —— ① **窗口按「审核时间」判**(不是创建时间):业务指出的 9818476
|
||
* (创建 10-18、审核 10-20 13:06)这类下午才审的单,不能进 13:00 轮;
|
||
* ② **取消「满 24 小时才算超时」的门槛**:到核验时刻未发即超时;
|
||
* ③ **历史未发单一律算**,不卡「今天 00:00 起」。
|
||
* 业务原话:「当天 6:30 到 7 点审核的订单 13:00 核验;当天 7 点到
|
||
* 16 点审核的订单 18:00 核验;然后再看历史未发的订单」。)
|
||
* 展示口径:**只显示超时未发**(业务确认)。
|
||
*
|
||
* 业务确认的口径(2026-10-20):
|
||
* ① 子仓归属主仓 —— **连子仓一起算**(如 横林二仓 含京东自营仓/运营C专用仓 等)
|
||
* 例外:「京仓-横林二仓京东云仓」业务指定**排除**,不计入任何主仓(见下)
|
||
* ② 崔桥工厂半成品仓 —— 系统无同名仓,**接入「简尚-崔桥工厂半成品包装项目仓」**(15718196)
|
||
* ③ 展示结构 —— **只显示超时未发**
|
||
* ④ 明细列 —— 要同时给「内部单号」(o_id) 与「线上订单号」(so_id)
|
||
*
|
||
* ── 数据源 ────────────────────────────────────────────────────────────────
|
||
* 订单:两张表**店铺互斥,必须都读**(见 mkhBomService 文件头)
|
||
* rpt.jst_Orders —— 京东 / 拼多多 / 抖音 / 得物 / 唯品会
|
||
* rpt.jst_qmcust_orderlist —— 天猫 / 淘宝 / 1688
|
||
* 字段口径(2026-10-20 四次修正,全部以聚水潭「销售出库单」页面为准):
|
||
* · 创建时间:**销售出库单的 `created`**(出库单创建时间)
|
||
* ⚠️ 不是 `order_date`(平台下单时间);
|
||
* ⚠️ 更不是 `modified` —— 实测数仓每天凌晨把整批订单的 modified 刷成
|
||
* 02:2x / 04:3x(样本 9765107 等 7 单全是 10-20 04:39~04:40,
|
||
* 实际下单在 10-13~10-18),拿它当创建时间会让所有单的时间被抹平;
|
||
* ⚠️ 也不是 `created/createTime/updateTime`(数仓 5 分钟批量灌库时间戳)。
|
||
* 待出库的单在出库单表里还没有记录 → 回退 `order_date` 并在接口里标注来源。
|
||
* · 已出库:销售出库单表能查到该 o_id(有 `io_date`),**或**订单 `send_date`
|
||
* 非空,**或**订单状态已是 `Sent` / `Delivering`。
|
||
* · 待出库:以上三条都不满足。
|
||
* · 发货仓: `wms_co_id`(订单**发货仓**,不是 SKU 的归属仓)
|
||
* ⚠️ 用户原话「从系统库存抓取」—— 本实现取的是**订单维度**的仓库归属,
|
||
* 因为 jst_inventoryInfo 只有「某 SKU 在某仓有多少件」,没有订单记录。
|
||
*
|
||
* ── 仓库归属(按名称包含匹配,最长匹配优先)────────────────────────────────
|
||
* `rpt.jst_warehouseInfo` 实测(2026-10-20):
|
||
* 横林二仓 15033302 + 15301814/15835893/16040225/16132681
|
||
* ⚠️ 15197138「京仓-横林二仓京东云仓」**业务指定排除**(见 EXCLUDED_WAREHOUSE_KEYWORDS)
|
||
* 横林三仓 15043561
|
||
* 横林二仓品牌仓 16132681(**同时命中也属于「横林二仓」,靠最长匹配优先归到品牌仓**)
|
||
* 崔桥工厂仓 15038426 + 15301815/16040410
|
||
* 崔桥工厂品牌仓 15870495
|
||
* 崔桥工厂半成品包装项目仓 15718196(原名「简尚-崔桥工厂半成品包装项目仓」)
|
||
* 新苏北仓 15038347 + 15315031/15301812
|
||
* 睢宁仓 15032603 + 15051907/15051960/15052203/15301813/15386911
|
||
* 山东青岗集仓 15137523 + 15283033/15406664/15835935/16040432/16129076
|
||
* 山东品牌仓 15182705
|
||
* 河南洛宁仓 15539084
|
||
* ⟹ 仓库 ID → 主仓 的映射**在启动时从库表读取并缓存**(不用手写 ID 常量,
|
||
* 避免上游新增子仓后要改代码;新增子仓只要名字里带主仓关键词就会自动归入)。
|
||
* 缓存 TTL 1 小时,可用 clearWarehouseCache() 手动清。
|
||
*
|
||
* ── 时效判定 ──────────────────────────────────────────────────────────────
|
||
* 两个时刻**必须区分**(2026-10-20 二次调整后最容易搞混的地方):
|
||
* `verifyHour` —— 几点核验(13:00 / 18:00)。判定「发了没发」「超时多久」都用它。
|
||
* `windowEndHour` —— 订单窗口上界(07:00 / 16:00)。只核验该时刻**前审核**的单。
|
||
* 取当前已生效且最晚的那一轮;未到 13:00 时无核验轮次:
|
||
* 13:00 → 核验「今天 00:00~07:00 审核」的订单 + 历史未发
|
||
* 18:00 → 核验「截止今天 16:00 前审核」的订单 + 今天之前审核仍未发的(历史未发)
|
||
* ⚠️ 不是「今天 00:00~16:00」这一个自然日切片,历史未发也在内;
|
||
* 山东两仓窗口上界 15:00、河南洛宁 13:00(核验时刻仍是 18:00)。
|
||
* 判定(2026-10-20 三次修正 —— **没有「满 24 小时」门槛**):
|
||
* · 超时未出库 = 到核验时刻仍未发货
|
||
* (出库单表查不到 + `send_time` 为空 + 状态不是 Sent/Delivering)
|
||
* 🔥 第七版错在加了「创建满 24h 才算超时」,导致 13:00 轮永远
|
||
* 看不到当天 7 点那批(才等了 6 小时),业务直接打回。
|
||
* · 已出库 = 出库单表有记录 / send_date 非空 / 状态 Sent、Delivering
|
||
* · 拆单父单(is_split=1)、被合并单(is_merge=1)**一律排除** ——
|
||
* 它们自己永不发货,货由子单/主单发出,算进来就是冤枉仓库。
|
||
* 业务只要「超时未出库」,所以列表只返回待出库的超时单。
|
||
*
|
||
* ── 时区 ──────────────────────────────────────────────────────────────────
|
||
* 订单时间列存的是**东八区本地时间**(实测 `order_date` 与 `created` 相差十几秒,
|
||
* 均为北京时间)。因此 SQL 里统一用 `CAST(GETDATE() AS date)` 取「今天」,
|
||
* 服务器与数据库都在东八区;**不要**在 Node 侧用 UTC 日期做判断。
|
||
*/
|
||
|
||
const cloudDatabase = require('../config/cloudDatabase');
|
||
|
||
// ── 主仓定义(业务给定的 11 个)──────────────────────────────────────────
|
||
// keywords:该主仓在 rpt.jst_warehouseInfo.name 里应包含的关键词(任一命中即归属)
|
||
// 顺序即展示顺序;`match` 用最长关键词优先,所以「横林二仓品牌仓」不会被「横林二仓」抢走。
|
||
const CENTERS = Object.freeze([
|
||
{ key: 'HL2', name: '横林二仓', keywords: ['横林二仓'] },
|
||
{ key: 'HL3', name: '横林三仓', keywords: ['横林三仓'] },
|
||
{ key: 'HL2_BRAND', name: '横林二仓品牌仓', keywords: ['横林二仓品牌仓'] },
|
||
{ key: 'CQ', name: '崔桥工厂仓', keywords: ['崔桥工厂仓'] },
|
||
{ key: 'CQ_BRAND', name: '崔桥工厂品牌仓', keywords: ['崔桥工厂品牌仓'] },
|
||
// 2026-10-20 二次调整:原先「崔桥工厂半成品仓」在仓库表里没有同名仓,只能留空占位;
|
||
// 业务指定用「简尚-崔桥工厂半成品包装项目仓」(wms_co_id=15718196, Id=198) 替代接入。
|
||
// ⚠️ 该 wms_co_id 同时也是配件仓(MKH 配件库存来源),但这里是**订单发货仓**维度,
|
||
// 统计的是「从该仓发出的订单」,与配件库存口径无关,不要混。
|
||
{ key: 'CQ_WIP', name: '崔桥工厂半成品包装项目仓', keywords: ['崔桥工厂半成品'] },
|
||
{ key: 'XSB', name: '新苏北仓', keywords: ['新苏北'] },
|
||
{ key: 'SN', name: '睢宁仓', keywords: ['睢宁'] },
|
||
// 🔥 山东两个仓 18:00 轮的窗口更早(15:00 前,不是 16:00)——
|
||
// 业务 2026-10-20:「山东青岗集仓和山东品牌仓,这 2 个仓库时间不是 16:00 前,
|
||
// 是 15:00 前订单」。核验时刻仍是 18:00,只是卡单的范围窄一小时。
|
||
{ key: 'SD_QGJ', name: '山东青岗集仓', keywords: ['山东青岗集'], windowEndHourByRound: { AFTERNOON: 15 } },
|
||
{ key: 'SD_BRAND', name: '山东品牌仓', keywords: ['山东品牌仓'], windowEndHourByRound: { AFTERNOON: 15 } },
|
||
// 🔥 河南洛宁仓更早:18:00 轮只看 13:00 前创建的单(业务同一条指令)
|
||
{ key: 'HN_LN', name: '河南洛宁仓', keywords: ['河南洛宁'], windowEndHourByRound: { AFTERNOON: 13 } },
|
||
]);
|
||
|
||
// 各主仓在某一轮的窗口上界小时。
|
||
// 返回 null 表示「用轮次默认值」(ROUNDS[].windowEndHour)。
|
||
// ⚠️ 13:00 轮没有例外,全部按 07:00;只有 18:00 轮有三处更早的上界。
|
||
function centerWindowEndHour(centerKey, roundKey) {
|
||
const center = CENTERS.find((c) => c.key === centerKey);
|
||
const override = center && center.windowEndHourByRound
|
||
? center.windowEndHourByRound[roundKey]
|
||
: undefined;
|
||
return Number.isFinite(override) ? override : null;
|
||
}
|
||
|
||
/** 该主仓在这一轮的窗口上界('YYYY-MM-DD HH:mm:00'),没有例外就用轮次默认 */
|
||
function centerWindowEnd(centerKey, round) {
|
||
const hour = centerWindowEndHour(centerKey, round.key);
|
||
if (hour === null) return round.windowEnd;
|
||
return `${round.date} ${String(hour).padStart(2, '0')}:00:00`;
|
||
}
|
||
|
||
// 「横林三仓」与「横林二仓」不含对方,安全;但「崔桥工厂品牌仓」含「崔桥工厂仓」片段,
|
||
// 靠关键词长度倒序匹配即可(见 resolveCenter)。
|
||
const SORTED_KEYWORDS = CENTERS
|
||
.filter((c) => c.keywords.length)
|
||
.flatMap((c) => c.keywords.map((kw) => ({ kw, center: c })))
|
||
.sort((a, b) => b.kw.length - a.kw.length);
|
||
|
||
// ── 排除仓(业务指定,2026-10-20)──────────────────────────────────────────
|
||
// 「京仓-横林二仓京东云仓」(wms_co_id=15197138, Id=129) 虽然名字里带「横林二仓」,
|
||
// 但属于京东云仓代运营,**不纳入横林二仓的时效考核**(业务明确要求排除)。
|
||
// ⚠️ 用完整片段「横林二仓京东云仓」而不是「京东云仓」:
|
||
// 仓库表里还有「京仓-崔桥工厂京东云仓」(15283032)、
|
||
// 「京仓-山东青岗集京东云仓」(15283033),写短了会把它们一起排掉;
|
||
// 也不会误伤「简尚-横林二仓京东仓」(16040225) 与「京仓-横林二仓京东自营仓」(15835893)。
|
||
const EXCLUDED_WAREHOUSE_KEYWORDS = Object.freeze([
|
||
'横林二仓京东云仓',
|
||
]);
|
||
|
||
// 长关键词优先,避免以后加了「XX京东云仓」被短词抢先
|
||
const SORTED_EXCLUSIONS = [...EXCLUDED_WAREHOUSE_KEYWORDS].sort((a, b) => b.length - a.length);
|
||
|
||
// ── 时效轮次(业务给定,2026-10-20 二次调整)──────────────────────────────
|
||
// verifyHour :当天几点开始核验(13:00 / 18:00)
|
||
// windowEndHour :只核验「**审核时间**不晚于该时刻」的订单(07:00 / 16:00)
|
||
// ⚠️ 两者**不是同一个东西**,别再用一个字段兼着当:
|
||
// 旧实现拿 12:00 同时当核验时刻和窗口上界,导致「12:00 轮」实际把
|
||
// 07:00~12:00 创建的单也算进来了,与页面上写的「核验 07:00 前」不一致。
|
||
// ⚠️ 也不能在 windowEnd 之后再叠「满 N 小时」门槛 —— 13:00 轮里 07:00 审核的单
|
||
// 只等了 6 小时,加门槛就永远不出现在超时列表里(第七版被打回的原因)。
|
||
const ROUNDS = Object.freeze([
|
||
{ key: 'NOON', label: '13:00 轮', verifyHour: 13, windowEndHour: 7 },
|
||
{ key: 'AFTERNOON', label: '18:00 轮', verifyHour: 18, windowEndHour: 16 },
|
||
]);
|
||
|
||
// ── 订单窗口下界(含历史的天数,**业务可调**)────────────────────────────
|
||
// 业务口径(2026-10-20 三次纠偏):「截止某时刻创建、至今仍未发货」—— 不按自然日切,
|
||
// 昨天、前天乃至更早挂着没发的单都要算进来(滚动累积的待发池)。
|
||
//
|
||
// ⚠️ 但不能无限往前:rpt 下全是视图、没有索引,扫得越远越慢。2026-10-20 实测
|
||
// (jst_Orders + jst_qmcust 并行,含已发单的窗口全量):
|
||
// 近 7 天 → 约 5s 近 30 天 → 约 45s
|
||
// 近 90 天 → 约 130s 近 365 天 → 约 500s(基本等于页面卡死)
|
||
// 所以默认取 30 天:覆盖「今日 + 近一个月累积未发」,兼顾口径与可用性。
|
||
//
|
||
// 同时实测 28 个目标仓的**全部未发单**:共 1,228 条,其中近 7 天 257、
|
||
// 近 30 天 496、近 90 天 792、更早 436(最早 2025-10-24)。
|
||
// ⟹ 30 天能看到约 40%;要看更久的老单,把下面数字改大,代价是首屏更慢。
|
||
const HISTORY_LOOKBACK_DAYS = 30;
|
||
|
||
// 只排除取消/合并单;Question 仍计入(与 mkhBomService 待发货口径一致)
|
||
//
|
||
// 🔥 Split(拆单):**必须排除**。拆单是「父单被拆成子单」的中间态,父单本身
|
||
// 不会发货(`send_date` 永远为空),子单才是真正进仓发货的那一单。
|
||
// 2026-10-20 实测:不加这个条件,一年前的 Split 父单全被算成「超时未发」,
|
||
// 全库未发 Split 单占比极高 —— 会让页面变成上万条的垃圾列表。
|
||
// ⚠️ 与 mkhBomService 的差异:那边是「预警今天要发的货」,Split 父单不带真实
|
||
// 发货需求,被算进去也无害;这边是「考核仓库时效」,误算就要背锅,必须排除。
|
||
const EXCLUDED_STATUS_SQL = `ISNULL(o.status, N'') NOT IN (N'Cancelled', N'Merged', N'Split')`;
|
||
|
||
// ── 「已发货 / 待发货」状态口径(2026-10-20 四次修正,业务反馈数据错误)────
|
||
// 🔥 之前只排除了 Cancelled/Merged/Split,靠 `send_date IS NULL` 判「未发」,
|
||
// 实测**大量已发货的单被算成超时未发**(业务一眼就看出来了)。原因有三个:
|
||
// ① `send_date` 在部分店铺/平台不回写,订单状态已经是 Sent/Delivering,
|
||
// send_date 却还是 NULL(样本 9838341:Delivering,send_date 空);
|
||
// ② 拆单父单 `status` 不是 'Split' 而是 'Delivering'(父单随子单状态走),
|
||
// `is_split=1` 才是真标记(样本 9838537 就是这样);
|
||
// ③ 合并单 `is_merge=1` 同理,被并入主单,自己永不发货。
|
||
// 订单状态实测分布(2026-10-20,近 7 天,两表合计):
|
||
// Sent 61,833 / Cancelled 22,292 / Split 4,927 / Delivering 2,567 /
|
||
// Question 1,500 / Merged 682 / WaitOuterSent 30 / WaitConfirm 12
|
||
// ⟹ 判定「已发货」= status ∈ (Sent, Delivering) **或** send_date 非空
|
||
// **或** 销售出库单表能查到该 o_id(见 OUTBOUND_TABLES)
|
||
const SHIPPED_STATUS = Object.freeze(['Sent', 'Delivering']);
|
||
|
||
// 待出库候选:把「已发货」状态一并排除。判定顺序是
|
||
// ① SQL 先粗筛(下面这个条件 + send_date IS NULL + 非拆单/合并)
|
||
// ② Node 侧再查销售出库单表精确剔除(有出库记录 = 其实已出库)
|
||
// ③ Node 侧再把 Question(疑问单)分流到「业务挂起」,**不计超时**
|
||
// —— 注意 Question 必须留在候选里,否则统计不出挂起单数;
|
||
// 它只是不进 orders(超时明细),走 heldCount / heldOrders。
|
||
const PENDING_STATUS_SQL = `ISNULL(o.status, N'') NOT IN (N'Cancelled', N'Merged', N'Split', N'Sent', N'Delivering')`;
|
||
|
||
// 拆单父单 / 被合并单:自己**永不发货**,货由子单或主单发出。
|
||
// 只排 status 不够(父单状态会跟着子单变成 Delivering),必须看标记位。
|
||
// ⚠️ is_split / is_merge 是 bit 且可能为 NULL —— 必须 ISNULL 兜底,
|
||
// 否则 `o.is_split = 0` 会把 NULL 行全部丢掉(实测会少掉大半候选)。
|
||
const NOT_SPLIT_MERGED_SQL = `ISNULL(o.is_split, 0) = 0 AND ISNULL(o.is_merge, 0) = 0`;
|
||
|
||
// ── 销售出库单表(聚水潭「销售出库单」页面的数据来源)──────────────────────
|
||
// 业务明确要求(2026-10-20):「已出库 / 待出库」与「创建时间」都以聚水潭
|
||
// **销售出库单**页面为准,不再用订单表的 send_date 猜。
|
||
//
|
||
// 两张表与订单表同构、店铺互斥:
|
||
// rpt.jst_salesOutboundInfo ← jst_Orders(京东/拼多多/抖音/得物/唯品会),103.7 万行
|
||
// rpt.jst_qmcust_salesoutbound ← jst_qmcust_orderlist(天猫/淘宝/1688),39.8 万行
|
||
// 字段口径(2026-10-20 实测确认):
|
||
// · `created` —— **出库单创建时间**(业务在聚水潭出库单页面看到的那个),
|
||
// 时点分布自然(8~23 点均匀,凌晨 4 点有一波补录),可用;
|
||
// · `io_date` —— **出库时间**(真正发货那一刻);
|
||
// · `modified` —— ⚠️ 会被数仓每天凌晨批量刷新(实测全被刷成 02:2x / 04:3x),
|
||
// 与订单表的 modified 一样**不可当作创建时间**;
|
||
// · **两张表都只含「已出库」的单**:103.7 万行里 io_date 为 0 条空值。
|
||
// ⟹ 「待出库」无法从出库单表直接查,只能靠「查不到 = 还没出库」反推。
|
||
const OUTBOUND_TABLES = Object.freeze({
|
||
jst_Orders: 'jst_salesOutboundInfo',
|
||
jst_qmcust_orderlist: 'jst_qmcust_salesoutbound',
|
||
});
|
||
|
||
// ── 聚水潭「订单实时状态」表(2026-10-20 五次修正:按审核时间核算)──────────
|
||
// 🔥 前面三版的根因都在这里:`jst_Orders.status` / `jst_qmcust_orderlist.status`
|
||
// 是**下单那一刻的快照**,之后聚水潭侧取消、售后、审核都不会回写。
|
||
// 实测(2026-10-20):业务反馈「聚水潭查不到」的 6 单,订单表里
|
||
// status 全是 `WaitConfirm`,而在本表里真实状态是 **已取消**(发货前售后);
|
||
// 另有 1 单真实状态是 **异常**。上一版把它们当成「待出库超时」报了出来。
|
||
//
|
||
// `rpt.jst_order_express_details`(402 万行,query_time 准实时,最近一次今天 11:55)
|
||
// 是聚水潭订单状态的准实时镜像,字段语义(近 30 天实测覆盖率):
|
||
// · `order_status_name` —— 聚水潭真实订单状态
|
||
// 已发货 269,991(send_time 100%)|已取消 64,440|异常 6,669
|
||
// 发货中 2,448(first_oa 99.96%、send_time 0)|已支付待审核 833
|
||
// · `first_oa_confirm_time` —— **订单审核时间**(首次 OA 确认)= 业务要的起算点
|
||
// · `last_oa_confirm_time` —— 最后一次审核(重新审核会刷新)
|
||
// · `send_time` —— **发货时间**(已发货单 100% 有值,未发单必为空)
|
||
// · `warehouse_name` —— 发货仓名称(可直接套 CENTERS 关键词匹配)
|
||
// · `logistic_status_name` —— 物流状态(无物流单 / 未揽收 / 运输中 …)
|
||
// ⚠️ 本表**没有金额**,金额要回订单表按 `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 = '发货中';
|
||
// 「已支付待审核」= 还没审单,仓库根本没接到 → 不计超时(单列为「待审核」)
|
||
const UNAUDITED_STATUS = '已支付待审核';
|
||
// 「异常」= 订单异常(缺货/地址/风控等),有审核时间的单列「异常待处理」,不计仓库超时
|
||
const ABNORMAL_STATUS = '异常';
|
||
// 不进考核也不展示的状态:已发货(已发)、已取消(业务侧取消/发货前售后)、
|
||
// 等供销商|外仓发货(不是本仓发)
|
||
// 🔥 「已取消」(业务口头叫**作废单**)必须排除(2026-10-20 业务定:「作废订单不用考核」):
|
||
// 这类单是客户付款后取消 / 发货前售后(标签带「发货前售后」),仓库根本不会发货。
|
||
// 2026-10-20 实测:近 30 天已取消 64,440 单,量级远大于真正该考核的「发货中」2,448 单,
|
||
// 漏掉这个排除,整个页面会被作废单淹没。
|
||
const IGNORED_STATES = Object.freeze(['已发货', '已取消', '等供销商|外仓发货']);
|
||
|
||
// ── 起算点:聚水潭「创建时间」──────────────────────────────────────────────
|
||
// 🔥 2026-10-28 **业务再次确认**:「审核时间是 jst_Orders 这个表里面的创建时间」。
|
||
// 这与 2026-10-20 的最初定版一致(本段下面就是那次的原话),中间一段
|
||
// 因「9818476 创建 10-18、审核 10-20」被临时改成按审核时间,现已改回创建时间。
|
||
//
|
||
// 实测差异(近 7 天 26086 单):`created` **100% 早于** `last_oa_confirm_time`,
|
||
// 正常单只差 7~40 分钟;夜间下单、次日上班才审的单差 6~9 小时(极端的差 2 天)。
|
||
// 影响量化(2026-10-27 18:00 轮):按审核 147 单 / 平均已等 4.5h;
|
||
// 按创建 272 单 / 平均已等 8.5h —— 业务已知悉并要求按创建时间算。
|
||
//
|
||
// 主数据源已经切成订单表(见上面 ORDER_SOURCE_TABLES),`created` 直接从主查询拿,
|
||
// **不再需要 backfill**。
|
||
const CREATED_SOURCE_TABLES = Object.freeze(['jst_Orders', 'jst_qmcust_orderlist']);
|
||
// 分批大小:200 实测最快(再大收益递减,再小批数太多)
|
||
// ⚠️ 2026-10-28 起主链路已不用(created 直接从主查询取),保留给历史脚本/测试引用。
|
||
const CREATED_BACKFILL_BATCH = 200;
|
||
|
||
// ── 订单表里的「作废」状态:这些单不会由本单发货,必须剔除 ─────────────────
|
||
// 业务 2026-10-20 反馈 so_id 5127276315127080023(内部单号 9486414)是作废单,
|
||
// 却出现在超时未发货里。四张表对照后的真相:
|
||
// · 状态表 jst_order_express_details:order_status_name=**发货中**、send_time 空
|
||
// ⟹ 只信状态表的话,这单就是「超时未发」—— 我们就是这么误判的;
|
||
// · 订单表 jst_qmcust_orderlist:status=**Merged**、send_date=2026-09-22 06:54:49;
|
||
// · 销售出库单:两张表**都有**出库记录(io_date 2026-09-22 10:59:55)。
|
||
// ⟹ 根因:状态表的 `send_time` 对**被合并单 / 拆单父单不回写**,
|
||
// 单子早随主单发走了,状态表还停在「发货中」。
|
||
// ⟹ 所以除了状态表的 send_time,还**必须**回订单表看 status 与合并/拆单标记位,
|
||
// 以及查销售出库单(业务最早的口径:有出库单 = 已出库)。
|
||
// 🔥 2026-10-22 补 `Question` —— **但不能塞进这个数组**:
|
||
// 这个数组在「状态分类之前」就判定,会把状态表「异常」的单(订单表常是 Question)
|
||
// 从「异常待处理」清单里一起吞掉(实测 heldCount 472 → 10,少掉 462 单)。
|
||
// ⟹ Question 单独在 `state === '发货中'` 之后判,见 VOID_QUESTION_STATUS。
|
||
const VOID_ORDER_STATUS = Object.freeze(['Merged', 'Split', 'Cancelled']);
|
||
|
||
// 订单表 status=Question(聚水潭后台显示「已作废/问题单」)—— 业务 2026-10-22 确认:
|
||
// so_id 3655421011854634(9823980) / 3658479011191285(9880810) 在后台都已作废,
|
||
// 而状态表 order_status_name 仍停在「发货中」+ send_time 空、售后「暂无售后」
|
||
// ⟹ 上游状态没同步,只信状态表判不出来。仅在「发货中」分支生效,不影响异常清单。
|
||
const VOID_QUESTION_STATUS = 'Question';
|
||
|
||
// 订单表 `labels` 里出现这些关键词,等价于「客户发货前退款/取消」= 作废,不考核仓库。
|
||
// 实测 9823980 的 labels = 「发货前售后,大单待确认,店铺-京东,晚发赔,禁止拆分」;
|
||
// 已取消单(order_status_name='已取消')几乎全都带这个标签,可作交叉验证。
|
||
// ⚠️ `labels` 是长文本列,回订单表补它会让 backfill 变慢(见 service 里的投影说明),
|
||
// 所以只投影一个 `CASE WHEN ... THEN 1 ELSE 0 END` 布尔列,不把原文传回来。
|
||
const VOID_ORDER_LABEL_KEYWORDS = Object.freeze(['发货前售后']);
|
||
|
||
// 状态表 `aftersale_status_name` 的「已作废」—— 售后维度最权威的作废标记。
|
||
// 目前带这个值的单在订单表基本都是 Sent(已被其它判据剔除),这里是防御性兜底:
|
||
// 万一哪天出现「发货中 + 已作废 + 订单表也不是 Sent」,不至于再被算成超时。
|
||
const AFTERSALE_VOID_STATUS = '已作废';
|
||
|
||
// 24 小时发货时效 —— ⚠️ 仅用于**页面命名/路由**,**不参与判定**。
|
||
// 2026-10-20 三次调整后判定改成「到 13:00 / 18:00 核验时刻仍未发即超时」,
|
||
// 没有滚动 24 小时窗口(见 SHIP_TIMELINESS_HOURS 的历史误用记录)。
|
||
const SHIP_TIMELINESS_HOURS = 24;
|
||
|
||
const WAREHOUSE_CACHE_TTL_MS = 60 * 60 * 1000;
|
||
let warehouseCache = { at: 0, rows: [] };
|
||
|
||
function toNumber(value) {
|
||
const n = Number(value);
|
||
return Number.isFinite(n) ? n : 0;
|
||
}
|
||
|
||
function pad2(n) {
|
||
return String(n).padStart(2, '0');
|
||
}
|
||
|
||
// 🔥 北京时间(UTC+8)偏移,分钟
|
||
const BEIJING_OFFSET_MINUTES = 480;
|
||
|
||
/**
|
||
* 取「按北京时间读出来」的当前时刻。
|
||
*
|
||
* 🔥 为什么不能直接 `new Date()`(2026-10-21 生产实测踩坑):
|
||
* 生产容器 `/etc/timezone = Etc/UTC`(date = UTC),而 MSSQL `GETDATE()` 返回**北京时间**,
|
||
* mssql 驱动 `useUTC:false` 又把 DB 值构造成「墙钟当 UTC」的 Date。
|
||
* 于是「服务器 now」比「库里的时间」慢 8 小时 —— **北京时间每天 00:00~08:00,
|
||
* 服务端取到的「今天」是昨天**,轮次窗口 / 核验时刻整体错位
|
||
* (实测 08:04 跑 13:00 轮得 overdueUnshipped=3895,本地北京时区同一轮只有 6 单)。
|
||
*
|
||
* 做法:把真实时刻平移成一个「本地 getter 读出来正好是北京时间」的 Date,
|
||
* 这样 `getHours()/getDate()/formatDateTime()` 与库里的字符串可以直接比较。
|
||
* 本机已是东八区时平移量为 0,行为不变(测试无需改)。
|
||
*
|
||
* @param {Date} [base] 真实时刻,默认 `new Date()`
|
||
* @returns {Date} 本地 getter = 北京时间的 Date
|
||
*/
|
||
function beijingNow(base = new Date()) {
|
||
const shift = BEIJING_OFFSET_MINUTES + base.getTimezoneOffset();
|
||
return shift === 0 ? base : new Date(base.getTime() + shift * 60 * 1000);
|
||
}
|
||
|
||
/**
|
||
* 用「关键词最长匹配」把仓库名归到主仓。
|
||
*
|
||
* ⚠️ **先判排除、再判归属**:被排除的仓(如「京仓-横林二仓京东云仓」)
|
||
* 即使名字里带主仓关键词,也直接返回 null —— 它的订单不计入任何主仓。
|
||
*
|
||
* @returns {object|null} CENTERS 里的元素;无法归属或被排除返回 null
|
||
*/
|
||
function resolveCenter(warehouseName) {
|
||
if (!warehouseName) return null;
|
||
const name = String(warehouseName);
|
||
for (const kw of SORTED_EXCLUSIONS) {
|
||
if (name.includes(kw)) return null;
|
||
}
|
||
for (const item of SORTED_KEYWORDS) {
|
||
if (name.includes(item.kw)) return item.center;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 拉取仓库表并建立「主仓 → wms_co_id 列表」映射。
|
||
* 缓存 1 小时;仓库表变更不频繁,且新增子仓会自动被关键词捞到。
|
||
*/
|
||
async function loadCenterWarehouseMap(force = false) {
|
||
const fresh = Date.now() - warehouseCache.at < WAREHOUSE_CACHE_TTL_MS;
|
||
if (!force && fresh && warehouseCache.rows.length) {
|
||
return warehouseCache.rows;
|
||
}
|
||
// ⚠️ cloudDatabase.query() 直接返回 recordset 数组,不是 {recordset} 对象
|
||
const rows = (await cloudDatabase.query(
|
||
`SELECT Id, name, wms_co_id
|
||
FROM dbo.jst_warehouseInfo`,
|
||
{},
|
||
{ requestTimeout: 60000 }
|
||
)) || [];
|
||
const byCenter = new Map(CENTERS.map((c) => [c.key, []]));
|
||
const unmatched = [];
|
||
const excluded = [];
|
||
for (const row of rows) {
|
||
const name = String(row.name || '');
|
||
// 被业务排除的仓单独记录,方便日后核对「是不是排多了/排少了」
|
||
if (SORTED_EXCLUSIONS.some((kw) => name.includes(kw))) {
|
||
excluded.push({ wmsCoId: Number(row.wms_co_id), name });
|
||
continue;
|
||
}
|
||
const center = resolveCenter(name);
|
||
if (center && byCenter.has(center.key)) {
|
||
byCenter.get(center.key).push({
|
||
wmsCoId: Number(row.wms_co_id),
|
||
warehouseId: Number(row.Id),
|
||
name,
|
||
});
|
||
} else {
|
||
unmatched.push({ wmsCoId: Number(row.wms_co_id), name });
|
||
}
|
||
}
|
||
const list = CENTERS.map((c) => ({
|
||
key: c.key,
|
||
name: c.name,
|
||
placeholder: Boolean(c.placeholder),
|
||
warehouses: byCenter.get(c.key) || [],
|
||
}));
|
||
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: [] };
|
||
}
|
||
|
||
/**
|
||
* 计算「当前已生效」的核验轮次。
|
||
*
|
||
* 13:00 之前 → **未到核验时间,返回 null**。
|
||
* 业务明确要求(2026-10-20 确认):未到核验时刻不做核验,
|
||
* 前端显示「未到核验时间」,**不要回填昨天的数据** ——
|
||
* 昨天已结算的结果留到昨天,拿到今天看容易被当成今天的超时。
|
||
* 13:00~17:59 → 用 13:00 轮(今天 00:00~07:00 **审核** + 历史未发)
|
||
* 18:00 之后 → 用 18:00 轮(截止 16:00 前**审核** + 今天之前未发的历史单)
|
||
*
|
||
* 🔥 「含历史」是 2026-10-20 业务第三次纠偏:
|
||
* 原实现把下界卡在「今天 00:00」,只统计当天新单,把昨天及更早挂着没发的
|
||
* 单全漏掉了。业务口径是「……然后再看历史未发的订单」——
|
||
* 这是一个滚动累积的待发池,不按自然日切。
|
||
*
|
||
* @param {Date} now 北京时间(默认 `beijingNow()`,容器是 UTC 时也安全)
|
||
* @returns {object|null} null 表示当前不在任何核验时段内
|
||
*/
|
||
function resolveActiveRound(now = beijingNow()) {
|
||
const hour = now.getHours();
|
||
const minute = now.getMinutes();
|
||
const minutes = hour * 60 + minute;
|
||
let active = null;
|
||
for (const round of ROUNDS) {
|
||
if (minutes >= round.verifyHour * 60) active = round;
|
||
}
|
||
|
||
// 13:00 前:首轮核验还没开始,不做判定
|
||
if (!active) return null;
|
||
|
||
const y = now.getFullYear();
|
||
const m = pad2(now.getMonth() + 1);
|
||
const d = pad2(now.getDate());
|
||
// 护栏起点(不是业务下界,见 HISTORY_LOOKBACK_DAYS)
|
||
const floor = new Date(y, now.getMonth(), now.getDate() - HISTORY_LOOKBACK_DAYS);
|
||
const floorStr = `${floor.getFullYear()}-${pad2(floor.getMonth() + 1)}-${pad2(floor.getDate())} 00:00:00`;
|
||
return {
|
||
...active,
|
||
// deadline = 核验时刻:判「发了没发 / 超时多久」都用它
|
||
deadline: `${y}-${m}-${d} ${pad2(active.verifyHour)}:00:00`,
|
||
windowStart: floorStr,
|
||
// windowEnd = 订单窗口上界:SQL 的取数上界,**不是** deadline
|
||
windowEnd: `${y}-${m}-${d} ${pad2(active.windowEndHour)}:00:00`,
|
||
// 含历史未发单 —— 前端据此把「统计窗口 00:00~07:00」改成「截止 07:00 前(含历史)」
|
||
includeHistory: true,
|
||
date: `${y}-${m}-${d}`,
|
||
settled: false,
|
||
isPreviousDay: false,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 取「当天两轮」的完整描述(页面顶部展示用),不论是否已到时间。
|
||
*/
|
||
function describeRounds(now = beijingNow()) {
|
||
const y = now.getFullYear();
|
||
const m = pad2(now.getMonth() + 1);
|
||
const d = pad2(now.getDate());
|
||
const date = `${y}-${m}-${d}`;
|
||
const minutes = now.getHours() * 60 + now.getMinutes();
|
||
const floor = new Date(y, now.getMonth(), now.getDate() - HISTORY_LOOKBACK_DAYS);
|
||
const floorStr = `${floor.getFullYear()}-${pad2(floor.getMonth() + 1)}-${pad2(floor.getDate())} 00:00:00`;
|
||
return ROUNDS.map((r) => ({
|
||
key: r.key,
|
||
label: r.label,
|
||
date,
|
||
verifyHour: r.verifyHour,
|
||
deadline: `${date} ${pad2(r.verifyHour)}:00:00`,
|
||
windowStart: floorStr,
|
||
windowEnd: `${date} ${pad2(r.windowEndHour)}:00:00`,
|
||
includeHistory: true,
|
||
active: minutes >= r.verifyHour * 60,
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 下一轮的核验时刻。
|
||
*
|
||
* ⚠️ 2026-10-20 第九版起**不再使用**:轮次归属本身就是按审核时间卡的,
|
||
* 「审核晚于 13:00 的单」根本进不了 13:00 轮,会自然落到 18:00 轮,
|
||
* 不需要额外顺延。函数保留是因为「按下一轮核验时刻算」这个业务表述
|
||
* 未来若用于别的口径(例如按创建时间分轮),逻辑还是这一套。
|
||
*
|
||
* @param {object} round resolveActiveRound / describeRounds 的返回项(需带 date/key)
|
||
* @returns {string} 'YYYY-MM-DD HH:mm:00'
|
||
*/
|
||
function nextRoundDeadline(round) {
|
||
const idx = ROUNDS.findIndex((r) => r.key === round.key);
|
||
if (idx >= 0 && idx + 1 < ROUNDS.length) {
|
||
return `${round.date} ${pad2(ROUNDS[idx + 1].verifyHour)}:00:00`;
|
||
}
|
||
// 当天最后一轮 → 次日首轮
|
||
const [y, m, d] = String(round.date).split('-').map(Number);
|
||
const next = new Date(y, (m || 1) - 1, (d || 1) + 1);
|
||
return `${next.getFullYear()}-${pad2(next.getMonth() + 1)}-${pad2(next.getDate())}`
|
||
+ ` ${pad2(ROUNDS[0].verifyHour)}:00:00`;
|
||
}
|
||
|
||
module.exports = {
|
||
CENTERS,
|
||
ROUNDS,
|
||
HISTORY_LOOKBACK_DAYS,
|
||
EXCLUDED_WAREHOUSE_KEYWORDS,
|
||
resolveCenter,
|
||
centerWindowEndHour,
|
||
centerWindowEnd,
|
||
nextRoundDeadline,
|
||
loadCenterWarehouseMap,
|
||
loadWarehouseNameMap,
|
||
clearWarehouseCache,
|
||
getWarehouseCache: () => warehouseCache,
|
||
resolveActiveRound,
|
||
describeRounds,
|
||
beijingNow,
|
||
BEIJING_OFFSET_MINUTES,
|
||
EXCLUDED_STATUS_SQL,
|
||
SHIPPED_STATUS,
|
||
PENDING_STATUS_SQL,
|
||
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,
|
||
IGNORED_STATES,
|
||
CREATED_SOURCE_TABLES,
|
||
CREATED_BACKFILL_BATCH,
|
||
VOID_ORDER_STATUS,
|
||
VOID_QUESTION_STATUS,
|
||
VOID_ORDER_LABEL_KEYWORDS,
|
||
AFTERSALE_VOID_STATUS,
|
||
SHIP_TIMELINESS_HOURS,
|
||
toNumber,
|
||
};
|