fix: 免审验收容错与成本中心归一回填超时修复

- worker:等级升级计数移出验收事务,瞬时错误重试一次,最终失败转人工待办且不再返回 500
- worker:待验收阶段修改验收资料时保留 reviewMode/evidenceStatus/evidenceDueAt
- cost-center:迁移 107 新增人工待归一覆盖部分索引(生产实测候选聚合 19.6s → 391ms)
- cost-center:归一人工商品/按工单回填商品/模板预览前端超时放宽到 300s
- cost-center:批处理接口语句超时放宽到 60s(SET LOCAL,在线接口仍为 15s)
This commit is contained in:
yml2213
2026-10-29 14:33:35 +08:00
parent 814b357d46
commit 979a1bfa41
13 changed files with 617 additions and 68 deletions
@@ -0,0 +1,15 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
/**
* 2026-10-29 生产事故:VIP5 免审验收卡在「待验收」,原因是验收结算事务里
* 顺带跑「已验收订单去重计数」做等级升级,库被回填压住时撞上 15s statement_timeout,
* 整个验收事务回滚。升级必须挪到事务提交后,失败也不能影响验收。
*/
test('验收结算仓库不再在事务内调用等级升级(必须放到提交后)', async () => {
const source = await readFile(new URL('./work-order-settlement-repo.ts', import.meta.url), 'utf8')
assert.equal(source.includes('maybeUpgradeWorkerLevelWithClient'), false)
assert.ok(source.includes('upgradeWorkerLevelsAfterCommit(upgradeWorkerIds'))
})
@@ -1,6 +1,7 @@
import type { PoolClient } from 'pg'
import { withTransaction } from '../../db/client.js'
import { logWarn } from '../../utils/logger.js'
import {
resolveSharingPartCancelPlan,
resolveSharingPartCancelRefund,
@@ -21,13 +22,34 @@ import {
enqueueDepositUnfreezeWithClient,
getOutstandingDepositAmountWithClient,
} from './work-order-deposit-repo.js'
import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js'
import { maybeUpgradeWorkerLevel } from './worker-ranking-repo.js'
import type { WorkOrderRow, WorkOrderShareRow } from './types.js'
import {
releaseConsignmentPublishDepositBySubmissionWithClient,
settleConsignmentSupplierPayoutByWorkOrderWithClient,
} from './consignment-deposit-repo.js'
/**
* 验收事务提交后再跑自动升级。
*
* 升级要先算「已验收订单去重计数」,在大打手身上是秒级查询;放进验收事务里会把
* 整笔验收拖到 statement_timeout(2026-10-29 生产事故:VIP5 免审验收因此 500)。
* 这里失败只记日志,不影响已经提交的验收与结算。
*/
async function upgradeWorkerLevelsAfterCommit(workerIds: number[], now: string) {
const uniqueWorkerIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))]
for (const workerId of uniqueWorkerIds) {
try {
await maybeUpgradeWorkerLevel(workerId, now)
} catch (error) {
logWarn('[worker/level-upgrade]', '验收后自动升级计算失败,已跳过', {
workerId,
error: error instanceof Error ? error.message : String(error),
})
}
}
}
/**
* 拼单母单收尾:标记已验收,并补寄卖的发布押金释放与供货结算。
*
@@ -97,7 +119,11 @@ export async function acceptWorkOrderAndSettle(input: {
order: WorkOrderRow | null
failureReason: 'sharing_pending_submissions' | null
}> {
return withTransaction(async (client) => {
const upgradeWorkerIds: number[] = []
const result = await withTransaction<{
order: WorkOrderRow | null
failureReason: 'sharing_pending_submissions' | null
}>(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
@@ -339,7 +365,7 @@ export async function acceptWorkOrderAndSettle(input: {
`,
[input.now, Number(share.id)],
)
await maybeUpgradeWorkerLevelWithClient(client, shareWorkerId, input.now)
upgradeWorkerIds.push(shareWorkerId)
}
}
@@ -513,7 +539,7 @@ export async function acceptWorkOrderAndSettle(input: {
})
if (workerId > 0) {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
upgradeWorkerIds.push(workerId)
}
return {
@@ -521,6 +547,8 @@ export async function acceptWorkOrderAndSettle(input: {
failureReason: null,
}
})
if (result.order) await upgradeWorkerLevelsAfterCommit(upgradeWorkerIds, input.now)
return result
}
/** 单独验收一个拼单份额,只结算该参与者,不影响其他份额。 */
@@ -537,7 +565,12 @@ export async function acceptWorkOrderShareAndSettle(input: {
share: WorkOrderShareRow | null
failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null
}> {
return withTransaction(async (client) => {
const upgradeWorkerIds: number[] = []
const result = await withTransaction<{
order: WorkOrderRow | null
share: WorkOrderShareRow | null
failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null
}>(async (client) => {
// in_progress 仅用于解围历史问题单处置产生的"份额已提交、母单却回到进行中"
// 的卡死存量单;正常拼单流程母单只会是 open / pending_acceptance。
const orderResult = await client.query<WorkOrderRow>(
@@ -689,8 +722,8 @@ export async function acceptWorkOrderShareAndSettle(input: {
[input.now, input.shareId],
)
if (workerId > 0) {
// 先落库份额已验收状态,再按去重后的订单数计算等级进度。
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
// 等级升级要跑全量已验收订单计数,挪到事务提交后执行,避免拖垮验收事务。
upgradeWorkerIds.push(workerId)
}
const progressResult = await client.query<{
joined_quantity: number
@@ -737,6 +770,8 @@ export async function acceptWorkOrderShareAndSettle(input: {
failureReason: null,
}
})
if (result.share) await upgradeWorkerLevelsAfterCommit(upgradeWorkerIds, input.now)
return result
}
/**
@@ -1,4 +1,4 @@
import { query } from '../../db/client.js'
import { query, withTransaction } from '../../db/client.js'
import type { WorkerUserRow } from './types.js'
import type { PoolClient } from 'pg'
@@ -221,6 +221,21 @@ export async function maybeUpgradeWorkerLevelWithClient(
return getWorkerUserByIdWithClient(client, workerId)
}
/**
* 验收后的自动升级:单独开事务执行。
*
* 升级要先做一次「已验收订单去重计数」,这在大打手身上是秒级查询;
* 放进验收结算事务里会把验收本身拖到 15s statement_timeout 上(2026-10-29 生产事故),
* 所以验收事务提交后再跑,失败了也不影响验收结果。
*/
export async function maybeUpgradeWorkerLevel(
workerId: number,
now: string,
): Promise<WorkerUserRow | null> {
if (!(Number(workerId) > 0)) return null
return withTransaction((client) => maybeUpgradeWorkerLevelWithClient(client, workerId, now))
}
export function shouldApplyAutomaticWorkerLevel(input: {
currentLevelId: number | null
currentThreshold: number