feat: 打手分内部/外部,内部免审核;支持后台指派打手
- worker_users 新增 worker_type(internal/external,迁移 017) - 内部打手提交验收后自动通过进入待打款(accepted+结算),外部打手保持后台审核 - 打手管理页支持设为内部/外部并显示类型标签 - 新增后台指派打手:未分配/待抢单工单可指定已注册打手,进入其我的订单并冻结押金 - 指派带审计与事件记录,校验打手状态/接单上限/余额押金
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- 打手类型:internal 内部打手(提交验收后免审核,直接进入待打款)/ external 外部打手(默认,需后台验收)。
|
||||||
|
ALTER TABLE worker_users
|
||||||
|
ADD COLUMN worker_type TEXT NOT NULL DEFAULT 'external';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN worker_users.worker_type IS '打手类型:internal 内部免审核 / external 外部需审核';
|
||||||
@@ -19,6 +19,7 @@ export type WorkerUserRow = {
|
|||||||
phone: string
|
phone: string
|
||||||
level_id: number | null
|
level_id: number | null
|
||||||
status: string
|
status: string
|
||||||
|
worker_type: string
|
||||||
review_note: string
|
review_note: string
|
||||||
session_version: number
|
session_version: number
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -241,6 +242,7 @@ export type CreateWorkerInput = {
|
|||||||
phone: string
|
phone: string
|
||||||
levelId: number | null
|
levelId: number | null
|
||||||
status: string
|
status: string
|
||||||
|
workerType?: string
|
||||||
inviterId?: number | null
|
inviterId?: number | null
|
||||||
inviteCode: string
|
inviteCode: string
|
||||||
now: 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: {
|
export async function cancelWorkerWorkOrder(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
workerId: number
|
workerId: number
|
||||||
|
|||||||
@@ -159,9 +159,9 @@ export async function createWorkerUser(input: CreateWorkerInput): Promise<Worker
|
|||||||
`
|
`
|
||||||
INSERT INTO worker_users (
|
INSERT INTO worker_users (
|
||||||
username, password_hash, display_name, phone, level_id, status,
|
username, password_hash, display_name, phone, level_id, status,
|
||||||
inviter_id, invite_code,
|
worker_type, inviter_id, invite_code,
|
||||||
created_at, updated_at
|
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
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
@@ -171,6 +171,7 @@ export async function createWorkerUser(input: CreateWorkerInput): Promise<Worker
|
|||||||
input.phone,
|
input.phone,
|
||||||
input.levelId,
|
input.levelId,
|
||||||
input.status,
|
input.status,
|
||||||
|
input.workerType || 'external',
|
||||||
input.inviterId || null,
|
input.inviterId || null,
|
||||||
input.inviteCode,
|
input.inviteCode,
|
||||||
input.now,
|
input.now,
|
||||||
@@ -188,7 +189,13 @@ export async function updateWorkerUser(
|
|||||||
patch: Partial<
|
patch: Partial<
|
||||||
Pick<
|
Pick<
|
||||||
WorkerUserRow,
|
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> {
|
): Promise<WorkerUserRow | null> {
|
||||||
@@ -201,15 +208,17 @@ export async function updateWorkerUser(
|
|||||||
UPDATE worker_users
|
UPDATE worker_users
|
||||||
SET
|
SET
|
||||||
status = $1,
|
status = $1,
|
||||||
review_note = $2,
|
worker_type = $2,
|
||||||
level_id = $3,
|
review_note = $3,
|
||||||
session_version = $4,
|
level_id = $4,
|
||||||
reviewed_at = $5,
|
session_version = $5,
|
||||||
updated_at = $6
|
reviewed_at = $6,
|
||||||
WHERE id = $7
|
updated_at = $7
|
||||||
|
WHERE id = $8
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
next.status,
|
next.status,
|
||||||
|
next.worker_type || 'external',
|
||||||
next.review_note || '',
|
next.review_note || '',
|
||||||
next.level_id || null,
|
next.level_id || null,
|
||||||
Number(next.session_version || 1),
|
Number(next.session_version || 1),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
acceptAdminWorkOrder,
|
acceptAdminWorkOrder,
|
||||||
|
assignAdminWorkOrderToWorker,
|
||||||
creditAdminWorkerWallet,
|
creditAdminWorkerWallet,
|
||||||
createAdminMockWorkOrder,
|
createAdminMockWorkOrder,
|
||||||
deductAdminWorkOrderPendingDeposit,
|
deductAdminWorkOrderPendingDeposit,
|
||||||
@@ -341,6 +342,25 @@ router.put(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/orders/:workOrderId/assign',
|
||||||
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => assignAdminWorkOrderToWorker(String(req.params.workOrderId || ''), req.body || {}),
|
||||||
|
{
|
||||||
|
successMessage: '已指派打手',
|
||||||
|
errorMessage: '指派打手失败',
|
||||||
|
scope: '[admin/worker-platform/orders/:workOrderId/assign]',
|
||||||
|
audit: (req, data) => ({
|
||||||
|
action: 'work_order_worker_assigned',
|
||||||
|
targetType: 'work_order',
|
||||||
|
targetId: String(req.params.workOrderId || ''),
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/worker-platform/orders/:workOrderId',
|
'/worker-platform/orders/:workOrderId',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
|||||||
import {
|
import {
|
||||||
acceptWorkOrderAndSettle,
|
acceptWorkOrderAndSettle,
|
||||||
addWorkerWalletCredit,
|
addWorkerWalletCredit,
|
||||||
|
assignWorkOrderToWorker,
|
||||||
countWorkerAcceptedOrders,
|
countWorkerAcceptedOrders,
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
countWorkCategoryUsages,
|
countWorkCategoryUsages,
|
||||||
@@ -88,7 +89,7 @@ import {
|
|||||||
saveWorkerFinanceConfig,
|
saveWorkerFinanceConfig,
|
||||||
} from './worker-finance-config-service.js'
|
} from './worker-finance-config-service.js'
|
||||||
|
|
||||||
import { DEFAULT_CATEGORY_KEY, DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, mapFinanceRequest, mapWallet, mapWorkCategory, mapWorkOrderAdmin, mapWorkOrderShare, mapWorkProductRule, mapWorkerLevel, mapWorkerUser, normalizeAdminFinanceReviewStatus, normalizeAmountFen, normalizeBoolean, normalizeEnabledStatus, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeMatchType, normalizeOptionalId, normalizePositiveInteger, normalizeProblemResolutionAction, normalizeRequirementFields, normalizeRequirementFieldsFromPayload, normalizeReviewStatus, normalizeSessionVersion, normalizeSlugKey, normalizeSubmittedFields, normalizeUploadedFiles, resolveMatchingProductRule, resolveRequirementFields } from './mappers.js'
|
import { DEFAULT_CATEGORY_KEY, DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, mapFinanceRequest, mapWallet, mapWorkCategory, mapWorkOrderAdmin, mapWorkOrderShare, mapWorkProductRule, mapWorkerLevel, mapWorkerUser, normalizeAdminFinanceReviewStatus, normalizeAmountFen, normalizeBoolean, normalizeEnabledStatus, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeMatchType, normalizeOptionalId, normalizePositiveInteger, normalizeProblemResolutionAction, normalizeRequirementFields, normalizeRequirementFieldsFromPayload, normalizeReviewStatus, normalizeSessionVersion, normalizeSlugKey, normalizeSubmittedFields, normalizeUploadedFiles, normalizeWorkerType, resolveFreezeDepositAmount, resolveMatchingProductRule, resolveRequirementFields, resolveWorkerPermissions } from './mappers.js'
|
||||||
import { ensureWorkerPlatformDefaults, getRequiredWorkOrder, getRequiredWorker, normalizeWorkOrderTimeoutPolicy } from './worker-service.js'
|
import { ensureWorkerPlatformDefaults, getRequiredWorkOrder, getRequiredWorker, normalizeWorkOrderTimeoutPolicy } from './worker-service.js'
|
||||||
|
|
||||||
export async function listAdminWorkerLevels() {
|
export async function listAdminWorkerLevels() {
|
||||||
@@ -346,6 +347,7 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) {
|
|||||||
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
|
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
|
||||||
const worker = await getRequiredWorker(workerId)
|
const worker = await getRequiredWorker(workerId)
|
||||||
const status = normalizeReviewStatus(payload.status)
|
const status = normalizeReviewStatus(payload.status)
|
||||||
|
const workerType = normalizeWorkerType(payload.workerType ?? payload.worker_type)
|
||||||
const requestedLevelId = normalizeOptionalId(payload.levelId)
|
const requestedLevelId = normalizeOptionalId(payload.levelId)
|
||||||
let levelId = worker.level_id || null
|
let levelId = worker.level_id || null
|
||||||
if (requestedLevelId) {
|
if (requestedLevelId) {
|
||||||
@@ -364,6 +366,7 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
|
|||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const updated = await updateWorkerUser(worker.id, {
|
const updated = await updateWorkerUser(worker.id, {
|
||||||
status,
|
status,
|
||||||
|
worker_type: workerType,
|
||||||
level_id: levelId,
|
level_id: levelId,
|
||||||
review_note: String(payload.reviewNote || payload.note || '').trim(),
|
review_note: String(payload.reviewNote || payload.note || '').trim(),
|
||||||
session_version: normalizeSessionVersion(worker.session_version) + 1,
|
session_version: normalizeSessionVersion(worker.session_version) + 1,
|
||||||
@@ -373,6 +376,91 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
|
|||||||
return { worker: mapWorkerUser(updated || worker) }
|
return { worker: mapWorkerUser(updated || worker) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function assignAdminWorkOrderToWorker(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
) {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (
|
||||||
|
!['unassigned', 'open'].includes(workOrder.status) ||
|
||||||
|
workOrder.assigned_worker_id
|
||||||
|
) {
|
||||||
|
throw createHttpError('只有未分配或待抢单且未指派打手的订单才能指派', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_not_assignable',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const workerId = normalizeOptionalId(payload.workerId ?? payload.worker_id)
|
||||||
|
if (!workerId) {
|
||||||
|
throw createHttpError('请选择要指派的打手', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_assign_worker_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const worker = await getWorkerUserById(workerId)
|
||||||
|
if (!worker || worker.status !== 'active') {
|
||||||
|
throw createHttpError('打手账号不存在或不可用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_assign_worker_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const permissions = resolveWorkerPermissions(worker)
|
||||||
|
const activeCount = await countWorkerActiveOrders(worker.id)
|
||||||
|
if (activeCount >= permissions.maxActiveOrders) {
|
||||||
|
throw createHttpError('该打手已达到最大同时接单量', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_active_order_limit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const freezeAmount = resolveFreezeDepositAmount(workOrder, permissions)
|
||||||
|
if (Number(worker.available_amount || 0) < freezeAmount) {
|
||||||
|
throw createHttpError('该打手余额不足,无法冻结所需押金', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_deposit_insufficient',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = nowIso()
|
||||||
|
const { order: updated, failureReason } = await assignWorkOrderToWorker({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
workerId: worker.id,
|
||||||
|
depositAmount: freezeAmount,
|
||||||
|
maxActiveOrders: permissions.maxActiveOrders,
|
||||||
|
deadlineAt: resolveAssignedDeadlineAt(Number(workOrder.timeout_minutes || 0), now),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (failureReason === 'work_order_not_assignable') {
|
||||||
|
throw createHttpError('订单状态已变化,无法指派', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_not_assignable',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (failureReason === 'worker_active_order_limit') {
|
||||||
|
throw createHttpError('该打手已达到最大同时接单量', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_active_order_limit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (failureReason === 'worker_deposit_insufficient') {
|
||||||
|
throw createHttpError('该打手余额不足,无法冻结所需押金', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_deposit_insufficient',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('指派失败,订单状态可能已变化', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_assign_conflict',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { order: mapWorkOrderAdmin(updated) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAssignedDeadlineAt(timeoutMinutes: number, now: string): string | null {
|
||||||
|
if (timeoutMinutes <= 0) return null
|
||||||
|
return new Date(new Date(now).getTime() + timeoutMinutes * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
export async function creditAdminWorkerWallet(workerId: number | string, payload: JsonObject = {}) {
|
export async function creditAdminWorkerWallet(workerId: number | string, payload: JsonObject = {}) {
|
||||||
await getRequiredWorker(workerId)
|
await getRequiredWorker(workerId)
|
||||||
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
|
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ export function mapWorkProductRule(rule: WorkProductRuleRow | null | undefined)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeWorkerType(value: unknown): 'internal' | 'external' {
|
||||||
|
return String(value || '').trim() === 'internal' ? 'internal' : 'external'
|
||||||
|
}
|
||||||
|
|
||||||
export function mapWorkerUser(worker: WorkerUserRow) {
|
export function mapWorkerUser(worker: WorkerUserRow) {
|
||||||
return {
|
return {
|
||||||
workerId: Number(worker.id),
|
workerId: Number(worker.id),
|
||||||
@@ -265,6 +269,7 @@ export function mapWorkerUser(worker: WorkerUserRow) {
|
|||||||
displayName: worker.display_name || worker.username,
|
displayName: worker.display_name || worker.username,
|
||||||
phone: worker.phone,
|
phone: worker.phone,
|
||||||
status: worker.status,
|
status: worker.status,
|
||||||
|
workerType: normalizeWorkerType(worker.worker_type),
|
||||||
reviewNote: worker.review_note || '',
|
reviewNote: worker.review_note || '',
|
||||||
inviteCode: worker.invite_code || '',
|
inviteCode: worker.invite_code || '',
|
||||||
inviter: worker.inviter_id
|
inviter: worker.inviter_id
|
||||||
|
|||||||
@@ -1144,10 +1144,43 @@ export async function submitWorkerOrderAcceptance(
|
|||||||
payloadJson: JSON.stringify(acceptance),
|
payloadJson: JSON.stringify(acceptance),
|
||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
if (worker.worker_type === 'internal') {
|
||||||
|
const { order: acceptedOrder, failureReason } = await acceptWorkOrderAndSettle({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (failureReason === 'sharing_pending_submissions') {
|
||||||
|
throw createHttpError('还有拼单打手未提交验收资料,暂不能统一验收', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_pending_submissions',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!acceptedOrder) {
|
||||||
|
throw createHttpError('内部打手免审核验收失败,请稍后重试', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_internal_auto_accept_failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(session.workerId),
|
||||||
|
eventType: 'accepted_by_internal',
|
||||||
|
fromStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||||
|
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||||
|
payloadJson: JSON.stringify({ autoAccepted: true }),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderForWorker(acceptedOrder, resolveWorkerPermissions(worker)),
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
order: mapWorkOrderForWorker(
|
order: mapWorkOrderForWorker(
|
||||||
updated || workOrder,
|
updated || workOrder,
|
||||||
resolveWorkerPermissions(await getRequiredWorker(session.workerId)),
|
resolveWorkerPermissions(worker),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
SendOutlined,
|
SendOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
|
UserAddOutlined,
|
||||||
WarningOutlined,
|
WarningOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
@@ -42,6 +43,7 @@ import ImagePreviewList from '@/components/files/ImagePreviewList'
|
|||||||
import PageHeader from '@/components/admin/PageHeader'
|
import PageHeader from '@/components/admin/PageHeader'
|
||||||
import {
|
import {
|
||||||
acceptAdminWorkOrder,
|
acceptAdminWorkOrder,
|
||||||
|
assignAdminWorkOrderToWorker,
|
||||||
createAdminMockWorkOrder,
|
createAdminMockWorkOrder,
|
||||||
creditAdminWorkerWallet,
|
creditAdminWorkerWallet,
|
||||||
deductAdminWorkOrderPendingDeposit,
|
deductAdminWorkOrderPendingDeposit,
|
||||||
@@ -125,6 +127,8 @@ export default function WorkOrdersPanel() {
|
|||||||
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
|
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
|
||||||
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
|
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
|
||||||
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
||||||
|
const [assignOrder, setAssignOrder] = useState<WorkOrder | null>(null)
|
||||||
|
const [assignWorkerId, setAssignWorkerId] = useState<number | undefined>(undefined)
|
||||||
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
||||||
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
||||||
const [problemForm] = Form.useForm()
|
const [problemForm] = Form.useForm()
|
||||||
@@ -162,6 +166,30 @@ export default function WorkOrdersPanel() {
|
|||||||
queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)),
|
queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)),
|
||||||
enabled: Boolean(sharingOrder),
|
enabled: Boolean(sharingOrder),
|
||||||
})
|
})
|
||||||
|
const assignWorkersQuery = useQuery({
|
||||||
|
queryKey: ['admin-worker-platform-workers', 'active', 1, 200],
|
||||||
|
queryFn: () => fetchAdminWorkerUsers({ status: 'active', page: 1, pageSize: 200 }),
|
||||||
|
enabled: Boolean(assignOrder),
|
||||||
|
})
|
||||||
|
|
||||||
|
function openAssignModal(row: WorkOrder) {
|
||||||
|
setAssignOrder(row)
|
||||||
|
setAssignWorkerId(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAssign() {
|
||||||
|
if (!assignOrder || !assignWorkerId) return
|
||||||
|
const succeeded = await runAction(
|
||||||
|
() =>
|
||||||
|
assignAdminWorkOrderToWorker(assignOrder.workOrderId, {
|
||||||
|
workerId: assignWorkerId,
|
||||||
|
}),
|
||||||
|
'已指派打手',
|
||||||
|
)
|
||||||
|
if (!succeeded) return
|
||||||
|
setAssignOrder(null)
|
||||||
|
setAssignWorkerId(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
function openSharingConfigModal(row: WorkOrder) {
|
function openSharingConfigModal(row: WorkOrder) {
|
||||||
setSharingOrder(row)
|
setSharingOrder(row)
|
||||||
@@ -551,6 +579,11 @@ export default function WorkOrdersPanel() {
|
|||||||
拼单配置
|
拼单配置
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{['unassigned', 'open'].includes(row.status) ? (
|
||||||
|
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
|
||||||
|
指派
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
{row.status === 'pending_material' ? (
|
{row.status === 'pending_material' ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<FormOutlined />}
|
icon={<FormOutlined />}
|
||||||
@@ -1142,6 +1175,41 @@ export default function WorkOrdersPanel() {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={`指派打手 · ${assignOrder?.productName || ''}`}
|
||||||
|
open={Boolean(assignOrder)}
|
||||||
|
onCancel={() => {
|
||||||
|
setAssignOrder(null)
|
||||||
|
setAssignWorkerId(undefined)
|
||||||
|
}}
|
||||||
|
onOk={() => submitAssign()}
|
||||||
|
okText="确认指派"
|
||||||
|
okButtonProps={{ disabled: !assignWorkerId }}
|
||||||
|
destroyOnHidden
|
||||||
|
width={520}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={12} className="full-width">
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
指派后订单直接进入该打手的「我的订单」,不再出现在抢单大厅;所需押金按订单规则从打手余额冻结。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Select
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="选择要指派的打手"
|
||||||
|
loading={assignWorkersQuery.isLoading}
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
value={assignWorkerId}
|
||||||
|
onChange={setAssignWorkerId}
|
||||||
|
options={(assignWorkersQuery.data?.data.items || []).map((worker) => ({
|
||||||
|
value: worker.workerId,
|
||||||
|
label: `${worker.displayName || worker.username}(${worker.username})${
|
||||||
|
worker.workerType === 'internal' ? '· 内部' : ''
|
||||||
|
}`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={`拼单配置 · ${sharingOrder?.productName || ''}`}
|
title={`拼单配置 · ${sharingOrder?.productName || ''}`}
|
||||||
open={Boolean(sharingOrder)}
|
open={Boolean(sharingOrder)}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ import { formatAdminDateTime } from '@/utils/admin-time'
|
|||||||
import { formatMoney, formatWorkerStatus } from './shared'
|
import { formatMoney, formatWorkerStatus } from './shared'
|
||||||
|
|
||||||
export default function WorkersPanel() {
|
export default function WorkersPanel() {
|
||||||
const { message } = App.useApp()
|
const { message, modal } = App.useApp()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [status, setStatus] = useState('')
|
const [status, setStatus] = useState('')
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
@@ -125,6 +125,30 @@ export default function WorkersPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleWorkerType(worker: WorkerUser, workerType: 'internal' | 'external') {
|
||||||
|
const isInternal = workerType === 'internal'
|
||||||
|
modal.confirm({
|
||||||
|
title: isInternal ? '设为内部打手?' : '设为外部打手?',
|
||||||
|
content: isInternal
|
||||||
|
? '内部打手提交验收后将免审核,直接进入待打款;请确认该打手可信。'
|
||||||
|
: '外部打手提交验收后需后台审核通过,才进入待打款。',
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await reviewAdminWorkerUser(worker.workerId, {
|
||||||
|
status: worker.status,
|
||||||
|
workerType,
|
||||||
|
})
|
||||||
|
message.success('打手类型已更新')
|
||||||
|
await refreshWorkers()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '操作失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function submitCredit(values: { amount?: number; note?: string }) {
|
async function submitCredit(values: { amount?: number; note?: string }) {
|
||||||
if (!creditWorker) return
|
if (!creditWorker) return
|
||||||
try {
|
try {
|
||||||
@@ -164,9 +188,14 @@ export default function WorkersPanel() {
|
|||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text strong>
|
<Space size={6} wrap>
|
||||||
{row.displayName || row.username}
|
<Typography.Text strong>
|
||||||
</Typography.Text>
|
{row.displayName || row.username}
|
||||||
|
</Typography.Text>
|
||||||
|
<Tag color={row.workerType === 'internal' ? 'blue' : 'default'}>
|
||||||
|
{row.workerType === 'internal' ? '内部' : '外部'}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
<Typography.Text type="secondary">{row.username}</Typography.Text>
|
<Typography.Text type="secondary">{row.username}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -227,6 +256,11 @@ export default function WorkersPanel() {
|
|||||||
{row.status !== 'rejected' ? (
|
{row.status !== 'rejected' ? (
|
||||||
<Button onClick={() => review(row, 'rejected')}>拒绝</Button>
|
<Button onClick={() => review(row, 'rejected')}>拒绝</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{row.workerType !== 'internal' ? (
|
||||||
|
<Button onClick={() => toggleWorkerType(row, 'internal')}>设为内部</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={() => toggleWorkerType(row, 'external')}>设为外部</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLevelWorker(row)
|
setLevelWorker(row)
|
||||||
|
|||||||
@@ -456,6 +456,9 @@ export default function WorkerProfilePage() {
|
|||||||
<Tag color={resolveWorkerStatusColor(worker.status)}>
|
<Tag color={resolveWorkerStatusColor(worker.status)}>
|
||||||
{formatWorkerStatus(worker.status)}
|
{formatWorkerStatus(worker.status)}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
<Tag color={worker.workerType === 'internal' ? 'blue' : 'default'}>
|
||||||
|
{worker.workerType === 'internal' ? '内部打手' : '外部打手'}
|
||||||
|
</Tag>
|
||||||
<Tag>{worker.level?.name || '未分级'}</Tag>
|
<Tag>{worker.level?.name || '未分级'}</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function deleteAdminWorkProductRule(ruleId: number) {
|
|||||||
|
|
||||||
export function reviewAdminWorkerUser(
|
export function reviewAdminWorkerUser(
|
||||||
workerId: number,
|
workerId: number,
|
||||||
payload: { status: string; reviewNote?: string; levelId?: number },
|
payload: { status: string; reviewNote?: string; levelId?: number; workerType?: string },
|
||||||
) {
|
) {
|
||||||
return apiPost<{ worker: WorkerUser }>(
|
return apiPost<{ worker: WorkerUser }>(
|
||||||
`/api/v1/admin/worker-platform/workers/${workerId}/review`,
|
`/api/v1/admin/worker-platform/workers/${workerId}/review`,
|
||||||
@@ -132,6 +132,16 @@ export function reviewAdminWorkerUser(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assignAdminWorkOrderToWorker(
|
||||||
|
workOrderId: number,
|
||||||
|
payload: { workerId: number },
|
||||||
|
) {
|
||||||
|
return apiPost<{ order: WorkOrder }>(
|
||||||
|
`/api/v1/admin/worker-platform/orders/${workOrderId}/assign`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function creditAdminWorkerWallet(
|
export function creditAdminWorkerWallet(
|
||||||
workerId: number,
|
workerId: number,
|
||||||
payload: { amount: number; note?: string },
|
payload: { amount: number; note?: string },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type WorkerUser = {
|
|||||||
displayName: string
|
displayName: string
|
||||||
phone: string
|
phone: string
|
||||||
status: string
|
status: string
|
||||||
|
workerType: string
|
||||||
reviewNote: string
|
reviewNote: string
|
||||||
inviteCode: string
|
inviteCode: string
|
||||||
inviter: null | {
|
inviter: null | {
|
||||||
|
|||||||
Reference in New Issue
Block a user