接入多发货平台与电子凭证

This commit is contained in:
yml2213
2026-07-07 12:06:19 +08:00
parent c101b5f359
commit ddfa6c278f
35 changed files with 2627 additions and 365 deletions
@@ -87,6 +87,10 @@ export async function getClaimContext(token: unknown) {
}
export function buildClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
if (String(task.executor_key || '').trim() === 'kuaishou_feifei') {
return buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem })
}
const kuaishouCloudSource = resolveClaimKuaishouCloudSource(task)
const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order)
const displaySkuName = resolveClaimOrderItemDisplaySkuName(
@@ -133,6 +137,7 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
product,
session: null as null,
kuaishouCloudFulfillment,
kuaishouFeifei: null as null,
result: task.redeemed_at
? {
resultCode: String(task.result_code || ''),
@@ -144,6 +149,93 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
}
}
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
const context = parseTaskContext(task)
const flow = mapClaimKuaishouFeifeiFulfillment(context.kuaishouFeifei)
const product = {
title: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
skuCode: String(orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
isBundle: false,
items: [{
cloudSkuId: 0,
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}],
}
return {
tokenStatus: claimToken.status,
claimUrl: buildClaimUrl(claimToken.token),
flowType: 'kuaishou_feifei',
task: {
taskId: task.id,
taskNo: task.task_no,
status: task.task_status,
executorKey: task.executor_key || '',
requiresSupportReview: false,
expiresAt: claimToken.expired_at,
claimedAt: task.claimed_at,
roleConfirmedAt: task.role_confirmed_at,
redeemedAt: task.redeemed_at,
loginType: task.login_type,
lastError: task.last_error,
runtimeSessionId: task.runtime_session_id,
},
order: {
orderId: order.id,
platform: order.platform,
platformOrderId: order.platform_order_id,
payStatus: order.pay_status,
orderStatus: order.order_status,
totalAmount: formatFenToAmount(order.total_amount),
totalAmountFen: normalizeFen(order.total_amount),
currency: order.currency,
},
orderItem: {
orderItemId: orderItem.id,
skuCode: orderItem.sku_code,
skuName: product.title,
quantity: orderItem.quantity,
},
product,
session: null as null,
kuaishouCloudFulfillment: null as null,
kuaishouFeifei: flow,
result: task.redeemed_at
? {
resultCode: String(task.result_code || ''),
resultMessage: String(task.result_message || ''),
screenshotReady: false,
screenshotUrl: '',
}
: null,
}
}
function mapClaimKuaishouFeifeiFulfillment(value: unknown) {
const source = isPlainObject(value) ? value : {}
const h5 = isPlainObject(source.h5) ? source.h5 : {}
return {
flowType: 'kuaishou_feifei',
productCode: String(source.productCode || '').trim(),
productName: String(source.productName || '').trim(),
platformOrderNo: String(source.platformOrderNo || '').trim(),
orderNo: String(source.orderNo || '').trim(),
rechargeStatus: Number(source.rechargeStatus || 0) || 0,
rechargeStatusLabel: String(source.rechargeStatusLabel || '').trim(),
rechargeResultMessage: String(source.rechargeResultMessage || '').trim(),
claimUrl: String(source.claimUrl || '').trim(),
consumeStatus: String(source.consumeStatus || 'pending').trim(),
h5: {
entryUrl: String(h5.entryUrl || '').trim(),
rechargeUrl: String(h5.rechargeUrl || '').trim(),
},
lastSyncedAt: source.lastSyncedAt || null,
}
}
function resolveClaimOrderItemDisplaySkuName(
orderItem: OrderItemRow,
kuaishouCloudFulfillment: JsonObject | null,
@@ -34,6 +34,7 @@ import {
rebindKuaishouCloudTaskRole,
refreshKuaishouCloudTaskRoleInfo,
} from '../fulfillment/kuaishou-cloud/index.js'
import { syncKuaishouFeifeiTaskStatus } from '../fulfillment/kuaishou-feifei/index.js'
import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim-context.js'
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
import type { TaskRow } from '../../types/repository/rows.js'
@@ -42,6 +43,7 @@ const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
type JsonObject = Record<string, any>
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: JsonObject = {}) {
const context = await getClaimContext(token)
@@ -50,7 +52,7 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
const executorKey = String(context.task.executor_key || '').trim()
if (executorKey === 'kuaishou-industry') {
return verifyIndustryeVoucherTicket(context, now)
return verifyIndustryVoucherTicket(context, now)
}
if (executorKey !== 'kuaishou_ct_assisted') {
@@ -61,6 +63,9 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
}
const flow = normalizeKuaishouCloudFlow(parseTaskContext(context.task).kuaishouCloudFulfillment)
if (hasUsableIndustryVoucher(parseTaskContext(context.task))) {
return verifyIndustryVoucherTicket(context, now)
}
const ticketCode = String(payload.ticketCode || payload.eTicketId || '').trim()
if (!ticketCode) {
@@ -243,10 +248,10 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
return getKuaishouCloudClaimDetail(token)
}
async function verifyIndustryeVoucherTicket(
async function verifyIndustryVoucherTicket(
context: Awaited<ReturnType<typeof getClaimContext>>,
now: string,
) {
): Promise<ClaimDetailPayload> {
const taskContext = parseTaskContext(context.task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
@@ -255,33 +260,58 @@ async function verifyIndustryeVoucherTicket(
}
const industryContext = typeof taskContext === 'object' ? taskContext : {}
const token = String(industryContext.token || '').trim()
const voucherContext = normalizeIndustryVoucherContext(industryContext.kuaishouIndustryVoucher)
if (voucherContext.status === 'DESTROYED') {
throw createHttpError('当前电子凭证已销毁,无法继续领取', {
statusCode: 409,
errorCode: 'claim_kuaishou_industry_voucher_destroyed',
})
}
const token = String(voucherContext.token || industryContext.token || '').trim()
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
const certExpireType = Number(industryContext.certExpireType || 0)
const certActualStartTime = Number(industryContext.certActualStartTime || 0)
const certActualEndTime = Number(industryContext.certActualEndTime || 0)
const certActualStartTime = Number(
voucherContext.validStartTime || industryContext.certActualStartTime || 0,
)
const certActualEndTime = Number(
voucherContext.validEndTime || industryContext.certActualEndTime || 0,
)
const nextContext = {
...taskContext,
kuaishouIndustryVoucher: {
...voucherContext,
oid: voucherContext.oid || context.order.platform_order_id,
token,
eticketId: voucherCode,
voucherCode,
status: voucherContext.status || 'UNUSED',
verifiedAt: voucherContext.verifiedAt || now,
},
kuaishouCloudFulfillment: {
...flow,
ticket: {
...flow.ticket,
code: '',
code: voucherCode,
status: 'verified',
capturedAt: flow.ticket.capturedAt || now,
capturedBy: flow.ticket.capturedBy || { source: 'send_code_callback' },
verifiedAt: now,
oid: context.order.platform_order_id,
oid: voucherContext.oid || context.order.platform_order_id,
formToken: token,
leftCount: 0,
leftCount: voucherContext.status === 'CONSUMED' ? 0 : 1,
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code || '',
},
consume: {
...flow.consume,
status: 'pending',
status: voucherContext.status === 'CONSUMED' ? 'success' : 'pending',
shopId: context.order.shop_id,
shopName: context.order.shop_name,
autoConsumeEnabled: true,
consumedAt: voucherContext.status === 'CONSUMED'
? voucherContext.consumedAt || flow.consume.consumedAt || now
: flow.consume.consumedAt,
},
certInfo: {
certExpireType,
@@ -353,17 +383,41 @@ export async function getKuaishouCloudClaimGuideAssetPath(filename: unknown) {
return filePath
}
export async function getKuaishouCloudClaimDetail(token: unknown) {
export async function getKuaishouCloudClaimDetail(token: unknown): Promise<ClaimDetailPayload> {
const context = await getClaimContext(token)
let task = context.task
const executorKey = String(task.executor_key || '').trim()
if (executorKey === 'kuaishou_feifei') {
task = (await syncKuaishouFeifeiTaskStatus(task)) || task
}
if (executorKey === 'kuaishou-industry') {
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
if (flow.consume.status !== 'success') {
const now = nowIso()
await verifyIndustryeVoucherTicket(context, now).catch(() => null)
await verifyIndustryVoucherTicket(context, now).catch(() => null)
}
}
if (executorKey === 'kuaishou_ct_assisted') {
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const needsIndustryVoucherPrepare =
hasUsableIndustryVoucher(taskContext) &&
(
flow.ticket.status !== 'verified' ||
(flow.binding.prepareStatus !== 'ready' && !flow.binding.bindUrl)
)
if (needsIndustryVoucherPrepare) {
const now = nowIso()
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
.catch(() => null)
if (prepared) {
return prepared
}
}
}
@@ -417,6 +471,35 @@ function parseTaskContext(task: Partial<TaskRow> | null | undefined): JsonObject
}
}
function hasUsableIndustryVoucher(context: JsonObject = {}) {
const voucher = normalizeIndustryVoucherContext(context.kuaishouIndustryVoucher)
if (!voucher.voucherCode && !voucher.eticketId) {
return false
}
return voucher.status !== 'DESTROYED'
}
function normalizeIndustryVoucherContext(value: unknown): JsonObject {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
const status = String(source.status || 'UNUSED').trim().toUpperCase()
return {
...source,
oid: String(source.oid || '').trim(),
token: String(source.token || '').trim(),
eticketId: String(source.eticketId || source.voucherCode || '').trim(),
voucherCode: String(source.voucherCode || source.eticketId || '').trim(),
status: status === 'CONSUMED' || status === 'DESTROYED' ? status : 'UNUSED',
validStartTime: Number(source.validStartTime || 0) || 0,
validEndTime: Number(source.validEndTime || 0) || 0,
verifiedAt: source.verifiedAt || null,
consumedAt: source.consumedAt || null,
}
}
async function queryKuaishouEticketConsumeDetailWithShopFallback({
ticketCode,
shopId,