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

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
@@ -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) {