From 8705f6a6a1757d50f829d034fcf22c4f72f058d4 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 16 Aug 2026 16:59:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=87=E6=B4=BE=E5=B7=A5=E5=8D=95=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=A0=B8=E9=94=80=E7=94=B5=E5=AD=90=E5=87=AD?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/worker-platform/admin-service.ts | 75 ++++++++++++++----- .../publish-voucher-service.test.ts | 65 ++++++++++++++++ .../publish-voucher-service.ts | 25 +++++-- .../pages/admin/panels/WorkOrdersPanel.tsx | 11 ++- .../src/services/admin/worker-platform.ts | 25 ++++--- 5 files changed, 164 insertions(+), 37 deletions(-) diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 8314e40a..715f6cf4 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -91,7 +91,10 @@ import { getWorkerFinanceConfig, saveWorkerFinanceConfig, } from './worker-finance-config-service.js' -import { consumeIndustryVouchersBeforeWorkOrderPublish } from './publish-voucher-service.js' +import { + consumeIndustryVouchersBeforeWorkOrderAssign, + consumeIndustryVouchersBeforeWorkOrderPublish, +} from './publish-voucher-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, normalizeStatuses, normalizeSubmittedFields, normalizeUploadedFiles, normalizeWorkerType, resolveFreezeDepositAmount, resolveMatchingProductRule, resolveRequirementFields, resolveSkuNameQuantity, resolveWorkerPermissions } from './mappers.js' import { ensureWorkerPlatformDefaults, getRequiredWorkOrder, getRequiredWorker, normalizeWorkOrderTimeoutPolicy } from './worker-service.js' @@ -425,6 +428,26 @@ export async function assignAdminWorkOrderToWorker( }) } + const voucherConsume = await consumeIndustryVouchersBeforeWorkOrderAssign(workOrder) + if (!voucherConsume.ok) { + const failedAt = nowIso() + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'admin', + actorId: '', + eventType: 'assign_voucher_consume_failed', + fromStatus: workOrder.status, + toStatus: workOrder.status, + payloadJson: JSON.stringify({ workerId: worker.id, voucherConsume }), + now: failedAt, + }) + throw createHttpError(voucherConsume.errorMessage || '电子凭证核销失败,订单未指派', { + statusCode: 409, + errorCode: 'work_order_assign_voucher_consume_failed', + context: voucherConsume, + }) + } + const now = nowIso() const { order: updated, failureReason } = await assignWorkOrderToWorker({ workOrderId: workOrder.id, @@ -435,30 +458,46 @@ export async function assignAdminWorkOrderToWorker( now, }) if (failureReason === 'work_order_not_assignable') { - throw createHttpError('订单状态已变化,无法指派', { - statusCode: 409, - errorCode: 'work_order_not_assignable', - }) + throw createHttpError( + resolveAssignFailureMessage(voucherConsume.voucherCount, '订单状态已变化,无法指派'), + { + statusCode: 409, + errorCode: 'work_order_not_assignable', + }, + ) } if (failureReason === 'worker_active_order_limit') { - throw createHttpError('该打手已达到最大同时接单量', { - statusCode: 409, - errorCode: 'worker_active_order_limit', - }) + throw createHttpError( + resolveAssignFailureMessage(voucherConsume.voucherCount, '该打手已达到最大同时接单量'), + { + statusCode: 409, + errorCode: 'worker_active_order_limit', + }, + ) } if (failureReason === 'worker_deposit_insufficient') { - throw createHttpError('该打手余额不足,无法冻结所需押金', { - statusCode: 409, - errorCode: 'worker_deposit_insufficient', - }) + throw createHttpError( + resolveAssignFailureMessage(voucherConsume.voucherCount, '该打手余额不足,无法冻结所需押金'), + { + statusCode: 409, + errorCode: 'worker_deposit_insufficient', + }, + ) } if (!updated) { - throw createHttpError('指派失败,订单状态可能已变化', { - statusCode: 409, - errorCode: 'work_order_assign_conflict', - }) + throw createHttpError( + resolveAssignFailureMessage(voucherConsume.voucherCount, '指派失败,订单状态可能已变化'), + { + statusCode: 409, + errorCode: 'work_order_assign_conflict', + }, + ) } - return { order: mapWorkOrderAdmin(updated) } + return { order: mapWorkOrderAdmin(updated), voucherConsume } +} + +function resolveAssignFailureMessage(voucherCount: number, fallback: string) { + return voucherCount > 0 ? `电子凭证已核销,但${fallback},请重试指派` : fallback } function resolveAssignedDeadlineAt(timeoutMinutes: number, now: string): string | null { diff --git a/apps/backend/src/services/worker-platform/publish-voucher-service.test.ts b/apps/backend/src/services/worker-platform/publish-voucher-service.test.ts index 6d9bbffa..a5234b4a 100644 --- a/apps/backend/src/services/worker-platform/publish-voucher-service.test.ts +++ b/apps/backend/src/services/worker-platform/publish-voucher-service.test.ts @@ -4,6 +4,7 @@ import test from 'node:test' import type { WorkOrderRow } from '../../repositories/worker-platform/index.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js' import { + consumeIndustryVouchersBeforeWorkOrderAssign, consumeIndustryVouchersBeforeWorkOrderPublish, resolveWorkOrderVoucherTargets, } from './publish-voucher-service.js' @@ -223,3 +224,67 @@ test('已核销券码按幂等成功处理', async () => { assert.equal(result.consumedCount, 0) assert.equal(result.alreadyConsumedCount, 1) }) + +test('指派前核销使用独立来源并同步关联任务状态', async () => { + const task = buildTask(100, 20) + const voucher = buildVoucher(1, 100) + const consumeSources: string[] = [] + const attachSources: string[] = [] + + const result = await consumeIndustryVouchersBeforeWorkOrderAssign(buildWorkOrder(), { + listVouchersByOid: async () => [voucher], + listTasksByOrderId: async () => [task], + consumeVoucher: async (currentVoucher, options) => { + consumeSources.push(String(options.source || '')) + return { + ok: true, + voucher: { ...currentVoucher, status: 'CONSUMED' }, + callbackSuccess: true, + } + }, + attachVoucherToTask: async (currentTask, _currentVoucher, options) => { + attachSources.push(String(options.source || '')) + return currentTask + }, + }) + + assert.equal(result.ok, true) + assert.deepEqual(consumeSources, ['work_order_assign']) + assert.deepEqual(attachSources, ['work_order_assign']) +}) + +test('指派前没有关联电子凭证时直接成功', async () => { + let consumeCalled = false + const result = await consumeIndustryVouchersBeforeWorkOrderAssign(buildWorkOrder(), { + listVouchersByOid: async () => [], + listTasksByOrderId: async () => [], + consumeVoucher: async (voucher) => { + consumeCalled = true + return { ok: true, voucher, callbackSuccess: true } + }, + attachVoucherToTask: async (task) => task, + }) + + assert.equal(result.ok, true) + assert.equal(result.voucherCount, 0) + assert.equal(consumeCalled, false) +}) + +test('指派前电子凭证核销失败时返回失败结果', async () => { + const voucher = buildVoucher(1, null) + const result = await consumeIndustryVouchersBeforeWorkOrderAssign(buildWorkOrder(), { + listVouchersByOid: async () => [voucher], + listTasksByOrderId: async () => [], + consumeVoucher: async () => ({ + ok: false, + voucher, + callbackSuccess: false, + errorMessage: '指派前核销失败', + }), + attachVoucherToTask: async (task) => task, + }) + + assert.equal(result.ok, false) + assert.equal(result.failedCount, 1) + assert.equal(result.errorMessage, '指派前核销失败') +}) diff --git a/apps/backend/src/services/worker-platform/publish-voucher-service.ts b/apps/backend/src/services/worker-platform/publish-voucher-service.ts index b021a393..5c43cd47 100644 --- a/apps/backend/src/services/worker-platform/publish-voucher-service.ts +++ b/apps/backend/src/services/worker-platform/publish-voucher-service.ts @@ -15,14 +15,14 @@ export type WorkOrderVoucherConsumeResult = { errorMessage: string } -type PublishVoucherServiceDeps = { +type WorkOrderVoucherServiceDeps = { listVouchersByOid: typeof listKuaishouIndustryVouchersByOid listTasksByOrderId: typeof listTasksByOrderId consumeVoucher: typeof consumeKuaishouIndustryVoucher attachVoucherToTask: typeof attachKuaishouIndustryVoucherToTask } -const defaultDeps: PublishVoucherServiceDeps = { +const defaultDeps: WorkOrderVoucherServiceDeps = { listVouchersByOid: listKuaishouIndustryVouchersByOid, listTasksByOrderId, consumeVoucher: consumeKuaishouIndustryVoucher, @@ -31,7 +31,22 @@ const defaultDeps: PublishVoucherServiceDeps = { export async function consumeIndustryVouchersBeforeWorkOrderPublish( workOrder: WorkOrderRow, - deps: PublishVoucherServiceDeps = defaultDeps, + deps: WorkOrderVoucherServiceDeps = defaultDeps, +): Promise { + return consumeIndustryVouchersBeforeWorkOrderAction(workOrder, 'work_order_publish', deps) +} + +export async function consumeIndustryVouchersBeforeWorkOrderAssign( + workOrder: WorkOrderRow, + deps: WorkOrderVoucherServiceDeps = defaultDeps, +): Promise { + return consumeIndustryVouchersBeforeWorkOrderAction(workOrder, 'work_order_assign', deps) +} + +async function consumeIndustryVouchersBeforeWorkOrderAction( + workOrder: WorkOrderRow, + source: 'work_order_publish' | 'work_order_assign', + deps: WorkOrderVoucherServiceDeps, ): Promise { const oid = String(workOrder.platform_order_id || '').trim() if (!oid) { @@ -53,7 +68,7 @@ export async function consumeIndustryVouchersBeforeWorkOrderPublish( try { consumeResult = await deps.consumeVoucher(target.voucher, { - source: 'work_order_publish', + source, consumeTime: Date.now(), ...(target.task ? { task: target.task } : {}), }) @@ -82,7 +97,7 @@ export async function consumeIndustryVouchersBeforeWorkOrderPublish( try { if (target.task) { await deps.attachVoucherToTask(target.task, consumeResult.voucher, { - source: 'work_order_publish', + source, now: new Date().toISOString(), }) } diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 712ccebd..215e372a 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -275,7 +275,10 @@ export default function WorkOrdersPanel() { assignAdminWorkOrderToWorker(assignOrder.workOrderId, { workerId: assignWorkerId, }), - '已指派打手', + (response) => + response.data.voucherConsume.voucherCount > 0 + ? '电子凭证已自动核销,订单已指派给打手' + : '已指派打手', ) if (!succeeded) return setAssignOrder(null) @@ -810,7 +813,7 @@ export default function WorkOrdersPanel() { ) : null} {['unassigned', 'open'].includes(row.status) ? ( ) : null} {row.status === 'in_progress' && row.worker ? ( @@ -1567,14 +1570,14 @@ export default function WorkOrdersPanel() { setAssignWorkerId(undefined) }} onOk={() => submitAssign()} - okText="确认指派" + okText="指派并核销" okButtonProps={{ disabled: !assignWorkerId }} destroyOnHidden width={520} > - 指派后订单直接进入该打手的「我的订单」,不再出现在抢单大厅;所需押金按订单规则从打手余额冻结。 + 指派后订单直接进入该打手的「我的订单」,不再出现在抢单大厅;有关联电子凭证时会先自动核销,再按订单规则冻结打手押金。核销后取消指派不会恢复电子凭证。