拆分工单押金事务仓储

This commit is contained in:
yml2213
2026-08-21 18:44:35 +08:00
parent 3c651ada9c
commit 175b2dbac6
3 changed files with 256 additions and 237 deletions
@@ -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'
@@ -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<WorkerDepositUnfreezeRow>(
`
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<WorkerDepositUnfreezeRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkerDepositUnfreezeRow>(
`
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,
}
})
}
@@ -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<WorkerDepositUnfreezeRow>(
`
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<WorkerDepositUnfreezeRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkerDepositUnfreezeRow>(
`
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])
}