指派工单时自动核销电子凭证
This commit is contained in:
@@ -91,7 +91,10 @@ import {
|
|||||||
getWorkerFinanceConfig,
|
getWorkerFinanceConfig,
|
||||||
saveWorkerFinanceConfig,
|
saveWorkerFinanceConfig,
|
||||||
} from './worker-finance-config-service.js'
|
} 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 { 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'
|
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 now = nowIso()
|
||||||
const { order: updated, failureReason } = await assignWorkOrderToWorker({
|
const { order: updated, failureReason } = await assignWorkOrderToWorker({
|
||||||
workOrderId: workOrder.id,
|
workOrderId: workOrder.id,
|
||||||
@@ -435,30 +458,46 @@ export async function assignAdminWorkOrderToWorker(
|
|||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
if (failureReason === 'work_order_not_assignable') {
|
if (failureReason === 'work_order_not_assignable') {
|
||||||
throw createHttpError('订单状态已变化,无法指派', {
|
throw createHttpError(
|
||||||
statusCode: 409,
|
resolveAssignFailureMessage(voucherConsume.voucherCount, '订单状态已变化,无法指派'),
|
||||||
errorCode: 'work_order_not_assignable',
|
{
|
||||||
})
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_not_assignable',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (failureReason === 'worker_active_order_limit') {
|
if (failureReason === 'worker_active_order_limit') {
|
||||||
throw createHttpError('该打手已达到最大同时接单量', {
|
throw createHttpError(
|
||||||
statusCode: 409,
|
resolveAssignFailureMessage(voucherConsume.voucherCount, '该打手已达到最大同时接单量'),
|
||||||
errorCode: 'worker_active_order_limit',
|
{
|
||||||
})
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_active_order_limit',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (failureReason === 'worker_deposit_insufficient') {
|
if (failureReason === 'worker_deposit_insufficient') {
|
||||||
throw createHttpError('该打手余额不足,无法冻结所需押金', {
|
throw createHttpError(
|
||||||
statusCode: 409,
|
resolveAssignFailureMessage(voucherConsume.voucherCount, '该打手余额不足,无法冻结所需押金'),
|
||||||
errorCode: 'worker_deposit_insufficient',
|
{
|
||||||
})
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_deposit_insufficient',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
throw createHttpError('指派失败,订单状态可能已变化', {
|
throw createHttpError(
|
||||||
statusCode: 409,
|
resolveAssignFailureMessage(voucherConsume.voucherCount, '指派失败,订单状态可能已变化'),
|
||||||
errorCode: 'work_order_assign_conflict',
|
{
|
||||||
})
|
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 {
|
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 { WorkOrderRow } from '../../repositories/worker-platform/index.js'
|
||||||
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
import {
|
import {
|
||||||
|
consumeIndustryVouchersBeforeWorkOrderAssign,
|
||||||
consumeIndustryVouchersBeforeWorkOrderPublish,
|
consumeIndustryVouchersBeforeWorkOrderPublish,
|
||||||
resolveWorkOrderVoucherTargets,
|
resolveWorkOrderVoucherTargets,
|
||||||
} from './publish-voucher-service.js'
|
} from './publish-voucher-service.js'
|
||||||
@@ -223,3 +224,67 @@ test('已核销券码按幂等成功处理', async () => {
|
|||||||
assert.equal(result.consumedCount, 0)
|
assert.equal(result.consumedCount, 0)
|
||||||
assert.equal(result.alreadyConsumedCount, 1)
|
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
|
errorMessage: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublishVoucherServiceDeps = {
|
type WorkOrderVoucherServiceDeps = {
|
||||||
listVouchersByOid: typeof listKuaishouIndustryVouchersByOid
|
listVouchersByOid: typeof listKuaishouIndustryVouchersByOid
|
||||||
listTasksByOrderId: typeof listTasksByOrderId
|
listTasksByOrderId: typeof listTasksByOrderId
|
||||||
consumeVoucher: typeof consumeKuaishouIndustryVoucher
|
consumeVoucher: typeof consumeKuaishouIndustryVoucher
|
||||||
attachVoucherToTask: typeof attachKuaishouIndustryVoucherToTask
|
attachVoucherToTask: typeof attachKuaishouIndustryVoucherToTask
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultDeps: PublishVoucherServiceDeps = {
|
const defaultDeps: WorkOrderVoucherServiceDeps = {
|
||||||
listVouchersByOid: listKuaishouIndustryVouchersByOid,
|
listVouchersByOid: listKuaishouIndustryVouchersByOid,
|
||||||
listTasksByOrderId,
|
listTasksByOrderId,
|
||||||
consumeVoucher: consumeKuaishouIndustryVoucher,
|
consumeVoucher: consumeKuaishouIndustryVoucher,
|
||||||
@@ -31,7 +31,22 @@ const defaultDeps: PublishVoucherServiceDeps = {
|
|||||||
|
|
||||||
export async function consumeIndustryVouchersBeforeWorkOrderPublish(
|
export async function consumeIndustryVouchersBeforeWorkOrderPublish(
|
||||||
workOrder: WorkOrderRow,
|
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> {
|
): Promise<WorkOrderVoucherConsumeResult> {
|
||||||
const oid = String(workOrder.platform_order_id || '').trim()
|
const oid = String(workOrder.platform_order_id || '').trim()
|
||||||
if (!oid) {
|
if (!oid) {
|
||||||
@@ -53,7 +68,7 @@ export async function consumeIndustryVouchersBeforeWorkOrderPublish(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
consumeResult = await deps.consumeVoucher(target.voucher, {
|
consumeResult = await deps.consumeVoucher(target.voucher, {
|
||||||
source: 'work_order_publish',
|
source,
|
||||||
consumeTime: Date.now(),
|
consumeTime: Date.now(),
|
||||||
...(target.task ? { task: target.task } : {}),
|
...(target.task ? { task: target.task } : {}),
|
||||||
})
|
})
|
||||||
@@ -82,7 +97,7 @@ export async function consumeIndustryVouchersBeforeWorkOrderPublish(
|
|||||||
try {
|
try {
|
||||||
if (target.task) {
|
if (target.task) {
|
||||||
await deps.attachVoucherToTask(target.task, consumeResult.voucher, {
|
await deps.attachVoucherToTask(target.task, consumeResult.voucher, {
|
||||||
source: 'work_order_publish',
|
source,
|
||||||
now: new Date().toISOString(),
|
now: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,7 +275,10 @@ export default function WorkOrdersPanel() {
|
|||||||
assignAdminWorkOrderToWorker(assignOrder.workOrderId, {
|
assignAdminWorkOrderToWorker(assignOrder.workOrderId, {
|
||||||
workerId: assignWorkerId,
|
workerId: assignWorkerId,
|
||||||
}),
|
}),
|
||||||
'已指派打手',
|
(response) =>
|
||||||
|
response.data.voucherConsume.voucherCount > 0
|
||||||
|
? '电子凭证已自动核销,订单已指派给打手'
|
||||||
|
: '已指派打手',
|
||||||
)
|
)
|
||||||
if (!succeeded) return
|
if (!succeeded) return
|
||||||
setAssignOrder(null)
|
setAssignOrder(null)
|
||||||
@@ -810,7 +813,7 @@ export default function WorkOrdersPanel() {
|
|||||||
) : null}
|
) : null}
|
||||||
{['unassigned', 'open'].includes(row.status) ? (
|
{['unassigned', 'open'].includes(row.status) ? (
|
||||||
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
|
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
|
||||||
指派
|
指派并核销
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.status === 'in_progress' && row.worker ? (
|
{row.status === 'in_progress' && row.worker ? (
|
||||||
@@ -1567,14 +1570,14 @@ export default function WorkOrdersPanel() {
|
|||||||
setAssignWorkerId(undefined)
|
setAssignWorkerId(undefined)
|
||||||
}}
|
}}
|
||||||
onOk={() => submitAssign()}
|
onOk={() => submitAssign()}
|
||||||
okText="确认指派"
|
okText="指派并核销"
|
||||||
okButtonProps={{ disabled: !assignWorkerId }}
|
okButtonProps={{ disabled: !assignWorkerId }}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
width={520}
|
width={520}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" size={12} className="full-width">
|
<Space direction="vertical" size={12} className="full-width">
|
||||||
<Typography.Paragraph type="secondary">
|
<Typography.Paragraph type="secondary">
|
||||||
指派后订单直接进入该打手的「我的订单」,不再出现在抢单大厅;所需押金按订单规则从打手余额冻结。
|
指派后订单直接进入该打手的「我的订单」,不再出现在抢单大厅;有关联电子凭证时会先自动核销,再按订单规则冻结打手押金。核销后取消指派不会恢复电子凭证。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ import type {
|
|||||||
WorkerUser,
|
WorkerUser,
|
||||||
} from '@/types/worker-platform'
|
} from '@/types/worker-platform'
|
||||||
|
|
||||||
|
type WorkOrderVoucherConsumeResult = {
|
||||||
|
ok: boolean
|
||||||
|
voucherCount: number
|
||||||
|
consumedCount: number
|
||||||
|
alreadyConsumedCount: number
|
||||||
|
failedCount: number
|
||||||
|
voucherCodes: string[]
|
||||||
|
errorMessage: string
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAdminWorkerPlatformSummary() {
|
export function fetchAdminWorkerPlatformSummary() {
|
||||||
return apiGet<{
|
return apiGet<{
|
||||||
pendingWorkerCount: number
|
pendingWorkerCount: number
|
||||||
@@ -137,7 +147,10 @@ export function assignAdminWorkOrderToWorker(
|
|||||||
workOrderId: number,
|
workOrderId: number,
|
||||||
payload: { workerId: number },
|
payload: { workerId: number },
|
||||||
) {
|
) {
|
||||||
return apiPost<{ order: WorkOrder }>(
|
return apiPost<{
|
||||||
|
order: WorkOrder
|
||||||
|
voucherConsume: WorkOrderVoucherConsumeResult
|
||||||
|
}>(
|
||||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/assign`,
|
`/api/v1/admin/worker-platform/orders/${workOrderId}/assign`,
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
@@ -249,15 +262,7 @@ export function submitAdminWorkOrderMaterial(
|
|||||||
export function publishAdminWorkOrder(workOrderId: number) {
|
export function publishAdminWorkOrder(workOrderId: number) {
|
||||||
return apiPost<{
|
return apiPost<{
|
||||||
order: WorkOrder
|
order: WorkOrder
|
||||||
voucherConsume: {
|
voucherConsume: WorkOrderVoucherConsumeResult
|
||||||
ok: boolean
|
|
||||||
voucherCount: number
|
|
||||||
consumedCount: number
|
|
||||||
alreadyConsumedCount: number
|
|
||||||
failedCount: number
|
|
||||||
voucherCodes: string[]
|
|
||||||
errorMessage: string
|
|
||||||
}
|
|
||||||
}>(
|
}>(
|
||||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/publish`,
|
`/api/v1/admin/worker-platform/orders/${workOrderId}/publish`,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user