接单工单支持任务时限,超时自动判定失败并处置

This commit is contained in:
yml2213
2026-08-01 19:02:57 +08:00
parent 66193c5d59
commit 23ad60a1ef
18 changed files with 681 additions and 61 deletions
@@ -109,6 +109,8 @@ export type WorkProductRuleRow = {
sharing_enabled: boolean
sharing_total_quantity: number
sharing_unit_reward: number
timeout_minutes: number
timeout_policy: string
requirement_json: string | Record<string, unknown>
sort_order: number
created_at: string
@@ -159,6 +161,9 @@ export type WorkOrderRow = {
assigned_at: string | null
submitted_at: string | null
accepted_at: string | null
deadline_at: string | null
timeout_minutes: number
timeout_policy: string
created_at: string
updated_at: string
category_name?: string
@@ -241,6 +246,8 @@ export type CreateWorkOrderInput = {
sharingEnabled?: boolean
sharingTotalQuantity?: number
sharingUnitReward?: number
timeoutMinutes?: number
timeoutPolicy?: string
materialJson: string
requirementJson: string
now: string
@@ -408,6 +408,8 @@ export async function upsertWorkProductRule(input: {
sharingEnabled?: boolean
sharingTotalQuantity?: number
sharingUnitReward?: number
timeoutMinutes?: number
timeoutPolicy?: string
requirementJson: string
sortOrder: number
now: string
@@ -419,6 +421,7 @@ export async function upsertWorkProductRule(input: {
match_type, category_id, enabled, auto_create, reward_amount,
required_deposit_amount, deposit_threshold_amount,
sharing_enabled, sharing_total_quantity, sharing_unit_reward,
timeout_minutes, timeout_policy,
requirement_json,
sort_order, created_at, updated_at
) VALUES (
@@ -426,8 +429,9 @@ export async function upsertWorkProductRule(input: {
$7, $8, $9, $10, $11,
$12, $13,
$14, $15, $16,
$17::jsonb,
$18, $19, $20
$17, $18,
$19::jsonb,
$20, $21, $22
)
ON CONFLICT (rule_key) DO UPDATE
SET
@@ -446,6 +450,8 @@ export async function upsertWorkProductRule(input: {
sharing_enabled = EXCLUDED.sharing_enabled,
sharing_total_quantity = EXCLUDED.sharing_total_quantity,
sharing_unit_reward = EXCLUDED.sharing_unit_reward,
timeout_minutes = EXCLUDED.timeout_minutes,
timeout_policy = EXCLUDED.timeout_policy,
requirement_json = EXCLUDED.requirement_json,
sort_order = EXCLUDED.sort_order,
updated_at = EXCLUDED.updated_at
@@ -468,6 +474,8 @@ export async function upsertWorkProductRule(input: {
input.sharingEnabled === true,
toPositiveInteger(input.sharingTotalQuantity, 1),
toPositiveInteger(input.sharingUnitReward, 0),
toPositiveInteger(input.timeoutMinutes, 0),
String(input.timeoutPolicy || 'reopen').trim(),
input.requirementJson,
input.sortOrder,
input.now,
@@ -484,10 +492,11 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise<Work
work_order_no, order_id, order_item_id, task_id, platform_order_id,
product_name, category_id, status, reward_amount, required_deposit_amount,
deposit_threshold_amount, sharing_enabled, sharing_total_quantity,
sharing_unit_reward, material_json, requirement_json, created_at, updated_at
sharing_unit_reward, timeout_minutes, timeout_policy,
material_json, requirement_json, created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15::jsonb, $16::jsonb, $17, $18
$11, $12, $13, $14, $15, $16, $17::jsonb, $18::jsonb, $19, $20
)
RETURNING id
`,
@@ -506,6 +515,8 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise<Work
input.sharingEnabled === true,
toPositiveInteger(input.sharingTotalQuantity, 1),
toPositiveInteger(input.sharingUnitReward, 0),
toPositiveInteger(input.timeoutMinutes, 0),
String(input.timeoutPolicy || 'reopen').trim() || 'reopen',
input.materialJson,
input.requirementJson,
input.now,
@@ -671,6 +682,8 @@ export async function updateWorkOrderBasic(
requiredDepositAmount?: number
depositThresholdAmount?: number
requirementJson?: string
timeoutMinutes?: number
timeoutPolicy?: string
updatedAt: string
},
): Promise<WorkOrderRow | null> {
@@ -687,8 +700,10 @@ export async function updateWorkOrderBasic(
required_deposit_amount = $5,
deposit_threshold_amount = $6,
requirement_json = $7::jsonb,
updated_at = $8
WHERE id = $9
timeout_minutes = $8,
timeout_policy = $9,
updated_at = $10
WHERE id = $11
`,
[
String((patch.platformOrderId ?? current.platform_order_id) || '').trim(),
@@ -704,6 +719,8 @@ export async function updateWorkOrderBasic(
0,
),
toJsonString(patch.requirementJson ?? current.requirement_json),
toPositiveInteger(patch.timeoutMinutes ?? current.timeout_minutes, 0),
String((patch.timeoutPolicy ?? current.timeout_policy) || 'reopen').trim(),
patch.updatedAt,
Number(workOrderId),
],
@@ -727,6 +744,7 @@ export async function grabWorkOrder(input: {
workerId: number
depositAmount: number
maxActiveOrders: number
deadlineAt: string | null
now: string
}): Promise<GrabWorkOrderResult> {
return withTransaction(async (client) => {
@@ -755,13 +773,14 @@ export async function grabWorkOrder(input: {
status = 'in_progress',
assigned_worker_id = $1,
assigned_at = $2,
deadline_at = $3,
updated_at = $2
WHERE id = $3
WHERE id = $4
AND status = 'open'
AND assigned_worker_id IS NULL
RETURNING id
`,
[input.workerId, input.now, input.workOrderId],
[input.workerId, input.now, input.deadlineAt, input.workOrderId],
)
if (!result.rows[0]) {
return {
@@ -857,6 +876,7 @@ export async function cancelWorkerWorkOrder(input: {
status = 'open',
assigned_worker_id = NULL,
assigned_at = NULL,
deadline_at = NULL,
updated_at = $1
WHERE id = $2
`,
@@ -1228,6 +1248,12 @@ export async function resolveProblemWorkOrder(input: {
acceptance_json = CASE WHEN $3 THEN '{}'::jsonb ELSE acceptance_json END,
submitted_at = CASE WHEN $3 THEN NULL ELSE submitted_at END,
problem_note = CASE WHEN $2 THEN '' ELSE problem_note END,
deadline_at = CASE
WHEN $1 = 'in_progress' AND timeout_minutes > 0
THEN NOW() + (timeout_minutes * INTERVAL '1 minute')
WHEN $1 = 'open' THEN NULL
ELSE deadline_at
END,
updated_at = $4
WHERE id = $5
`,
@@ -1253,6 +1279,163 @@ export async function resolveProblemWorkOrder(input: {
})
}
export async function listOverdueWorkOrders({
limit = 50,
workerId = 0,
}: { limit?: number; workerId?: number } = {}): Promise<WorkOrderRow[]> {
const params: unknown[] = []
let workerClause = ''
if (workerId > 0) {
params.push(workerId)
workerClause = `AND wo.assigned_worker_id = $${params.length}`
}
params.push(limit)
const result = await query<WorkOrderRow>(
`${WORK_ORDER_SELECT}
WHERE wo.status = 'in_progress'
AND wo.deadline_at IS NOT NULL
AND wo.deadline_at < NOW()
${workerClause}
ORDER BY wo.deadline_at ASC
LIMIT $${params.length}`,
params,
)
return result.rows
}
export async function settleOverdueWorkOrder(input: {
workOrderId: number
policy: 'reopen' | 'cancel_release' | 'cancel_deduct'
now: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_overdue' | 'work_order_has_active_shares' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
AND status = 'in_progress'
AND deadline_at IS NOT NULL
AND deadline_at < NOW()
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) {
return { order: null, failureReason: 'work_order_not_overdue' }
}
const shareResult = 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(shareResult.rows[0]?.total || 0) > 0) {
return { order: null, failureReason: 'work_order_has_active_shares' }
}
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.policy)
const shouldDeductDeposit = input.policy === 'cancel_deduct'
const workerId = Number(workOrder.assigned_worker_id || 0)
let resolvedDepositAmount = 0
if (workerId > 0 && (shouldReleaseDeposit || shouldDeductDeposit)) {
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
resolvedDepositAmount = Math.min(
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
Number(wallet?.frozen_deposit_amount || 0),
)
if (resolvedDepositAmount > 0) {
const nextAvailable =
Number(wallet?.available_amount || 0) + (shouldReleaseDeposit ? resolvedDepositAmount : 0)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - resolvedDepositAmount,
)
const ledgerType = shouldReleaseDeposit ? 'deposit_release' : 'deposit_deduction'
const ledgerAmount = shouldReleaseDeposit ? resolvedDepositAmount : -resolvedDepositAmount
const ledgerNote = shouldReleaseDeposit ? '超时处置释放押金' : '超时处置扣除押金'
await client.query(
`
UPDATE worker_wallets
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
WHERE worker_id = $4
`,
[nextAvailable, nextFrozen, input.now, workerId],
)
await client.query(
`
INSERT INTO worker_wallet_ledgers (
worker_id, ledger_type, amount, balance_after, frozen_after,
related_work_order_id, note, payload_json, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[
workerId,
ledgerType,
ledgerAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
ledgerNote,
JSON.stringify({ action: `timeout_${input.policy}` }),
input.now,
],
)
}
}
const targetStatus = input.policy === 'reopen' ? 'open' : 'cancelled'
await client.query(
`
UPDATE work_orders
SET
status = $1,
assigned_worker_id = NULL,
assigned_at = NULL,
deadline_at = NULL,
acceptance_json = CASE WHEN $1 = 'open' THEN '{}'::jsonb ELSE acceptance_json END,
submitted_at = CASE WHEN $1 = 'open' THEN NULL ELSE submitted_at END,
problem_note = CASE WHEN $1 = 'open' THEN '' ELSE problem_note END,
published_at = CASE WHEN $1 = 'open' THEN $2 ELSE published_at END,
updated_at = $2
WHERE id = $3
`,
[targetStatus, input.now, input.workOrderId],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'system',
actorId: '',
eventType: `timeout_${input.policy}`,
fromStatus: 'in_progress',
toStatus: targetStatus,
payloadJson: JSON.stringify({
policy: input.policy,
depositAmount: resolvedDepositAmount,
}),
now: input.now,
})
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
export async function countWorkerActiveOrders(workerId: number | string): Promise<number> {
const result = await query<{ total: number }>(
`