feat: 打手分内部/外部,内部免审核;支持后台指派打手
- worker_users 新增 worker_type(internal/external,迁移 017) - 内部打手提交验收后自动通过进入待打款(accepted+结算),外部打手保持后台审核 - 打手管理页支持设为内部/外部并显示类型标签 - 新增后台指派打手:未分配/待抢单工单可指定已注册打手,进入其我的订单并冻结押金 - 指派带审计与事件记录,校验打手状态/接单上限/余额押金
This commit is contained in:
@@ -5,6 +5,7 @@ import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import {
|
||||
acceptWorkOrderAndSettle,
|
||||
addWorkerWalletCredit,
|
||||
assignWorkOrderToWorker,
|
||||
countWorkerAcceptedOrders,
|
||||
countWorkerActiveOrders,
|
||||
countWorkCategoryUsages,
|
||||
@@ -88,7 +89,7 @@ import {
|
||||
saveWorkerFinanceConfig,
|
||||
} 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'
|
||||
|
||||
export async function listAdminWorkerLevels() {
|
||||
@@ -346,6 +347,7 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) {
|
||||
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
|
||||
const worker = await getRequiredWorker(workerId)
|
||||
const status = normalizeReviewStatus(payload.status)
|
||||
const workerType = normalizeWorkerType(payload.workerType ?? payload.worker_type)
|
||||
const requestedLevelId = normalizeOptionalId(payload.levelId)
|
||||
let levelId = worker.level_id || null
|
||||
if (requestedLevelId) {
|
||||
@@ -364,6 +366,7 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
|
||||
const now = nowIso()
|
||||
const updated = await updateWorkerUser(worker.id, {
|
||||
status,
|
||||
worker_type: workerType,
|
||||
level_id: levelId,
|
||||
review_note: String(payload.reviewNote || payload.note || '').trim(),
|
||||
session_version: normalizeSessionVersion(worker.session_version) + 1,
|
||||
@@ -373,6 +376,91 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
|
||||
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 = {}) {
|
||||
await getRequiredWorker(workerId)
|
||||
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) {
|
||||
return {
|
||||
workerId: Number(worker.id),
|
||||
@@ -265,6 +269,7 @@ export function mapWorkerUser(worker: WorkerUserRow) {
|
||||
displayName: worker.display_name || worker.username,
|
||||
phone: worker.phone,
|
||||
status: worker.status,
|
||||
workerType: normalizeWorkerType(worker.worker_type),
|
||||
reviewNote: worker.review_note || '',
|
||||
inviteCode: worker.invite_code || '',
|
||||
inviter: worker.inviter_id
|
||||
|
||||
@@ -1144,10 +1144,43 @@ export async function submitWorkerOrderAcceptance(
|
||||
payloadJson: JSON.stringify(acceptance),
|
||||
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 {
|
||||
order: mapWorkOrderForWorker(
|
||||
updated || workOrder,
|
||||
resolveWorkerPermissions(await getRequiredWorker(session.workerId)),
|
||||
resolveWorkerPermissions(worker),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user