feat: 打手分内部/外部,内部免审核;支持后台指派打手

- worker_users 新增 worker_type(internal/external,迁移 017)
- 内部打手提交验收后自动通过进入待打款(accepted+结算),外部打手保持后台审核
- 打手管理页支持设为内部/外部并显示类型标签
- 新增后台指派打手:未分配/待抢单工单可指定已注册打手,进入其我的订单并冻结押金
- 指派带审计与事件记录,校验打手状态/接单上限/余额押金
This commit is contained in:
yml2213
2026-08-14 16:35:15 +08:00
parent 6a17113374
commit 7bb3142958
13 changed files with 403 additions and 16 deletions
@@ -19,6 +19,7 @@ export type WorkerUserRow = {
phone: string
level_id: number | null
status: string
worker_type: string
review_note: string
session_version: number
created_at: string
@@ -241,6 +242,7 @@ export type CreateWorkerInput = {
phone: string
levelId: number | null
status: string
workerType?: string
inviterId?: number | null
inviteCode: string
now: string
@@ -859,6 +859,115 @@ export async function grabWorkOrder(input: {
})
}
export async function assignWorkOrderToWorker(input: {
workOrderId: number
workerId: number
depositAmount: number
maxActiveOrders: number
deadlineAt: string | null
now: 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: '',
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
@@ -159,9 +159,9 @@ export async function createWorkerUser(input: CreateWorkerInput): Promise<Worker
`
INSERT INTO worker_users (
username, password_hash, display_name, phone, level_id, status,
inviter_id, invite_code,
worker_type, inviter_id, invite_code,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id
`,
[
@@ -171,6 +171,7 @@ export async function createWorkerUser(input: CreateWorkerInput): Promise<Worker
input.phone,
input.levelId,
input.status,
input.workerType || 'external',
input.inviterId || null,
input.inviteCode,
input.now,
@@ -188,7 +189,13 @@ export async function updateWorkerUser(
patch: Partial<
Pick<
WorkerUserRow,
'status' | 'review_note' | 'level_id' | 'session_version' | 'updated_at' | 'reviewed_at'
| 'status'
| 'worker_type'
| 'review_note'
| 'level_id'
| 'session_version'
| 'updated_at'
| 'reviewed_at'
>
>,
): Promise<WorkerUserRow | null> {
@@ -201,15 +208,17 @@ export async function updateWorkerUser(
UPDATE worker_users
SET
status = $1,
review_note = $2,
level_id = $3,
session_version = $4,
reviewed_at = $5,
updated_at = $6
WHERE id = $7
worker_type = $2,
review_note = $3,
level_id = $4,
session_version = $5,
reviewed_at = $6,
updated_at = $7
WHERE id = $8
`,
[
next.status,
next.worker_type || 'external',
next.review_note || '',
next.level_id || null,
Number(next.session_version || 1),