拆分工单流转事务仓储

This commit is contained in:
yml2213
2026-08-21 18:47:05 +08:00
parent 175b2dbac6
commit a9665b180f
4 changed files with 704 additions and 669 deletions
@@ -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