拆分工单验收结算仓储

This commit is contained in:
yml2213
2026-08-21 18:50:03 +08:00
parent 26f1a00e24
commit eeb3d2f297
2 changed files with 718 additions and 741 deletions
@@ -1,46 +1,3 @@
import { query, withTransaction } from '../../db/client.js'
import { offsetWorkerAfterSalesDebtsWithClient } from './after-sales-repo.js'
import {
ensureWorkerWalletWithClient,
getWorkerWalletWithClient,
toJsonString,
toPositiveInteger,
WORK_ORDER_SHARE_SELECT,
resolveWorkOrderShareCancellationStatus,
} from './shared.js'
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
import {
countTimeoutEventsByWorkerIds,
countWorkerCancellationsSince,
countWorkerActiveOrders,
countWorkerActiveOrdersWithClient,
countWorkerTimeoutEvents,
getWorkOrderById,
getWorkOrderByIdWithClient,
getWorkOrderByOrderItemId,
getWorkerOrderOverview,
isWorkOrderWithinHallCapacityWithClient,
listOverdueWorkOrders,
WORK_ORDER_SELECT,
} from './work-order-query-repo.js'
import type {
CreateWorkOrderInput,
GrabWorkOrderResult,
ListInput,
ProblemWorkOrderResolutionAction,
WorkerDepositUnfreezeRow,
WorkOrderRow,
WorkOrderShareRow,
WorkOrderStatistics,
} 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 * from './work-order-query-repo.js'
export { sumPendingUnfreezeByOrderIds } from './work-order-deposit-query-repo.js' export { sumPendingUnfreezeByOrderIds } from './work-order-deposit-query-repo.js'
export { listDueDepositUnfreezes } from './work-order-deposit-query-repo.js' export { listDueDepositUnfreezes } from './work-order-deposit-query-repo.js'
@@ -73,701 +30,8 @@ export {
resolveProblemWorkOrder, resolveProblemWorkOrder,
settleOverdueWorkOrder, settleOverdueWorkOrder,
} from './work-order-issue-repo.js' } from './work-order-issue-repo.js'
export {
export async function acceptWorkOrderAndSettle(input: { acceptWorkOrderAndSettle,
workOrderId: number acceptWorkOrderShareAndSettle,
unfreezeDays: number cancelWorkOrderShare,
now: string } from './work-order-settlement-repo.js'
/** 操作人类型:admin / worker / system */
actorType?: string
/** 操作人显示名 */
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'sharing_pending_submissions' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
AND status = 'pending_acceptance'
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) {
return { order: null, failureReason: null }
}
const sharesResult = await client.query<WorkOrderShareRow>(
`
SELECT *
FROM work_order_shares
WHERE work_order_id = $1
AND status != 'cancelled'
ORDER BY id ASC
FOR UPDATE
`,
[input.workOrderId],
)
const shares = sharesResult.rows
if (shares.length > 0) {
const pendingShares = shares.filter((share) => share.status === 'joined')
if (pendingShares.length > 0) {
return {
order: null,
failureReason: 'sharing_pending_submissions',
}
}
}
await client.query(
`
UPDATE work_orders
SET status = 'accepted', accepted_at = $1, updated_at = $1
WHERE id = $2
`,
[input.now, input.workOrderId],
)
if (shares.length > 0) {
// 单独验收过的份额已经完成资金结算,统一验收时只处理尚未结算的份额。
const unsettledShares = shares.filter((share) => share.status !== 'accepted')
for (const share of unsettledShares) {
const shareWorkerId = Number(share.worker_id || 0)
if (!shareWorkerId) continue
await ensureWorkerWalletWithClient(client, shareWorkerId, input.now)
const wallet = await getWorkerWalletWithClient(client, shareWorkerId)
const releaseAmount = Math.min(
Number(share.share_deposit || 0),
Number(wallet?.frozen_deposit_amount || 0),
)
const shareReward = Number(share.share_reward || 0)
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
workerId: shareWorkerId,
amount: shareReward,
workOrderId: input.workOrderId,
now: input.now,
})
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
const shouldDelayUnfreeze = input.unfreezeDays > 0
const nextAvailable =
Number(wallet?.available_amount || 0) +
shareReward +
(shouldDelayUnfreeze ? 0 : releaseAmount) -
debtOffsetAmount
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,
pending_unfreeze_amount = pending_unfreeze_amount + $3,
total_settled_amount = total_settled_amount + $4,
updated_at = $5
WHERE worker_id = $6
`,
[
nextAvailable,
nextFrozen,
shouldDelayUnfreeze ? releaseAmount : 0,
shareReward,
input.now,
shareWorkerId,
],
)
if (releaseAmount > 0) {
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) {
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, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)
`,
[
shareWorkerId,
shareReward,
nextAvailable,
nextFrozen,
input.workOrderId,
`拼单结算报酬 ${share.quantity}`,
JSON.stringify({
workOrderId: input.workOrderId,
shareId: Number(share.id),
quantity: share.quantity,
}),
input.now,
],
)
}
for (const debtOffset of debtOffsets) {
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, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
`,
[
shareWorkerId,
-debtOffset.amount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({
caseId: debtOffset.caseId,
workOrderId: input.workOrderId,
shareId: Number(share.id),
}),
input.now,
],
)
}
await client.query(
`
UPDATE work_order_shares
SET status = 'accepted', accepted_at = $1, updated_at = $1
WHERE id = $2
`,
[input.now, Number(share.id)],
)
await maybeUpgradeWorkerLevelWithClient(client, shareWorkerId, input.now)
}
}
const workerId = Number(workOrder.assigned_worker_id || 0)
if (workerId > 0) {
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),
)
const rewardAmount = Number(workOrder.reward_amount || 0)
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
workerId,
amount: rewardAmount,
workOrderId: input.workOrderId,
now: input.now,
})
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
const shouldDelayUnfreeze = input.unfreezeDays > 0
const nextAvailable =
Number(wallet?.available_amount || 0) +
rewardAmount +
(shouldDelayUnfreeze ? 0 : releaseAmount) -
debtOffsetAmount
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,
pending_unfreeze_amount = pending_unfreeze_amount + $3,
total_settled_amount = total_settled_amount + $4,
updated_at = $5
WHERE worker_id = $6
`,
[
nextAvailable,
nextFrozen,
shouldDelayUnfreeze ? releaseAmount : 0,
rewardAmount,
input.now,
workerId,
],
)
if (releaseAmount > 0) {
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) {
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, 'reward_settlement', $2, $3, $4, $5, '验收通过结算报酬', '{}'::jsonb, $6)
`,
[workerId, rewardAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
)
}
for (const debtOffset of debtOffsets) {
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, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
`,
[
workerId,
-debtOffset.amount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({ caseId: debtOffset.caseId, workOrderId: input.workOrderId }),
input.now,
],
)
}
}
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: input.actorType || 'admin',
actorId: input.actorName || '',
eventType: 'accepted',
fromStatus: 'pending_acceptance',
toStatus: 'accepted',
payloadJson: JSON.stringify({
shareCount: shares.length,
settledShareCount: shares.filter((share) => share.status !== 'accepted').length,
}),
now: input.now,
})
if (workerId > 0) {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
/** 单独验收一个拼单份额,只结算该参与者,不影响其他份额。 */
export async function acceptWorkOrderShareAndSettle(input: {
workOrderId: number
shareId: number
unfreezeDays: number
now: string
actorType?: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
share: WorkOrderShareRow | null
failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null
}> {
return withTransaction(async (client) => {
const orderResult = await client.query<WorkOrderRow>(
`SELECT * FROM work_orders WHERE id = $1 AND status IN ('open', 'pending_acceptance') FOR UPDATE`,
[input.workOrderId],
)
const workOrder = orderResult.rows[0] || null
if (!workOrder) {
return { order: null, share: null, failureReason: 'work_order_not_accepting' }
}
const shareResult = await client.query<WorkOrderShareRow>(
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
[input.shareId, input.workOrderId],
)
const share = shareResult.rows[0] || null
if (!share || share.status !== 'submitted') {
return { order: workOrder, share, failureReason: 'share_not_submitted' }
}
const workerId = Number(share.worker_id || 0)
if (workerId > 0) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
const releaseAmount = Math.min(
Number(share.share_deposit || 0),
Number(wallet?.frozen_deposit_amount || 0),
)
const shareReward = Number(share.share_reward || 0)
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
workerId,
amount: shareReward,
workOrderId: input.workOrderId,
now: input.now,
})
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
const shouldDelayUnfreeze = input.unfreezeDays > 0
const nextAvailable =
Number(wallet?.available_amount || 0) +
shareReward +
(shouldDelayUnfreeze ? 0 : releaseAmount) -
debtOffsetAmount
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,
pending_unfreeze_amount = pending_unfreeze_amount + $3,
total_settled_amount = total_settled_amount + $4, updated_at = $5
WHERE worker_id = $6`,
[
nextAvailable,
nextFrozen,
shouldDelayUnfreeze ? releaseAmount : 0,
shareReward,
input.now,
workerId,
],
)
if (releaseAmount > 0 && shouldDelayUnfreeze) {
await enqueueDepositUnfreezeWithClient(client, {
workerId,
workOrderId: input.workOrderId,
amount: releaseAmount,
unfreezeDays: input.unfreezeDays,
now: input.now,
})
}
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, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)`,
[
workerId,
shouldDelayUnfreeze ? 'deposit_pending_unfreeze' : 'deposit_release',
releaseAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
shouldDelayUnfreeze ? '拼单份额验收押金待解冻' : '拼单份额验收释放押金',
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
input.now,
],
)
}
if (shareReward > 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, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)`,
[
workerId,
shareReward,
nextAvailable,
nextFrozen,
input.workOrderId,
`拼单份额验收结算报酬 ${share.quantity}`,
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
input.now,
],
)
}
for (const debtOffset of debtOffsets) {
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, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)`,
[
workerId,
-debtOffset.amount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({
caseId: debtOffset.caseId,
workOrderId: input.workOrderId,
shareId: input.shareId,
}),
input.now,
],
)
}
}
await client.query(
`UPDATE work_order_shares SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
[input.now, input.shareId],
)
if (workerId > 0) {
// 先落库份额已验收状态,再按去重后的订单数计算等级进度。
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
const progressResult = await client.query<{
joined_quantity: number
unfinished_count: number
}>(
`SELECT COALESCE(SUM(quantity), 0)::int AS joined_quantity,
COUNT(*) FILTER (WHERE status != 'accepted' AND status != 'cancelled')::int AS unfinished_count
FROM work_order_shares WHERE work_order_id = $1 AND status != 'cancelled'`,
[input.workOrderId],
)
const progress = progressResult.rows[0]
const filled =
Number(progress?.joined_quantity || 0) >= Number(workOrder.sharing_total_quantity || 1)
const allAccepted = Number(progress?.unfinished_count || 0) === 0
const nextOrderStatus = filled && allAccepted ? 'accepted' : workOrder.status
if (nextOrderStatus === 'accepted') {
await client.query(
`UPDATE work_orders SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
[input.now, input.workOrderId],
)
}
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: input.actorType || 'admin',
actorId: input.actorName || '',
eventType: 'sharing_share_accepted',
fromStatus: workOrder.status,
toStatus: nextOrderStatus,
payloadJson: JSON.stringify({ shareId: input.shareId, workerId }),
now: input.now,
})
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
const updatedShareResult = await client.query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
[input.shareId],
)
return {
order: updatedOrder,
share: updatedShareResult.rows[0] || null,
failureReason: null,
}
})
}
/** 后台撤销单个拼单份额,释放该打手押金并把份数重新放回大厅。 */
export async function cancelWorkOrderShare(input: {
workOrderId: number
shareId: number
now: string
reason?: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
share: WorkOrderShareRow | null
releasedDepositAmount: number
failureReason:
| 'work_order_not_cancellable'
| 'share_not_found'
| 'share_status_not_cancellable'
| null
}> {
return withTransaction(async (client) => {
const orderResult = await client.query<WorkOrderRow>(
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
[input.workOrderId],
)
const workOrder = orderResult.rows[0] || null
if (!workOrder || !['open', 'pending_acceptance'].includes(workOrder.status)) {
return {
order: workOrder,
share: null,
releasedDepositAmount: 0,
failureReason: 'work_order_not_cancellable',
}
}
const shareResult = await client.query<WorkOrderShareRow>(
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
[input.shareId, input.workOrderId],
)
const share = shareResult.rows[0] || null
if (!share) {
return {
order: workOrder,
share: null,
releasedDepositAmount: 0,
failureReason: 'share_not_found',
}
}
const nextOrderStatus = resolveWorkOrderShareCancellationStatus(workOrder.status, share.status)
if (!nextOrderStatus) {
return {
order: workOrder,
share,
releasedDepositAmount: 0,
failureReason: 'share_status_not_cancellable',
}
}
const workerId = Number(share.worker_id || 0)
let releasedDepositAmount = 0
if (workerId > 0) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
releasedDepositAmount = Math.min(
Number(share.share_deposit || 0),
Number(wallet?.frozen_deposit_amount || 0),
)
if (releasedDepositAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
)
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, '后台撤销拼单份额退还押金', $6::jsonb, $7)`,
[
workerId,
releasedDepositAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({
workOrderId: input.workOrderId,
shareId: input.shareId,
reason: String(input.reason || '').trim(),
}),
input.now,
],
)
}
}
await client.query(
`UPDATE work_order_shares
SET status = 'cancelled', updated_at = $1
WHERE id = $2`,
[input.now, input.shareId],
)
await client.query(
`UPDATE work_orders
SET status = $1,
submitted_at = NULL,
published_at = COALESCE(published_at, $2),
updated_at = $2
WHERE id = $3`,
[nextOrderStatus, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: input.actorName || '',
eventType: 'sharing_share_cancelled_by_admin',
fromStatus: workOrder.status,
toStatus: nextOrderStatus,
payloadJson: JSON.stringify({
shareId: input.shareId,
workerId,
quantity: Number(share.quantity || 0),
releasedDepositAmount,
reason: String(input.reason || '').trim(),
}),
now: input.now,
})
const updatedShareResult = await client.query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
[input.shareId],
)
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
share: updatedShareResult.rows[0] || null,
releasedDepositAmount,
failureReason: null,
}
})
}
@@ -0,0 +1,713 @@
import { withTransaction } from '../../db/client.js'
import { offsetWorkerAfterSalesDebtsWithClient } from './after-sales-repo.js'
import {
ensureWorkerWalletWithClient,
getWorkerWalletWithClient,
WORK_ORDER_SHARE_SELECT,
resolveWorkOrderShareCancellationStatus,
} from './shared.js'
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
import {
enqueueDepositUnfreezeWithClient,
getOutstandingDepositAmountWithClient,
} from './work-order-deposit-repo.js'
import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js'
import type { WorkOrderRow, WorkOrderShareRow } from './types.js'
export async function acceptWorkOrderAndSettle(input: {
workOrderId: number
unfreezeDays: number
now: string
/** 操作人类型:admin / worker / system */
actorType?: string
/** 操作人显示名 */
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'sharing_pending_submissions' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
AND status = 'pending_acceptance'
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) {
return { order: null, failureReason: null }
}
const sharesResult = await client.query<WorkOrderShareRow>(
`
SELECT *
FROM work_order_shares
WHERE work_order_id = $1
AND status != 'cancelled'
ORDER BY id ASC
FOR UPDATE
`,
[input.workOrderId],
)
const shares = sharesResult.rows
if (shares.length > 0) {
const pendingShares = shares.filter((share) => share.status === 'joined')
if (pendingShares.length > 0) {
return {
order: null,
failureReason: 'sharing_pending_submissions',
}
}
}
await client.query(
`
UPDATE work_orders
SET status = 'accepted', accepted_at = $1, updated_at = $1
WHERE id = $2
`,
[input.now, input.workOrderId],
)
if (shares.length > 0) {
// 单独验收过的份额已经完成资金结算,统一验收时只处理尚未结算的份额。
const unsettledShares = shares.filter((share) => share.status !== 'accepted')
for (const share of unsettledShares) {
const shareWorkerId = Number(share.worker_id || 0)
if (!shareWorkerId) continue
await ensureWorkerWalletWithClient(client, shareWorkerId, input.now)
const wallet = await getWorkerWalletWithClient(client, shareWorkerId)
const releaseAmount = Math.min(
Number(share.share_deposit || 0),
Number(wallet?.frozen_deposit_amount || 0),
)
const shareReward = Number(share.share_reward || 0)
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
workerId: shareWorkerId,
amount: shareReward,
workOrderId: input.workOrderId,
now: input.now,
})
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
const shouldDelayUnfreeze = input.unfreezeDays > 0
const nextAvailable =
Number(wallet?.available_amount || 0) +
shareReward +
(shouldDelayUnfreeze ? 0 : releaseAmount) -
debtOffsetAmount
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,
pending_unfreeze_amount = pending_unfreeze_amount + $3,
total_settled_amount = total_settled_amount + $4,
updated_at = $5
WHERE worker_id = $6
`,
[
nextAvailable,
nextFrozen,
shouldDelayUnfreeze ? releaseAmount : 0,
shareReward,
input.now,
shareWorkerId,
],
)
if (releaseAmount > 0) {
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) {
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, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)
`,
[
shareWorkerId,
shareReward,
nextAvailable,
nextFrozen,
input.workOrderId,
`拼单结算报酬 ${share.quantity}`,
JSON.stringify({
workOrderId: input.workOrderId,
shareId: Number(share.id),
quantity: share.quantity,
}),
input.now,
],
)
}
for (const debtOffset of debtOffsets) {
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, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
`,
[
shareWorkerId,
-debtOffset.amount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({
caseId: debtOffset.caseId,
workOrderId: input.workOrderId,
shareId: Number(share.id),
}),
input.now,
],
)
}
await client.query(
`
UPDATE work_order_shares
SET status = 'accepted', accepted_at = $1, updated_at = $1
WHERE id = $2
`,
[input.now, Number(share.id)],
)
await maybeUpgradeWorkerLevelWithClient(client, shareWorkerId, input.now)
}
}
const workerId = Number(workOrder.assigned_worker_id || 0)
if (workerId > 0) {
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),
)
const rewardAmount = Number(workOrder.reward_amount || 0)
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
workerId,
amount: rewardAmount,
workOrderId: input.workOrderId,
now: input.now,
})
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
const shouldDelayUnfreeze = input.unfreezeDays > 0
const nextAvailable =
Number(wallet?.available_amount || 0) +
rewardAmount +
(shouldDelayUnfreeze ? 0 : releaseAmount) -
debtOffsetAmount
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,
pending_unfreeze_amount = pending_unfreeze_amount + $3,
total_settled_amount = total_settled_amount + $4,
updated_at = $5
WHERE worker_id = $6
`,
[
nextAvailable,
nextFrozen,
shouldDelayUnfreeze ? releaseAmount : 0,
rewardAmount,
input.now,
workerId,
],
)
if (releaseAmount > 0) {
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) {
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, 'reward_settlement', $2, $3, $4, $5, '验收通过结算报酬', '{}'::jsonb, $6)
`,
[workerId, rewardAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
)
}
for (const debtOffset of debtOffsets) {
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, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
`,
[
workerId,
-debtOffset.amount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({ caseId: debtOffset.caseId, workOrderId: input.workOrderId }),
input.now,
],
)
}
}
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: input.actorType || 'admin',
actorId: input.actorName || '',
eventType: 'accepted',
fromStatus: 'pending_acceptance',
toStatus: 'accepted',
payloadJson: JSON.stringify({
shareCount: shares.length,
settledShareCount: shares.filter((share) => share.status !== 'accepted').length,
}),
now: input.now,
})
if (workerId > 0) {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
/** 单独验收一个拼单份额,只结算该参与者,不影响其他份额。 */
export async function acceptWorkOrderShareAndSettle(input: {
workOrderId: number
shareId: number
unfreezeDays: number
now: string
actorType?: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
share: WorkOrderShareRow | null
failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null
}> {
return withTransaction(async (client) => {
const orderResult = await client.query<WorkOrderRow>(
`SELECT * FROM work_orders WHERE id = $1 AND status IN ('open', 'pending_acceptance') FOR UPDATE`,
[input.workOrderId],
)
const workOrder = orderResult.rows[0] || null
if (!workOrder) {
return { order: null, share: null, failureReason: 'work_order_not_accepting' }
}
const shareResult = await client.query<WorkOrderShareRow>(
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
[input.shareId, input.workOrderId],
)
const share = shareResult.rows[0] || null
if (!share || share.status !== 'submitted') {
return { order: workOrder, share, failureReason: 'share_not_submitted' }
}
const workerId = Number(share.worker_id || 0)
if (workerId > 0) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
const releaseAmount = Math.min(
Number(share.share_deposit || 0),
Number(wallet?.frozen_deposit_amount || 0),
)
const shareReward = Number(share.share_reward || 0)
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
workerId,
amount: shareReward,
workOrderId: input.workOrderId,
now: input.now,
})
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
const shouldDelayUnfreeze = input.unfreezeDays > 0
const nextAvailable =
Number(wallet?.available_amount || 0) +
shareReward +
(shouldDelayUnfreeze ? 0 : releaseAmount) -
debtOffsetAmount
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,
pending_unfreeze_amount = pending_unfreeze_amount + $3,
total_settled_amount = total_settled_amount + $4, updated_at = $5
WHERE worker_id = $6`,
[
nextAvailable,
nextFrozen,
shouldDelayUnfreeze ? releaseAmount : 0,
shareReward,
input.now,
workerId,
],
)
if (releaseAmount > 0 && shouldDelayUnfreeze) {
await enqueueDepositUnfreezeWithClient(client, {
workerId,
workOrderId: input.workOrderId,
amount: releaseAmount,
unfreezeDays: input.unfreezeDays,
now: input.now,
})
}
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, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)`,
[
workerId,
shouldDelayUnfreeze ? 'deposit_pending_unfreeze' : 'deposit_release',
releaseAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
shouldDelayUnfreeze ? '拼单份额验收押金待解冻' : '拼单份额验收释放押金',
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
input.now,
],
)
}
if (shareReward > 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, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)`,
[
workerId,
shareReward,
nextAvailable,
nextFrozen,
input.workOrderId,
`拼单份额验收结算报酬 ${share.quantity}`,
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
input.now,
],
)
}
for (const debtOffset of debtOffsets) {
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, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)`,
[
workerId,
-debtOffset.amount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({
caseId: debtOffset.caseId,
workOrderId: input.workOrderId,
shareId: input.shareId,
}),
input.now,
],
)
}
}
await client.query(
`UPDATE work_order_shares SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
[input.now, input.shareId],
)
if (workerId > 0) {
// 先落库份额已验收状态,再按去重后的订单数计算等级进度。
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
const progressResult = await client.query<{
joined_quantity: number
unfinished_count: number
}>(
`SELECT COALESCE(SUM(quantity), 0)::int AS joined_quantity,
COUNT(*) FILTER (WHERE status != 'accepted' AND status != 'cancelled')::int AS unfinished_count
FROM work_order_shares WHERE work_order_id = $1 AND status != 'cancelled'`,
[input.workOrderId],
)
const progress = progressResult.rows[0]
const filled =
Number(progress?.joined_quantity || 0) >= Number(workOrder.sharing_total_quantity || 1)
const allAccepted = Number(progress?.unfinished_count || 0) === 0
const nextOrderStatus = filled && allAccepted ? 'accepted' : workOrder.status
if (nextOrderStatus === 'accepted') {
await client.query(
`UPDATE work_orders SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
[input.now, input.workOrderId],
)
}
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: input.actorType || 'admin',
actorId: input.actorName || '',
eventType: 'sharing_share_accepted',
fromStatus: workOrder.status,
toStatus: nextOrderStatus,
payloadJson: JSON.stringify({ shareId: input.shareId, workerId }),
now: input.now,
})
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
const updatedShareResult = await client.query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
[input.shareId],
)
return {
order: updatedOrder,
share: updatedShareResult.rows[0] || null,
failureReason: null,
}
})
}
/** 后台撤销单个拼单份额,释放该打手押金并把份数重新放回大厅。 */
export async function cancelWorkOrderShare(input: {
workOrderId: number
shareId: number
now: string
reason?: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
share: WorkOrderShareRow | null
releasedDepositAmount: number
failureReason:
| 'work_order_not_cancellable'
| 'share_not_found'
| 'share_status_not_cancellable'
| null
}> {
return withTransaction(async (client) => {
const orderResult = await client.query<WorkOrderRow>(
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
[input.workOrderId],
)
const workOrder = orderResult.rows[0] || null
if (!workOrder || !['open', 'pending_acceptance'].includes(workOrder.status)) {
return {
order: workOrder,
share: null,
releasedDepositAmount: 0,
failureReason: 'work_order_not_cancellable',
}
}
const shareResult = await client.query<WorkOrderShareRow>(
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
[input.shareId, input.workOrderId],
)
const share = shareResult.rows[0] || null
if (!share) {
return {
order: workOrder,
share: null,
releasedDepositAmount: 0,
failureReason: 'share_not_found',
}
}
const nextOrderStatus = resolveWorkOrderShareCancellationStatus(workOrder.status, share.status)
if (!nextOrderStatus) {
return {
order: workOrder,
share,
releasedDepositAmount: 0,
failureReason: 'share_status_not_cancellable',
}
}
const workerId = Number(share.worker_id || 0)
let releasedDepositAmount = 0
if (workerId > 0) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
releasedDepositAmount = Math.min(
Number(share.share_deposit || 0),
Number(wallet?.frozen_deposit_amount || 0),
)
if (releasedDepositAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
)
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, '后台撤销拼单份额退还押金', $6::jsonb, $7)`,
[
workerId,
releasedDepositAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
JSON.stringify({
workOrderId: input.workOrderId,
shareId: input.shareId,
reason: String(input.reason || '').trim(),
}),
input.now,
],
)
}
}
await client.query(
`UPDATE work_order_shares
SET status = 'cancelled', updated_at = $1
WHERE id = $2`,
[input.now, input.shareId],
)
await client.query(
`UPDATE work_orders
SET status = $1,
submitted_at = NULL,
published_at = COALESCE(published_at, $2),
updated_at = $2
WHERE id = $3`,
[nextOrderStatus, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: input.actorName || '',
eventType: 'sharing_share_cancelled_by_admin',
fromStatus: workOrder.status,
toStatus: nextOrderStatus,
payloadJson: JSON.stringify({
shareId: input.shareId,
workerId,
quantity: Number(share.quantity || 0),
releasedDepositAmount,
reason: String(input.reason || '').trim(),
}),
now: input.now,
})
const updatedShareResult = await client.query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
[input.shareId],
)
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
share: updatedShareResult.rows[0] || null,
releasedDepositAmount,
failureReason: null,
}
})
}