fix(wst): 起算点改为「审核时间 / 订单 modified 取较晚者」,修 13:00 轮 65 单假超时 #91
@ -223,6 +223,20 @@ function formatDateTime(value) {
|
||||
return s.replace('T', ' ').replace(/\.\d+Z?$/, '').slice(0, 19);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取两个 `YYYY-MM-DD HH:mm:ss` 里较晚的那个(空值视为不存在)。
|
||||
*
|
||||
* 🔥 2026-10-28 下午:起算点必须在「状态表审核时间」和「订单表 modified」之间取较晚者。
|
||||
* 同格式字符串可以直接字典序比较,不需要转 Date(避免时区/跨天坑)。
|
||||
*/
|
||||
function laterOf(a, b) {
|
||||
const x = String(a || '');
|
||||
const y = String(b || '');
|
||||
if (!x) return y;
|
||||
if (!y) return x;
|
||||
return y > x ? y : x;
|
||||
}
|
||||
|
||||
/** 两个时间文本相差多少分钟(to - from);任一为空返回 0 */
|
||||
function diffMinutes(fromText, toText) {
|
||||
const a = formatDateTime(fromText);
|
||||
@ -294,12 +308,13 @@ async function loadPendingOrders(round) {
|
||||
const rows = await cloudDatabase.querySlow(
|
||||
`SELECT x.o_id, x.so_id, x.shop_name, x.status, x.created, x.pay_date,
|
||||
x.logistics_company, x.l_id, x.presaleVoid, x.src,
|
||||
x.updated_at, x.wms_co_id
|
||||
x.updated_at, x.wms_co_id, x.modified
|
||||
FROM (
|
||||
SELECT o.o_id, o.so_id, o.shop_name, o.status, o.created, o.pay_date,
|
||||
o.logistics_company, o.l_id, o.wms_co_id,
|
||||
${presaleSql} AS presaleVoid,
|
||||
o.updateTime AS updated_at,
|
||||
o.modified,
|
||||
N'jst_Orders' AS src
|
||||
FROM rpt.jst_Orders o WITH (NOLOCK)
|
||||
WHERE o.send_date IS NULL
|
||||
@ -310,6 +325,7 @@ async function loadPendingOrders(round) {
|
||||
c.logistics_company, c.l_id, c.wms_co_id,
|
||||
${presaleSql} AS presaleVoid,
|
||||
c.local_updated AS updated_at,
|
||||
c.modified,
|
||||
N'jst_qmcust_orderlist' AS src
|
||||
FROM rpt.jst_qmcust_orderlist c WITH (NOLOCK)
|
||||
WHERE c.send_date IS NULL
|
||||
@ -352,25 +368,40 @@ async function loadOaConfirmInfo(stateRows) {
|
||||
const map = new Map();
|
||||
if (!ids.length) return map;
|
||||
|
||||
// 🔥 2026-10-28 下午:串行 9 批实测 **135s**(全并行 9 批只要 6s,并行 3 批 28.8s)。
|
||||
// 生产上「主查询 63~105s + 状态表 135s」直接把 /list 顶过 300s 超时,
|
||||
// 页面打不开 —— 所以**必须并发**。并发 6 是实测过的安全值:
|
||||
// 单批约 15s,6 并发后整体 10~15s,且不至于把 402 万行视图的连接池打爆。
|
||||
const BATCH = 600;
|
||||
const CONCURRENCY = 6;
|
||||
const chunks = [];
|
||||
for (let i = 0; i < ids.length; i += BATCH) {
|
||||
// ids 已用 Number.isFinite 过滤过,拼进 IN 是纯数字,无注入面
|
||||
const chunk = ids.slice(i, i + BATCH);
|
||||
const rows = await cloudDatabase.querySlow(
|
||||
`SELECT inner_order_id, first_oa_confirm_time, last_oa_confirm_time, send_time
|
||||
FROM rpt.jst_order_express_details WITH (NOLOCK)
|
||||
WHERE inner_order_id IN (${chunk.join(',')})`,
|
||||
{},
|
||||
{ connectRequestTimeoutMs: 300000 }
|
||||
);
|
||||
for (const r of rows || []) {
|
||||
map.set(Number(r.inner_order_id), {
|
||||
first: formatDateTime(r.first_oa_confirm_time),
|
||||
last: formatDateTime(r.last_oa_confirm_time),
|
||||
// 顺带带回来做「已发」二次兜底:订单表 send_date 也有漏回写的情况
|
||||
// (2026-10-28 实测 144 单里 6 单状态表 send_time 有值、订单表却是空)
|
||||
sendTime: formatDateTime(r.send_time),
|
||||
});
|
||||
chunks.push(ids.slice(i, i + BATCH));
|
||||
}
|
||||
|
||||
for (let i = 0; i < chunks.length; i += CONCURRENCY) {
|
||||
const group = chunks.slice(i, i + CONCURRENCY);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const results = await Promise.all(group.map((chunk) => {
|
||||
// ids 已用 Number.isFinite 过滤过,拼进 IN 是纯数字,无注入面
|
||||
return cloudDatabase.querySlow(
|
||||
`SELECT inner_order_id, first_oa_confirm_time, last_oa_confirm_time, send_time
|
||||
FROM rpt.jst_order_express_details WITH (NOLOCK)
|
||||
WHERE inner_order_id IN (${chunk.join(',')})`,
|
||||
{},
|
||||
{ connectRequestTimeoutMs: 300000 }
|
||||
);
|
||||
}));
|
||||
for (const rows of results) {
|
||||
for (const r of rows || []) {
|
||||
map.set(Number(r.inner_order_id), {
|
||||
first: formatDateTime(r.first_oa_confirm_time),
|
||||
last: formatDateTime(r.last_oa_confirm_time),
|
||||
// 顺带带回来做「已发」二次兜底:订单表 send_date 也有漏回写的情况
|
||||
// (2026-10-28 实测 144 单里 6 单状态表 send_time 有值、订单表却是空)
|
||||
sendTime: formatDateTime(r.send_time),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@ -559,8 +590,23 @@ async function getShipTimeliness(options = {}) {
|
||||
// 状态表的 last_oa_confirm_time,不是订单表的 created(10-19 23:44:22)。
|
||||
// 积压单 created → 审核能差 8~11 天,按 created 算等于把审核积压
|
||||
// 算到仓库头上(详见文件头 9837824 的证据)。
|
||||
// 审核时间取不到(实测 599 单里 209 单)→ 回退 created,并标注来源。
|
||||
//
|
||||
// 🔥 2026-10-28 下午再纠偏:**状态表的审核时间会严重滞后,必须和订单表
|
||||
// `modified` 取较晚者**。铁证(9978643,业务给的):
|
||||
// · qmcust.modified = 2026-10-28 14:52:29
|
||||
// · 聚水潭页面真实审核时间 = 2026-10-28 14:52:28 ← 秒级吻合
|
||||
// · 状态表 last_oa_confirm = 2026-10-27 17:08:43 ← 旧值,早了一天
|
||||
// · 状态表 query_time = 2026-10-28 10:29:11 ← 10:29 后就没再抓过
|
||||
// 只信状态表 → 这单被当成「10-27 就审核了」,混进 13:00 轮超时(冤枉仓库)。
|
||||
// 更惨的是**审核时间为空的单**(13:00 轮 65 单里 57 单):回退 created 后,
|
||||
// 明明是今天 13:40~15:03 才审核的单,被按 10-25~10-27 的下单时间算超时。
|
||||
//
|
||||
// ⟹ 口径:起算点 = max(状态表 last_oa_confirm_time, 订单表 modified),
|
||||
// 两者都没有才回退 created。`modified` 是聚水潭源端的「最后修改时间」,
|
||||
// 审核动作一定会刷新它;因客服改单/改备注而推后的情况属「放行」方向
|
||||
// (少报超时,不冤枉仓库),业务已知悉。
|
||||
const createdAt = formatDateTime(row.created);
|
||||
const modifiedAt = formatDateTime(row.modified);
|
||||
const oaInfo = oaConfirmMap.get(Number(row.o_id));
|
||||
|
||||
// ── ② 已发兜底:状态表说发了,就不再算未发 ─────────────────────────────
|
||||
@ -576,8 +622,14 @@ async function getShipTimeliness(options = {}) {
|
||||
}
|
||||
|
||||
const oaConfirmAt = oaInfo ? (oaInfo.last || '') : '';
|
||||
const benchmarkAt = oaConfirmAt || createdAt;
|
||||
const benchmarkSource = oaConfirmAt ? 'oa_confirm' : 'created';
|
||||
// 起算点 = 审核时间 / 订单 modified / 下单时间 三者取最大。
|
||||
// · 取较晚者是为了修「状态表审核时间滞后」(详见上面 9978643 的证据);
|
||||
// · 把 createdAt 也纳入比较,是因为个别单的 modified 会早于 created(脏数据),
|
||||
// 起算点绝不该早于下单时刻。
|
||||
const benchmarkAt = laterOf(laterOf(oaConfirmAt, modifiedAt), createdAt) || createdAt;
|
||||
let benchmarkSource = 'created';
|
||||
if (benchmarkAt && benchmarkAt === modifiedAt) benchmarkSource = 'modified';
|
||||
else if (benchmarkAt && benchmarkAt === oaConfirmAt) benchmarkSource = 'oa_confirm';
|
||||
const payAt = formatDateTime(row.pay_date);
|
||||
const statusRaw = String(row.status || '');
|
||||
|
||||
|
||||
@ -281,7 +281,20 @@ function stubDb(dbPath, scenario = 'default', oaRows = []) {
|
||||
o(26, WMS.CQ, '2026-10-18 12:00:00', { updated_at: '2026-10-20 11:18:27' }), // 2940 分
|
||||
];
|
||||
|
||||
const rows = scenario === 'noon' ? noonRows : defaultRows;
|
||||
// 「起算点 = max(审核时间, 订单 modified)」专用场景(2026-10-28 下午定版)
|
||||
// 🔥 为什么必须取较晚者:状态表的 last_oa_confirm_time 会滞后。
|
||||
// 实测 9978643:状态表写 10-27 17:08,聚水潭真实审核 10-28 14:52,
|
||||
// 只信状态表 → 把「今天下午才审核的单」算进 13:00 轮,冤枉仓库。
|
||||
const modifiedRows = [
|
||||
// 51:created 06:45(7 点前),但 modified 是今天 14:52 → 起算点 14:52 晚于窗口 07:00 → 剔除
|
||||
o(51, WMS.CQ, '2026-10-20 06:45:00', { modified: '2026-10-20 14:52:29' }),
|
||||
// 52:历史单 created 10-18,状态表无审核时间,modified 今天 06:00 → 算超时,起算点 06:00
|
||||
o(52, WMS.CQ, '2026-10-18 12:00:00', { modified: '2026-10-20 06:00:00' }),
|
||||
// 53:modified 比 created 还早(异常数据)→ 起算点回落到 created
|
||||
o(53, WMS.CQ, '2026-10-20 05:00:00', { modified: '2026-10-19 23:00:00' }),
|
||||
];
|
||||
|
||||
const rows = scenario === 'modified' ? modifiedRows : (scenario === 'noon' ? noonRows : defaultRows);
|
||||
|
||||
const calls = { orders: 0, oaConfirm: 0, warehouse: 0, sql: [], params: [] };
|
||||
|
||||
@ -619,3 +632,36 @@ test('导出行为纯函数:缺字段一律给空串,不导出 undefined', (
|
||||
assert.equal(rows[1].oId, '');
|
||||
assert.doesNotMatch(JSON.stringify(rows), /undefined|null|NaN/u);
|
||||
});
|
||||
|
||||
test('起算点 = max(审核时间, 订单 modified):状态表滞后时以 modified 为准', withStubbedService(async (svc) => {
|
||||
// 13:05 → 13:00 轮,窗口上界 07:00
|
||||
const data = await svc.getShipTimeliness({ now: at(13, 5), limit: 10 });
|
||||
|
||||
// 51 的 modified 是今天 14:52,晚于窗口 07:00 → 必须剔除(这就是 9978643 那类单)
|
||||
assert.ok(!data.orders.some((x) => String(x.oId) === '51'),
|
||||
'modified 晚于窗口的单不能算进 13:00 轮(状态表滞后会把它冤枉成 7 点前审核)');
|
||||
|
||||
// 52:状态表无审核时间,modified 今天 06:00 → 起算点 06:00,不是 created 10-18 12:00
|
||||
const r52 = data.orders.find((x) => String(x.oId) === '52');
|
||||
assert.ok(r52, '52 应算超时(modified 06:00 早于窗口 07:00)');
|
||||
assert.equal(r52.auditTime, '2026-10-20 06:00:00', '起算点取 modified');
|
||||
assert.equal(r52.benchmarkSource, 'modified', '来源必须是 modified');
|
||||
assert.equal(r52.createdAt, '2026-10-18 12:00:00', '下单时间照旧展示');
|
||||
assert.equal(r52.overdueMinutes, 420, '06:00 → 13:00 = 420 分钟(不是按下单时间算的 4380)');
|
||||
|
||||
// 53:modified 早于 created → 取较晚者 = created 05:00
|
||||
const r53 = data.orders.find((x) => String(x.oId) === '53');
|
||||
assert.ok(r53, '53 应算超时');
|
||||
assert.equal(r53.auditTime, '2026-10-20 05:00:00', 'modified 比 created 早时不能倒退,取 created');
|
||||
assert.equal(r53.benchmarkSource, 'created');
|
||||
}, 'modified'));
|
||||
|
||||
test('状态表审核时间比 modified 更晚时,以状态表为准', withStubbedService(async (svc) => {
|
||||
const data = await svc.getShipTimeliness({ now: at(13, 5), limit: 10 });
|
||||
const r52 = data.orders.find((x) => String(x.oId) === '52');
|
||||
assert.ok(r52, '52 应算超时');
|
||||
assert.equal(r52.auditTime, '2026-10-20 06:30:00', '审核时间 06:30 晚于 modified 06:00,取审核时间');
|
||||
assert.equal(r52.benchmarkSource, 'oa_confirm');
|
||||
}, 'modified', [
|
||||
{ inner_order_id: 52, first_oa_confirm_time: new Date(2026, 9, 20, 6, 30, 0), last_oa_confirm_time: new Date(2026, 9, 20, 6, 30, 0), send_time: null },
|
||||
]));
|
||||
|
||||
@ -166,7 +166,7 @@ const HELD_COLUMNS = [
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Tooltip title="聚水潭「审核时间」(last_oa_confirm_time)。带 * 的表示没取到审核时间,已回退订单创建时间">
|
||||
<Tooltip title="聚水潭「审核时间」= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者(状态表会滞后,9978643 旧值比真实审核早一天)。两者都没有才回退订单创建时间,标 *">
|
||||
<span>审核时间</span>
|
||||
</Tooltip>
|
||||
),
|
||||
@ -232,10 +232,10 @@ const DETAIL_COLUMNS = [
|
||||
},
|
||||
},
|
||||
{
|
||||
// 审核时间 = 本页的**考核起算点**(业务 2026-10-28 下午二次定版)
|
||||
// 取状态表 last_oa_confirm_time;取不到回退订单创建时间并打 `*`
|
||||
// 审核时间 = 本页的**考核起算点**(业务 2026-10-28 下午三次定版)
|
||||
// = max(状态表 last_oa_confirm_time, 订单表 modified),都没有才回退创建时间并打 `*`
|
||||
title: (
|
||||
<Tooltip title="聚水潭「审核时间」(last_oa_confirm_time),这才是仓库真正接到单的时刻。今天 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>
|
||||
),
|
||||
@ -246,7 +246,7 @@ const DETAIL_COLUMNS = [
|
||||
if (!v) return <span className="wst-detail__fallback">—</span>;
|
||||
if (row?.benchmarkSource === 'created') {
|
||||
return (
|
||||
<Tooltip title="状态表里取不到这个单的审核时间,已回退订单创建时间参与分轮与计时(`*` 为标记)">
|
||||
<Tooltip title="状态表审核时间和订单表 modified 都取不到,已回退订单创建时间参与分轮与计时(`*` 为标记)">
|
||||
<span>{v} *</span>
|
||||
</Tooltip>
|
||||
);
|
||||
@ -359,9 +359,11 @@ const roundFileText = (roundInfo) => (
|
||||
* (含 7 点前审核未发出的、含今天之前审核至今未发的历史单;山东两仓 15:00、河南洛宁 13:00);
|
||||
* 今天之前仍未发的(历史未发)两个轮次都算;
|
||||
* **到核验时刻未发即超时**,没有「满 24 小时」门槛(业务纠偏点);
|
||||
* 轮次归属和「已等时长」都按**审核时间**(状态表 last_oa_confirm_time)算 ——
|
||||
* 业务 2026-10-28 下午二次定版:仓库只有审核后才会接到单,按下单时间算会把审核积压算到仓库头上;
|
||||
* 审核时间取不到的单回退订单创建时间,页面上带 `*` 标记;
|
||||
* 轮次归属和「已等时长」都按**审核时间**算 ——
|
||||
* 业务 2026-10-28 下午三次定版:审核时间 = max(状态表 last_oa_confirm_time, 订单表 modified)。
|
||||
* 只取状态表会漏 —— 状态表会滞后(9978643 状态表写的是 10-27 17:08,聚水潭真实审核 10-28 14:52,
|
||||
* 整整早一天),导致「今天下午才审核的单」被算进 13:00 轮,冤枉仓库;
|
||||
* 两者都没有才回退订单创建时间,页面上带 `*` 标记;
|
||||
* · **13:00 之前不核验**,直接显示「未到核验时间」,不拿昨天的结果冒充今天;
|
||||
* · 已出库 / 待出库以订单表 **send_date** 为准(SQL 直接 `send_date IS NULL` 才进候选);
|
||||
* · 只显示超时未出库(发完的仓显示「—」),点仓库可筛右侧明细。
|
||||
@ -666,8 +668,9 @@ 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>(聚水潭 last_oa_confirm_time:
|
||||
审核才是仓库接到单的时刻,按下单时间算会把客服审核积压算到仓库头上);
|
||||
<b>轮次与「已等时长」都按审核时间算</b>(= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者:
|
||||
审核才是仓库接到单的时刻,按下单时间算会把客服审核积压算到仓库头上;
|
||||
状态表的审核时间会滞后,所以要和订单 modified 取较晚的那个);
|
||||
明细并列「审核时间 / 下单时间 / 发货时间」,方便直接和聚水潭对账。
|
||||
<b>已发 / 未发以订单表 send_date 为准</b>(有值即已发,SQL 侧直接过滤不进候选);
|
||||
订单表 send_date 没回写、但状态表 send_time 有值的,也按已发剔除(不漏冤仓库)。
|
||||
@ -1111,8 +1114,8 @@ function WarehouseShipTimeliness() {
|
||||
<b>今天 7:00 前审核</b>的订单 13:00 核验;<b>截止 16:00 前审核</b>的单 18:00 核验
|
||||
(含 7 点前审核、13:00 还没发出的,也含今天之前审核至今未发的历史单)。
|
||||
<b>到点仍未发货即超时</b>(不设「满 24 小时」门槛);
|
||||
轮次与「已等时长」都按<b>审核时间</b>算(状态表 last_oa_confirm_time,审核才是仓库接到单的时刻;
|
||||
取不到审核时间的单回退订单创建时间,列上带 <b>*</b>)。
|
||||
轮次与「已等时长」都按<b>审核时间</b>算(= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者,
|
||||
审核才是仓库接到单的时刻;状态表会滞后,故取较晚者)。两者都取不到才回退订单创建时间,列上带 <b>*</b>。
|
||||
发货 = 订单表 send_date(为空才算未发,但状态表 send_time 有值的也判已发);状态只取「发货中」。
|
||||
<b>作废单不考核</b>(labels 含「发货前售后」= 客户发货前取消)
|
||||
{summary.voidCount > 0 ? ` —— 本轮已自动剔除 ${summary.voidCount} 单` : ''};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user