From 26f1a00e24eaf5b4f475067233243a681a976461 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 21 Aug 2026 18:48:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=B7=A5=E5=8D=95=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=BD=AE=E4=BB=93=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/repositories/worker-platform/index.ts | 1 + .../worker-platform/work-order-issue-repo.ts | 269 ++++++++++++++++++ .../worker-platform/work-order-repo.ts | 264 +---------------- 3 files changed, 274 insertions(+), 260 deletions(-) create mode 100644 apps/backend/src/repositories/worker-platform/work-order-issue-repo.ts diff --git a/apps/backend/src/repositories/worker-platform/index.ts b/apps/backend/src/repositories/worker-platform/index.ts index cfc19b89..f8f8fad8 100644 --- a/apps/backend/src/repositories/worker-platform/index.ts +++ b/apps/backend/src/repositories/worker-platform/index.ts @@ -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' diff --git a/apps/backend/src/repositories/worker-platform/work-order-issue-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-issue-repo.ts new file mode 100644 index 00000000..f7aa1ab3 --- /dev/null +++ b/apps/backend/src/repositories/worker-platform/work-order-issue-repo.ts @@ -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 { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + 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( + ` + 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, + } + }) +} diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index c9cecae0..bfd6eb23 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -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 { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - 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( - ` - 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, - } - }) -} -