拆分工单流转事务仓储
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-query-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-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,
|
||||
} from './work-order-management-repo.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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
assignWorkOrderToWorker,
|
||||
grabWorkOrder,
|
||||
} from './work-order-grab-repo.js'
|
||||
export {
|
||||
cancelAssignedWorkOrder,
|
||||
cancelWorkerWorkOrder,
|
||||
reopenCancelledWorkOrder,
|
||||
returnAssignedWorkOrderToHall,
|
||||
unassignWorkOrder,
|
||||
} from './work-order-cancel-repo.js'
|
||||
|
||||
export async function acceptWorkOrderAndSettle(input: {
|
||||
workOrderId: number
|
||||
|
||||
Reference in New Issue
Block a user