From 175b2dbac63dcf3f15bdd19d93129eca1d392c39 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 21 Aug 2026 18:44:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=B7=A5=E5=8D=95=E6=8A=BC?= =?UTF-8?q?=E9=87=91=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 | 1 + .../work-order-deposit-repo.ts | 244 +++++++++++++++++ .../worker-platform/work-order-repo.ts | 248 +----------------- 3 files changed, 256 insertions(+), 237 deletions(-) create mode 100644 apps/backend/src/repositories/worker-platform/work-order-deposit-repo.ts diff --git a/apps/backend/src/repositories/worker-platform/index.ts b/apps/backend/src/repositories/worker-platform/index.ts index 83003ae8..c305ad84 100644 --- a/apps/backend/src/repositories/worker-platform/index.ts +++ b/apps/backend/src/repositories/worker-platform/index.ts @@ -10,6 +10,7 @@ export * from './work-order-repo.js' export * from './work-order-query-repo.js' export * from './work-order-management-repo.js' export * from './work-order-deposit-query-repo.js' +export * from './work-order-deposit-repo.js' export * from './work-order-event-repo.js' export * from './work-order-share-repo.js' export * from './work-category-repo.js' diff --git a/apps/backend/src/repositories/worker-platform/work-order-deposit-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-deposit-repo.ts new file mode 100644 index 00000000..aa8d7064 --- /dev/null +++ b/apps/backend/src/repositories/worker-platform/work-order-deposit-repo.ts @@ -0,0 +1,244 @@ +import { withTransaction } from '../../db/client.js' +import { + ensureWorkerWalletWithClient, + getWorkerWalletWithClient, +} from './shared.js' +import type { WorkerDepositUnfreezeRow } from './types.js' +import type { PoolClient } from 'pg' + +export function resolveOutstandingDepositAmount( + totals: + | { + frozen_amount?: number + released_amount?: number + deducted_amount?: number + } + | null + | undefined, +) { + return Math.max( + 0, + Number(totals?.frozen_amount || 0) - + Number(totals?.released_amount || 0) - + Number(totals?.deducted_amount || 0), + ) +} + +export async function getOutstandingDepositAmountWithClient( + client: PoolClient, + workerId: number, + workOrderId: number, +) { + const result = await client.query<{ + frozen_amount: number + released_amount: number + deducted_amount: number + }>( + ` + SELECT + COALESCE(SUM(CASE WHEN ledger_type = 'deposit_freeze' THEN ABS(amount) ELSE 0 END), 0)::int AS frozen_amount, + COALESCE(SUM(CASE WHEN ledger_type = 'deposit_release' THEN ABS(amount) ELSE 0 END), 0)::int AS released_amount, + COALESCE(SUM(CASE WHEN ledger_type = 'deposit_deduction' THEN ABS(amount) ELSE 0 END), 0)::int AS deducted_amount + FROM worker_wallet_ledgers + WHERE worker_id = $1 AND related_work_order_id = $2 + `, + [workerId, workOrderId], + ) + return resolveOutstandingDepositAmount(result.rows[0]) +} + +export async function enqueueDepositUnfreezeWithClient( + client: PoolClient, + input: { + workerId: number + workOrderId: number + amount: number + unfreezeDays: number + now: string + }, +) { + if (input.amount <= 0) return + const unfreezeAt = new Date( + new Date(input.now).getTime() + input.unfreezeDays * 86_400_000, + ).toISOString() + await client.query( + ` + INSERT INTO worker_deposit_unfreezes ( + worker_id, work_order_id, amount, unfreeze_at, status, created_at, updated_at + ) VALUES ($1, $2, $3, $4, 'pending', $5, $5) + `, + [input.workerId, input.workOrderId, input.amount, unfreezeAt, input.now], + ) +} + +export async function deductPendingDepositUnfreeze(input: { + workOrderId: number + amount: number + note: string + now: string +}): Promise<{ + deductedAmount: number + failureReason: 'no_pending_deposit' | 'amount_exceeds_pending' | null +}> { + return withTransaction(async (client) => { + const pendingResult = await client.query( + ` + SELECT * + FROM worker_deposit_unfreezes + WHERE work_order_id = $1 AND status = 'pending' + ORDER BY unfreeze_at ASC + FOR UPDATE + `, + [input.workOrderId], + ) + const pending = pendingResult.rows + if (pending.length === 0) { + return { deductedAmount: 0, failureReason: 'no_pending_deposit' } + } + const totalPending = pending.reduce((sum, row) => sum + Number(row.amount || 0), 0) + if (input.amount <= 0 || input.amount > totalPending) { + return { deductedAmount: 0, failureReason: 'amount_exceeds_pending' } + } + + let remainingToDeduct = input.amount + let deductedAmount = 0 + + for (const row of pending) { + if (remainingToDeduct <= 0) break + const rowAmount = Number(row.amount || 0) + if (rowAmount <= 0) continue + const deductFromRow = Math.min(rowAmount, remainingToDeduct) + const isFullyDeducted = deductFromRow >= rowAmount + + if (isFullyDeducted) { + await client.query( + ` + UPDATE worker_deposit_unfreezes + SET status = 'deducted', updated_at = $1 + WHERE id = $2 + `, + [input.now, row.id], + ) + } else { + await client.query( + ` + UPDATE worker_deposit_unfreezes + SET amount = amount - $1, updated_at = $2 + WHERE id = $3 + `, + [deductFromRow, input.now, row.id], + ) + } + + const workerWallet = await getWorkerWalletWithClient(client, row.worker_id) + await client.query( + ` + UPDATE worker_wallets + SET pending_unfreeze_amount = GREATEST(0, pending_unfreeze_amount - $1), updated_at = $2 + WHERE worker_id = $3 + `, + [deductFromRow, input.now, row.worker_id], + ) + 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_deduction', $2, $3, $4, $5, '问题单扣减待解冻押金', $6::jsonb, $7) + `, + [ + row.worker_id, + -deductFromRow, + Number(workerWallet?.available_amount || 0), + Number(workerWallet?.frozen_deposit_amount || 0), + input.workOrderId, + JSON.stringify({ + workOrderId: input.workOrderId, + unfreezeId: row.id, + note: input.note, + }), + input.now, + ], + ) + deductedAmount += deductFromRow + remainingToDeduct -= deductFromRow + } + + return { deductedAmount, failureReason: null } + }) +} + +export async function releaseDepositUnfreeze({ + unfreezeId, + now, +}: { + unfreezeId: number + now: string +}): Promise { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM worker_deposit_unfreezes + WHERE id = $1 AND status = 'pending' + FOR UPDATE + `, + [unfreezeId], + ) + const unfreeze = currentResult.rows[0] || null + if (!unfreeze) return null + + await ensureWorkerWalletWithClient(client, unfreeze.worker_id, now) + const wallet = await getWorkerWalletWithClient(client, unfreeze.worker_id) + const amount = Math.min( + Number(unfreeze.amount || 0), + Number(wallet?.pending_unfreeze_amount || 0), + ) + + await client.query( + ` + UPDATE worker_deposit_unfreezes + SET status = 'released', released_at = $1, updated_at = $1 + WHERE id = $2 + `, + [now, unfreeze.id], + ) + + if (amount > 0) { + const nextAvailable = Number(wallet?.available_amount || 0) + amount + const nextPending = Math.max(0, Number(wallet?.pending_unfreeze_amount || 0) - amount) + await client.query( + ` + UPDATE worker_wallets + SET available_amount = $1, pending_unfreeze_amount = $2, updated_at = $3 + WHERE worker_id = $4 + `, + [nextAvailable, nextPending, now, unfreeze.worker_id], + ) + 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_unfreeze', $2, $3, $4, $5, '押金已解冻到账', $6::jsonb, $7) + `, + [ + unfreeze.worker_id, + amount, + nextAvailable, + Number(wallet?.frozen_deposit_amount || 0), + unfreeze.work_order_id, + JSON.stringify({ unfreezeId: unfreeze.id }), + now, + ], + ) + } + + return { + ...unfreeze, + status: 'released', + released_at: now, + updated_at: now, + } + }) +} 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 d510a69e..07c13560 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -35,10 +35,21 @@ import type { } from './types.js' import type { PoolClient } from 'pg' import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js' +import { + enqueueDepositUnfreezeWithClient, + getOutstandingDepositAmountWithClient, + resolveOutstandingDepositAmount, +} from './work-order-deposit-repo.js' export * from './work-order-query-repo.js' export { sumPendingUnfreezeByOrderIds } from './work-order-deposit-query-repo.js' export { listDueDepositUnfreezes } from './work-order-deposit-query-repo.js' +export { + deductPendingDepositUnfreeze, + getOutstandingDepositAmountWithClient, + releaseDepositUnfreeze, + resolveOutstandingDepositAmount, +} from './work-order-deposit-repo.js' export { createWorkOrder, @@ -1674,240 +1685,3 @@ export async function settleOverdueWorkOrder(input: { }) } -export async function deductPendingDepositUnfreeze(input: { - workOrderId: number - amount: number - note: string - now: string -}): Promise<{ - deductedAmount: number - failureReason: 'no_pending_deposit' | 'amount_exceeds_pending' | null -}> { - return withTransaction(async (client) => { - const pendingResult = await client.query( - ` - SELECT * - FROM worker_deposit_unfreezes - WHERE work_order_id = $1 AND status = 'pending' - ORDER BY unfreeze_at ASC - FOR UPDATE - `, - [input.workOrderId], - ) - const pending = pendingResult.rows - if (pending.length === 0) { - return { deductedAmount: 0, failureReason: 'no_pending_deposit' } - } - const totalPending = pending.reduce((sum, row) => sum + Number(row.amount || 0), 0) - if (input.amount <= 0 || input.amount > totalPending) { - return { deductedAmount: 0, failureReason: 'amount_exceeds_pending' } - } - - let remainingToDeduct = input.amount - let deductedAmount = 0 - - for (const row of pending) { - if (remainingToDeduct <= 0) break - const rowAmount = Number(row.amount || 0) - if (rowAmount <= 0) continue - const deductFromRow = Math.min(rowAmount, remainingToDeduct) - const isFullyDeducted = deductFromRow >= rowAmount - - if (isFullyDeducted) { - await client.query( - ` - UPDATE worker_deposit_unfreezes - SET status = 'deducted', updated_at = $1 - WHERE id = $2 - `, - [input.now, row.id], - ) - } else { - await client.query( - ` - UPDATE worker_deposit_unfreezes - SET amount = amount - $1, updated_at = $2 - WHERE id = $3 - `, - [deductFromRow, input.now, row.id], - ) - } - - const workerWallet = await getWorkerWalletWithClient(client, row.worker_id) - await client.query( - ` - UPDATE worker_wallets - SET pending_unfreeze_amount = GREATEST(0, pending_unfreeze_amount - $1), updated_at = $2 - WHERE worker_id = $3 - `, - [deductFromRow, input.now, row.worker_id], - ) - 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_deduction', $2, $3, $4, $5, '问题单扣减待解冻押金', $6::jsonb, $7) - `, - [ - row.worker_id, - -deductFromRow, - Number(workerWallet?.available_amount || 0), - Number(workerWallet?.frozen_deposit_amount || 0), - input.workOrderId, - JSON.stringify({ - workOrderId: input.workOrderId, - unfreezeId: row.id, - note: input.note, - }), - input.now, - ], - ) - deductedAmount += deductFromRow - remainingToDeduct -= deductFromRow - } - - return { deductedAmount, failureReason: null } - }) -} - -export async function releaseDepositUnfreeze({ - unfreezeId, - now, -}: { - unfreezeId: number - now: string -}): Promise { - return withTransaction(async (client) => { - const currentResult = await client.query( - ` - SELECT * - FROM worker_deposit_unfreezes - WHERE id = $1 AND status = 'pending' - FOR UPDATE - `, - [unfreezeId], - ) - const unfreeze = currentResult.rows[0] || null - if (!unfreeze) return null - - await ensureWorkerWalletWithClient(client, unfreeze.worker_id, now) - const wallet = await getWorkerWalletWithClient(client, unfreeze.worker_id) - const amount = Math.min( - Number(unfreeze.amount || 0), - Number(wallet?.pending_unfreeze_amount || 0), - ) - - await client.query( - ` - UPDATE worker_deposit_unfreezes - SET status = 'released', released_at = $1, updated_at = $1 - WHERE id = $2 - `, - [now, unfreeze.id], - ) - - if (amount > 0) { - const nextAvailable = Number(wallet?.available_amount || 0) + amount - const nextPending = Math.max(0, Number(wallet?.pending_unfreeze_amount || 0) - amount) - await client.query( - ` - UPDATE worker_wallets - SET available_amount = $1, pending_unfreeze_amount = $2, updated_at = $3 - WHERE worker_id = $4 - `, - [nextAvailable, nextPending, now, unfreeze.worker_id], - ) - 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_unfreeze', $2, $3, $4, $5, '押金已解冻到账', $6::jsonb, $7) - `, - [ - unfreeze.worker_id, - amount, - nextAvailable, - Number(wallet?.frozen_deposit_amount || 0), - unfreeze.work_order_id, - JSON.stringify({ unfreezeId: unfreeze.id }), - now, - ], - ) - } - - return { - ...unfreeze, - status: 'released', - released_at: now, - updated_at: now, - } - }) -} - -async function enqueueDepositUnfreezeWithClient( - client: PoolClient, - input: { - workerId: number - workOrderId: number - amount: number - unfreezeDays: number - now: string - }, -) { - if (input.amount <= 0) return - const unfreezeAt = new Date( - new Date(input.now).getTime() + input.unfreezeDays * 86_400_000, - ).toISOString() - await client.query( - ` - INSERT INTO worker_deposit_unfreezes ( - worker_id, work_order_id, amount, unfreeze_at, status, created_at, updated_at - ) VALUES ($1, $2, $3, $4, 'pending', $5, $5) - `, - [input.workerId, input.workOrderId, input.amount, unfreezeAt, input.now], - ) -} - -export function resolveOutstandingDepositAmount( - totals: - | { - frozen_amount?: number - released_amount?: number - deducted_amount?: number - } - | null - | undefined, -) { - return Math.max( - 0, - Number(totals?.frozen_amount || 0) - - Number(totals?.released_amount || 0) - - Number(totals?.deducted_amount || 0), - ) -} - -export async function getOutstandingDepositAmountWithClient( - client: PoolClient, - workerId: number, - workOrderId: number, -) { - const result = await client.query<{ - frozen_amount: number - released_amount: number - deducted_amount: number - }>( - ` - SELECT - COALESCE(SUM(CASE WHEN ledger_type = 'deposit_freeze' THEN ABS(amount) ELSE 0 END), 0)::int AS frozen_amount, - COALESCE(SUM(CASE WHEN ledger_type = 'deposit_release' THEN ABS(amount) ELSE 0 END), 0)::int AS released_amount, - COALESCE(SUM(CASE WHEN ledger_type = 'deposit_deduction' THEN ABS(amount) ELSE 0 END), 0)::int AS deducted_amount - FROM worker_wallet_ledgers - WHERE worker_id = $1 - AND related_work_order_id = $2 - `, - [workerId, workOrderId], - ) - return resolveOutstandingDepositAmount(result.rows[0]) -}