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

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(),
})
}
@@ -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) ? (
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
</Button>
) : 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}
>
<Space direction="vertical" size={12} className="full-width">
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<Select
style={{ width: '100%' }}
@@ -12,6 +12,16 @@ import type {
WorkerUser,
} from '@/types/worker-platform'
type WorkOrderVoucherConsumeResult = {
ok: boolean
voucherCount: number
consumedCount: number
alreadyConsumedCount: number
failedCount: number
voucherCodes: string[]
errorMessage: string
}
export function fetchAdminWorkerPlatformSummary() {
return apiGet<{
pendingWorkerCount: number
@@ -137,7 +147,10 @@ export function assignAdminWorkOrderToWorker(
workOrderId: number,
payload: { workerId: number },
) {
return apiPost<{ order: WorkOrder }>(
return apiPost<{
order: WorkOrder
voucherConsume: WorkOrderVoucherConsumeResult
}>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/assign`,
payload,
)
@@ -249,15 +262,7 @@ export function submitAdminWorkOrderMaterial(
export function publishAdminWorkOrder(workOrderId: number) {
return apiPost<{
order: WorkOrder
voucherConsume: {
ok: boolean
voucherCount: number
consumedCount: number
alreadyConsumedCount: number
failedCount: number
voucherCodes: string[]
errorMessage: string
}
voucherConsume: WorkOrderVoucherConsumeResult
}>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/publish`,
)