指派工单时自动核销电子凭证

This commit is contained in:
yml2213
2026-08-16 16:59:02 +08:00
parent e641064b63
commit 8705f6a6a1
5 changed files with 164 additions and 37 deletions
@@ -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 {
@@ -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, '指派前核销失败')
})
@@ -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<WorkOrderVoucherConsumeResult> {
return consumeIndustryVouchersBeforeWorkOrderAction(workOrder, 'work_order_publish', deps)
}
export async function consumeIndustryVouchersBeforeWorkOrderAssign(
workOrder: WorkOrderRow,
deps: WorkOrderVoucherServiceDeps = defaultDeps,
): Promise<WorkOrderVoucherConsumeResult> {
return consumeIndustryVouchersBeforeWorkOrderAction(workOrder, 'work_order_assign', deps)
}
async function consumeIndustryVouchersBeforeWorkOrderAction(
workOrder: WorkOrderRow,
source: 'work_order_publish' | 'work_order_assign',
deps: WorkOrderVoucherServiceDeps,
): Promise<WorkOrderVoucherConsumeResult> {
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(),
})
}