From a9665b180f52a7f7084e7b958b44c1a39faf3abc Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 21 Aug 2026 18:47:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=B7=A5=E5=8D=95=E6=B5=81?= =?UTF-8?q?=E8=BD=AC=E4=BA=8B=E5=8A=A1=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 | 2 + .../worker-platform/work-order-cancel-repo.ts | 454 ++++++++++++ .../worker-platform/work-order-grab-repo.ts | 237 ++++++ .../worker-platform/work-order-repo.ts | 680 +----------------- 4 files changed, 704 insertions(+), 669 deletions(-) create mode 100644 apps/backend/src/repositories/worker-platform/work-order-cancel-repo.ts create mode 100644 apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts diff --git a/apps/backend/src/repositories/worker-platform/index.ts b/apps/backend/src/repositories/worker-platform/index.ts index c305ad84..cfc19b89 100644 --- a/apps/backend/src/repositories/worker-platform/index.ts +++ b/apps/backend/src/repositories/worker-platform/index.ts @@ -9,6 +9,8 @@ export * from './worker-ranking-repo.js' export * from './work-order-repo.js' 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-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-cancel-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-cancel-repo.ts new file mode 100644 index 00000000..7af7ea43 --- /dev/null +++ b/apps/backend/src/repositories/worker-platform/work-order-cancel-repo.ts @@ -0,0 +1,454 @@ +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 { WorkOrderRow } from './types.js' + +export async function cancelWorkerWorkOrder(input: { + workOrderId: number + workerId: number + now: string +}): Promise<{ + order: WorkOrderRow | null + failureReason: 'work_order_not_in_progress' | 'work_order_owner_required' | null +}> { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM work_orders + WHERE id = $1 + FOR UPDATE + `, + [input.workOrderId], + ) + const workOrder = currentResult.rows[0] || null + if (!workOrder || workOrder.status !== 'in_progress') { + return { order: null, failureReason: 'work_order_not_in_progress' } + } + if (Number(workOrder.assigned_worker_id || 0) !== input.workerId) { + return { order: null, failureReason: 'work_order_owner_required' } + } + + await ensureWorkerWalletWithClient(client, input.workerId, input.now) + const wallet = await getWorkerWalletWithClient(client, input.workerId) + const releaseAmount = Math.min( + await getOutstandingDepositAmountWithClient(client, input.workerId, input.workOrderId), + Number(wallet?.frozen_deposit_amount || 0), + ) + + await client.query( + ` + UPDATE work_orders + SET + status = 'open', + assigned_worker_id = NULL, + assigned_at = NULL, + deadline_at = NULL, + hall_queued_at = $1, + updated_at = $1 + WHERE id = $2 + `, + [input.now, input.workOrderId], + ) + + if (releaseAmount > 0) { + const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount + const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) + 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, input.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, 'deposit_release', $2, $3, $4, $5, '取消接单退还押金', '{}'::jsonb, $6) + `, + [input.workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], + ) + } + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'worker', + actorId: String(input.workerId), + eventType: 'cancelled_by_worker', + fromStatus: 'in_progress', + toStatus: 'open', + payloadJson: JSON.stringify({ releaseAmount }), + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} + +/** 打手提交工单问题反馈,不改变工单状态、押金或资金流水。 */ + +export async function unassignWorkOrder(input: { + workOrderId: number + now: string + actorName?: string +}): Promise<{ + order: WorkOrderRow | null + failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null +}> { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM work_orders + WHERE id = $1 + FOR UPDATE + `, + [input.workOrderId], + ) + const workOrder = currentResult.rows[0] || null + if (!workOrder || workOrder.status !== 'in_progress') { + return { order: null, failureReason: 'work_order_not_in_progress' } + } + const workerId = Number(workOrder.assigned_worker_id || 0) + if (!workerId) { + return { order: null, failureReason: 'work_order_no_worker' } + } + + await ensureWorkerWalletWithClient(client, workerId, input.now) + const wallet = await getWorkerWalletWithClient(client, workerId) + const releaseAmount = Math.min( + await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId), + Number(wallet?.frozen_deposit_amount || 0), + ) + + await client.query( + ` + UPDATE work_orders + SET + status = 'unassigned', + assigned_worker_id = NULL, + assigned_at = NULL, + deadline_at = NULL, + updated_at = $1 + WHERE id = $2 + `, + [input.now, input.workOrderId], + ) + + if (releaseAmount > 0) { + const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount + const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) + 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, 'deposit_release', $2, $3, $4, $5, '后台取消指派退还押金', '{}'::jsonb, $6) + `, + [workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], + ) + } + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'admin', + actorId: input.actorName || '', + eventType: 'unassigned_by_admin', + fromStatus: 'in_progress', + toStatus: 'unassigned', + payloadJson: JSON.stringify({ workerId, releaseAmount }), + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} + +export async function returnAssignedWorkOrderToHall(input: { + workOrderId: number + now: string + reason?: string + actorName?: string +}): Promise<{ + order: WorkOrderRow | null + failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null +}> { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM work_orders + WHERE id = $1 + FOR UPDATE + `, + [input.workOrderId], + ) + const workOrder = currentResult.rows[0] || null + if (!workOrder || workOrder.status !== 'in_progress') { + return { order: null, failureReason: 'work_order_not_in_progress' } + } + const workerId = Number(workOrder.assigned_worker_id || 0) + if (!workerId) { + return { order: null, failureReason: 'work_order_no_worker' } + } + + await ensureWorkerWalletWithClient(client, workerId, input.now) + const wallet = await getWorkerWalletWithClient(client, workerId) + const releaseAmount = Math.min( + await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId), + Number(wallet?.frozen_deposit_amount || 0), + ) + + await client.query( + ` + UPDATE work_orders + SET + status = 'open', + assigned_worker_id = NULL, + assigned_at = NULL, + deadline_at = NULL, + published_at = $1, + hall_queued_at = $1, + updated_at = $1 + WHERE id = $2 + `, + [input.now, input.workOrderId], + ) + + if (releaseAmount > 0) { + const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount + const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) + 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, 'deposit_release', $2, $3, $4, $5, '后台退回大厅退还押金', '{}'::jsonb, $6) + `, + [workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], + ) + } + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'admin', + actorId: input.actorName || '', + eventType: 'returned_to_hall_by_admin', + fromStatus: 'in_progress', + toStatus: 'open', + payloadJson: JSON.stringify({ + workerId, + releaseAmount, + reason: String(input.reason || '').trim(), + }), + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} + +export async function cancelAssignedWorkOrder(input: { + workOrderId: number + now: string + reason?: string + actorName?: string +}): Promise<{ + order: WorkOrderRow | null + failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null +}> { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM work_orders + WHERE id = $1 + FOR UPDATE + `, + [input.workOrderId], + ) + const workOrder = currentResult.rows[0] || null + if (!workOrder || workOrder.status !== 'in_progress') { + return { order: null, failureReason: 'work_order_not_in_progress' } + } + const workerId = Number(workOrder.assigned_worker_id || 0) + if (!workerId) { + return { order: null, failureReason: 'work_order_no_worker' } + } + + await ensureWorkerWalletWithClient(client, workerId, input.now) + const wallet = await getWorkerWalletWithClient(client, workerId) + const releaseAmount = Math.min( + await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId), + Number(wallet?.frozen_deposit_amount || 0), + ) + + await client.query( + ` + UPDATE work_orders + SET + status = 'cancelled', + assigned_worker_id = NULL, + assigned_at = NULL, + deadline_at = NULL, + published_at = NULL, + updated_at = $1 + WHERE id = $2 + `, + [input.now, input.workOrderId], + ) + + if (releaseAmount > 0) { + const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount + const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) + 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, 'deposit_release', $2, $3, $4, $5, '后台撤单退还押金', '{}'::jsonb, $6) + `, + [workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], + ) + } + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'admin', + actorId: input.actorName || '', + eventType: 'cancelled_by_admin', + fromStatus: 'in_progress', + toStatus: 'cancelled', + payloadJson: JSON.stringify({ + workerId, + releaseAmount, + reason: String(input.reason || '').trim(), + }), + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} + +/** 将已取消工单恢复为未分配状态,不变更历史押金流水。 */ +export async function reopenCancelledWorkOrder(input: { + workOrderId: number + now: string + actorName?: string +}): Promise<{ + order: WorkOrderRow | null + failureReason: 'work_order_not_cancelled' | 'work_order_has_active_shares' | null +}> { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM work_orders + WHERE id = $1 + FOR UPDATE + `, + [input.workOrderId], + ) + const workOrder = currentResult.rows[0] || null + if (!workOrder || workOrder.status !== 'cancelled') { + return { order: null, failureReason: 'work_order_not_cancelled' } + } + + const activeShareResult = 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(activeShareResult.rows[0]?.total || 0) > 0) { + return { order: null, failureReason: 'work_order_has_active_shares' } + } + + await client.query( + ` + UPDATE work_orders + SET + status = 'unassigned', + assigned_worker_id = NULL, + assigned_at = NULL, + deadline_at = NULL, + acceptance_json = '{}'::jsonb, + draft_acceptance_json = '{}'::jsonb, + submitted_at = NULL, + accepted_at = NULL, + problem_note = '', + published_at = NULL, + pinned_at = NULL, + updated_at = $1 + WHERE id = $2 + `, + [input.now, input.workOrderId], + ) + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'admin', + actorId: input.actorName || '', + eventType: 'reopened_from_cancelled', + fromStatus: 'cancelled', + toStatus: 'unassigned', + payloadJson: JSON.stringify({ + previousAssignedWorkerId: Number(workOrder.last_assigned_worker_id || 0) || null, + }), + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} diff --git a/apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts new file mode 100644 index 00000000..62c9a0f2 --- /dev/null +++ b/apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts @@ -0,0 +1,237 @@ +import { withTransaction } from '../../db/client.js' +import { + ensureWorkerWalletWithClient, + getWorkerWalletWithClient, +} from './shared.js' +import { createWorkOrderEventWithClient } from './work-order-event-repo.js' +import { + countWorkerActiveOrdersWithClient, + getWorkOrderByIdWithClient, + isWorkOrderWithinHallCapacityWithClient, +} from './work-order-query-repo.js' +import type { GrabWorkOrderResult, WorkOrderRow } from './types.js' + +export async function grabWorkOrder(input: { + workOrderId: number + workerId: number + depositAmount: number + maxActiveOrders: number + hallCandidateLimit?: number + visibleAfterIso?: string + deadlineAt: string | null + now: string +}): Promise { + return withTransaction(async (client) => { + await ensureWorkerWalletWithClient(client, input.workerId, input.now) + const wallet = await getWorkerWalletWithClient(client, input.workerId) + const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId) + if (activeOrderCount >= input.maxActiveOrders) { + return { + order: null, + failureReason: 'worker_active_order_limit', + } + } + + const available = Number(wallet?.available_amount || 0) + if (available < input.depositAmount) { + return { + order: null, + failureReason: 'worker_deposit_insufficient', + } + } + + if ( + input.hallCandidateLimit && + !(await isWorkOrderWithinHallCapacityWithClient(client, { + workOrderId: input.workOrderId, + hallCandidateLimit: input.hallCandidateLimit, + visibleAfterIso: input.visibleAfterIso, + })) + ) { + return { + order: null, + failureReason: 'work_order_not_open', + } + } + + const result = await client.query<{ id: number }>( + ` + UPDATE work_orders + SET + status = 'in_progress', + assigned_worker_id = $1, + last_assigned_worker_id = $1, + assigned_at = $2, + deadline_at = $3, + updated_at = $2 + WHERE id = $4 + AND status = 'open' + AND assigned_worker_id IS NULL + RETURNING id + `, + [input.workerId, input.now, input.deadlineAt, input.workOrderId], + ) + if (!result.rows[0]) { + return { + order: null, + failureReason: 'work_order_not_open', + } + } + + if (input.depositAmount > 0) { + const nextAvailable = available - input.depositAmount + const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount + 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, input.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, 'deposit_freeze', $2, $3, $4, $5, '抢单冻结押金', '{}'::jsonb, $6) + `, + [ + input.workerId, + -input.depositAmount, + nextAvailable, + nextFrozen, + input.workOrderId, + input.now, + ], + ) + } + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'worker', + actorId: String(input.workerId), + eventType: 'grabbed', + fromStatus: 'open', + toStatus: 'in_progress', + payloadJson: '{}', + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} + +export async function assignWorkOrderToWorker(input: { + workOrderId: number + workerId: number + depositAmount: number + maxActiveOrders: number + deadlineAt: string | null + now: string + /** 操作人显示名(管理员用户名等) */ + actorName?: string +}): Promise<{ + order: WorkOrderRow | null + failureReason: + | 'work_order_not_assignable' + | 'worker_active_order_limit' + | 'worker_deposit_insufficient' + | null +}> { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM work_orders + WHERE id = $1 + FOR UPDATE + `, + [input.workOrderId], + ) + const workOrder = currentResult.rows[0] || null + if ( + !workOrder || + !['unassigned', 'open'].includes(workOrder.status) || + workOrder.assigned_worker_id + ) { + return { order: null, failureReason: 'work_order_not_assignable' } + } + const fromStatus = workOrder.status + + await ensureWorkerWalletWithClient(client, input.workerId, input.now) + const wallet = await getWorkerWalletWithClient(client, input.workerId) + const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId) + if (activeOrderCount >= input.maxActiveOrders) { + return { order: null, failureReason: 'worker_active_order_limit' } + } + + const available = Number(wallet?.available_amount || 0) + if (available < input.depositAmount) { + return { order: null, failureReason: 'worker_deposit_insufficient' } + } + + await client.query( + ` + UPDATE work_orders + SET + status = 'in_progress', + assigned_worker_id = $1, + last_assigned_worker_id = $1, + assigned_at = $2, + deadline_at = $3, + updated_at = $2 + WHERE id = $4 + `, + [input.workerId, input.now, input.deadlineAt, input.workOrderId], + ) + + if (input.depositAmount > 0) { + const nextAvailable = available - input.depositAmount + const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount + 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, input.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, 'deposit_freeze', $2, $3, $4, $5, '后台指派冻结押金', '{}'::jsonb, $6) + `, + [ + input.workerId, + -input.depositAmount, + nextAvailable, + nextFrozen, + input.workOrderId, + input.now, + ], + ) + } + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'admin', + actorId: input.actorName || '', + eventType: 'assigned_by_admin', + fromStatus, + toStatus: 'in_progress', + payloadJson: JSON.stringify({ workerId: input.workerId, depositAmount: input.depositAmount }), + 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 07c13560..c9cecae0 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -58,675 +58,17 @@ export { updateWorkOrderBasic, } from './work-order-management-repo.js' -export async function grabWorkOrder(input: { - workOrderId: number - workerId: number - depositAmount: number - maxActiveOrders: number - hallCandidateLimit?: number - visibleAfterIso?: string - deadlineAt: string | null - now: string -}): Promise { - return withTransaction(async (client) => { - await ensureWorkerWalletWithClient(client, input.workerId, input.now) - const wallet = await getWorkerWalletWithClient(client, input.workerId) - const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId) - if (activeOrderCount >= input.maxActiveOrders) { - return { - order: null, - failureReason: 'worker_active_order_limit', - } - } - - const available = Number(wallet?.available_amount || 0) - if (available < input.depositAmount) { - return { - order: null, - failureReason: 'worker_deposit_insufficient', - } - } - - if ( - input.hallCandidateLimit && - !(await isWorkOrderWithinHallCapacityWithClient(client, { - workOrderId: input.workOrderId, - hallCandidateLimit: input.hallCandidateLimit, - visibleAfterIso: input.visibleAfterIso, - })) - ) { - return { - order: null, - failureReason: 'work_order_not_open', - } - } - - const result = await client.query<{ id: number }>( - ` - UPDATE work_orders - SET - status = 'in_progress', - assigned_worker_id = $1, - last_assigned_worker_id = $1, - assigned_at = $2, - deadline_at = $3, - updated_at = $2 - WHERE id = $4 - AND status = 'open' - AND assigned_worker_id IS NULL - RETURNING id - `, - [input.workerId, input.now, input.deadlineAt, input.workOrderId], - ) - if (!result.rows[0]) { - return { - order: null, - failureReason: 'work_order_not_open', - } - } - - if (input.depositAmount > 0) { - const nextAvailable = available - input.depositAmount - const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount - 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, input.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, 'deposit_freeze', $2, $3, $4, $5, '抢单冻结押金', '{}'::jsonb, $6) - `, - [ - input.workerId, - -input.depositAmount, - nextAvailable, - nextFrozen, - input.workOrderId, - input.now, - ], - ) - } - - await createWorkOrderEventWithClient(client, { - workOrderId: input.workOrderId, - actorType: 'worker', - actorId: String(input.workerId), - eventType: 'grabbed', - fromStatus: 'open', - toStatus: 'in_progress', - payloadJson: '{}', - now: input.now, - }) - - return { - order: await getWorkOrderByIdWithClient(client, input.workOrderId), - failureReason: null, - } - }) -} - -export async function assignWorkOrderToWorker(input: { - workOrderId: number - workerId: number - depositAmount: number - maxActiveOrders: number - deadlineAt: string | null - now: string - /** 操作人显示名(管理员用户名等) */ - actorName?: string -}): Promise<{ - order: WorkOrderRow | null - failureReason: - | 'work_order_not_assignable' - | 'worker_active_order_limit' - | 'worker_deposit_insufficient' - | null -}> { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - SELECT * - FROM work_orders - WHERE id = $1 - FOR UPDATE - `, - [input.workOrderId], - ) - const workOrder = currentResult.rows[0] || null - if ( - !workOrder || - !['unassigned', 'open'].includes(workOrder.status) || - workOrder.assigned_worker_id - ) { - return { order: null, failureReason: 'work_order_not_assignable' } - } - const fromStatus = workOrder.status - - await ensureWorkerWalletWithClient(client, input.workerId, input.now) - const wallet = await getWorkerWalletWithClient(client, input.workerId) - const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId) - if (activeOrderCount >= input.maxActiveOrders) { - return { order: null, failureReason: 'worker_active_order_limit' } - } - - const available = Number(wallet?.available_amount || 0) - if (available < input.depositAmount) { - return { order: null, failureReason: 'worker_deposit_insufficient' } - } - - await client.query( - ` - UPDATE work_orders - SET - status = 'in_progress', - assigned_worker_id = $1, - last_assigned_worker_id = $1, - assigned_at = $2, - deadline_at = $3, - updated_at = $2 - WHERE id = $4 - `, - [input.workerId, input.now, input.deadlineAt, input.workOrderId], - ) - - if (input.depositAmount > 0) { - const nextAvailable = available - input.depositAmount - const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount - 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, input.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, 'deposit_freeze', $2, $3, $4, $5, '后台指派冻结押金', '{}'::jsonb, $6) - `, - [ - input.workerId, - -input.depositAmount, - nextAvailable, - nextFrozen, - input.workOrderId, - input.now, - ], - ) - } - - await createWorkOrderEventWithClient(client, { - workOrderId: input.workOrderId, - actorType: 'admin', - actorId: input.actorName || '', - eventType: 'assigned_by_admin', - fromStatus, - toStatus: 'in_progress', - payloadJson: JSON.stringify({ workerId: input.workerId, depositAmount: input.depositAmount }), - now: input.now, - }) - - return { - order: await getWorkOrderByIdWithClient(client, input.workOrderId), - failureReason: null, - } - }) -} - -export async function cancelWorkerWorkOrder(input: { - workOrderId: number - workerId: number - now: string -}): Promise<{ - order: WorkOrderRow | null - failureReason: 'work_order_not_in_progress' | 'work_order_owner_required' | null -}> { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - SELECT * - FROM work_orders - WHERE id = $1 - FOR UPDATE - `, - [input.workOrderId], - ) - const workOrder = currentResult.rows[0] || null - if (!workOrder || workOrder.status !== 'in_progress') { - return { order: null, failureReason: 'work_order_not_in_progress' } - } - if (Number(workOrder.assigned_worker_id || 0) !== input.workerId) { - return { order: null, failureReason: 'work_order_owner_required' } - } - - await ensureWorkerWalletWithClient(client, input.workerId, input.now) - const wallet = await getWorkerWalletWithClient(client, input.workerId) - const releaseAmount = Math.min( - await getOutstandingDepositAmountWithClient(client, input.workerId, input.workOrderId), - Number(wallet?.frozen_deposit_amount || 0), - ) - - await client.query( - ` - UPDATE work_orders - SET - status = 'open', - assigned_worker_id = NULL, - assigned_at = NULL, - deadline_at = NULL, - hall_queued_at = $1, - updated_at = $1 - WHERE id = $2 - `, - [input.now, input.workOrderId], - ) - - if (releaseAmount > 0) { - const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount - const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) - 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, input.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, 'deposit_release', $2, $3, $4, $5, '取消接单退还押金', '{}'::jsonb, $6) - `, - [input.workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], - ) - } - - await createWorkOrderEventWithClient(client, { - workOrderId: input.workOrderId, - actorType: 'worker', - actorId: String(input.workerId), - eventType: 'cancelled_by_worker', - fromStatus: 'in_progress', - toStatus: 'open', - payloadJson: JSON.stringify({ releaseAmount }), - now: input.now, - }) - - return { - order: await getWorkOrderByIdWithClient(client, input.workOrderId), - failureReason: null, - } - }) -} - -/** 打手提交工单问题反馈,不改变工单状态、押金或资金流水。 */ - -export async function unassignWorkOrder(input: { - workOrderId: number - now: string - actorName?: string -}): Promise<{ - order: WorkOrderRow | null - failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null -}> { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - SELECT * - FROM work_orders - WHERE id = $1 - FOR UPDATE - `, - [input.workOrderId], - ) - const workOrder = currentResult.rows[0] || null - if (!workOrder || workOrder.status !== 'in_progress') { - return { order: null, failureReason: 'work_order_not_in_progress' } - } - const workerId = Number(workOrder.assigned_worker_id || 0) - if (!workerId) { - return { order: null, failureReason: 'work_order_no_worker' } - } - - await ensureWorkerWalletWithClient(client, workerId, input.now) - const wallet = await getWorkerWalletWithClient(client, workerId) - const releaseAmount = Math.min( - await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId), - Number(wallet?.frozen_deposit_amount || 0), - ) - - await client.query( - ` - UPDATE work_orders - SET - status = 'unassigned', - assigned_worker_id = NULL, - assigned_at = NULL, - deadline_at = NULL, - updated_at = $1 - WHERE id = $2 - `, - [input.now, input.workOrderId], - ) - - if (releaseAmount > 0) { - const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount - const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) - 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, 'deposit_release', $2, $3, $4, $5, '后台取消指派退还押金', '{}'::jsonb, $6) - `, - [workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], - ) - } - - await createWorkOrderEventWithClient(client, { - workOrderId: input.workOrderId, - actorType: 'admin', - actorId: input.actorName || '', - eventType: 'unassigned_by_admin', - fromStatus: 'in_progress', - toStatus: 'unassigned', - payloadJson: JSON.stringify({ workerId, releaseAmount }), - now: input.now, - }) - - return { - order: await getWorkOrderByIdWithClient(client, input.workOrderId), - failureReason: null, - } - }) -} - -export async function returnAssignedWorkOrderToHall(input: { - workOrderId: number - now: string - reason?: string - actorName?: string -}): Promise<{ - order: WorkOrderRow | null - failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null -}> { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - SELECT * - FROM work_orders - WHERE id = $1 - FOR UPDATE - `, - [input.workOrderId], - ) - const workOrder = currentResult.rows[0] || null - if (!workOrder || workOrder.status !== 'in_progress') { - return { order: null, failureReason: 'work_order_not_in_progress' } - } - const workerId = Number(workOrder.assigned_worker_id || 0) - if (!workerId) { - return { order: null, failureReason: 'work_order_no_worker' } - } - - await ensureWorkerWalletWithClient(client, workerId, input.now) - const wallet = await getWorkerWalletWithClient(client, workerId) - const releaseAmount = Math.min( - await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId), - Number(wallet?.frozen_deposit_amount || 0), - ) - - await client.query( - ` - UPDATE work_orders - SET - status = 'open', - assigned_worker_id = NULL, - assigned_at = NULL, - deadline_at = NULL, - published_at = $1, - hall_queued_at = $1, - updated_at = $1 - WHERE id = $2 - `, - [input.now, input.workOrderId], - ) - - if (releaseAmount > 0) { - const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount - const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) - 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, 'deposit_release', $2, $3, $4, $5, '后台退回大厅退还押金', '{}'::jsonb, $6) - `, - [workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], - ) - } - - await createWorkOrderEventWithClient(client, { - workOrderId: input.workOrderId, - actorType: 'admin', - actorId: input.actorName || '', - eventType: 'returned_to_hall_by_admin', - fromStatus: 'in_progress', - toStatus: 'open', - payloadJson: JSON.stringify({ - workerId, - releaseAmount, - reason: String(input.reason || '').trim(), - }), - now: input.now, - }) - - return { - order: await getWorkOrderByIdWithClient(client, input.workOrderId), - failureReason: null, - } - }) -} - -export async function cancelAssignedWorkOrder(input: { - workOrderId: number - now: string - reason?: string - actorName?: string -}): Promise<{ - order: WorkOrderRow | null - failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null -}> { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - SELECT * - FROM work_orders - WHERE id = $1 - FOR UPDATE - `, - [input.workOrderId], - ) - const workOrder = currentResult.rows[0] || null - if (!workOrder || workOrder.status !== 'in_progress') { - return { order: null, failureReason: 'work_order_not_in_progress' } - } - const workerId = Number(workOrder.assigned_worker_id || 0) - if (!workerId) { - return { order: null, failureReason: 'work_order_no_worker' } - } - - await ensureWorkerWalletWithClient(client, workerId, input.now) - const wallet = await getWorkerWalletWithClient(client, workerId) - const releaseAmount = Math.min( - await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId), - Number(wallet?.frozen_deposit_amount || 0), - ) - - await client.query( - ` - UPDATE work_orders - SET - status = 'cancelled', - assigned_worker_id = NULL, - assigned_at = NULL, - deadline_at = NULL, - published_at = NULL, - updated_at = $1 - WHERE id = $2 - `, - [input.now, input.workOrderId], - ) - - if (releaseAmount > 0) { - const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount - const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount) - 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, 'deposit_release', $2, $3, $4, $5, '后台撤单退还押金', '{}'::jsonb, $6) - `, - [workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], - ) - } - - await createWorkOrderEventWithClient(client, { - workOrderId: input.workOrderId, - actorType: 'admin', - actorId: input.actorName || '', - eventType: 'cancelled_by_admin', - fromStatus: 'in_progress', - toStatus: 'cancelled', - payloadJson: JSON.stringify({ - workerId, - releaseAmount, - reason: String(input.reason || '').trim(), - }), - now: input.now, - }) - - return { - order: await getWorkOrderByIdWithClient(client, input.workOrderId), - failureReason: null, - } - }) -} - -/** 将已取消工单恢复为未分配状态,不变更历史押金流水。 */ -export async function reopenCancelledWorkOrder(input: { - workOrderId: number - now: string - actorName?: string -}): Promise<{ - order: WorkOrderRow | null - failureReason: 'work_order_not_cancelled' | 'work_order_has_active_shares' | null -}> { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - SELECT * - FROM work_orders - WHERE id = $1 - FOR UPDATE - `, - [input.workOrderId], - ) - const workOrder = currentResult.rows[0] || null - if (!workOrder || workOrder.status !== 'cancelled') { - return { order: null, failureReason: 'work_order_not_cancelled' } - } - - const activeShareResult = 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(activeShareResult.rows[0]?.total || 0) > 0) { - return { order: null, failureReason: 'work_order_has_active_shares' } - } - - await client.query( - ` - UPDATE work_orders - SET - status = 'unassigned', - assigned_worker_id = NULL, - assigned_at = NULL, - deadline_at = NULL, - acceptance_json = '{}'::jsonb, - draft_acceptance_json = '{}'::jsonb, - submitted_at = NULL, - accepted_at = NULL, - problem_note = '', - published_at = NULL, - pinned_at = NULL, - updated_at = $1 - WHERE id = $2 - `, - [input.now, input.workOrderId], - ) - - await createWorkOrderEventWithClient(client, { - workOrderId: input.workOrderId, - actorType: 'admin', - actorId: input.actorName || '', - eventType: 'reopened_from_cancelled', - fromStatus: 'cancelled', - toStatus: 'unassigned', - payloadJson: JSON.stringify({ - previousAssignedWorkerId: Number(workOrder.last_assigned_worker_id || 0) || null, - }), - now: input.now, - }) - - return { - order: await getWorkOrderByIdWithClient(client, input.workOrderId), - failureReason: null, - } - }) -} +export { + assignWorkOrderToWorker, + grabWorkOrder, +} from './work-order-grab-repo.js' +export { + cancelAssignedWorkOrder, + cancelWorkerWorkOrder, + reopenCancelledWorkOrder, + returnAssignedWorkOrderToHall, + unassignWorkOrder, +} from './work-order-cancel-repo.js' export async function acceptWorkOrderAndSettle(input: { workOrderId: number