发布工单时自动核销电子凭证
This commit is contained in:
@@ -91,6 +91,7 @@ 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 { 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'
|
||||||
@@ -1039,12 +1040,45 @@ export async function publishAdminWorkOrder(workOrderId: number | string) {
|
|||||||
errorCode: 'work_order_publish_reward_invalid',
|
errorCode: 'work_order_publish_reward_invalid',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const voucherConsume = await consumeIndustryVouchersBeforeWorkOrderPublish(workOrder)
|
||||||
|
if (!voucherConsume.ok) {
|
||||||
|
const failedAt = nowIso()
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: '',
|
||||||
|
eventType: 'publish_voucher_consume_failed',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify(voucherConsume),
|
||||||
|
now: failedAt,
|
||||||
|
})
|
||||||
|
throw createHttpError(voucherConsume.errorMessage || '电子凭证核销失败,订单未发布', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_publish_voucher_consume_failed',
|
||||||
|
context: voucherConsume,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const updated = await updateWorkOrder(workOrder.id, {
|
const updated = await updateWorkOrder(workOrder.id, {
|
||||||
status: WORK_ORDER_STATUS.OPEN,
|
status: WORK_ORDER_STATUS.OPEN,
|
||||||
published_at: now,
|
published_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError(
|
||||||
|
voucherConsume.voucherCount > 0
|
||||||
|
? '电子凭证已核销,但订单发布失败,请重试发布'
|
||||||
|
: '订单发布失败,请重试',
|
||||||
|
{
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_publish_update_failed',
|
||||||
|
context: voucherConsume,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
await createWorkOrderEvent({
|
await createWorkOrderEvent({
|
||||||
workOrderId: workOrder.id,
|
workOrderId: workOrder.id,
|
||||||
actorType: 'admin',
|
actorType: 'admin',
|
||||||
@@ -1052,9 +1086,10 @@ export async function publishAdminWorkOrder(workOrderId: number | string) {
|
|||||||
eventType: 'published',
|
eventType: 'published',
|
||||||
fromStatus: workOrder.status,
|
fromStatus: workOrder.status,
|
||||||
toStatus: WORK_ORDER_STATUS.OPEN,
|
toStatus: WORK_ORDER_STATUS.OPEN,
|
||||||
|
payloadJson: JSON.stringify({ voucherConsume }),
|
||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
return { order: mapWorkOrderAdmin(updated), voucherConsume }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unpublishAdminWorkOrder(workOrderId: number | string) {
|
export async function unpublishAdminWorkOrder(workOrderId: number | string) {
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import type { WorkOrderRow } from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
|
import {
|
||||||
|
consumeIndustryVouchersBeforeWorkOrderPublish,
|
||||||
|
resolveWorkOrderVoucherTargets,
|
||||||
|
} from './publish-voucher-service.js'
|
||||||
|
|
||||||
|
function buildWorkOrder(overrides: Partial<WorkOrderRow> = {}): WorkOrderRow {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
work_order_no: 'WO001',
|
||||||
|
order_id: 10,
|
||||||
|
order_item_id: 20,
|
||||||
|
task_id: null,
|
||||||
|
platform_order_id: 'OID001',
|
||||||
|
product_name: '测试商品',
|
||||||
|
category_id: null,
|
||||||
|
status: 'unassigned',
|
||||||
|
reward_amount: 100,
|
||||||
|
required_deposit_amount: 0,
|
||||||
|
deposit_threshold_amount: 20000,
|
||||||
|
assigned_worker_id: null,
|
||||||
|
last_assigned_worker_id: null,
|
||||||
|
material_json: '{}',
|
||||||
|
requirement_json: '{}',
|
||||||
|
acceptance_json: '{}',
|
||||||
|
draft_acceptance_json: '{}',
|
||||||
|
problem_note: '',
|
||||||
|
sharing_enabled: false,
|
||||||
|
sharing_total_quantity: 1,
|
||||||
|
sharing_unit_reward: 0,
|
||||||
|
published_at: null,
|
||||||
|
pinned_at: null,
|
||||||
|
assigned_at: null,
|
||||||
|
submitted_at: null,
|
||||||
|
accepted_at: null,
|
||||||
|
deadline_at: null,
|
||||||
|
timeout_minutes: 0,
|
||||||
|
timeout_policy: 'reopen',
|
||||||
|
created_at: '2026-08-16T00:00:00.000Z',
|
||||||
|
updated_at: '2026-08-16T00:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTask(id: number, orderItemId: number): TaskRow {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
order_id: 10,
|
||||||
|
order_item_id: orderItemId,
|
||||||
|
unit_index: 1,
|
||||||
|
platform_order_id: 'OID001',
|
||||||
|
profile_id: 1,
|
||||||
|
task_no: `DT${id}`,
|
||||||
|
executor_key: 'manual_dispatch',
|
||||||
|
task_status: 'pending',
|
||||||
|
delivery_status: 'pending',
|
||||||
|
result_code: '',
|
||||||
|
result_message: '',
|
||||||
|
automation_mode: 'manual',
|
||||||
|
requires_claim: false,
|
||||||
|
user_action_status: 'not_required',
|
||||||
|
attempt_count: 0,
|
||||||
|
runtime_session_id: '',
|
||||||
|
login_type: '',
|
||||||
|
nickname: '',
|
||||||
|
role_name: '',
|
||||||
|
role_id: '',
|
||||||
|
area: '',
|
||||||
|
partition_name: '',
|
||||||
|
claim_token: '',
|
||||||
|
primary_claim_token: '',
|
||||||
|
primary_claim_token_id: null,
|
||||||
|
primary_claim_token_status: '',
|
||||||
|
artifacts_json: '{}',
|
||||||
|
context_json: '{}',
|
||||||
|
screenshot_path: '',
|
||||||
|
last_error: '',
|
||||||
|
retry_count: 0,
|
||||||
|
created_at: '2026-08-16T00:00:00.000Z',
|
||||||
|
updated_at: '2026-08-16T00:00:00.000Z',
|
||||||
|
claimed_at: null,
|
||||||
|
role_confirmed_at: null,
|
||||||
|
redeemed_at: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVoucher(
|
||||||
|
id: number,
|
||||||
|
taskId: number | null,
|
||||||
|
status = 'UNUSED',
|
||||||
|
): KuaishouIndustryVoucherRow {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
voucher_code: `KSV${id}`,
|
||||||
|
oid: 'OID001',
|
||||||
|
order_id: 10,
|
||||||
|
task_id: taskId,
|
||||||
|
unit_index: id,
|
||||||
|
seller_id: 'seller-1',
|
||||||
|
token: 'token',
|
||||||
|
eticket_type: 'GAME_OPEN_TICKET_CONSUME',
|
||||||
|
status,
|
||||||
|
valid_start_time: 0,
|
||||||
|
valid_end_time: 0,
|
||||||
|
consume_serial_num: '',
|
||||||
|
consume_details_json: [],
|
||||||
|
consumed_at: status === 'CONSUMED' ? '2026-08-16T00:00:00.000Z' : null,
|
||||||
|
destroyed_at: null,
|
||||||
|
send_callback_status: 'success',
|
||||||
|
send_callback_attempt_count: 1,
|
||||||
|
send_callback_last_error: '',
|
||||||
|
send_callback_response_json: {},
|
||||||
|
send_callback_sent_at: '2026-08-16T00:00:00.000Z',
|
||||||
|
raw_payload_json: {},
|
||||||
|
created_at: '2026-08-16T00:00:00.000Z',
|
||||||
|
updated_at: '2026-08-16T00:00:00.000Z',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('发布核销优先选择当前订单商品关联任务的券码', () => {
|
||||||
|
const tasks = [buildTask(100, 20), buildTask(200, 30)]
|
||||||
|
const vouchers = [buildVoucher(1, 100), buildVoucher(2, 200)]
|
||||||
|
|
||||||
|
const targets = resolveWorkOrderVoucherTargets(buildWorkOrder(), tasks, vouchers)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
targets.map((target) => target.voucher.voucher_code),
|
||||||
|
['KSV1'],
|
||||||
|
)
|
||||||
|
assert.equal(targets[0]?.task?.id, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('历史券码未绑定任务时回退核销同一平台订单的券码', () => {
|
||||||
|
const targets = resolveWorkOrderVoucherTargets(
|
||||||
|
buildWorkOrder(),
|
||||||
|
[buildTask(100, 20)],
|
||||||
|
[buildVoucher(1, null)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
targets.map((target) => target.voucher.voucher_code),
|
||||||
|
['KSV1'],
|
||||||
|
)
|
||||||
|
assert.equal(targets[0]?.task, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('不会核销已明确绑定到其他商品任务的券码', () => {
|
||||||
|
const targets = resolveWorkOrderVoucherTargets(
|
||||||
|
buildWorkOrder(),
|
||||||
|
[buildTask(100, 20), buildTask(200, 30)],
|
||||||
|
[buildVoucher(2, 200)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(targets, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('核销失败时阻止发布并返回失败信息', async () => {
|
||||||
|
const voucher = buildVoucher(1, null)
|
||||||
|
const result = await consumeIndustryVouchersBeforeWorkOrderPublish(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, '快手核销回调失败')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('发布前核销券码并同步关联任务状态', async () => {
|
||||||
|
const task = buildTask(100, 20)
|
||||||
|
const voucher = buildVoucher(1, 100)
|
||||||
|
const consumedVoucher = {
|
||||||
|
...voucher,
|
||||||
|
status: 'CONSUMED',
|
||||||
|
consumed_at: '2026-08-16T01:00:00.000Z',
|
||||||
|
}
|
||||||
|
const consumedCodes: string[] = []
|
||||||
|
const attachedTaskIds: number[] = []
|
||||||
|
|
||||||
|
const result = await consumeIndustryVouchersBeforeWorkOrderPublish(buildWorkOrder(), {
|
||||||
|
listVouchersByOid: async () => [voucher],
|
||||||
|
listTasksByOrderId: async () => [task],
|
||||||
|
consumeVoucher: async (currentVoucher) => {
|
||||||
|
consumedCodes.push(currentVoucher.voucher_code)
|
||||||
|
return { ok: true, voucher: consumedVoucher, callbackSuccess: true }
|
||||||
|
},
|
||||||
|
attachVoucherToTask: async (currentTask) => {
|
||||||
|
attachedTaskIds.push(currentTask.id)
|
||||||
|
return currentTask
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.ok, true)
|
||||||
|
assert.equal(result.voucherCount, 1)
|
||||||
|
assert.equal(result.consumedCount, 1)
|
||||||
|
assert.equal(result.alreadyConsumedCount, 0)
|
||||||
|
assert.deepEqual(consumedCodes, ['KSV1'])
|
||||||
|
assert.deepEqual(attachedTaskIds, [100])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('已核销券码按幂等成功处理', async () => {
|
||||||
|
const voucher = buildVoucher(1, null, 'CONSUMED')
|
||||||
|
const result = await consumeIndustryVouchersBeforeWorkOrderPublish(buildWorkOrder(), {
|
||||||
|
listVouchersByOid: async () => [voucher],
|
||||||
|
listTasksByOrderId: async () => [],
|
||||||
|
consumeVoucher: async () => ({ ok: true, voucher, callbackSuccess: true }),
|
||||||
|
attachVoucherToTask: async (task) => task,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.ok, true)
|
||||||
|
assert.equal(result.voucherCount, 1)
|
||||||
|
assert.equal(result.consumedCount, 0)
|
||||||
|
assert.equal(result.alreadyConsumedCount, 1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
|
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
||||||
|
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
|
import type { WorkOrderRow } from '../../repositories/worker-platform/index.js'
|
||||||
|
import { attachKuaishouIndustryVoucherToTask } from '../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||||
|
import { consumeKuaishouIndustryVoucher } from '../platforms/kuaishou-industry/voucher-service.js'
|
||||||
|
|
||||||
|
export type WorkOrderVoucherConsumeResult = {
|
||||||
|
ok: boolean
|
||||||
|
voucherCount: number
|
||||||
|
consumedCount: number
|
||||||
|
alreadyConsumedCount: number
|
||||||
|
failedCount: number
|
||||||
|
voucherCodes: string[]
|
||||||
|
errorMessage: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishVoucherServiceDeps = {
|
||||||
|
listVouchersByOid: typeof listKuaishouIndustryVouchersByOid
|
||||||
|
listTasksByOrderId: typeof listTasksByOrderId
|
||||||
|
consumeVoucher: typeof consumeKuaishouIndustryVoucher
|
||||||
|
attachVoucherToTask: typeof attachKuaishouIndustryVoucherToTask
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDeps: PublishVoucherServiceDeps = {
|
||||||
|
listVouchersByOid: listKuaishouIndustryVouchersByOid,
|
||||||
|
listTasksByOrderId,
|
||||||
|
consumeVoucher: consumeKuaishouIndustryVoucher,
|
||||||
|
attachVoucherToTask: attachKuaishouIndustryVoucherToTask,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function consumeIndustryVouchersBeforeWorkOrderPublish(
|
||||||
|
workOrder: WorkOrderRow,
|
||||||
|
deps: PublishVoucherServiceDeps = defaultDeps,
|
||||||
|
): Promise<WorkOrderVoucherConsumeResult> {
|
||||||
|
const oid = String(workOrder.platform_order_id || '').trim()
|
||||||
|
if (!oid) {
|
||||||
|
return buildConsumeResult([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const [vouchers, tasks] = await Promise.all([
|
||||||
|
deps.listVouchersByOid(oid),
|
||||||
|
workOrder.order_id ? deps.listTasksByOrderId(workOrder.order_id) : Promise.resolve([]),
|
||||||
|
])
|
||||||
|
const targets = resolveWorkOrderVoucherTargets(workOrder, tasks, vouchers)
|
||||||
|
let consumedCount = 0
|
||||||
|
let alreadyConsumedCount = 0
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const voucherCode = String(target.voucher.voucher_code || '').trim()
|
||||||
|
const alreadyConsumed = normalizeVoucherStatus(target.voucher.status) === 'CONSUMED'
|
||||||
|
let consumeResult: Awaited<ReturnType<typeof consumeKuaishouIndustryVoucher>>
|
||||||
|
|
||||||
|
try {
|
||||||
|
consumeResult = await deps.consumeVoucher(target.voucher, {
|
||||||
|
source: 'work_order_publish',
|
||||||
|
consumeTime: Date.now(),
|
||||||
|
...(target.task ? { task: target.task } : {}),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return buildConsumeResult(targets, {
|
||||||
|
consumedCount,
|
||||||
|
alreadyConsumedCount,
|
||||||
|
errorMessage: error instanceof Error ? error.message : '电子凭证核销失败',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!consumeResult.ok || !consumeResult.voucher) {
|
||||||
|
return buildConsumeResult(targets, {
|
||||||
|
consumedCount,
|
||||||
|
alreadyConsumedCount,
|
||||||
|
errorMessage: consumeResult.errorMessage || `电子凭证 ${voucherCode} 核销失败`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alreadyConsumed) {
|
||||||
|
alreadyConsumedCount += 1
|
||||||
|
} else {
|
||||||
|
consumedCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (target.task) {
|
||||||
|
await deps.attachVoucherToTask(target.task, consumeResult.voucher, {
|
||||||
|
source: 'work_order_publish',
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return buildConsumeResult(targets, {
|
||||||
|
consumedCount,
|
||||||
|
alreadyConsumedCount,
|
||||||
|
errorMessage: `电子凭证 ${voucherCode} 已核销,但任务状态同步失败:${
|
||||||
|
error instanceof Error ? error.message : '未知错误'
|
||||||
|
}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildConsumeResult(targets, {
|
||||||
|
consumedCount,
|
||||||
|
alreadyConsumedCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkOrderVoucherTargets(
|
||||||
|
workOrder: WorkOrderRow,
|
||||||
|
tasks: TaskRow[],
|
||||||
|
vouchers: KuaishouIndustryVoucherRow[],
|
||||||
|
) {
|
||||||
|
const taskById = new Map(tasks.map((task) => [Number(task.id), task]))
|
||||||
|
const linkedTaskIds = new Set<number>()
|
||||||
|
|
||||||
|
if (workOrder.task_id) {
|
||||||
|
linkedTaskIds.add(Number(workOrder.task_id))
|
||||||
|
}
|
||||||
|
if (workOrder.order_item_id) {
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (Number(task.order_item_id) === Number(workOrder.order_item_id)) {
|
||||||
|
linkedTaskIds.add(Number(task.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedVouchers = vouchers.filter(
|
||||||
|
(voucher) => voucher.task_id && linkedTaskIds.has(Number(voucher.task_id)),
|
||||||
|
)
|
||||||
|
// 历史数据可能尚未完成任务绑定;此时回退到同一平台订单,避免仍需人工核销。
|
||||||
|
const hasTaskBoundVoucher = vouchers.some((voucher) => Number(voucher.task_id || 0) > 0)
|
||||||
|
const selectedVouchers =
|
||||||
|
linkedVouchers.length > 0
|
||||||
|
? linkedVouchers
|
||||||
|
: linkedTaskIds.size > 0 && hasTaskBoundVoucher
|
||||||
|
? []
|
||||||
|
: vouchers
|
||||||
|
|
||||||
|
return deduplicateVouchers(selectedVouchers).map((voucher) => ({
|
||||||
|
voucher,
|
||||||
|
task: voucher.task_id ? taskById.get(Number(voucher.task_id)) || null : null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConsumeResult(
|
||||||
|
targets: Array<{ voucher: KuaishouIndustryVoucherRow }>,
|
||||||
|
input: {
|
||||||
|
consumedCount?: number
|
||||||
|
alreadyConsumedCount?: number
|
||||||
|
errorMessage?: string
|
||||||
|
} = {},
|
||||||
|
): WorkOrderVoucherConsumeResult {
|
||||||
|
const errorMessage = String(input.errorMessage || '').trim()
|
||||||
|
const processedCount = Number(input.consumedCount || 0) + Number(input.alreadyConsumedCount || 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: !errorMessage,
|
||||||
|
voucherCount: targets.length,
|
||||||
|
consumedCount: Number(input.consumedCount || 0),
|
||||||
|
alreadyConsumedCount: Number(input.alreadyConsumedCount || 0),
|
||||||
|
failedCount: errorMessage ? Math.max(1, targets.length - processedCount) : 0,
|
||||||
|
voucherCodes: targets.map((target) => String(target.voucher.voucher_code || '').trim()),
|
||||||
|
errorMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deduplicateVouchers(vouchers: KuaishouIndustryVoucherRow[]) {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return vouchers.filter((voucher) => {
|
||||||
|
const key = String(voucher.voucher_code || voucher.id || '').trim()
|
||||||
|
if (!key || seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVoucherStatus(value: unknown) {
|
||||||
|
return String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Checkbox,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
@@ -128,6 +129,8 @@ const FILTERABLE_WORK_ORDER_STATUSES = [
|
|||||||
'cancelled',
|
'cancelled',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
const SKIP_PUBLISH_CONFIRM_KEY = 'admin-work-order-skip-publish-confirm'
|
||||||
|
|
||||||
type MaterialFormValues = {
|
type MaterialFormValues = {
|
||||||
fields?: Record<string, string>
|
fields?: Record<string, string>
|
||||||
rewardAmount?: number
|
rewardAmount?: number
|
||||||
@@ -347,13 +350,15 @@ export default function WorkOrdersPanel() {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAction(
|
async function runAction<T>(
|
||||||
action: () => Promise<unknown>,
|
action: () => Promise<T>,
|
||||||
successMessage: string,
|
successMessage: string | ((result: T) => string),
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await action()
|
const result = await action()
|
||||||
message.success(successMessage)
|
message.success(
|
||||||
|
typeof successMessage === 'function' ? successMessage(result) : successMessage,
|
||||||
|
)
|
||||||
await refreshAll()
|
await refreshAll()
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -362,6 +367,48 @@ export default function WorkOrdersPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publishWorkOrder(row: WorkOrder) {
|
||||||
|
return runAction(
|
||||||
|
() => publishAdminWorkOrder(row.workOrderId),
|
||||||
|
(response) =>
|
||||||
|
response.data.voucherConsume.voucherCount > 0
|
||||||
|
? '电子凭证已自动核销,订单已发布到大厅'
|
||||||
|
: '订单已发布到大厅',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmPublishWorkOrder(row: WorkOrder) {
|
||||||
|
if (shouldSkipPublishConfirm()) {
|
||||||
|
void publishWorkOrder(row)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let skipFutureConfirm = false
|
||||||
|
modal.confirm({
|
||||||
|
title: '确认发布并核销该订单?',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<Typography.Paragraph>
|
||||||
|
如订单关联电子凭证,系统会先自动核销,成功后再发布到大厅。核销后即使下架也不会自动恢复凭证。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(event) => {
|
||||||
|
skipFutureConfirm = event.target.checked
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
不再提示
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
okText: '发布并核销',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
if (skipFutureConfirm) setSkipPublishConfirm()
|
||||||
|
return publishWorkOrder(row)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function confirmBatchAcceptance() {
|
function confirmBatchAcceptance() {
|
||||||
const workOrderIds = selectedAcceptanceOrderIds
|
const workOrderIds = selectedAcceptanceOrderIds
|
||||||
if (workOrderIds.length === 0) {
|
if (workOrderIds.length === 0) {
|
||||||
@@ -792,14 +839,9 @@ export default function WorkOrdersPanel() {
|
|||||||
? '接单金额为 0,不能发布到大厅'
|
? '接单金额为 0,不能发布到大厅'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() => confirmPublishWorkOrder(row)}
|
||||||
runAction(
|
|
||||||
() => publishAdminWorkOrder(row.workOrderId),
|
|
||||||
'已发布到大厅',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
发布
|
发布并核销
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.status === 'open' ? (
|
{row.status === 'open' ? (
|
||||||
@@ -1687,6 +1729,22 @@ export default function WorkOrdersPanel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldSkipPublishConfirm() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(SKIP_PUBLISH_CONFIRM_KEY) === '1'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSkipPublishConfirm() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SKIP_PUBLISH_CONFIRM_KEY, '1')
|
||||||
|
} catch {
|
||||||
|
// 浏览器禁用本地存储时继续发布,但不记住本次选择。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatUserPaymentAmount(order: WorkOrder): string {
|
function formatUserPaymentAmount(order: WorkOrder): string {
|
||||||
const paymentFen = getUserPaymentFen(order)
|
const paymentFen = getUserPaymentFen(order)
|
||||||
return paymentFen === null ? '-' : formatMoney(paymentFen)
|
return paymentFen === null ? '-' : formatMoney(paymentFen)
|
||||||
|
|||||||
@@ -247,7 +247,18 @@ export function submitAdminWorkOrderMaterial(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function publishAdminWorkOrder(workOrderId: number) {
|
export function publishAdminWorkOrder(workOrderId: number) {
|
||||||
return apiPost<{ order: WorkOrder }>(
|
return apiPost<{
|
||||||
|
order: WorkOrder
|
||||||
|
voucherConsume: {
|
||||||
|
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