修复接单平台并发、分页与补资料

This commit is contained in:
yml2213
2026-07-28 10:15:44 +08:00
parent a2587eda01
commit f0ba0fe17f
22 changed files with 1096 additions and 145 deletions
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { resolveOutstandingDepositAmount } from './worker-platform-repo.js'
test('resolveOutstandingDepositAmount deducts released and deducted history', () => {
assert.equal(
resolveOutstandingDepositAmount({
frozen_amount: 20_000,
released_amount: 5_000,
deducted_amount: 3_000,
}),
12_000,
)
})
test('resolveOutstandingDepositAmount never returns a negative deposit', () => {
assert.equal(
resolveOutstandingDepositAmount({
frozen_amount: 10_000,
released_amount: 12_000,
deducted_amount: 1_000,
}),
0,
)
assert.equal(resolveOutstandingDepositAmount(null), 0)
})
@@ -104,6 +104,22 @@ export type WorkOrderRow = {
worker_display_name?: string
}
export type ProblemWorkOrderResolutionAction =
| 'return_to_worker'
| 'reopen'
| 'cancel_release'
| 'cancel_deduct'
export type GrabWorkOrderFailureReason =
| 'worker_active_order_limit'
| 'worker_deposit_insufficient'
| 'work_order_not_open'
export type GrabWorkOrderResult = {
order: WorkOrderRow | null
failureReason: GrabWorkOrderFailureReason | null
}
type ListInput = {
page?: number
pageSize?: number
@@ -317,6 +333,25 @@ export async function updateWorkerUser(
return getWorkerUserById(workerId)
}
export async function incrementWorkerSessionVersion(
workerId: number | string,
now: string,
): Promise<WorkerUserRow | null> {
const result = await query<{ id: number }>(
`
UPDATE worker_users
SET session_version = session_version + 1, updated_at = $2
WHERE id = $1
RETURNING id
`,
[Number(workerId), now],
)
if (!result.rows[0]) {
return null
}
return getWorkerUserById(workerId)
}
export async function listWorkerUsers({
page = 1,
pageSize = 20,
@@ -604,6 +639,18 @@ export async function findPendingMaterialWorkOrderByPlatformOrderId(
return result.rows[0] || null
}
export async function listPendingMaterialWorkOrdersByPlatformOrderId(
platformOrderId: string,
): Promise<WorkOrderRow[]> {
const result = await query<WorkOrderRow>(
`${WORK_ORDER_SELECT}
WHERE wo.platform_order_id = $1 AND wo.status = 'pending_material'
ORDER BY wo.id ASC`,
[String(platformOrderId || '').trim()],
)
return result.rows
}
export async function listWorkOrders({
page = 1,
pageSize = 20,
@@ -690,14 +737,26 @@ export async function grabWorkOrder(input: {
workOrderId: number
workerId: number
depositAmount: number
maxActiveOrders: number
now: string
}): Promise<WorkOrderRow | null> {
}): Promise<GrabWorkOrderResult> {
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 null
return {
order: null,
failureReason: 'worker_deposit_insufficient',
}
}
const result = await client.query<{ id: number }>(
@@ -716,7 +775,10 @@ export async function grabWorkOrder(input: {
[input.workerId, input.now, input.workOrderId],
)
if (!result.rows[0]) {
return null
return {
order: null,
failureReason: 'work_order_not_open',
}
}
if (input.depositAmount > 0) {
@@ -759,7 +821,10 @@ export async function grabWorkOrder(input: {
now: input.now,
})
return getWorkOrderByIdWithClient(client, input.workOrderId)
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
@@ -796,18 +861,8 @@ export async function acceptWorkOrderAndSettle(input: {
if (workerId > 0) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
const frozenLedgerResult = await client.query<{ amount: number }>(
`
SELECT COALESCE(ABS(SUM(amount)), 0)::int AS amount
FROM worker_wallet_ledgers
WHERE worker_id = $1
AND related_work_order_id = $2
AND ledger_type = 'deposit_freeze'
`,
[workerId, input.workOrderId],
)
const releaseAmount = Math.min(
Number(frozenLedgerResult.rows[0]?.amount || 0),
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
Number(wallet?.frozen_deposit_amount || 0),
)
const rewardAmount = Number(workOrder.reward_amount || 0)
@@ -867,6 +922,124 @@ export async function acceptWorkOrderAndSettle(input: {
})
}
export async function resolveProblemWorkOrder(input: {
workOrderId: number
action: ProblemWorkOrderResolutionAction
note: string
now: string
}): Promise<WorkOrderRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
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,
updated_at = $4
WHERE id = $5
`,
[targetStatus, shouldClearAssignment, shouldClearSubmission, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: '',
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 countWorkerActiveOrders(workerId: number | string): Promise<number> {
const result = await query<{ total: number }>(
`
@@ -955,12 +1128,67 @@ async function getWorkerWalletWithClient(
workerId: number,
): Promise<WorkerWalletRow | null> {
const result = await client.query<WorkerWalletRow>(
'SELECT * FROM worker_wallets WHERE worker_id = $1 LIMIT 1',
'SELECT * FROM worker_wallets WHERE worker_id = $1 LIMIT 1 FOR UPDATE',
[workerId],
)
return result.rows[0] || null
}
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])
}
async function countWorkerActiveOrdersWithClient(client: PoolClient, workerId: number): Promise<number> {
const result = await client.query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_orders
WHERE assigned_worker_id = $1
AND status IN ('in_progress', 'pending_acceptance', 'problem')
`,
[workerId],
)
return Number(result.rows[0]?.total || 0)
}
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),
)
}
async function getWorkerUserByIdWithClient(
client: PoolClient,
workerId: number | string,