From 3ad7c2e50eea6fca31aa945bed72bed9856145af Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 8 Jul 2026 17:28:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BF=AB=E6=89=8B=E7=94=B5?= =?UTF-8?q?=E5=AD=90=E5=87=AD=E8=AF=81=E5=8F=91=E7=A0=81=E5=92=8C=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/routes/admin/orders.ts | 32 +- apps/backend/src/routes/admin/tasks.ts | 37 ++ .../src/services/admin/admin-read-service.ts | 85 ++++- .../admin/write/kuaishou-cloud-actions.ts | 322 +++++++++++++++++- .../kuaishou-industry/send-code-service.ts | 69 ++++ apps/backend/src/types/admin/route-inputs.ts | 2 + apps/backend/src/types/admin/write-inputs.ts | 8 + .../frontend-vue/src/services/admin/orders.ts | 11 +- apps/frontend-vue/src/services/admin/tasks.ts | 23 ++ apps/frontend-vue/src/types/admin/orders.ts | 15 + apps/frontend-vue/src/types/admin/tasks.ts | 14 + .../admin/orders/AdminOrderDetailView.vue | 81 ++++- .../views/admin/tasks/AdminTaskDetailView.vue | 4 + .../tasks/components/AdminTaskActionBar.vue | 20 ++ .../AdminTaskKuaishouCloudPanel.vue | 19 ++ .../tasks/composables/useAdminTaskDetail.ts | 1 + .../composables/useAdminTaskKuaishouCloud.ts | 28 ++ 17 files changed, 761 insertions(+), 10 deletions(-) diff --git a/apps/backend/src/routes/admin/orders.ts b/apps/backend/src/routes/admin/orders.ts index b45e6513..34a5868d 100644 --- a/apps/backend/src/routes/admin/orders.ts +++ b/apps/backend/src/routes/admin/orders.ts @@ -1,7 +1,8 @@ import { Router } from 'express' import { getAdminOrderDetail, getAdminOrders } from '../../services/admin/admin-read-service.js' -import { createJsonHandler } from './session.js' +import { resendAdminOrderKuaishouIndustryVoucherCodes } from '../../services/admin/write/kuaishou-cloud-actions.js' +import { createJsonHandler, requireAdminRoles } from './session.js' import type { AdminOrderRouteParams, AdminOrderRouteQuery, @@ -19,7 +20,11 @@ router.get('/orders', createJsonHandler( )) router.get('/orders/:orderId', createJsonHandler( - (req) => getAdminOrderDetail(String((req.params as AdminOrderRouteParams).orderId || '')), + (req) => + getAdminOrderDetail( + String((req.params as AdminOrderRouteParams).orderId || ''), + req.adminSession || null, + ), { successMessage: 'ok', errorMessage: '读取订单详情失败', @@ -27,4 +32,27 @@ router.get('/orders/:orderId', createJsonHandler( }, )) +router.post( + '/orders/:orderId/kuaishou-industry/resend-code', + requireAdminRoles(['admin', 'operator']), + createJsonHandler( + (req) => + resendAdminOrderKuaishouIndustryVoucherCodes( + String((req.params as AdminOrderRouteParams).orderId || ''), + req.adminSession || null, + ), + { + successMessage: '电子凭证发码回调已重发', + errorMessage: '重发电子凭证发码回调失败', + scope: '[admin/orders/:orderId/kuaishou-industry/resend-code]', + audit: (req, data) => ({ + action: 'order_kuaishou_industry_resend_code', + targetType: 'order', + targetId: String((req.params as AdminOrderRouteParams).orderId || ''), + data: data && typeof data === 'object' ? data as Record : {}, + }), + }, + ), +) + export default router diff --git a/apps/backend/src/routes/admin/tasks.ts b/apps/backend/src/routes/admin/tasks.ts index 5af2e215..f2601cb1 100644 --- a/apps/backend/src/routes/admin/tasks.ts +++ b/apps/backend/src/routes/admin/tasks.ts @@ -4,15 +4,18 @@ import { Router } from 'express' import { getAdminTaskDetail, getAdminTasks } from '../../services/admin/admin-read-service.js' import { closeAdminTask } from '../../services/admin/write/task-actions.js' import { + consumeAdminTaskKuaishouIndustryVoucher, dispatchAdminTaskKuaishouCloudFulfillment, prepareAdminTaskKuaishouCloudFulfillment, rebindAdminTaskKuaishouCloudRole, refreshAdminTaskKuaishouCloudRoleInfo, + resendAdminTaskKuaishouIndustryVoucherCode, returnNumberAdminTaskKuaishouCloudFulfillment, } from '../../services/admin/write/kuaishou-cloud-actions.js' import { createJsonHandler, requireAdminRoles } from './session.js' import type { AdminTaskKuaishouCloudDispatchRouteBody, + AdminTaskKuaishouIndustryConsumeRouteBody, AdminTaskRouteParams, AdminTaskRouteQuery, } from '../../types/admin/route-inputs.js' @@ -148,6 +151,40 @@ router.post( ), ) +router.post( + '/tasks/:taskId/kuaishou-industry/resend-code', + requireAdminRoles(['admin', 'operator']), + createJsonHandler( + (req) => + resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null), + { + successMessage: '电子凭证发码回调已重发', + errorMessage: '重发电子凭证发码回调失败', + scope: '[admin/tasks/:taskId/kuaishou-industry/resend-code]', + audit: (req, data) => buildTaskAudit('task_kuaishou_industry_resend_code', req, data), + }, + ), +) + +router.post( + '/tasks/:taskId/kuaishou-industry/consume', + requireAdminRoles(['admin', 'operator']), + createJsonHandler( + (req) => + consumeAdminTaskKuaishouIndustryVoucher( + getTaskId(req), + req.body as AdminTaskKuaishouIndustryConsumeRouteBody, + req.adminSession || null, + ), + { + successMessage: '电子凭证已执行核销', + errorMessage: '执行电子凭证核销失败', + scope: '[admin/tasks/:taskId/kuaishou-industry/consume]', + audit: (req, data) => buildTaskAudit('task_kuaishou_industry_consume', req, data), + }, + ), +) + function buildTaskAudit(action: string, req: Request, data: unknown) { const result = data as AdminTaskActionResponse diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index 73bd5147..7f6a968c 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -4,6 +4,10 @@ import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js' import { getOrderById, listOrders } from '../../repositories/order-repo.js' import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js' import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js' +import { + listKuaishouIndustryVouchersByOid, + listKuaishouIndustryVouchersByTaskId, +} from '../../repositories/kuaishou-industry-voucher-repo.js' import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js' import { resolveKuaishouFeifeiClaimUrl } from '../fulfillment/kuaishou-feifei/index.js' import { createHttpError } from '../../utils/http.js' @@ -83,7 +87,10 @@ export async function getAdminOrders( } } -export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise { +export async function getAdminOrderDetail( + orderId: AdminEntityIdInput, + session: AdminViewerSessionInput | null = null, +): Promise { const order = await getOrderById(Number(orderId)) if (!order) { @@ -97,7 +104,9 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise< listOrderItemsByOrderId(order.id), listTasksByOrderId(order.id), ]) + const kuaishouIndustryVouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id) const itemSummary = summarizeOrderItems(items) + const viewerContext = createAdminViewerContext(session) return { order: { @@ -138,6 +147,37 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise< : JSON.parse(String(item.spec_json || '{}')), })), tasks: tasks.map((task) => mapAdminTaskSummary(task)), + kuaishouIndustryVouchers: kuaishouIndustryVouchers.map(mapAdminKuaishouIndustryVoucher), + operations: { + canResendKuaishouIndustryVoucherCode: + viewerContext.canManageTaskLifecycle && kuaishouIndustryVouchers.length > 0, + }, + } +} + +function mapAdminKuaishouIndustryVoucher(voucher: { + voucher_code?: unknown + oid?: unknown + unit_index?: unknown + status?: unknown + send_callback_status?: unknown + send_callback_attempt_count?: unknown + send_callback_last_error?: unknown + send_callback_sent_at?: unknown + consumed_at?: unknown + destroyed_at?: unknown +}) { + return { + oid: String(voucher.oid || '').trim(), + voucherCode: String(voucher.voucher_code || '').trim(), + unitIndex: Number(voucher.unit_index || 0) || 0, + status: String(voucher.status || '').trim(), + sendCallbackStatus: String(voucher.send_callback_status || '').trim(), + sendCallbackAttemptCount: Number(voucher.send_callback_attempt_count || 0) || 0, + sendCallbackLastError: String(voucher.send_callback_last_error || '').trim(), + sendCallbackSentAt: voucher.send_callback_sent_at || null, + consumedAt: voucher.consumed_at || null, + destroyedAt: voucher.destroyed_at || null, } } @@ -180,10 +220,11 @@ export async function getAdminTaskDetail( } const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task) - const [order, claimToken, taskEvents] = await Promise.all([ + const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers] = await Promise.all([ getOrderById(task.order_id), primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null), listTaskEventsByTaskId(task.id), + listKuaishouIndustryVouchersByTaskId(task.id), ]) const orderItems = order ? await listOrderItemsByOrderId(order.id) : [] const orderItem = orderItems.find((item) => item.id === task.order_item_id) || null @@ -200,6 +241,18 @@ export async function getAdminTaskDetail( mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment, { cloudSourceLabelMap, }) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap }) + const firstKuaishouIndustryVoucher = taskKuaishouIndustryVouchers[0] || null + const kuaishouIndustryVoucher = + firstKuaishouIndustryVoucher + ? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher) + : mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher) + const hasKuaishouIndustryVoucher = Boolean( + kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(), + ) + const kuaishouIndustryVoucherStatus = + String(kuaishouIndustryVoucher?.status || '').trim().toUpperCase() + const kuaishouIndustryVoucherSendCallbackStatus = + String(kuaishouIndustryVoucher?.sendCallbackStatus || '').trim().toLowerCase() return { task: mapAdminTaskListItem( @@ -250,6 +303,7 @@ export async function getAdminTaskDetail( }, redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution), kuaishouCloudFulfillment, + kuaishouIndustryVoucher, manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext), events: taskEvents.map(mapAdminTaskEvent), operations: { @@ -289,11 +343,38 @@ export async function getAdminTaskDetail( viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && canReturnKuaishouCloudFulfillmentStatus(task.task_status), + canResendKuaishouIndustryVoucherCode: + viewerContext.canManageTaskLifecycle && hasKuaishouIndustryVoucher, + canConsumeKuaishouIndustryVoucher: + viewerContext.canManageTaskLifecycle && + hasKuaishouIndustryVoucher && + kuaishouIndustryVoucherStatus !== 'CONSUMED' && + kuaishouIndustryVoucherSendCallbackStatus === 'success', canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData, }, } } +function mapKuaishouIndustryVoucherContext(value: unknown): JsonRecord | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return null + } + + const record = value as JsonRecord + return { + oid: String(record.oid || '').trim(), + voucherCode: String(record.voucherCode || record.eticketId || '').trim(), + unitIndex: Number(record.unitIndex || 0) || 0, + status: String(record.status || '').trim(), + sendCallbackStatus: String(record.sendCallbackStatus || '').trim(), + sendCallbackLastError: String(record.sendCallbackLastError || '').trim(), + sendCallbackAttemptCount: Number(record.sendCallbackAttemptCount || 0) || 0, + sendCallbackSentAt: record.sendCallbackSentAt || null, + consumedAt: record.consumedAt || null, + destroyedAt: record.destroyedAt || null, + } +} + function buildCloudSourceLabelMap() { const labelMap = new Map() diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts index 854f690a..e55c16ae 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts @@ -1,6 +1,11 @@ import { getOrderById } from '../../../repositories/order-repo.js' -import { updateTask } from '../../../repositories/task-repo.js' +import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js' +import { + findKuaishouIndustryVoucherByCode, + listKuaishouIndustryVouchersByOid, + listKuaishouIndustryVouchersByTaskId, +} from '../../../repositories/kuaishou-industry-voucher-repo.js' import { buyCloudtentaclesSku, getCloudtentaclesAsset, @@ -14,6 +19,12 @@ import { } from '../../platforms/cloudtentacles/virtual-number-service.js' import { consumeKuaishouEticket } from '../../platforms/kuaishou-eticket/consume-service.js' import { isKuaishouEticketMockTicketCode } from '../../platforms/kuaishou-eticket/mock-ticket-service.js' +import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js' +import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js' +import { + attachKuaishouIndustryVoucherToTask, + bindKuaishouIndustryVouchersToOrderTasks, +} from '../../platforms/kuaishou-industry/voucher-binding-service.js' import { getKuaishouEticketSourceConfig, resolveKuaishouEticketShopConfig, @@ -46,8 +57,12 @@ import type { AdminEntityIdInput, AdminViewerSessionInput, } from '../../../types/admin/read-inputs.js' -import type { AdminTaskKuaishouCloudDispatchInput } from '../../../types/admin/write-inputs.js' +import type { + AdminTaskKuaishouCloudDispatchInput, + AdminTaskKuaishouIndustryConsumeInput, +} from '../../../types/admin/write-inputs.js' import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js' +import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js' export async function prepareAdminTaskKuaishouCloudFulfillment( taskId: AdminEntityIdInput, @@ -675,3 +690,306 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment( task: mapTaskActionPayload(updatedTask), } } + +export async function resendAdminTaskKuaishouIndustryVoucherCode( + taskId: AdminEntityIdInput, + session: AdminViewerSessionInput | null = null, +): Promise { + const task = await getRequiredTask(taskId) + const now = nowIso() + const viewerContext = createAdminViewerContext(session) + + if (!viewerContext.canManageTaskLifecycle) { + throw createHttpError('当前账号没有权限重发电子凭证发码回调', { + statusCode: 403, + errorCode: 'admin_task_kuaishou_industry_resend_forbidden', + }) + } + + const voucher = await getRequiredIndustryVoucherForTask(task) + const order = await getOrderById(task.order_id) + const result = await resendKuaishouIndustryVoucherSendCallback({ + voucherCode: voucher.voucher_code, + oid: voucher.oid, + preferredTotalGoodsValue: Number(order?.total_amount || 0) || 0, + }) + const updatedVoucher = result.voucher || voucher + + if (!result.success) { + const message = String(result.error || '电子凭证发码回调重发失败').trim() + const failedTask = await updateTask(task.id, { + task_status: TASK_STATUS.MANUAL_REVIEW, + result_code: 'kuaishou_industry_send_callback_resend_failed', + result_message: message, + last_error: message, + updated_at: now, + }) + await createTaskEvent( + task.id, + 'kuaishou_industry_send_callback_resend_failed', + { + oid: voucher.oid, + voucherCode: voucher.voucher_code, + errorMessage: message, + actor: buildAdminActionActor(session), + }, + now, + ) + void failedTask + throw createHttpError(message, { + statusCode: 409, + errorCode: 'admin_task_kuaishou_industry_resend_failed', + }) + } + + const attachedTask = await attachKuaishouIndustryVoucherToTask(task, updatedVoucher, { + source: 'admin_resend_kuaishou_industry_code', + now, + }) + const resendTargetTask = attachedTask || task + const refreshedTask = await updateTask(resendTargetTask.id, { + result_code: 'kuaishou_industry_send_callback_resent', + result_message: '电子凭证发码回调已重发成功', + last_error: '', + updated_at: now, + }) + + await createTaskEvent( + task.id, + 'kuaishou_industry_send_callback_resent', + { + oid: voucher.oid, + voucherCode: voucher.voucher_code, + actor: buildAdminActionActor(session), + }, + now, + ) + + return { task: mapTaskActionPayload(refreshedTask || resendTargetTask) } +} + +export async function resendAdminOrderKuaishouIndustryVoucherCodes( + orderId: AdminEntityIdInput, + session: AdminViewerSessionInput | null = null, +): Promise<{ success: boolean; resentCount: number; failedCount: number; errorMessage: string }> { + const order = await getOrderById(Number(orderId)) + const viewerContext = createAdminViewerContext(session) + const now = nowIso() + + if (!viewerContext.canManageTaskLifecycle) { + throw createHttpError('当前账号没有权限重发电子凭证发码回调', { + statusCode: 403, + errorCode: 'admin_order_kuaishou_industry_resend_forbidden', + }) + } + + if (!order) { + throw createHttpError('订单不存在', { + statusCode: 404, + errorCode: 'admin_order_not_found', + }) + } + + const vouchers = await resolveIndustryVouchersForOrder(order.platform_order_id) + if (vouchers.length === 0) { + throw createHttpError('当前订单没有关联电子凭证,无法重发发码回调', { + statusCode: 409, + errorCode: 'admin_order_kuaishou_industry_voucher_missing', + }) + } + + let resentCount = 0 + let failedCount = 0 + let errorMessage = '' + + for (const voucher of vouchers) { + const result = await resendKuaishouIndustryVoucherSendCallback({ + voucherCode: voucher.voucher_code, + oid: voucher.oid, + preferredTotalGoodsValue: Number(order.total_amount || 0) || 0, + }) + + if (result.success) { + resentCount += 1 + continue + } + + failedCount += 1 + errorMessage = errorMessage || String(result.error || '电子凭证发码回调重发失败').trim() + } + + if (failedCount > 0) { + throw createHttpError(errorMessage || '电子凭证发码回调重发失败', { + statusCode: 409, + errorCode: 'admin_order_kuaishou_industry_resend_failed', + context: { resentCount, failedCount }, + }) + } + + const tasks = await listTasksByOrderId(order.id) + await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, { + source: 'admin_order_resend_kuaishou_industry_code', + now, + }) + + return { + success: failedCount === 0, + resentCount, + failedCount, + errorMessage, + } +} + +export async function consumeAdminTaskKuaishouIndustryVoucher( + taskId: AdminEntityIdInput, + payload: AdminTaskKuaishouIndustryConsumeInput = {}, + session: AdminViewerSessionInput | null = null, +): Promise { + const task = await getRequiredTask(taskId) + const now = nowIso() + const viewerContext = createAdminViewerContext(session) + + if (!viewerContext.canManageTaskLifecycle) { + throw createHttpError('当前账号没有权限执行电子凭证核销', { + statusCode: 403, + errorCode: 'admin_task_kuaishou_industry_consume_forbidden', + }) + } + + const voucher = await getRequiredIndustryVoucherForTask(task) + const result = await consumeKuaishouIndustryVoucher(voucher, { + source: 'admin_manual_consume', + task, + token: voucher.token, + consumeType: String(payload.consumeType || 'delivery').trim() || 'delivery', + consumeTime: Date.now(), + ...(payload.storeName ? { storeName: payload.storeName } : {}), + ...(payload.storeAddress ? { storeAddress: payload.storeAddress } : {}), + ...(payload.expressCode ? { expressCode: payload.expressCode } : {}), + ...(payload.expressNo ? { expressNo: payload.expressNo } : {}), + }) + + if (!result.ok || !result.voucher) { + const message = String(result.errorMessage || '电子凭证核销失败').trim() + const failedTask = await updateTask(task.id, { + task_status: TASK_STATUS.MANUAL_REVIEW, + result_code: 'kuaishou_industry_manual_consume_failed', + result_message: message, + last_error: message, + updated_at: now, + }) + await createTaskEvent( + task.id, + 'kuaishou_industry_manual_consume_failed', + { + oid: voucher.oid, + voucherCode: voucher.voucher_code, + errorMessage: message, + actor: buildAdminActionActor(session), + }, + now, + ) + void failedTask + throw createHttpError(message, { + statusCode: 409, + errorCode: 'admin_task_kuaishou_industry_consume_failed', + }) + } + + const attachedConsumeTask = await attachKuaishouIndustryVoucherToTask(task, result.voucher, { + source: 'admin_manual_consume', + now, + }) + const consumeTargetTask = attachedConsumeTask || task + const taskContext = parseTaskContext(consumeTargetTask) + const hasCloudFulfillmentContext = Boolean( + taskContext.kuaishouCloudFulfillment && + typeof taskContext.kuaishouCloudFulfillment === 'object' && + !Array.isArray(taskContext.kuaishouCloudFulfillment), + ) + const flow = hasCloudFulfillmentContext + ? normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) + : null + const nextContext = { + ...taskContext, + ...(flow + ? { + kuaishouCloudFulfillment: { + ...flow, + consume: { + ...flow.consume, + status: 'success', + consumedAt: result.voucher.consumed_at || now, + errorMessage: '', + }, + }, + } + : {}), + } + const updatedTask = await updateTask(consumeTargetTask.id, { + task_status: TASK_STATUS.COMPLETED, + delivery_status: 'delivered', + result_code: 'kuaishou_industry_manual_consumed', + result_message: '电子凭证已手动核销完成', + redeemed_at: result.voucher.consumed_at || now, + last_error: '', + context_json: JSON.stringify(nextContext), + updated_at: now, + }) + + await createTaskEvent( + task.id, + 'kuaishou_industry_manual_consumed', + { + oid: result.voucher.oid, + voucherCode: result.voucher.voucher_code, + consumeSerialNum: result.voucher.consume_serial_num, + actor: buildAdminActionActor(session), + }, + now, + ) + + return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) } +} + +async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise { + const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id) + const firstVoucher = vouchers[0] + if (firstVoucher) { + return firstVoucher + } + + const taskContext = parseTaskContext(task) + const voucherContext = taskContext.kuaishouIndustryVoucher || {} + const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim() + if (voucherCode) { + const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id) + if (voucher) { + return voucher + } + } + + throw createHttpError('当前任务没有关联电子凭证,无法执行该操作', { + statusCode: 409, + errorCode: 'admin_task_kuaishou_industry_voucher_missing', + }) +} + +async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise { + const normalizedOid = String(platformOrderId || '').trim() + if (!normalizedOid) { + return [] + } + + return listKuaishouIndustryVouchersByOid(normalizedOid) +} + +function buildAdminActionActor(session: AdminViewerSessionInput | null) { + return session + ? { + userId: Number(session.userId || 0) || 0, + username: String(session.username || '').trim(), + role: String(session.role || '').trim(), + } + : null +} diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts index 744aa4a8..cb43cc78 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts @@ -1,6 +1,7 @@ import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js' import { listTasksByOrderId } from '../../../repositories/task-repo.js' import { + findKuaishouIndustryVoucherByCode, updateKuaishouIndustryVoucherByCode, upsertKuaishouIndustryVoucher, } from '../../../repositories/kuaishou-industry-voucher-repo.js' @@ -31,6 +32,15 @@ import type { KuaishouIndustryVoucherRow, OrderRow } from '../../../types/reposi type JsonObject = Record +type SendCodeCallbackParams = { + oid: string + sendType: string + sellerId: string + token: string + eticketType?: string + ext?: string +} + export async function handleSendCode(rawBody: JsonObject = {}) { const config = getKuaishouIndustryConfig() @@ -125,6 +135,47 @@ export async function handleSendCode(rawBody: JsonObject = {}) { ) } +export async function resendKuaishouIndustryVoucherSendCallback(input: { + voucherCode: string + oid?: string + preferredTotalGoodsValue?: number +}) { + const voucher = await findKuaishouIndustryVoucherByCode(input.voucherCode, input.oid || '') + if (!voucher) { + return { + success: false, + error: '电子凭证不存在,无法重发发码回调', + voucher: null, + } + } + + const rawPayload = parseJsonObject(voucher.raw_payload_json) + const body = parseJsonObject(rawPayload.body) + const params = resolveSendCodeCallbackParams(voucher, body) + const eticketType = String(params.eticketType || '').trim() + const eticket = buildKuaishouIndustryEticketFromVoucher(voucher, eticketType) + const result = await sendAndRecordCallback({ + oid: params.oid, + sendType: params.sendType, + etickets: [eticket], + vouchers: [voucher], + sellerId: params.sellerId, + token: params.token, + eticketType, + source: 'admin_resend', + ...(input.preferredTotalGoodsValue !== undefined + ? { preferredTotalGoodsValue: input.preferredTotalGoodsValue } + : {}), + ...(params.ext ? { ext: params.ext } : {}), + }) + const updatedVoucher = await findKuaishouIndustryVoucherByCode(voucher.voucher_code, voucher.oid) + + return { + ...result, + voucher: updatedVoucher || voucher, + } +} + async function sendAndRecordCallback(input: { oid: string sendType: string @@ -135,6 +186,7 @@ async function sendAndRecordCallback(input: { eticketType?: string ext?: string preferredTotalGoodsValue?: number + source?: string }) { const eticketItems = input.etickets.map((e) => ({ id: String(e.id || ''), @@ -156,6 +208,7 @@ async function sendAndRecordCallback(input: { const totalGoodsValue = goodsValuePlan.totalGoodsValue logIntegration('[kuaishou-industry/send-code]', '准备发起电子凭证发货回调', { + source: input.source || 'send_code', oid: input.oid, sellerId: input.sellerId, sendType: input.sendType, @@ -202,6 +255,7 @@ async function sendAndRecordCallback(input: { if (!result.success) { logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调执行失败,发码请求已拒绝继续', { + source: input.source || 'send_code', oid: input.oid, error: result.error || '', response: result.response || null, @@ -210,6 +264,7 @@ async function sendAndRecordCallback(input: { } logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调执行完成', { + source: input.source || 'send_code', oid: input.oid, response: result.response || null, }) @@ -217,6 +272,20 @@ async function sendAndRecordCallback(input: { return result } +function resolveSendCodeCallbackParams( + voucher: KuaishouIndustryVoucherRow, + body: JsonObject, +): SendCodeCallbackParams { + return { + oid: String(body.oid || voucher.oid || '').trim(), + sendType: String(body.sendType || 'VIRTUAL').trim() || 'VIRTUAL', + sellerId: String(body.sellerId || voucher.seller_id || '').trim(), + token: String(body.token || voucher.token || '').trim(), + eticketType: String(body.eticketType || '').trim(), + ext: String(body.ext || '').trim(), + } +} + function resolveSendCallbackFailureMessage(result: Awaited>) { const error = String(result.error || '').trim() if (error) { diff --git a/apps/backend/src/types/admin/route-inputs.ts b/apps/backend/src/types/admin/route-inputs.ts index dce6dbae..f8652df3 100644 --- a/apps/backend/src/types/admin/route-inputs.ts +++ b/apps/backend/src/types/admin/route-inputs.ts @@ -24,6 +24,7 @@ import type { AdminNotificationConfigInput, AdminNotificationTestInput, AdminScheduledJobsConfigInput, + AdminTaskKuaishouIndustryConsumeInput, AdminTaskKuaishouCloudDispatchInput, AdminTaskManualDispatchInput, } from './write-inputs.js' @@ -54,6 +55,7 @@ export type AdminCloudtentaclesVirtualNumberRouteBody = AdminCloudtentaclesVirtu export type AdminCloudtentaclesFullFlowRouteBody = AdminCloudtentaclesFullFlowInput export type AdminTaskManualDispatchRouteBody = AdminTaskManualDispatchInput export type AdminTaskKuaishouCloudDispatchRouteBody = AdminTaskKuaishouCloudDispatchInput +export type AdminTaskKuaishouIndustryConsumeRouteBody = AdminTaskKuaishouIndustryConsumeInput export type AdminOrderRouteParams = { orderId?: string } export type AdminTaskRouteParams = { taskId?: string, bindingId?: string } diff --git a/apps/backend/src/types/admin/write-inputs.ts b/apps/backend/src/types/admin/write-inputs.ts index a2db9e0c..eee530f9 100644 --- a/apps/backend/src/types/admin/write-inputs.ts +++ b/apps/backend/src/types/admin/write-inputs.ts @@ -277,3 +277,11 @@ export type AdminTaskManualDispatchInput = { export type AdminTaskKuaishouCloudDispatchInput = { ticketCode?: string } + +export type AdminTaskKuaishouIndustryConsumeInput = { + consumeType?: string + storeName?: string + storeAddress?: string + expressCode?: string + expressNo?: string +} diff --git a/apps/frontend-vue/src/services/admin/orders.ts b/apps/frontend-vue/src/services/admin/orders.ts index 02de9ed5..90fdc551 100644 --- a/apps/frontend-vue/src/services/admin/orders.ts +++ b/apps/frontend-vue/src/services/admin/orders.ts @@ -1,4 +1,4 @@ -import { apiGet } from '@/lib/http' +import { apiGet, apiPost } from '@/lib/http' import type { AdminOrderDetail, AdminOrderListItem, AdminPagination } from '@/types/admin' export function fetchAdminOrders(params?: Record) { @@ -11,3 +11,12 @@ export function fetchAdminOrders(params?: Record) { export function fetchAdminOrderDetail(orderId: number | string) { return apiGet(`/api/v1/admin/orders/${orderId}`) } + +export function resendAdminOrderKuaishouIndustryVoucherCode(orderId: number | string) { + return apiPost<{ + success: boolean + resentCount: number + failedCount: number + errorMessage: string + }>(`/api/v1/admin/orders/${orderId}/kuaishou-industry/resend-code`, {}) +} diff --git a/apps/frontend-vue/src/services/admin/tasks.ts b/apps/frontend-vue/src/services/admin/tasks.ts index 4e715205..eb643e0d 100644 --- a/apps/frontend-vue/src/services/admin/tasks.ts +++ b/apps/frontend-vue/src/services/admin/tasks.ts @@ -91,3 +91,26 @@ export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | s {}, ) } + +export function resendAdminTaskKuaishouIndustryVoucherCode(taskId: number | string) { + return apiPost( + `/api/v1/admin/tasks/${taskId}/kuaishou-industry/resend-code`, + {}, + ) +} + +export function consumeAdminTaskKuaishouIndustryVoucher( + taskId: number | string, + payload: { + consumeType?: string + storeName?: string + storeAddress?: string + expressCode?: string + expressNo?: string + } = {}, +) { + return apiPost( + `/api/v1/admin/tasks/${taskId}/kuaishou-industry/consume`, + payload, + ) +} diff --git a/apps/frontend-vue/src/types/admin/orders.ts b/apps/frontend-vue/src/types/admin/orders.ts index c41d39aa..77c5e1a0 100644 --- a/apps/frontend-vue/src/types/admin/orders.ts +++ b/apps/frontend-vue/src/types/admin/orders.ts @@ -69,4 +69,19 @@ export interface AdminOrderDetail { spec: Record }> tasks: AdminTaskListItem[] + kuaishouIndustryVouchers: Array<{ + oid: string + voucherCode: string + unitIndex: number + status: string + sendCallbackStatus: string + sendCallbackAttemptCount: number + sendCallbackLastError: string + sendCallbackSentAt: string | null + consumedAt: string | null + destroyedAt: string | null + }> + operations: { + canResendKuaishouIndustryVoucherCode: boolean + } } diff --git a/apps/frontend-vue/src/types/admin/tasks.ts b/apps/frontend-vue/src/types/admin/tasks.ts index 20193cb5..84ff4f61 100644 --- a/apps/frontend-vue/src/types/admin/tasks.ts +++ b/apps/frontend-vue/src/types/admin/tasks.ts @@ -37,6 +37,8 @@ export interface AdminTaskOperations { canRebindKuaishouCloudRole: boolean canDispatchKuaishouCloudFulfillment: boolean canReturnKuaishouCloudFulfillment: boolean + canResendKuaishouIndustryVoucherCode: boolean + canConsumeKuaishouIndustryVoucher: boolean canViewSensitiveTaskData: boolean } @@ -201,6 +203,18 @@ export interface AdminTaskDetail { } notes: string } + kuaishouIndustryVoucher: null | { + oid: string + voucherCode: string + unitIndex: number + status: string + sendCallbackStatus: string + sendCallbackLastError: string + sendCallbackAttemptCount: number + sendCallbackSentAt: string | null + consumedAt: string | null + destroyedAt: string | null + } manualDispatch: null | { outcome: string deliveryReference: string diff --git a/apps/frontend-vue/src/views/admin/orders/AdminOrderDetailView.vue b/apps/frontend-vue/src/views/admin/orders/AdminOrderDetailView.vue index 09f48685..6e1c3192 100644 --- a/apps/frontend-vue/src/views/admin/orders/AdminOrderDetailView.vue +++ b/apps/frontend-vue/src/views/admin/orders/AdminOrderDetailView.vue @@ -3,27 +3,63 @@ import { onMounted, ref } from 'vue' import { useRoute } from 'vue-router' import AdminStatusTag from '@/components/admin/AdminStatusTag.vue' -import { fetchAdminOrderDetail } from '@/services/admin' +import { fetchAdminOrderDetail, resendAdminOrderKuaishouIndustryVoucherCode } from '@/services/admin' import type { AdminOrderDetail } from '@/types/admin' +import { showConfirm, showError, showSuccess } from '@/lib/feedback' import { formatAdminDateTime } from '@/utils/admin-time' const route = useRoute() const loading = ref(true) +const actionLoading = ref(false) const errorMessage = ref('') const detail = ref(null) type TagType = 'success' | 'primary' | 'warning' | 'danger' | 'info' -onMounted(async () => { +async function loadDetail() { + loading.value = true try { const response = await fetchAdminOrderDetail(String(route.params.orderId || '')) detail.value = response.data + errorMessage.value = '' } catch (error) { errorMessage.value = error instanceof Error ? error.message : '读取订单详情失败' } finally { loading.value = false } -}) +} + +onMounted(loadDetail) + +async function handleResendKuaishouIndustryCode() { + if (!detail.value) return + + try { + await showConfirm( + `确认重发订单 ${detail.value.order.platformOrderId} 的电子凭证发码回调吗?系统会复用当前核销码。`, + '确认操作', + { + type: 'warning', + confirmButtonText: '继续执行', + cancelButtonText: '取消', + }, + ) + } catch { + return + } + + actionLoading.value = true + try { + const response = await resendAdminOrderKuaishouIndustryVoucherCode(detail.value.order.orderId) + showSuccess(`电子凭证发码回调已重发,共 ${response.data.resentCount} 张`) + await loadDetail() + } catch (error) { + showError(error instanceof Error ? error.message : '重发电子凭证发码回调失败') + await loadDetail() + } finally { + actionLoading.value = false + } +} function formatJsonBlock(value: Record) { return JSON.stringify(value || {}, null, 2) @@ -171,6 +207,33 @@ function resolveOrderProgressTone(summary: AdminOrderDetail['order']['fulfillmen + +
+

电子凭证

+ + 重发发码回调 + +
+ + + + + + + + + + + + +
+

履约任务

@@ -240,6 +303,18 @@ function resolveOrderProgressTone(summary: AdminOrderDetail['order']['fulfillmen color: var(--text-primary); } +.section-headline { + display: flex; + justify-content: space-between; + gap: var(--space-3); + align-items: center; + margin-bottom: var(--space-4); +} + +.section-headline h3 { + margin: 0; +} + .order-hero { display: flex; justify-content: space-between; diff --git a/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue b/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue index 69fa3bdd..358763f1 100644 --- a/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue +++ b/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue @@ -49,6 +49,8 @@ const { submitKuaishouCloudDispatch, submitKuaishouCloudRefreshRoleInfo, submitKuaishouCloudReturnNumber, + submitKuaishouIndustryConsume, + submitKuaishouIndustryResendCode, } = useAdminTaskKuaishouCloud(detail, runAction) onMounted(loadDetail) @@ -93,6 +95,8 @@ onMounted(loadDetail) @prepare-kuaishou-cloud="submitKuaishouCloudPrepare" @dispatch-kuaishou-cloud="submitKuaishouCloudDispatch" @return-kuaishou-cloud="submitKuaishouCloudReturnNumber" + @resend-industry-code="submitKuaishouIndustryResendCode" + @consume-industry-voucher="submitKuaishouIndustryConsume" @mark-manual-review=" runAction( () => markAdminTaskManualReview(detail!.task.taskId), diff --git a/apps/frontend-vue/src/views/admin/tasks/components/AdminTaskActionBar.vue b/apps/frontend-vue/src/views/admin/tasks/components/AdminTaskActionBar.vue index d88163f2..66bf5e06 100644 --- a/apps/frontend-vue/src/views/admin/tasks/components/AdminTaskActionBar.vue +++ b/apps/frontend-vue/src/views/admin/tasks/components/AdminTaskActionBar.vue @@ -16,6 +16,8 @@ const emit = defineEmits<{ prepareKuaishouCloud: [] dispatchKuaishouCloud: [] returnKuaishouCloud: [] + resendIndustryCode: [] + consumeIndustryVoucher: [] markManualReview: [] submitManualDispatch: [outcome: 'delivered' | 'failed'] closeTask: [] @@ -70,6 +72,24 @@ const emit = defineEmits<{ > 退还号码 + + 重发发码回调 + + + 手动核销 + 核销码{{ flow.ticket.code || '-' }} +
+ 电子凭证状态{{ detail.kuaishouIndustryVoucher?.status || '-' }} +
+
+ 发码回调{{ detail.kuaishouIndustryVoucher?.sendCallbackStatus || '-' }} +
+
+ 回调次数{{ detail.kuaishouIndustryVoucher?.sendCallbackAttemptCount || 0 }} +
+
+ 回调时间{{ formatAdminDateTime(detail.kuaishouIndustryVoucher?.sendCallbackSentAt) }} +
+
+ 回调异常{{ detail.kuaishouIndustryVoucher?.sendCallbackLastError || '-' }} +
校验状态{{ flow.ticket.status || '-' }}
diff --git a/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskDetail.ts b/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskDetail.ts index 22677b78..95e7bcde 100644 --- a/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskDetail.ts +++ b/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskDetail.ts @@ -102,6 +102,7 @@ export function useAdminTaskDetail() { await loadDetail() } catch (error) { showError(error instanceof Error ? error.message : '操作失败') + await loadDetail() } finally { actionLoading.value = false } diff --git a/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskKuaishouCloud.ts b/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskKuaishouCloud.ts index e479bc80..0d36cf22 100644 --- a/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskKuaishouCloud.ts +++ b/apps/frontend-vue/src/views/admin/tasks/composables/useAdminTaskKuaishouCloud.ts @@ -1,9 +1,11 @@ import { computed, type Ref } from 'vue' import { + consumeAdminTaskKuaishouIndustryVoucher, dispatchAdminTaskKuaishouCloudFulfillment, prepareAdminTaskKuaishouCloudFulfillment, refreshAdminTaskKuaishouCloudRoleInfo, + resendAdminTaskKuaishouIndustryVoucherCode, returnNumberAdminTaskKuaishouCloudFulfillment, } from '@/services/admin' import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin' @@ -178,6 +180,30 @@ export function useAdminTaskKuaishouCloud( ) } + async function submitKuaishouIndustryResendCode() { + if (!detail.value) { + return + } + + await runAction( + () => resendAdminTaskKuaishouIndustryVoucherCode(detail.value!.task.taskId), + '电子凭证发码回调已重发', + `确认重发任务 ${detail.value.task.taskNo} 的电子凭证发码回调吗?系统会复用当前核销码,不会生成新码。`, + ) + } + + async function submitKuaishouIndustryConsume() { + if (!detail.value) { + return + } + + await runAction( + () => consumeAdminTaskKuaishouIndustryVoucher(detail.value!.task.taskId), + '电子凭证已执行核销', + `确认手动执行任务 ${detail.value.task.taskNo} 的电子凭证核销吗?请先确认快手侧已收到发码回调。`, + ) + } + return { kuaishouCloudFlow, kuaishouCloudRole, @@ -189,5 +215,7 @@ export function useAdminTaskKuaishouCloud( submitKuaishouCloudDispatch, submitKuaishouCloudRefreshRoleInfo, submitKuaishouCloudReturnNumber, + submitKuaishouIndustryResendCode, + submitKuaishouIndustryConsume, } }