拆分工单流转事务仓储
This commit is contained in:
@@ -9,6 +9,8 @@ export * from './worker-ranking-repo.js'
|
|||||||
export * from './work-order-repo.js'
|
export * from './work-order-repo.js'
|
||||||
export * from './work-order-query-repo.js'
|
export * from './work-order-query-repo.js'
|
||||||
export * from './work-order-management-repo.js'
|
export * from './work-order-management-repo.js'
|
||||||
|
export * from './work-order-grab-repo.js'
|
||||||
|
export * from './work-order-cancel-repo.js'
|
||||||
export * from './work-order-deposit-query-repo.js'
|
export * from './work-order-deposit-query-repo.js'
|
||||||
export * from './work-order-deposit-repo.js'
|
export * from './work-order-deposit-repo.js'
|
||||||
export * from './work-order-event-repo.js'
|
export * from './work-order-event-repo.js'
|
||||||
|
|||||||
@@ -0,0 +1,454 @@
|
|||||||
|
import { withTransaction } from '../../db/client.js'
|
||||||
|
import {
|
||||||
|
ensureWorkerWalletWithClient,
|
||||||
|
getWorkerWalletWithClient,
|
||||||
|
} from './shared.js'
|
||||||
|
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||||
|
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
|
||||||
|
import { getOutstandingDepositAmountWithClient } from './work-order-deposit-repo.js'
|
||||||
|
import type { WorkOrderRow } from './types.js'
|
||||||
|
|
||||||
|
export async function cancelWorkerWorkOrder(input: {
|
||||||
|
workOrderId: number
|
||||||
|
workerId: number
|
||||||
|
now: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
failureReason: 'work_order_not_in_progress' | 'work_order_owner_required' | null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const currentResult = await client.query<WorkOrderRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM work_orders
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = currentResult.rows[0] || null
|
||||||
|
if (!workOrder || workOrder.status !== 'in_progress') {
|
||||||
|
return { order: null, failureReason: 'work_order_not_in_progress' }
|
||||||
|
}
|
||||||
|
if (Number(workOrder.assigned_worker_id || 0) !== input.workerId) {
|
||||||
|
return { order: null, failureReason: 'work_order_owner_required' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||||
|
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||||
|
const releaseAmount = Math.min(
|
||||||
|
await getOutstandingDepositAmountWithClient(client, input.workerId, input.workOrderId),
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'open',
|
||||||
|
assigned_worker_id = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
deadline_at = NULL,
|
||||||
|
hall_queued_at = $1,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
[input.now, input.workOrderId],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (releaseAmount > 0) {
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
||||||
|
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, updated_at = $3
|
||||||
|
WHERE worker_id = $4
|
||||||
|
`,
|
||||||
|
[nextAvailable, nextFrozen, input.now, input.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, '取消接单退还押金', '{}'::jsonb, $6)
|
||||||
|
`,
|
||||||
|
[input.workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(input.workerId),
|
||||||
|
eventType: 'cancelled_by_worker',
|
||||||
|
fromStatus: 'in_progress',
|
||||||
|
toStatus: 'open',
|
||||||
|
payloadJson: JSON.stringify({ releaseAmount }),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打手提交工单问题反馈,不改变工单状态、押金或资金流水。 */
|
||||||
|
|
||||||
|
export async function unassignWorkOrder(input: {
|
||||||
|
workOrderId: number
|
||||||
|
now: string
|
||||||
|
actorName?: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const currentResult = await client.query<WorkOrderRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM work_orders
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = currentResult.rows[0] || null
|
||||||
|
if (!workOrder || workOrder.status !== 'in_progress') {
|
||||||
|
return { order: null, failureReason: 'work_order_not_in_progress' }
|
||||||
|
}
|
||||||
|
const workerId = Number(workOrder.assigned_worker_id || 0)
|
||||||
|
if (!workerId) {
|
||||||
|
return { order: null, failureReason: 'work_order_no_worker' }
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'unassigned',
|
||||||
|
assigned_worker_id = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
deadline_at = NULL,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
[input.now, input.workOrderId],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (releaseAmount > 0) {
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
||||||
|
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, 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, '后台取消指派退还押金', '{}'::jsonb, $6)
|
||||||
|
`,
|
||||||
|
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: 'unassigned_by_admin',
|
||||||
|
fromStatus: 'in_progress',
|
||||||
|
toStatus: 'unassigned',
|
||||||
|
payloadJson: JSON.stringify({ workerId, releaseAmount }),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function returnAssignedWorkOrderToHall(input: {
|
||||||
|
workOrderId: number
|
||||||
|
now: string
|
||||||
|
reason?: string
|
||||||
|
actorName?: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const currentResult = await client.query<WorkOrderRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM work_orders
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = currentResult.rows[0] || null
|
||||||
|
if (!workOrder || workOrder.status !== 'in_progress') {
|
||||||
|
return { order: null, failureReason: 'work_order_not_in_progress' }
|
||||||
|
}
|
||||||
|
const workerId = Number(workOrder.assigned_worker_id || 0)
|
||||||
|
if (!workerId) {
|
||||||
|
return { order: null, failureReason: 'work_order_no_worker' }
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'open',
|
||||||
|
assigned_worker_id = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
deadline_at = NULL,
|
||||||
|
published_at = $1,
|
||||||
|
hall_queued_at = $1,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
[input.now, input.workOrderId],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (releaseAmount > 0) {
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
||||||
|
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, 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, '后台退回大厅退还押金', '{}'::jsonb, $6)
|
||||||
|
`,
|
||||||
|
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: 'returned_to_hall_by_admin',
|
||||||
|
fromStatus: 'in_progress',
|
||||||
|
toStatus: 'open',
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
workerId,
|
||||||
|
releaseAmount,
|
||||||
|
reason: String(input.reason || '').trim(),
|
||||||
|
}),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelAssignedWorkOrder(input: {
|
||||||
|
workOrderId: number
|
||||||
|
now: string
|
||||||
|
reason?: string
|
||||||
|
actorName?: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const currentResult = await client.query<WorkOrderRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM work_orders
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = currentResult.rows[0] || null
|
||||||
|
if (!workOrder || workOrder.status !== 'in_progress') {
|
||||||
|
return { order: null, failureReason: 'work_order_not_in_progress' }
|
||||||
|
}
|
||||||
|
const workerId = Number(workOrder.assigned_worker_id || 0)
|
||||||
|
if (!workerId) {
|
||||||
|
return { order: null, failureReason: 'work_order_no_worker' }
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'cancelled',
|
||||||
|
assigned_worker_id = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
deadline_at = NULL,
|
||||||
|
published_at = NULL,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
[input.now, input.workOrderId],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (releaseAmount > 0) {
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
||||||
|
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, 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, '后台撤单退还押金', '{}'::jsonb, $6)
|
||||||
|
`,
|
||||||
|
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: 'cancelled_by_admin',
|
||||||
|
fromStatus: 'in_progress',
|
||||||
|
toStatus: 'cancelled',
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
workerId,
|
||||||
|
releaseAmount,
|
||||||
|
reason: String(input.reason || '').trim(),
|
||||||
|
}),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将已取消工单恢复为未分配状态,不变更历史押金流水。 */
|
||||||
|
export async function reopenCancelledWorkOrder(input: {
|
||||||
|
workOrderId: number
|
||||||
|
now: string
|
||||||
|
actorName?: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
failureReason: 'work_order_not_cancelled' | 'work_order_has_active_shares' | null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const currentResult = await client.query<WorkOrderRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM work_orders
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = currentResult.rows[0] || null
|
||||||
|
if (!workOrder || workOrder.status !== 'cancelled') {
|
||||||
|
return { order: null, failureReason: 'work_order_not_cancelled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeShareResult = await client.query<{ total: number }>(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*)::int AS total
|
||||||
|
FROM work_order_shares
|
||||||
|
WHERE work_order_id = $1
|
||||||
|
AND status != 'cancelled'
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
if (Number(activeShareResult.rows[0]?.total || 0) > 0) {
|
||||||
|
return { order: null, failureReason: 'work_order_has_active_shares' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'unassigned',
|
||||||
|
assigned_worker_id = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
deadline_at = NULL,
|
||||||
|
acceptance_json = '{}'::jsonb,
|
||||||
|
draft_acceptance_json = '{}'::jsonb,
|
||||||
|
submitted_at = NULL,
|
||||||
|
accepted_at = NULL,
|
||||||
|
problem_note = '',
|
||||||
|
published_at = NULL,
|
||||||
|
pinned_at = NULL,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
[input.now, input.workOrderId],
|
||||||
|
)
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: 'reopened_from_cancelled',
|
||||||
|
fromStatus: 'cancelled',
|
||||||
|
toStatus: 'unassigned',
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
previousAssignedWorkerId: Number(workOrder.last_assigned_worker_id || 0) || null,
|
||||||
|
}),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { withTransaction } from '../../db/client.js'
|
||||||
|
import {
|
||||||
|
ensureWorkerWalletWithClient,
|
||||||
|
getWorkerWalletWithClient,
|
||||||
|
} from './shared.js'
|
||||||
|
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||||
|
import {
|
||||||
|
countWorkerActiveOrdersWithClient,
|
||||||
|
getWorkOrderByIdWithClient,
|
||||||
|
isWorkOrderWithinHallCapacityWithClient,
|
||||||
|
} from './work-order-query-repo.js'
|
||||||
|
import type { GrabWorkOrderResult, WorkOrderRow } from './types.js'
|
||||||
|
|
||||||
|
export async function grabWorkOrder(input: {
|
||||||
|
workOrderId: number
|
||||||
|
workerId: number
|
||||||
|
depositAmount: number
|
||||||
|
maxActiveOrders: number
|
||||||
|
hallCandidateLimit?: number
|
||||||
|
visibleAfterIso?: string
|
||||||
|
deadlineAt: string | null
|
||||||
|
now: string
|
||||||
|
}): 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 {
|
||||||
|
order: null,
|
||||||
|
failureReason: 'worker_deposit_insufficient',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.hallCandidateLimit &&
|
||||||
|
!(await isWorkOrderWithinHallCapacityWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
hallCandidateLimit: input.hallCandidateLimit,
|
||||||
|
visibleAfterIso: input.visibleAfterIso,
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
order: null,
|
||||||
|
failureReason: 'work_order_not_open',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await client.query<{ id: number }>(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'in_progress',
|
||||||
|
assigned_worker_id = $1,
|
||||||
|
last_assigned_worker_id = $1,
|
||||||
|
assigned_at = $2,
|
||||||
|
deadline_at = $3,
|
||||||
|
updated_at = $2
|
||||||
|
WHERE id = $4
|
||||||
|
AND status = 'open'
|
||||||
|
AND assigned_worker_id IS NULL
|
||||||
|
RETURNING id
|
||||||
|
`,
|
||||||
|
[input.workerId, input.now, input.deadlineAt, input.workOrderId],
|
||||||
|
)
|
||||||
|
if (!result.rows[0]) {
|
||||||
|
return {
|
||||||
|
order: null,
|
||||||
|
failureReason: 'work_order_not_open',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.depositAmount > 0) {
|
||||||
|
const nextAvailable = available - input.depositAmount
|
||||||
|
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount
|
||||||
|
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, input.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_freeze', $2, $3, $4, $5, '抢单冻结押金', '{}'::jsonb, $6)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
-input.depositAmount,
|
||||||
|
nextAvailable,
|
||||||
|
nextFrozen,
|
||||||
|
input.workOrderId,
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(input.workerId),
|
||||||
|
eventType: 'grabbed',
|
||||||
|
fromStatus: 'open',
|
||||||
|
toStatus: 'in_progress',
|
||||||
|
payloadJson: '{}',
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignWorkOrderToWorker(input: {
|
||||||
|
workOrderId: number
|
||||||
|
workerId: number
|
||||||
|
depositAmount: number
|
||||||
|
maxActiveOrders: number
|
||||||
|
deadlineAt: string | null
|
||||||
|
now: string
|
||||||
|
/** 操作人显示名(管理员用户名等) */
|
||||||
|
actorName?: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
failureReason:
|
||||||
|
| 'work_order_not_assignable'
|
||||||
|
| 'worker_active_order_limit'
|
||||||
|
| 'worker_deposit_insufficient'
|
||||||
|
| null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const currentResult = await client.query<WorkOrderRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM work_orders
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = currentResult.rows[0] || null
|
||||||
|
if (
|
||||||
|
!workOrder ||
|
||||||
|
!['unassigned', 'open'].includes(workOrder.status) ||
|
||||||
|
workOrder.assigned_worker_id
|
||||||
|
) {
|
||||||
|
return { order: null, failureReason: 'work_order_not_assignable' }
|
||||||
|
}
|
||||||
|
const fromStatus = workOrder.status
|
||||||
|
|
||||||
|
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 { order: null, failureReason: 'worker_deposit_insufficient' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'in_progress',
|
||||||
|
assigned_worker_id = $1,
|
||||||
|
last_assigned_worker_id = $1,
|
||||||
|
assigned_at = $2,
|
||||||
|
deadline_at = $3,
|
||||||
|
updated_at = $2
|
||||||
|
WHERE id = $4
|
||||||
|
`,
|
||||||
|
[input.workerId, input.now, input.deadlineAt, input.workOrderId],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (input.depositAmount > 0) {
|
||||||
|
const nextAvailable = available - input.depositAmount
|
||||||
|
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount
|
||||||
|
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, input.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_freeze', $2, $3, $4, $5, '后台指派冻结押金', '{}'::jsonb, $6)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
-input.depositAmount,
|
||||||
|
nextAvailable,
|
||||||
|
nextFrozen,
|
||||||
|
input.workOrderId,
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: 'assigned_by_admin',
|
||||||
|
fromStatus,
|
||||||
|
toStatus: 'in_progress',
|
||||||
|
payloadJson: JSON.stringify({ workerId: input.workerId, depositAmount: input.depositAmount }),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -58,675 +58,17 @@ export {
|
|||||||
updateWorkOrderBasic,
|
updateWorkOrderBasic,
|
||||||
} from './work-order-management-repo.js'
|
} from './work-order-management-repo.js'
|
||||||
|
|
||||||
export async function grabWorkOrder(input: {
|
export {
|
||||||
workOrderId: number
|
assignWorkOrderToWorker,
|
||||||
workerId: number
|
grabWorkOrder,
|
||||||
depositAmount: number
|
} from './work-order-grab-repo.js'
|
||||||
maxActiveOrders: number
|
export {
|
||||||
hallCandidateLimit?: number
|
cancelAssignedWorkOrder,
|
||||||
visibleAfterIso?: string
|
cancelWorkerWorkOrder,
|
||||||
deadlineAt: string | null
|
reopenCancelledWorkOrder,
|
||||||
now: string
|
returnAssignedWorkOrderToHall,
|
||||||
}): Promise<GrabWorkOrderResult> {
|
unassignWorkOrder,
|
||||||
return withTransaction(async (client) => {
|
} from './work-order-cancel-repo.js'
|
||||||
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 {
|
|
||||||
order: null,
|
|
||||||
failureReason: 'worker_deposit_insufficient',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
input.hallCandidateLimit &&
|
|
||||||
!(await isWorkOrderWithinHallCapacityWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
hallCandidateLimit: input.hallCandidateLimit,
|
|
||||||
visibleAfterIso: input.visibleAfterIso,
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
order: null,
|
|
||||||
failureReason: 'work_order_not_open',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await client.query<{ id: number }>(
|
|
||||||
`
|
|
||||||
UPDATE work_orders
|
|
||||||
SET
|
|
||||||
status = 'in_progress',
|
|
||||||
assigned_worker_id = $1,
|
|
||||||
last_assigned_worker_id = $1,
|
|
||||||
assigned_at = $2,
|
|
||||||
deadline_at = $3,
|
|
||||||
updated_at = $2
|
|
||||||
WHERE id = $4
|
|
||||||
AND status = 'open'
|
|
||||||
AND assigned_worker_id IS NULL
|
|
||||||
RETURNING id
|
|
||||||
`,
|
|
||||||
[input.workerId, input.now, input.deadlineAt, input.workOrderId],
|
|
||||||
)
|
|
||||||
if (!result.rows[0]) {
|
|
||||||
return {
|
|
||||||
order: null,
|
|
||||||
failureReason: 'work_order_not_open',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.depositAmount > 0) {
|
|
||||||
const nextAvailable = available - input.depositAmount
|
|
||||||
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount
|
|
||||||
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, input.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_freeze', $2, $3, $4, $5, '抢单冻结押金', '{}'::jsonb, $6)
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
input.workerId,
|
|
||||||
-input.depositAmount,
|
|
||||||
nextAvailable,
|
|
||||||
nextFrozen,
|
|
||||||
input.workOrderId,
|
|
||||||
input.now,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createWorkOrderEventWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
actorType: 'worker',
|
|
||||||
actorId: String(input.workerId),
|
|
||||||
eventType: 'grabbed',
|
|
||||||
fromStatus: 'open',
|
|
||||||
toStatus: 'in_progress',
|
|
||||||
payloadJson: '{}',
|
|
||||||
now: input.now,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function assignWorkOrderToWorker(input: {
|
|
||||||
workOrderId: number
|
|
||||||
workerId: number
|
|
||||||
depositAmount: number
|
|
||||||
maxActiveOrders: number
|
|
||||||
deadlineAt: string | null
|
|
||||||
now: string
|
|
||||||
/** 操作人显示名(管理员用户名等) */
|
|
||||||
actorName?: string
|
|
||||||
}): Promise<{
|
|
||||||
order: WorkOrderRow | null
|
|
||||||
failureReason:
|
|
||||||
| 'work_order_not_assignable'
|
|
||||||
| 'worker_active_order_limit'
|
|
||||||
| 'worker_deposit_insufficient'
|
|
||||||
| null
|
|
||||||
}> {
|
|
||||||
return withTransaction(async (client) => {
|
|
||||||
const currentResult = await client.query<WorkOrderRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM work_orders
|
|
||||||
WHERE id = $1
|
|
||||||
FOR UPDATE
|
|
||||||
`,
|
|
||||||
[input.workOrderId],
|
|
||||||
)
|
|
||||||
const workOrder = currentResult.rows[0] || null
|
|
||||||
if (
|
|
||||||
!workOrder ||
|
|
||||||
!['unassigned', 'open'].includes(workOrder.status) ||
|
|
||||||
workOrder.assigned_worker_id
|
|
||||||
) {
|
|
||||||
return { order: null, failureReason: 'work_order_not_assignable' }
|
|
||||||
}
|
|
||||||
const fromStatus = workOrder.status
|
|
||||||
|
|
||||||
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 { order: null, failureReason: 'worker_deposit_insufficient' }
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE work_orders
|
|
||||||
SET
|
|
||||||
status = 'in_progress',
|
|
||||||
assigned_worker_id = $1,
|
|
||||||
last_assigned_worker_id = $1,
|
|
||||||
assigned_at = $2,
|
|
||||||
deadline_at = $3,
|
|
||||||
updated_at = $2
|
|
||||||
WHERE id = $4
|
|
||||||
`,
|
|
||||||
[input.workerId, input.now, input.deadlineAt, input.workOrderId],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (input.depositAmount > 0) {
|
|
||||||
const nextAvailable = available - input.depositAmount
|
|
||||||
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount
|
|
||||||
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, input.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_freeze', $2, $3, $4, $5, '后台指派冻结押金', '{}'::jsonb, $6)
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
input.workerId,
|
|
||||||
-input.depositAmount,
|
|
||||||
nextAvailable,
|
|
||||||
nextFrozen,
|
|
||||||
input.workOrderId,
|
|
||||||
input.now,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createWorkOrderEventWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: input.actorName || '',
|
|
||||||
eventType: 'assigned_by_admin',
|
|
||||||
fromStatus,
|
|
||||||
toStatus: 'in_progress',
|
|
||||||
payloadJson: JSON.stringify({ workerId: input.workerId, depositAmount: input.depositAmount }),
|
|
||||||
now: input.now,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cancelWorkerWorkOrder(input: {
|
|
||||||
workOrderId: number
|
|
||||||
workerId: number
|
|
||||||
now: string
|
|
||||||
}): Promise<{
|
|
||||||
order: WorkOrderRow | null
|
|
||||||
failureReason: 'work_order_not_in_progress' | 'work_order_owner_required' | null
|
|
||||||
}> {
|
|
||||||
return withTransaction(async (client) => {
|
|
||||||
const currentResult = await client.query<WorkOrderRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM work_orders
|
|
||||||
WHERE id = $1
|
|
||||||
FOR UPDATE
|
|
||||||
`,
|
|
||||||
[input.workOrderId],
|
|
||||||
)
|
|
||||||
const workOrder = currentResult.rows[0] || null
|
|
||||||
if (!workOrder || workOrder.status !== 'in_progress') {
|
|
||||||
return { order: null, failureReason: 'work_order_not_in_progress' }
|
|
||||||
}
|
|
||||||
if (Number(workOrder.assigned_worker_id || 0) !== input.workerId) {
|
|
||||||
return { order: null, failureReason: 'work_order_owner_required' }
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
|
||||||
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
|
||||||
const releaseAmount = Math.min(
|
|
||||||
await getOutstandingDepositAmountWithClient(client, input.workerId, input.workOrderId),
|
|
||||||
Number(wallet?.frozen_deposit_amount || 0),
|
|
||||||
)
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE work_orders
|
|
||||||
SET
|
|
||||||
status = 'open',
|
|
||||||
assigned_worker_id = NULL,
|
|
||||||
assigned_at = NULL,
|
|
||||||
deadline_at = NULL,
|
|
||||||
hall_queued_at = $1,
|
|
||||||
updated_at = $1
|
|
||||||
WHERE id = $2
|
|
||||||
`,
|
|
||||||
[input.now, input.workOrderId],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (releaseAmount > 0) {
|
|
||||||
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
|
||||||
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, updated_at = $3
|
|
||||||
WHERE worker_id = $4
|
|
||||||
`,
|
|
||||||
[nextAvailable, nextFrozen, input.now, input.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, '取消接单退还押金', '{}'::jsonb, $6)
|
|
||||||
`,
|
|
||||||
[input.workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createWorkOrderEventWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
actorType: 'worker',
|
|
||||||
actorId: String(input.workerId),
|
|
||||||
eventType: 'cancelled_by_worker',
|
|
||||||
fromStatus: 'in_progress',
|
|
||||||
toStatus: 'open',
|
|
||||||
payloadJson: JSON.stringify({ releaseAmount }),
|
|
||||||
now: input.now,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打手提交工单问题反馈,不改变工单状态、押金或资金流水。 */
|
|
||||||
|
|
||||||
export async function unassignWorkOrder(input: {
|
|
||||||
workOrderId: number
|
|
||||||
now: string
|
|
||||||
actorName?: string
|
|
||||||
}): Promise<{
|
|
||||||
order: WorkOrderRow | null
|
|
||||||
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
|
|
||||||
}> {
|
|
||||||
return withTransaction(async (client) => {
|
|
||||||
const currentResult = await client.query<WorkOrderRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM work_orders
|
|
||||||
WHERE id = $1
|
|
||||||
FOR UPDATE
|
|
||||||
`,
|
|
||||||
[input.workOrderId],
|
|
||||||
)
|
|
||||||
const workOrder = currentResult.rows[0] || null
|
|
||||||
if (!workOrder || workOrder.status !== 'in_progress') {
|
|
||||||
return { order: null, failureReason: 'work_order_not_in_progress' }
|
|
||||||
}
|
|
||||||
const workerId = Number(workOrder.assigned_worker_id || 0)
|
|
||||||
if (!workerId) {
|
|
||||||
return { order: null, failureReason: 'work_order_no_worker' }
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE work_orders
|
|
||||||
SET
|
|
||||||
status = 'unassigned',
|
|
||||||
assigned_worker_id = NULL,
|
|
||||||
assigned_at = NULL,
|
|
||||||
deadline_at = NULL,
|
|
||||||
updated_at = $1
|
|
||||||
WHERE id = $2
|
|
||||||
`,
|
|
||||||
[input.now, input.workOrderId],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (releaseAmount > 0) {
|
|
||||||
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
|
||||||
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, 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, '后台取消指派退还押金', '{}'::jsonb, $6)
|
|
||||||
`,
|
|
||||||
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createWorkOrderEventWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: input.actorName || '',
|
|
||||||
eventType: 'unassigned_by_admin',
|
|
||||||
fromStatus: 'in_progress',
|
|
||||||
toStatus: 'unassigned',
|
|
||||||
payloadJson: JSON.stringify({ workerId, releaseAmount }),
|
|
||||||
now: input.now,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function returnAssignedWorkOrderToHall(input: {
|
|
||||||
workOrderId: number
|
|
||||||
now: string
|
|
||||||
reason?: string
|
|
||||||
actorName?: string
|
|
||||||
}): Promise<{
|
|
||||||
order: WorkOrderRow | null
|
|
||||||
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
|
|
||||||
}> {
|
|
||||||
return withTransaction(async (client) => {
|
|
||||||
const currentResult = await client.query<WorkOrderRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM work_orders
|
|
||||||
WHERE id = $1
|
|
||||||
FOR UPDATE
|
|
||||||
`,
|
|
||||||
[input.workOrderId],
|
|
||||||
)
|
|
||||||
const workOrder = currentResult.rows[0] || null
|
|
||||||
if (!workOrder || workOrder.status !== 'in_progress') {
|
|
||||||
return { order: null, failureReason: 'work_order_not_in_progress' }
|
|
||||||
}
|
|
||||||
const workerId = Number(workOrder.assigned_worker_id || 0)
|
|
||||||
if (!workerId) {
|
|
||||||
return { order: null, failureReason: 'work_order_no_worker' }
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE work_orders
|
|
||||||
SET
|
|
||||||
status = 'open',
|
|
||||||
assigned_worker_id = NULL,
|
|
||||||
assigned_at = NULL,
|
|
||||||
deadline_at = NULL,
|
|
||||||
published_at = $1,
|
|
||||||
hall_queued_at = $1,
|
|
||||||
updated_at = $1
|
|
||||||
WHERE id = $2
|
|
||||||
`,
|
|
||||||
[input.now, input.workOrderId],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (releaseAmount > 0) {
|
|
||||||
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
|
||||||
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, 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, '后台退回大厅退还押金', '{}'::jsonb, $6)
|
|
||||||
`,
|
|
||||||
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createWorkOrderEventWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: input.actorName || '',
|
|
||||||
eventType: 'returned_to_hall_by_admin',
|
|
||||||
fromStatus: 'in_progress',
|
|
||||||
toStatus: 'open',
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
workerId,
|
|
||||||
releaseAmount,
|
|
||||||
reason: String(input.reason || '').trim(),
|
|
||||||
}),
|
|
||||||
now: input.now,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cancelAssignedWorkOrder(input: {
|
|
||||||
workOrderId: number
|
|
||||||
now: string
|
|
||||||
reason?: string
|
|
||||||
actorName?: string
|
|
||||||
}): Promise<{
|
|
||||||
order: WorkOrderRow | null
|
|
||||||
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
|
|
||||||
}> {
|
|
||||||
return withTransaction(async (client) => {
|
|
||||||
const currentResult = await client.query<WorkOrderRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM work_orders
|
|
||||||
WHERE id = $1
|
|
||||||
FOR UPDATE
|
|
||||||
`,
|
|
||||||
[input.workOrderId],
|
|
||||||
)
|
|
||||||
const workOrder = currentResult.rows[0] || null
|
|
||||||
if (!workOrder || workOrder.status !== 'in_progress') {
|
|
||||||
return { order: null, failureReason: 'work_order_not_in_progress' }
|
|
||||||
}
|
|
||||||
const workerId = Number(workOrder.assigned_worker_id || 0)
|
|
||||||
if (!workerId) {
|
|
||||||
return { order: null, failureReason: 'work_order_no_worker' }
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE work_orders
|
|
||||||
SET
|
|
||||||
status = 'cancelled',
|
|
||||||
assigned_worker_id = NULL,
|
|
||||||
assigned_at = NULL,
|
|
||||||
deadline_at = NULL,
|
|
||||||
published_at = NULL,
|
|
||||||
updated_at = $1
|
|
||||||
WHERE id = $2
|
|
||||||
`,
|
|
||||||
[input.now, input.workOrderId],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (releaseAmount > 0) {
|
|
||||||
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
|
|
||||||
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, 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, '后台撤单退还押金', '{}'::jsonb, $6)
|
|
||||||
`,
|
|
||||||
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createWorkOrderEventWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: input.actorName || '',
|
|
||||||
eventType: 'cancelled_by_admin',
|
|
||||||
fromStatus: 'in_progress',
|
|
||||||
toStatus: 'cancelled',
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
workerId,
|
|
||||||
releaseAmount,
|
|
||||||
reason: String(input.reason || '').trim(),
|
|
||||||
}),
|
|
||||||
now: input.now,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将已取消工单恢复为未分配状态,不变更历史押金流水。 */
|
|
||||||
export async function reopenCancelledWorkOrder(input: {
|
|
||||||
workOrderId: number
|
|
||||||
now: string
|
|
||||||
actorName?: string
|
|
||||||
}): Promise<{
|
|
||||||
order: WorkOrderRow | null
|
|
||||||
failureReason: 'work_order_not_cancelled' | 'work_order_has_active_shares' | null
|
|
||||||
}> {
|
|
||||||
return withTransaction(async (client) => {
|
|
||||||
const currentResult = await client.query<WorkOrderRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM work_orders
|
|
||||||
WHERE id = $1
|
|
||||||
FOR UPDATE
|
|
||||||
`,
|
|
||||||
[input.workOrderId],
|
|
||||||
)
|
|
||||||
const workOrder = currentResult.rows[0] || null
|
|
||||||
if (!workOrder || workOrder.status !== 'cancelled') {
|
|
||||||
return { order: null, failureReason: 'work_order_not_cancelled' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeShareResult = await client.query<{ total: number }>(
|
|
||||||
`
|
|
||||||
SELECT COUNT(*)::int AS total
|
|
||||||
FROM work_order_shares
|
|
||||||
WHERE work_order_id = $1
|
|
||||||
AND status != 'cancelled'
|
|
||||||
`,
|
|
||||||
[input.workOrderId],
|
|
||||||
)
|
|
||||||
if (Number(activeShareResult.rows[0]?.total || 0) > 0) {
|
|
||||||
return { order: null, failureReason: 'work_order_has_active_shares' }
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE work_orders
|
|
||||||
SET
|
|
||||||
status = 'unassigned',
|
|
||||||
assigned_worker_id = NULL,
|
|
||||||
assigned_at = NULL,
|
|
||||||
deadline_at = NULL,
|
|
||||||
acceptance_json = '{}'::jsonb,
|
|
||||||
draft_acceptance_json = '{}'::jsonb,
|
|
||||||
submitted_at = NULL,
|
|
||||||
accepted_at = NULL,
|
|
||||||
problem_note = '',
|
|
||||||
published_at = NULL,
|
|
||||||
pinned_at = NULL,
|
|
||||||
updated_at = $1
|
|
||||||
WHERE id = $2
|
|
||||||
`,
|
|
||||||
[input.now, input.workOrderId],
|
|
||||||
)
|
|
||||||
|
|
||||||
await createWorkOrderEventWithClient(client, {
|
|
||||||
workOrderId: input.workOrderId,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: input.actorName || '',
|
|
||||||
eventType: 'reopened_from_cancelled',
|
|
||||||
fromStatus: 'cancelled',
|
|
||||||
toStatus: 'unassigned',
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
previousAssignedWorkerId: Number(workOrder.last_assigned_worker_id || 0) || null,
|
|
||||||
}),
|
|
||||||
now: input.now,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function acceptWorkOrderAndSettle(input: {
|
export async function acceptWorkOrderAndSettle(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
|
|||||||
Reference in New Issue
Block a user