指派工单时自动核销电子凭证
This commit is contained in:
@@ -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('订单状态已变化,无法指派', {
|
||||
throw createHttpError(
|
||||
resolveAssignFailureMessage(voucherConsume.voucherCount, '订单状态已变化,无法指派'),
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_not_assignable',
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
if (failureReason === 'worker_active_order_limit') {
|
||||
throw createHttpError('该打手已达到最大同时接单量', {
|
||||
throw createHttpError(
|
||||
resolveAssignFailureMessage(voucherConsume.voucherCount, '该打手已达到最大同时接单量'),
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_active_order_limit',
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
if (failureReason === 'worker_deposit_insufficient') {
|
||||
throw createHttpError('该打手余额不足,无法冻结所需押金', {
|
||||
throw createHttpError(
|
||||
resolveAssignFailureMessage(voucherConsume.voucherCount, '该打手余额不足,无法冻结所需押金'),
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_deposit_insufficient',
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
if (!updated) {
|
||||
throw createHttpError('指派失败,订单状态可能已变化', {
|
||||
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`,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user