优化快手电子凭证发码和手动处理

This commit is contained in:
yml2213
2026-07-08 17:28:01 +08:00
parent 647f038061
commit 3ad7c2e50e
17 changed files with 761 additions and 10 deletions
+30 -2
View File
@@ -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<string, unknown> : {},
}),
},
),
)
export default router
+37
View File
@@ -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
@@ -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<JsonRecord> {
export async function getAdminOrderDetail(
orderId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<JsonRecord> {
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<string, string>()
@@ -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<AdminTaskActionResponse> {
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<AdminTaskActionResponse> {
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<KuaishouIndustryVoucherRow> {
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<KuaishouIndustryVoucherRow[]> {
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
}
@@ -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<string, any>
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<ReturnType<typeof sendCallback>>) {
const error = String(result.error || '').trim()
if (error) {
@@ -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 }
@@ -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
}
+10 -1
View File
@@ -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<string, unknown>) {
@@ -11,3 +11,12 @@ export function fetchAdminOrders(params?: Record<string, unknown>) {
export function fetchAdminOrderDetail(orderId: number | string) {
return apiGet<AdminOrderDetail>(`/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`, {})
}
@@ -91,3 +91,26 @@ export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | s
{},
)
}
export function resendAdminTaskKuaishouIndustryVoucherCode(taskId: number | string) {
return apiPost<AdminTaskActionResponse>(
`/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<AdminTaskActionResponse>(
`/api/v1/admin/tasks/${taskId}/kuaishou-industry/consume`,
payload,
)
}
@@ -69,4 +69,19 @@ export interface AdminOrderDetail {
spec: Record<string, unknown>
}>
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
}
}
@@ -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
@@ -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<AdminOrderDetail | null>(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<string, unknown>) {
return JSON.stringify(value || {}, null, 2)
@@ -171,6 +207,33 @@ function resolveOrderProgressTone(summary: AdminOrderDetail['order']['fulfillmen
</dl>
</el-card>
<el-card v-if="detail.kuaishouIndustryVouchers.length > 0" shadow="never" class="section-card">
<div class="section-headline">
<h3>电子凭证</h3>
<el-button
v-if="detail.operations.canResendKuaishouIndustryVoucherCode"
:loading="actionLoading"
type="primary"
plain
@click="handleResendKuaishouIndustryCode"
>
重发发码回调
</el-button>
</div>
<el-table :data="detail.kuaishouIndustryVouchers" size="small">
<el-table-column prop="voucherCode" label="核销码" min-width="190" />
<el-table-column prop="status" label="券状态" width="110" />
<el-table-column prop="sendCallbackStatus" label="发码回调" width="120" />
<el-table-column label="回调时间" width="180">
<template #default="{ row }">{{ formatAdminDateTime(row.sendCallbackSentAt) }}</template>
</el-table-column>
<el-table-column prop="sendCallbackAttemptCount" label="次数" width="80" />
<el-table-column label="异常">
<template #default="{ row }">{{ row.sendCallbackLastError || '-' }}</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" class="section-card">
<h3>履约任务</h3>
<el-table :data="detail.tasks" size="small">
@@ -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;
@@ -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),
@@ -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<{
>
退还号码
</el-button>
<el-button
v-if="canManageTaskLifecycle && detail.operations.canResendKuaishouIndustryVoucherCode"
:loading="actionLoading"
round
@click="emit('resendIndustryCode')"
>
重发发码回调
</el-button>
<el-button
v-if="canManageTaskLifecycle && detail.operations.canConsumeKuaishouIndustryVoucher"
:loading="actionLoading"
round
type="warning"
plain
@click="emit('consumeIndustryVoucher')"
>
手动核销
</el-button>
<el-button
v-if="canManageTaskLifecycle"
:disabled="!detail.operations.canMarkManualReview"
@@ -300,6 +300,25 @@ function formatRebindBinding(
<div>
<span>核销码</span><strong>{{ flow.ticket.code || '-' }}</strong>
</div>
<div>
<span>电子凭证状态</span><strong>{{ detail.kuaishouIndustryVoucher?.status || '-' }}</strong>
</div>
<div>
<span>发码回调</span
><strong>{{ detail.kuaishouIndustryVoucher?.sendCallbackStatus || '-' }}</strong>
</div>
<div>
<span>回调次数</span
><strong>{{ detail.kuaishouIndustryVoucher?.sendCallbackAttemptCount || 0 }}</strong>
</div>
<div>
<span>回调时间</span
><strong>{{ formatAdminDateTime(detail.kuaishouIndustryVoucher?.sendCallbackSentAt) }}</strong>
</div>
<div>
<span>回调异常</span
><strong>{{ detail.kuaishouIndustryVoucher?.sendCallbackLastError || '-' }}</strong>
</div>
<div>
<span>校验状态</span><strong>{{ flow.ticket.status || '-' }}</strong>
</div>
@@ -102,6 +102,7 @@ export function useAdminTaskDetail() {
await loadDetail()
} catch (error) {
showError(error instanceof Error ? error.message : '操作失败')
await loadDetail()
} finally {
actionLoading.value = false
}
@@ -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,
}
}