新增打手撤单审核

This commit is contained in:
yml2213
2026-08-20 19:43:03 +08:00
parent d76d1e8d90
commit e504b3a207
20 changed files with 1331 additions and 8 deletions
@@ -96,6 +96,32 @@ export async function createWorkerAcceptanceAdminNotification(input: {
})
}
export async function createWorkerCancelRequestAdminNotification(input: {
requestId: number
workOrderId: number
workOrderNo: string
productName: string
workerName: string
reason: string
}) {
return createConfiguredWorkerPlatformNotification('cancel_requested', {
notificationType: 'worker_cancel_requested',
priority: 'high',
entityType: 'worker_cancel_request',
entityId: input.requestId,
dedupeKey: `worker_cancel_request:${input.requestId}`,
title: '有打手撤单申请待审核',
body: [
`订单:${input.workOrderNo || input.workOrderId}`,
`商品:${input.productName || '-'}`,
`打手:${input.workerName || '-'}`,
`原因:${input.reason || '-'}`,
].join(''),
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=cancel-requests&cancelRequestId=${input.requestId}`,
now: new Date().toISOString(),
})
}
export async function remindWorkerAcceptanceAdminNotification(workOrderId: number) {
const now = new Date().toISOString()
const eventConfig = getWorkerPlatformNotificationConfig().events.acceptance_reminded
@@ -50,6 +50,8 @@ import {
resolveProblemWorkOrder,
reviewWorkerFinanceRequest,
reopenCancelledWorkOrder,
listAdminWorkerCancelRequests as listWorkerCancelRequestsForAdmin,
reviewWorkerCancelRequest,
updateWorkOrder,
updateWorkOrderBasic,
updateWorkerUser,
@@ -108,6 +110,7 @@ import {
mapWorkProductRule,
mapWorkerLevel,
mapWorkerUser,
mapWorkerCancelRequest,
normalizeAdminFinanceReviewStatus,
normalizeAmountFen,
normalizeBoolean,
@@ -1355,6 +1358,74 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
}
}
export async function listAdminWorkerCancelRequests(query: JsonObject = {}) {
const status = String(query.status || 'pending').trim()
const normalizedStatus = ['pending', 'approved', 'rejected'].includes(status) ? status : 'pending'
return {
items: (await listWorkerCancelRequestsForAdmin(normalizedStatus)).map(mapWorkerCancelRequest),
}
}
export async function reviewAdminWorkerCancelRequest(
requestId: number | string,
payload: JsonObject = {},
actorName = '',
) {
const approved = payload.approved === true || String(payload.action || '').trim() === 'approve'
const reviewNote = String(payload.reviewNote || payload.note || '').trim()
if (!reviewNote) {
throw createHttpError('请填写审核备注', {
statusCode: 400,
errorCode: 'worker_cancel_request_review_note_required',
})
}
const result = await reviewWorkerCancelRequest({
requestId: Number(requestId),
approved,
reviewNote,
actorName,
now: nowIso(),
})
if (result.failureReason === 'request_not_found') {
throw createHttpError('撤单申请不存在', {
statusCode: 404,
errorCode: 'worker_cancel_request_not_found',
})
}
if (result.failureReason === 'request_not_pending') {
throw createHttpError('该撤单申请已审核,请刷新后重试', {
statusCode: 409,
errorCode: 'worker_cancel_request_not_pending',
})
}
if (!result.request || !result.order) {
throw createHttpError('订单状态已变化,无法审核该撤单申请', {
statusCode: 409,
errorCode: 'worker_cancel_request_target_changed',
})
}
await resolveAdminNotificationEntity(
'worker_cancel_request',
Number(result.request.id),
approved ? 'approved' : 'rejected',
)
const workerId = Number(result.request.worker_id)
publishWorkOrderRealtimeChange({
workOrderId: Number(result.order.id),
workerIds: workerId > 0 ? [workerId] : [],
hallChanged: approved,
})
if (approved && workerId > 0) publishWorkerWalletRealtimeChange(workerId)
return {
request: mapWorkerCancelRequest(result.request),
order: mapWorkOrderAdmin(result.order),
approved,
releasedDepositAmount: result.releasedDepositAmount,
levelChanged: result.levelChanged,
accountDisabled: result.accountDisabled,
}
}
export async function deductAdminWorkOrderPendingDeposit(
workOrderId: number | string,
payload: JsonObject = {},
@@ -257,6 +257,41 @@ export function mapWorkerUser(worker: WorkerUserRow) {
}
}
export function mapWorkerCancelRequest(request: {
id: number
work_order_id: number
work_order_share_id: number | null
worker_id: number
reason: string
status: string
review_note: string
reviewed_by: string
created_at: string
reviewed_at: string | null
work_order_no?: string
product_name?: string
worker_username?: string
worker_display_name?: string
share_quantity?: number | null
}) {
return {
requestId: Number(request.id),
workOrderId: Number(request.work_order_id),
shareId: request.work_order_share_id ? Number(request.work_order_share_id) : null,
workerId: Number(request.worker_id),
workOrderNo: String(request.work_order_no || ''),
productName: String(request.product_name || ''),
workerName: String(request.worker_display_name || request.worker_username || ''),
shareQuantity: request.share_quantity ? Number(request.share_quantity) : null,
reason: String(request.reason || ''),
status: String(request.status || ''),
reviewNote: String(request.review_note || ''),
reviewedBy: String(request.reviewed_by || ''),
createdAt: request.created_at,
reviewedAt: request.reviewed_at,
}
}
export function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined) {
if (!ledger) return null
return {
@@ -8,6 +8,7 @@ export const WORKER_PLATFORM_NOTIFICATION_EVENT_KEYS = [
'acceptance_submitted',
'acceptance_reminded',
'material_required',
'cancel_requested',
] as const
export type WorkerPlatformNotificationEventKey =
@@ -51,6 +52,7 @@ export function createDefaultWorkerPlatformNotificationConfig(): WorkerPlatformN
soundEnabled: false,
externalPushEnabled: false,
},
cancel_requested: createEnabledEventConfig(),
},
}
}
@@ -6,6 +6,7 @@ import {
acceptWorkOrderAndSettle,
acceptWorkOrderShareAndSettle,
countWorkerAcceptedOrders,
createWorkerCancelRequest,
countWorkerActiveOrders,
countWorkerTimeoutEvents,
countWorkerWithdrawRequestsOnDay,
@@ -42,6 +43,7 @@ import {
listPendingMaterialWorkOrdersByPlatformOrderId,
listWorkerFinanceRequests,
listWorkerCompletionLeaderboard,
listWorkerCancelRequests,
listWorkerSharesByWorker,
listWorkerWalletLedgers,
listWorkerWorkOrderNotes,
@@ -53,6 +55,7 @@ import {
listWorkOrderShares,
listWorkOrderSharesByOrderIds,
listWorkerLevels,
getWorkerCancelRequestRisk,
listDueDepositUnfreezes,
listOverdueWorkOrders,
releaseDepositUnfreeze,
@@ -82,6 +85,7 @@ import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
import {
createWorkerAcceptanceAdminNotification,
createWorkerCancelRequestAdminNotification,
createWorkerRechargeAdminNotification,
createWorkerWithdrawAdminNotification,
getWorkerAcceptanceReminder,
@@ -111,6 +115,7 @@ import {
mapWorkOrderPublic,
mapWorkOrderEvents,
mapWorkOrderShare,
mapWorkerCancelRequest,
mapWorkerUser,
normalizeAmountFen,
assertWithdrawChannelAllowedForAmount,
@@ -1738,6 +1743,82 @@ export async function getWorkerMyOrderEvents(workOrderId: number | string, sessi
return { items: await mapWorkOrderEvents(events) }
}
export async function getWorkerCancelRequestSummary(session: WorkerSession) {
requireActiveWorkerSession(session)
const [risk, requests] = await Promise.all([
getWorkerCancelRequestRisk(session.workerId, nowIso()),
listWorkerCancelRequests(session.workerId),
])
return { risk, items: requests.map(mapWorkerCancelRequest) }
}
export async function submitWorkerCancelRequest(
workOrderId: number | string,
payload: JsonObject = {},
session: WorkerSession,
) {
requireActiveWorkerSession(session)
const reason = String(payload.reason || '').trim()
if (!reason) {
throw createHttpError('请填写申请取消原因', {
statusCode: 400,
errorCode: 'worker_cancel_request_reason_required',
})
}
if (reason.length > 300) {
throw createHttpError('申请取消原因不能超过 300 个字符', {
statusCode: 400,
errorCode: 'worker_cancel_request_reason_too_long',
})
}
const now = nowIso()
const result = await createWorkerCancelRequest({
workOrderId: Number(workOrderId),
workerId: Number(session.workerId),
reason,
now,
})
if (result.failureReason === 'daily_limit_reached') {
throw createHttpError('今日最多申请取消 5 次,请明日再试', {
statusCode: 429,
errorCode: 'worker_cancel_request_daily_limit',
})
}
if (result.failureReason === 'request_pending') {
throw createHttpError('该订单已有待审核的取消申请', {
statusCode: 409,
errorCode: 'worker_cancel_request_pending',
})
}
if (result.failureReason === 'share_not_cancellable') {
throw createHttpError('当前拼单份额不能申请取消', {
statusCode: 409,
errorCode: 'worker_cancel_request_share_status_invalid',
})
}
if (!result.request) {
throw createHttpError('当前订单不能申请取消', {
statusCode: 409,
errorCode: 'worker_cancel_request_status_invalid',
})
}
const worker = await getRequiredWorker(session.workerId)
await createWorkerCancelRequestAdminNotification({
requestId: Number(result.request.id),
workOrderId: Number(result.request.work_order_id),
workOrderNo: result.request.work_order_no || '',
productName: result.request.product_name || '',
workerName: worker.display_name || worker.username,
reason,
})
const risk = await getWorkerCancelRequestRisk(session.workerId, now)
publishWorkOrderRealtimeChange({
workOrderId: Number(result.request.work_order_id),
workerIds: [Number(session.workerId)],
})
return { request: mapWorkerCancelRequest(result.request), risk }
}
export async function saveWorkerOrderNote(
workOrderId: number | string,
payload: JsonObject = {},