修复问题单退回后拼单份额无法验收

This commit is contained in:
yml2213
2026-08-22 13:07:54 +08:00
parent f4e552c51c
commit 7b2a5321b9
6 changed files with 61 additions and 7 deletions
@@ -56,7 +56,8 @@ export function resolveWorkOrderShareCancellationStatus(
workOrderStatus: string, workOrderStatus: string,
shareStatus: string, shareStatus: string,
): 'open' | null { ): 'open' | null {
if (!['open', 'pending_acceptance'].includes(workOrderStatus)) return null // in_progress 用于解围历史问题单处置产生的"份额已提交、母单回到进行中"的卡死存量单
if (!['open', 'pending_acceptance', 'in_progress'].includes(workOrderStatus)) return null
if (!['joined', 'submitted'].includes(shareStatus)) return null if (!['joined', 'submitted'].includes(shareStatus)) return null
return 'open' return 'open'
} }
@@ -6,6 +6,16 @@ import { getOutstandingDepositAmountWithClient } from './work-order-deposit-repo
import { RESET_GIFT_FLOW_SQL } from './work-order-gift-repo.js' import { RESET_GIFT_FLOW_SQL } from './work-order-gift-repo.js'
import type { ProblemWorkOrderResolutionAction, WorkOrderRow } from './types.js' import type { ProblemWorkOrderResolutionAction, WorkOrderRow } from './types.js'
/** 问题单退回打手时,按标记前状态恢复;异常历史数据回退单人单进行中。 */
export function resolveProblemReturnToWorkerStatus(
fromStatus: string,
): 'open' | 'in_progress' | 'pending_acceptance' {
if (['open', 'in_progress', 'pending_acceptance'].includes(fromStatus)) {
return fromStatus as 'open' | 'in_progress' | 'pending_acceptance'
}
return 'in_progress'
}
export async function resolveProblemWorkOrder(input: { export async function resolveProblemWorkOrder(input: {
workOrderId: number workOrderId: number
action: ProblemWorkOrderResolutionAction action: ProblemWorkOrderResolutionAction
@@ -27,6 +37,26 @@ export async function resolveProblemWorkOrder(input: {
const workOrder = currentResult.rows[0] || null const workOrder = currentResult.rows[0] || null
if (!workOrder) return null if (!workOrder) return null
// return_to_worker 需恢复到标记问题单前的真实状态:拼单份额已全部提交时
// 标记前是 pending_acceptance,若一律回到 in_progress 会陷入"份额已提交、
// 母单却不可验收也不可重新提交"的卡死状态,因此按最近一次 marked_problem
// 事件的 from_status 恢复。
let restoreStatus = ''
if (input.action === 'return_to_worker') {
const eventResult = await client.query<{ from_status: string }>(
`
SELECT from_status
FROM work_order_events
WHERE work_order_id = $1 AND event_type = 'marked_problem'
ORDER BY id DESC
LIMIT 1
`,
[input.workOrderId],
)
const fromStatus = String(eventResult.rows[0]?.from_status || '').trim()
restoreStatus = resolveProblemReturnToWorkerStatus(fromStatus)
}
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.action) const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.action)
const shouldDeductDeposit = input.action === 'cancel_deduct' const shouldDeductDeposit = input.action === 'cancel_deduct'
const workerId = Number(workOrder.assigned_worker_id || 0) const workerId = Number(workOrder.assigned_worker_id || 0)
@@ -83,12 +113,16 @@ export async function resolveProblemWorkOrder(input: {
const targetStatus = const targetStatus =
input.action === 'return_to_worker' input.action === 'return_to_worker'
? 'in_progress' ? restoreStatus || 'in_progress'
: input.action === 'reopen' : input.action === 'reopen'
? 'open' ? 'open'
: 'cancelled' : 'cancelled'
const shouldClearAssignment = input.action === 'reopen' const shouldClearAssignment = input.action === 'reopen'
const shouldClearSubmission = ['return_to_worker', 'reopen'].includes(input.action) // 仅在真正退回打手重做(回到进行中)时清验收提交;份额已提交待验收的拼单
// 需保留母单待验收状态,否则份额验收链路断裂。
const shouldClearSubmission =
input.action === 'reopen' ||
(input.action === 'return_to_worker' && targetStatus === 'in_progress')
await client.query( await client.query(
` `
@@ -392,8 +392,10 @@ export async function acceptWorkOrderShareAndSettle(input: {
failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null
}> { }> {
return withTransaction(async (client) => { return withTransaction(async (client) => {
// in_progress 仅用于解围历史问题单处置产生的"份额已提交、母单却回到进行中"
// 的卡死存量单;正常拼单流程母单只会是 open / pending_acceptance。
const orderResult = await client.query<WorkOrderRow>( const orderResult = await client.query<WorkOrderRow>(
`SELECT * FROM work_orders WHERE id = $1 AND status IN ('open', 'pending_acceptance') FOR UPDATE`, `SELECT * FROM work_orders WHERE id = $1 AND status IN ('open', 'pending_acceptance', 'in_progress') FOR UPDATE`,
[input.workOrderId], [input.workOrderId],
) )
const workOrder = orderResult.rows[0] || null const workOrder = orderResult.rows[0] || null
@@ -592,7 +594,7 @@ export async function cancelWorkOrderShare(input: {
[input.workOrderId], [input.workOrderId],
) )
const workOrder = orderResult.rows[0] || null const workOrder = orderResult.rows[0] || null
if (!workOrder || !['open', 'pending_acceptance'].includes(workOrder.status)) { if (!workOrder || !['open', 'pending_acceptance', 'in_progress'].includes(workOrder.status)) {
return { return {
order: workOrder, order: workOrder,
share: null, share: null,
@@ -295,7 +295,7 @@ export async function submitWorkOrderShareAcceptance(input: {
now: string now: string
}): Promise<{ }): Promise<{
share: WorkOrderShareRow | null share: WorkOrderShareRow | null
failureReason: 'share_not_found' | 'share_status_invalid' | null failureReason: 'share_not_found' | 'share_status_invalid' | 'work_order_status_invalid' | null
}> { }> {
return withTransaction(async (client) => { return withTransaction(async (client) => {
const orderResult = await client.query<{ const orderResult = await client.query<{
@@ -309,9 +309,12 @@ export async function submitWorkOrderShareAcceptance(input: {
[input.workOrderId], [input.workOrderId],
) )
const workOrder = orderResult.rows[0] || null const workOrder = orderResult.rows[0] || null
if (!workOrder || !['open', 'pending_acceptance'].includes(workOrder.status)) { if (!workOrder) {
return { share: null, failureReason: 'share_not_found' } return { share: null, failureReason: 'share_not_found' }
} }
if (!['open', 'pending_acceptance'].includes(workOrder.status)) {
return { share: null, failureReason: 'work_order_status_invalid' }
}
const lockResult = await client.query<{ id: number }>( const lockResult = await client.query<{ id: number }>(
` `
@@ -435,6 +435,12 @@ async function submitWorkerSharingAcceptance(
errorCode: 'work_order_sharing_share_submitted', errorCode: 'work_order_sharing_share_submitted',
}) })
} }
if (result.failureReason === 'work_order_status_invalid') {
throw createHttpError('当前拼单状态不能提交验收资料,请联系管理员', {
statusCode: 409,
errorCode: 'work_order_sharing_submit_status_invalid',
})
}
if (!result.share) { if (!result.share) {
throw createHttpError('未找到拼单份额', { throw createHttpError('未找到拼单份额', {
statusCode: 404, statusCode: 404,
@@ -16,6 +16,7 @@ import {
resolveWorkOrderShareJoinQuantity, resolveWorkOrderShareJoinQuantity,
resolveOpenSharingSyncReward, resolveOpenSharingSyncReward,
} from '../../repositories/worker-platform/index.js' } from '../../repositories/worker-platform/index.js'
import { resolveProblemReturnToWorkerStatus } from '../../repositories/worker-platform/work-order-issue-repo.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js' import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
import { import {
assertWithdrawChannelAllowedForAmount, assertWithdrawChannelAllowedForAmount,
@@ -1045,6 +1046,13 @@ test('拼单份额撤单只允许未验收份额并让母工单回到大厅状
assert.equal(resolveWorkOrderShareCancellationStatus('open', 'cancelled'), null) assert.equal(resolveWorkOrderShareCancellationStatus('open', 'cancelled'), null)
}) })
test('问题单退回打手按标记前状态恢复', () => {
assert.equal(resolveProblemReturnToWorkerStatus('pending_acceptance'), 'pending_acceptance')
assert.equal(resolveProblemReturnToWorkerStatus('open'), 'open')
assert.equal(resolveProblemReturnToWorkerStatus('in_progress'), 'in_progress')
assert.equal(resolveProblemReturnToWorkerStatus(''), 'in_progress')
})
test('后台更新资料时普通接单可以修改接单价格', () => { test('后台更新资料时普通接单可以修改接单价格', () => {
assert.equal( assert.equal(
resolveAdminMaterialRewardAmount({ resolveAdminMaterialRewardAmount({