拆分工单异常处置仓储

This commit is contained in:
yml2213
2026-08-21 18:48:08 +08:00
parent a9665b180f
commit 26f1a00e24
3 changed files with 274 additions and 260 deletions
@@ -11,6 +11,7 @@ export * from './work-order-query-repo.js'
export * from './work-order-management-repo.js'
export * from './work-order-grab-repo.js'
export * from './work-order-cancel-repo.js'
export * from './work-order-issue-repo.js'
export * from './work-order-deposit-query-repo.js'
export * from './work-order-deposit-repo.js'
export * from './work-order-event-repo.js'
@@ -0,0 +1,269 @@
import { withTransaction } from '../../db/client.js'
import {
ensureWorkerWalletWithClient,
getWorkerWalletWithClient,
} from './shared.js'
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
import { getOutstandingDepositAmountWithClient } from './work-order-deposit-repo.js'
import type { ProblemWorkOrderResolutionAction, WorkOrderRow } from './types.js'
export async function resolveProblemWorkOrder(input: {
workOrderId: number
action: ProblemWorkOrderResolutionAction
note: string
now: string
actorName?: string
}): Promise<WorkOrderRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
AND status = 'problem'
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) return null
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.action)
const shouldDeductDeposit = input.action === 'cancel_deduct'
const workerId = Number(workOrder.assigned_worker_id || 0)
let resolvedDepositAmount = 0
if (workerId > 0 && (shouldReleaseDeposit || shouldDeductDeposit)) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
resolvedDepositAmount = Math.min(
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
Number(wallet?.frozen_deposit_amount || 0),
)
if (resolvedDepositAmount > 0) {
const nextAvailable =
Number(wallet?.available_amount || 0) + (shouldReleaseDeposit ? resolvedDepositAmount : 0)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - resolvedDepositAmount,
)
const ledgerType = shouldReleaseDeposit ? 'deposit_release' : 'deposit_deduction'
const ledgerAmount = shouldReleaseDeposit ? resolvedDepositAmount : -resolvedDepositAmount
const ledgerNote = shouldReleaseDeposit ? '问题单处置释放押金' : '问题单处置扣除押金'
await client.query(
`
UPDATE worker_wallets
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
WHERE worker_id = $4
`,
[nextAvailable, nextFrozen, input.now, workerId],
)
await client.query(
`
INSERT INTO worker_wallet_ledgers (
worker_id, ledger_type, amount, balance_after, frozen_after,
related_work_order_id, note, payload_json, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[
workerId,
ledgerType,
ledgerAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
ledgerNote,
JSON.stringify({ action: input.action, note: input.note }),
input.now,
],
)
}
}
const targetStatus =
input.action === 'return_to_worker'
? 'in_progress'
: input.action === 'reopen'
? 'open'
: 'cancelled'
const shouldClearAssignment = input.action === 'reopen'
const shouldClearSubmission = ['return_to_worker', 'reopen'].includes(input.action)
await client.query(
`
UPDATE work_orders
SET
status = $1,
assigned_worker_id = CASE WHEN $2 THEN NULL ELSE assigned_worker_id END,
assigned_at = CASE WHEN $2 THEN NULL ELSE assigned_at END,
acceptance_json = CASE WHEN $3 THEN '{}'::jsonb ELSE acceptance_json END,
submitted_at = CASE WHEN $3 THEN NULL ELSE submitted_at END,
problem_note = CASE WHEN $2 THEN '' ELSE problem_note END,
deadline_at = CASE
WHEN $1 = 'in_progress' AND timeout_minutes > 0
THEN NOW() + (timeout_minutes * INTERVAL '1 minute')
WHEN $1 = 'open' THEN NULL
ELSE deadline_at
END,
updated_at = $4
WHERE id = $5
`,
[targetStatus, shouldClearAssignment, shouldClearSubmission, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: input.actorName || '',
eventType: `problem_${input.action}`,
fromStatus: 'problem',
toStatus: targetStatus,
payloadJson: JSON.stringify({
action: input.action,
note: input.note,
depositAmount: resolvedDepositAmount,
}),
now: input.now,
})
return getWorkOrderByIdWithClient(client, input.workOrderId)
})
}
export async function settleOverdueWorkOrder(input: {
workOrderId: number
policy: 'reopen' | 'cancel_release' | 'cancel_deduct'
now: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_overdue' | 'work_order_has_active_shares' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
AND status = 'in_progress'
AND deadline_at IS NOT NULL
AND deadline_at < NOW()
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) {
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) {
return { order: null, failureReason: 'work_order_has_active_shares' }
}
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.policy)
const shouldDeductDeposit = input.policy === 'cancel_deduct'
const workerId = Number(workOrder.assigned_worker_id || 0)
let resolvedDepositAmount = 0
if (workerId > 0 && (shouldReleaseDeposit || shouldDeductDeposit)) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
resolvedDepositAmount = Math.min(
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
Number(wallet?.frozen_deposit_amount || 0),
)
if (resolvedDepositAmount > 0) {
const nextAvailable =
Number(wallet?.available_amount || 0) + (shouldReleaseDeposit ? resolvedDepositAmount : 0)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - resolvedDepositAmount,
)
const ledgerType = shouldReleaseDeposit ? 'deposit_release' : 'deposit_deduction'
const ledgerAmount = shouldReleaseDeposit ? resolvedDepositAmount : -resolvedDepositAmount
const ledgerNote = shouldReleaseDeposit ? '超时处置释放押金' : '超时处置扣除押金'
await client.query(
`
UPDATE worker_wallets
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
WHERE worker_id = $4
`,
[nextAvailable, nextFrozen, input.now, workerId],
)
await client.query(
`
INSERT INTO worker_wallet_ledgers (
worker_id, ledger_type, amount, balance_after, frozen_after,
related_work_order_id, note, payload_json, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[
workerId,
ledgerType,
ledgerAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
ledgerNote,
JSON.stringify({ action: `timeout_${input.policy}` }),
input.now,
],
)
}
}
const targetStatus = input.policy === 'reopen' ? 'open' : 'cancelled'
await client.query(
`
UPDATE work_orders
SET
status = $1,
assigned_worker_id = NULL,
assigned_at = NULL,
deadline_at = NULL,
acceptance_json = CASE WHEN $1 = 'open' THEN '{}'::jsonb ELSE acceptance_json END,
submitted_at = CASE WHEN $1 = 'open' THEN NULL ELSE submitted_at END,
problem_note = CASE WHEN $1 = 'open' THEN '' ELSE problem_note END,
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,
updated_at = $2
WHERE id = $3
`,
[targetStatus, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'system',
actorId: '',
eventType: `timeout_${input.policy}`,
fromStatus: 'in_progress',
toStatus: targetStatus,
payloadJson: JSON.stringify({
policy: input.policy,
depositAmount: resolvedDepositAmount,
workerId: workerId > 0 ? workerId : null,
}),
now: input.now,
})
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
@@ -69,6 +69,10 @@ export {
returnAssignedWorkOrderToHall,
unassignWorkOrder,
} from './work-order-cancel-repo.js'
export {
resolveProblemWorkOrder,
settleOverdueWorkOrder,
} from './work-order-issue-repo.js'
export async function acceptWorkOrderAndSettle(input: {
workOrderId: number
@@ -767,263 +771,3 @@ export async function cancelWorkOrderShare(input: {
})
}
export async function resolveProblemWorkOrder(input: {
workOrderId: number
action: ProblemWorkOrderResolutionAction
note: string
now: string
actorName?: string
}): Promise<WorkOrderRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
AND status = 'problem'
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) return null
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.action)
const shouldDeductDeposit = input.action === 'cancel_deduct'
const workerId = Number(workOrder.assigned_worker_id || 0)
let resolvedDepositAmount = 0
if (workerId > 0 && (shouldReleaseDeposit || shouldDeductDeposit)) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
resolvedDepositAmount = Math.min(
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
Number(wallet?.frozen_deposit_amount || 0),
)
if (resolvedDepositAmount > 0) {
const nextAvailable =
Number(wallet?.available_amount || 0) + (shouldReleaseDeposit ? resolvedDepositAmount : 0)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - resolvedDepositAmount,
)
const ledgerType = shouldReleaseDeposit ? 'deposit_release' : 'deposit_deduction'
const ledgerAmount = shouldReleaseDeposit ? resolvedDepositAmount : -resolvedDepositAmount
const ledgerNote = shouldReleaseDeposit ? '问题单处置释放押金' : '问题单处置扣除押金'
await client.query(
`
UPDATE worker_wallets
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
WHERE worker_id = $4
`,
[nextAvailable, nextFrozen, input.now, workerId],
)
await client.query(
`
INSERT INTO worker_wallet_ledgers (
worker_id, ledger_type, amount, balance_after, frozen_after,
related_work_order_id, note, payload_json, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[
workerId,
ledgerType,
ledgerAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
ledgerNote,
JSON.stringify({ action: input.action, note: input.note }),
input.now,
],
)
}
}
const targetStatus =
input.action === 'return_to_worker'
? 'in_progress'
: input.action === 'reopen'
? 'open'
: 'cancelled'
const shouldClearAssignment = input.action === 'reopen'
const shouldClearSubmission = ['return_to_worker', 'reopen'].includes(input.action)
await client.query(
`
UPDATE work_orders
SET
status = $1,
assigned_worker_id = CASE WHEN $2 THEN NULL ELSE assigned_worker_id END,
assigned_at = CASE WHEN $2 THEN NULL ELSE assigned_at END,
acceptance_json = CASE WHEN $3 THEN '{}'::jsonb ELSE acceptance_json END,
submitted_at = CASE WHEN $3 THEN NULL ELSE submitted_at END,
problem_note = CASE WHEN $2 THEN '' ELSE problem_note END,
deadline_at = CASE
WHEN $1 = 'in_progress' AND timeout_minutes > 0
THEN NOW() + (timeout_minutes * INTERVAL '1 minute')
WHEN $1 = 'open' THEN NULL
ELSE deadline_at
END,
updated_at = $4
WHERE id = $5
`,
[targetStatus, shouldClearAssignment, shouldClearSubmission, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: input.actorName || '',
eventType: `problem_${input.action}`,
fromStatus: 'problem',
toStatus: targetStatus,
payloadJson: JSON.stringify({
action: input.action,
note: input.note,
depositAmount: resolvedDepositAmount,
}),
now: input.now,
})
return getWorkOrderByIdWithClient(client, input.workOrderId)
})
}
export async function settleOverdueWorkOrder(input: {
workOrderId: number
policy: 'reopen' | 'cancel_release' | 'cancel_deduct'
now: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_overdue' | 'work_order_has_active_shares' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
AND status = 'in_progress'
AND deadline_at IS NOT NULL
AND deadline_at < NOW()
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) {
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) {
return { order: null, failureReason: 'work_order_has_active_shares' }
}
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.policy)
const shouldDeductDeposit = input.policy === 'cancel_deduct'
const workerId = Number(workOrder.assigned_worker_id || 0)
let resolvedDepositAmount = 0
if (workerId > 0 && (shouldReleaseDeposit || shouldDeductDeposit)) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
resolvedDepositAmount = Math.min(
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
Number(wallet?.frozen_deposit_amount || 0),
)
if (resolvedDepositAmount > 0) {
const nextAvailable =
Number(wallet?.available_amount || 0) + (shouldReleaseDeposit ? resolvedDepositAmount : 0)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - resolvedDepositAmount,
)
const ledgerType = shouldReleaseDeposit ? 'deposit_release' : 'deposit_deduction'
const ledgerAmount = shouldReleaseDeposit ? resolvedDepositAmount : -resolvedDepositAmount
const ledgerNote = shouldReleaseDeposit ? '超时处置释放押金' : '超时处置扣除押金'
await client.query(
`
UPDATE worker_wallets
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
WHERE worker_id = $4
`,
[nextAvailable, nextFrozen, input.now, workerId],
)
await client.query(
`
INSERT INTO worker_wallet_ledgers (
worker_id, ledger_type, amount, balance_after, frozen_after,
related_work_order_id, note, payload_json, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[
workerId,
ledgerType,
ledgerAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
ledgerNote,
JSON.stringify({ action: `timeout_${input.policy}` }),
input.now,
],
)
}
}
const targetStatus = input.policy === 'reopen' ? 'open' : 'cancelled'
await client.query(
`
UPDATE work_orders
SET
status = $1,
assigned_worker_id = NULL,
assigned_at = NULL,
deadline_at = NULL,
acceptance_json = CASE WHEN $1 = 'open' THEN '{}'::jsonb ELSE acceptance_json END,
submitted_at = CASE WHEN $1 = 'open' THEN NULL ELSE submitted_at END,
problem_note = CASE WHEN $1 = 'open' THEN '' ELSE problem_note END,
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,
updated_at = $2
WHERE id = $3
`,
[targetStatus, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'system',
actorId: '',
eventType: `timeout_${input.policy}`,
fromStatus: 'in_progress',
toStatus: targetStatus,
payloadJson: JSON.stringify({
policy: input.policy,
depositAmount: resolvedDepositAmount,
workerId: workerId > 0 ? workerId : null,
}),
now: input.now,
})
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}