fix(work-order): 问题单处置存在活跃拼单份额时禁止取消/退回大厅/重开

生产实锤 WK26101503335487:客服对份额已提交的拼单母单走问题单处置-取消,
母单整单 cancelled 后份额验收/单人撤单全部被母单状态挡死,后台无补救入口。

- 抽出 getActiveWorkOrderShareCountWithClient 统一活跃份额统计口径
- resolveProblemWorkOrder 的 cancel/cancel_deduct/reopen 先过活跃份额防线
  (return_to_worker 不拦),返回结构改为 {order, failureReason}
- settleOverdueWorkOrder 复用同一统计函数,消除两份 COUNT 口径
- 服务层映射 409 work_order_problem_resolution_has_active_shares,
  文案引导客服先单独验收/单人撤单
This commit is contained in:
yml2213
2026-10-15 10:34:43 +08:00
parent 10464fea89
commit 584b6f52aa
3 changed files with 119 additions and 14 deletions
@@ -1,4 +1,5 @@
import { withTransaction } from '../../db/client.js'
import type { PoolClient } from 'pg'
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
import { forfeitConsignmentPublishDepositByWorkOrderWithClient } from './consignment-deposit-repo.js'
import { releaseConsignmentInventoryBindingForWorkOrderWithClient } from './consignment-repo.js'
@@ -9,6 +10,27 @@ import { getOutstandingDepositAmountWithClient } from './work-order-deposit-repo
import { CLEAR_DELIVERY_RESIDUE_SQL, RESET_GIFT_FLOW_SQL } from './work-order-gift-repo.js'
import type { ProblemWorkOrderResolutionAction, WorkOrderRow } from './types.js'
/**
* 统计工单的活跃拼单份额(除已取消外都算活跃)。
*
* 母单被整单带走(取消 / 退回大厅 / 重开)会让份额验收与单人撤单全部被
* 母单状态挡死,因此问题单处置与超时处置都必须先过这道防线。
*/
export async function getActiveWorkOrderShareCountWithClient(
client: PoolClient,
workOrderId: number | string,
): Promise<number> {
const shareResult = await client.query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_order_shares
WHERE work_order_id = $1 AND status != 'cancelled'
`,
[workOrderId],
)
return Number(shareResult.rows[0]?.total || 0)
}
/** 问题单退回打手时,按标记前状态恢复;异常历史数据回退单人单进行中。 */
export function resolveProblemReturnToWorkerStatus(
fromStatus: string,
@@ -25,7 +47,10 @@ export async function resolveProblemWorkOrder(input: {
note: string
now: string
actorName?: string
}): Promise<WorkOrderRow | null> {
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_problem' | 'work_order_has_active_shares' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
@@ -38,7 +63,24 @@ export async function resolveProblemWorkOrder(input: {
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) return null
if (!workOrder) {
return { order: null, failureReason: 'work_order_not_problem' }
}
// 母单一旦被整单带走(取消 / 退回大厅 / 重开),份额验收与单人撤单都会被
// 母单状态挡死、后台再无补救入口,因此只要还有活跃份额就禁止这三类处置;
// 已提交验收资料的份额必须先走「单独验收」或「单人撤单」。
const targetStatusDraft =
input.action === 'return_to_worker' ? null : input.action === 'reopen' ? 'open' : 'cancelled'
if (targetStatusDraft !== null) {
const activeShareCount = await getActiveWorkOrderShareCountWithClient(
client,
input.workOrderId,
)
if (activeShareCount > 0) {
return { order: workOrder, failureReason: 'work_order_has_active_shares' }
}
}
// return_to_worker 需恢复到标记问题单前的真实状态:拼单份额已全部提交时
// 标记前是 pending_acceptance,若一律回到 in_progress 会陷入"份额已提交、
@@ -229,7 +271,10 @@ export async function resolveProblemWorkOrder(input: {
now: input.now,
})
return getWorkOrderByIdWithClient(client, input.workOrderId)
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
@@ -259,15 +304,8 @@ export async function settleOverdueWorkOrder(input: {
return { order: null, failureReason: 'work_order_not_overdue' }
}
const shareResult = await client.query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_order_shares
WHERE work_order_id = $1 AND status != 'cancelled'
`,
[input.workOrderId],
)
if (Number(shareResult.rows[0]?.total || 0) > 0) {
const activeShareCount = await getActiveWorkOrderShareCountWithClient(client, input.workOrderId)
if (activeShareCount > 0) {
return { order: null, failureReason: 'work_order_has_active_shares' }
}