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

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
@@ -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
}