fix(worker): 拼单单人撤单后空出份额回大厅并重算母单状态

This commit is contained in:
2026-11-01 15:12:11 +08:00
parent b9d8630026
commit 4fd533c678
8 changed files with 85 additions and 29 deletions
@@ -539,7 +539,7 @@ export async function disposeWorkOrderShareProblem(input: {
)
}
// 母单状态保持不变:与「后台单人撤单」同口径,只处置这一份。
// 份额级问题单只处置这一份,不重算母单状态:被标记的份额不走「退回大厅」的份额释放口径。
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
@@ -821,9 +821,9 @@ export async function acceptWorkOrderShareAndSettle(input: {
/**
* 后台撤销单个拼单份额:退还该打手押金、释放这一份的占用。
*
* **不改母工单状态**:后台撤单常用于剔除错误参与者/纠错,撤掉一份不应把
* 待验收(或进行中)的母单踢回大厅;需要改母单状态时走「拼单撤单」的
* 退回大厅/取消整单显式操作。
* 份额取消后空出的份数必须回到接单大厅,所以这里用统一口径重算母单状态:
* 还有其它有效份额 → open(后台按份额推导为代练中);一份不剩 → open(待抢单)。
* 重算时保留原 published_at / hall_queued_at,空出的份额沿用原大厅队列位置。
*/
export async function cancelWorkOrderShare(input: {
workOrderId: number
@@ -936,22 +936,27 @@ export async function cancelWorkOrderShare(input: {
WHERE id = $3`,
[input.now, String(input.reason || '').trim(), input.shareId],
)
// 母单状态保持不变:这里只取消这一份份额,不再按剩余份额重推母单状态。
// (旧实现会 recompute:待验收的母单撤掉一份后 allocated < total → 变回 open,
// 等于整单被踢回抢单大厅,与「后台撤单只影响该份额」的预期不符。)
// 空出的份数回大厅:母单状态必须跟着剩余份额重算,不能停留在待验收。
// 保留原队列位置,避免撤掉一份就把整单排到大厅队尾。
await recomputeWorkOrderSharingStatusWithClient(client, input.workOrderId, input.now, {
preserveHallQueue: true,
})
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: input.actorName || '',
eventType: 'sharing_share_cancelled_by_admin',
fromStatus: workOrder.status,
toStatus: workOrder.status,
toStatus: updatedOrder?.status || workOrder.status,
payloadJson: JSON.stringify({
shareId: input.shareId,
workerId,
quantity: Number(share.quantity || 0),
releasedDepositAmount,
reason: String(input.reason || '').trim(),
orderStatusAfter: updatedOrder?.status || workOrder.status,
activeShareQuantity: Number(updatedOrder?.active_share_quantity || 0),
}),
now: input.now,
})
@@ -961,7 +966,7 @@ export async function cancelWorkOrderShare(input: {
[input.shareId],
)
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
order: updatedOrder || workOrder,
share: updatedShareResult.rows[0] || null,
releasedDepositAmount,
failureReason: null,
@@ -1127,8 +1132,11 @@ export async function settleOverdueWorkOrderShare(input: {
[input.now, input.shareId],
)
// 母单状态只由份额集合推导:这一份超时不应把其他已提交份额的母单踢回大厅
await recomputeWorkOrderSharingStatusWithClient(client, input.workOrderId, input.now)
// 母单状态只由份额集合推导:这一份超时不应把其他已提交份额的母单踢回大厅;
// 空出的份数回大厅时保留原队列位置,避免超时一次就重排到队尾。
await recomputeWorkOrderSharingStatusWithClient(client, input.workOrderId, input.now, {
preserveHallQueue: true,
})
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
@@ -497,13 +497,19 @@ export async function reduceWorkOrderShareQuantity(input: {
],
)
// 减少的份数会回到大厅,母单状态跟着剩余份额重算;保留原队列位置。
await recomputeWorkOrderSharingStatusWithClient(client, input.workOrderId, input.now, {
preserveHallQueue: true,
})
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: input.actorName || '',
eventType: 'sharing_share_quantity_reduced',
fromStatus: workOrder.status,
toStatus: workOrder.status,
toStatus: updatedOrder?.status || workOrder.status,
payloadJson: JSON.stringify({
shareId: input.shareId,
workerId: Number(share.worker_id || 0),
@@ -515,6 +521,8 @@ export async function reduceWorkOrderShareQuantity(input: {
// 押金口径:本次保持原冻结额,不退不补
shareDeposit: Number(share.share_deposit || 0),
note: String(input.note || '').trim(),
orderStatusAfter: updatedOrder?.status || workOrder.status,
activeShareQuantity: Number(updatedOrder?.active_share_quantity || 0),
}),
now: input.now,
})
@@ -524,7 +532,6 @@ export async function reduceWorkOrderShareQuantity(input: {
[input.shareId],
)
// 必须回读聚合口径的母单(active_share_quantity 等),否则前端拿到的拼单进度会归零
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
return {
order: updatedOrder || workOrder,
share: updatedShare.rows[0] || null,
@@ -625,11 +632,15 @@ export async function submitWorkOrderShareAcceptance(input: {
* 母单 accepted / cancelled → 不动
*
* 返回更新后的母单行(供调用方回给前端)。
*
* preserveHallQueue:份额退回大厅时保留原 published_at / hall_queued_at,
* 避免因为母单状态从待验收回退到 open 而被重新排到大厅队尾。
*/
export async function recomputeWorkOrderSharingStatusWithClient(
client: PoolClient,
workOrderId: number,
now: string,
options: { preserveHallQueue?: boolean } = {},
): Promise<WorkOrderRow | null> {
const orderResult = await client.query<WorkOrderRow>(
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
@@ -676,13 +687,17 @@ export async function recomputeWorkOrderSharingStatusWithClient(
return workOrder
}
const hallQueueAssignments = options.preserveHallQueue
? ''
: `published_at = CASE WHEN $1 = 'open' THEN $2 ELSE published_at END,
hall_queued_at = CASE WHEN $1 = 'open' THEN $2 ELSE hall_queued_at END,`
const updated = await client.query<WorkOrderRow>(
`
UPDATE work_orders
SET
status = $1,
published_at = CASE WHEN $1 = 'open' THEN $2 ELSE published_at END,
hall_queued_at = CASE WHEN $1 = 'open' THEN $2 ELSE hall_queued_at END,
${hallQueueAssignments}
submitted_at = CASE
WHEN $1 = 'pending_acceptance' THEN COALESCE(submitted_at, $2)
ELSE submitted_at