押金解冻天数可配置与待解冻押金扣减,超时订单保留记录
This commit is contained in:
@@ -33,6 +33,7 @@ export type WorkerUserRow = {
|
||||
level_permission_json?: string | Record<string, unknown>
|
||||
available_amount?: number
|
||||
frozen_deposit_amount?: number
|
||||
pending_unfreeze_amount?: number
|
||||
total_credited_amount?: number
|
||||
total_settled_amount?: number
|
||||
}
|
||||
@@ -41,12 +42,25 @@ export type WorkerWalletRow = {
|
||||
worker_id: number
|
||||
available_amount: number
|
||||
frozen_deposit_amount: number
|
||||
pending_unfreeze_amount: number
|
||||
total_credited_amount: number
|
||||
total_settled_amount: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WorkerDepositUnfreezeRow = {
|
||||
id: number
|
||||
worker_id: number
|
||||
work_order_id: number
|
||||
amount: number
|
||||
unfreeze_at: string
|
||||
status: string
|
||||
released_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WorkerWalletLedgerRow = {
|
||||
id: number
|
||||
worker_id: number
|
||||
@@ -150,6 +164,7 @@ export type WorkOrderRow = {
|
||||
required_deposit_amount: number
|
||||
deposit_threshold_amount: number
|
||||
assigned_worker_id: number | null
|
||||
last_assigned_worker_id: number | null
|
||||
material_json: string | Record<string, unknown>
|
||||
requirement_json: string | Record<string, unknown>
|
||||
acceptance_json: string | Record<string, unknown>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient, toJsonString, toPositiveInteger } from './shared.js'
|
||||
import type { CreateWorkOrderInput, GrabWorkOrderResult, ListInput, ProblemWorkOrderResolutionAction, ProductRuleListInput, WorkCategoryRow, WorkOrderRow, WorkOrderShareRow, WorkProductRuleRow } from './types.js'
|
||||
import type { CreateWorkOrderInput, GrabWorkOrderResult, ListInput, ProblemWorkOrderResolutionAction, ProductRuleListInput, WorkCategoryRow, WorkerDepositUnfreezeRow, WorkOrderRow, WorkOrderShareRow, WorkProductRuleRow } from './types.js'
|
||||
import type { PoolClient } from 'pg'
|
||||
import { maybeUpgradeWorkerLevelWithClient } from './worker-repo.js'
|
||||
|
||||
@@ -772,6 +772,7 @@ export async function grabWorkOrder(input: {
|
||||
SET
|
||||
status = 'in_progress',
|
||||
assigned_worker_id = $1,
|
||||
last_assigned_worker_id = $1,
|
||||
assigned_at = $2,
|
||||
deadline_at = $3,
|
||||
updated_at = $2
|
||||
@@ -946,6 +947,7 @@ export async function countWorkerCancellationsSince(
|
||||
|
||||
export async function acceptWorkOrderAndSettle(input: {
|
||||
workOrderId: number
|
||||
unfreezeDays: number
|
||||
now: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
@@ -1009,8 +1011,11 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const shareReward = Number(share.share_reward || 0)
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) + releaseAmount + shareReward
|
||||
Number(wallet?.available_amount || 0) +
|
||||
shareReward +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount)
|
||||
const nextFrozen = Math.max(
|
||||
0,
|
||||
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
|
||||
@@ -1022,31 +1027,70 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
SET
|
||||
available_amount = $1,
|
||||
frozen_deposit_amount = $2,
|
||||
total_settled_amount = total_settled_amount + $3,
|
||||
updated_at = $4
|
||||
WHERE worker_id = $5
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4,
|
||||
updated_at = $5
|
||||
WHERE worker_id = $6
|
||||
`,
|
||||
[nextAvailable, nextFrozen, shareReward, input.now, shareWorkerId],
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
shareReward,
|
||||
input.now,
|
||||
shareWorkerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0) {
|
||||
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, '拼单验收释放押金', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: Number(share.id) }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
if (shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId: shareWorkerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
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_pending_unfreeze', $2, $3, $4, $5, '拼单验收押金待解冻', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: Number(share.id),
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
} else {
|
||||
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, '拼单验收释放押金', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: Number(share.id) }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (shareReward > 0) {
|
||||
@@ -1095,7 +1139,11 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const rewardAmount = Number(workOrder.reward_amount || 0)
|
||||
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount + rewardAmount
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
rewardAmount +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount)
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
@@ -1104,23 +1152,61 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
SET
|
||||
available_amount = $1,
|
||||
frozen_deposit_amount = $2,
|
||||
total_settled_amount = total_settled_amount + $3,
|
||||
updated_at = $4
|
||||
WHERE worker_id = $5
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4,
|
||||
updated_at = $5
|
||||
WHERE worker_id = $6
|
||||
`,
|
||||
[nextAvailable, nextFrozen, rewardAmount, input.now, workerId],
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
rewardAmount,
|
||||
input.now,
|
||||
workerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0) {
|
||||
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],
|
||||
)
|
||||
if (shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
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_pending_unfreeze', $2, $3, $4, $5, '验收通过押金待解冻', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
workerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
} else {
|
||||
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],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (rewardAmount > 0) {
|
||||
@@ -1437,6 +1523,244 @@ export async function settleOverdueWorkOrder(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export async function sumPendingUnfreezeByOrderIds(
|
||||
workOrderIds: number[],
|
||||
): Promise<Map<number, number>> {
|
||||
const totals = new Map<number, number>()
|
||||
const uniqueIds = [...new Set(workOrderIds.map(Number).filter((id) => id > 0))]
|
||||
if (uniqueIds.length === 0) return totals
|
||||
const result = await query<{ work_order_id: number; total: number }>(
|
||||
`
|
||||
SELECT work_order_id, SUM(amount)::int AS total
|
||||
FROM worker_deposit_unfreezes
|
||||
WHERE work_order_id = ANY($1::bigint[])
|
||||
AND status = 'pending'
|
||||
GROUP BY work_order_id
|
||||
`,
|
||||
[uniqueIds],
|
||||
)
|
||||
for (const row of result.rows) {
|
||||
totals.set(Number(row.work_order_id), Number(row.total || 0))
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
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 listDueDepositUnfreezes({
|
||||
limit = 100,
|
||||
workerId = 0,
|
||||
}: { limit?: number; workerId?: number } = {}): Promise<WorkerDepositUnfreezeRow[]> {
|
||||
const params: unknown[] = []
|
||||
let workerClause = ''
|
||||
if (workerId > 0) {
|
||||
params.push(workerId)
|
||||
workerClause = `AND worker_id = $${params.length}`
|
||||
}
|
||||
params.push(limit)
|
||||
const result = await query<WorkerDepositUnfreezeRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM worker_deposit_unfreezes
|
||||
WHERE status = 'pending'
|
||||
AND unfreeze_at <= NOW()
|
||||
${workerClause}
|
||||
ORDER BY unfreeze_at ASC
|
||||
LIMIT $${params.length}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
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 async function countWorkerActiveOrders(workerId: number | string): Promise<number> {
|
||||
const result = await query<{ total: number }>(
|
||||
`
|
||||
@@ -1629,6 +1953,7 @@ function buildWorkOrderWhere({
|
||||
params.push(workerSharingId)
|
||||
filters.push(
|
||||
`(wo.assigned_worker_id = $${params.length}
|
||||
OR wo.last_assigned_worker_id = $${params.length}
|
||||
OR wo.id IN (
|
||||
SELECT work_order_id FROM work_order_shares
|
||||
WHERE worker_id = $${params.length} AND status != 'cancelled'
|
||||
|
||||
@@ -14,6 +14,7 @@ const WORKER_USER_SELECT = `
|
||||
inv.display_name AS inviter_display_name,
|
||||
ww.available_amount,
|
||||
ww.frozen_deposit_amount,
|
||||
ww.pending_unfreeze_amount,
|
||||
ww.total_credited_amount,
|
||||
ww.total_settled_amount
|
||||
FROM worker_users wu
|
||||
@@ -409,6 +410,26 @@ export async function getWorkerFinanceRequestById(
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function countWorkerWithdrawRequestsOnDay(
|
||||
workerId: number | string,
|
||||
dayStartIso: string,
|
||||
dayEndIso: string,
|
||||
): Promise<number> {
|
||||
const result = await query<{ total: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM worker_finance_requests
|
||||
WHERE worker_id = $1
|
||||
AND request_type = 'withdraw'
|
||||
AND status != 'cancelled'
|
||||
AND created_at >= $2
|
||||
AND created_at < $3
|
||||
`,
|
||||
[Number(workerId), dayStartIso, dayEndIso],
|
||||
)
|
||||
return Number(result.rows[0]?.total || 0)
|
||||
}
|
||||
|
||||
export async function listWorkerFinanceRequests({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
|
||||
Reference in New Issue
Block a user