行业电子凭证接入

- 新增 4 个回调接口:send-code/destroy-code/query-code/consume-code
- 支持快手开放平台 MD5/HMAC_SHA256 签名验签
- send-code 回调自动创建 order/order_items/fulfillment_tasks/profiles
- consume-code 回调与既有 flow.consume 结构兼容
- isKuaishouCloudTask 同时匹配 kuaishou-industry executor_key
- claim 页面自动验证行业凭证 task,跳过核销码输入
- 退回时行业 task 跳过快手核销 API
- 新增 KUASHOU_INDUSTRY_* 环境变量配置
This commit is contained in:
yml2213
2026-06-30 17:33:51 +08:00
parent 693f4de45a
commit f38efa2edf
16 changed files with 471 additions and 4 deletions
@@ -47,7 +47,13 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
const context = await getClaimContext(token)
const now = nowIso()
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
const executorKey = String(context.task.executor_key || '').trim()
if (executorKey === 'kuaishou-industry') {
return verifyIndustryeVoucherTicket(context, now)
}
if (executorKey !== 'kuaishou_ct_assisted') {
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
statusCode: 409,
errorCode: 'claim_not_kuaishou_cloud',
@@ -237,6 +243,96 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
return getKuaishouCloudClaimDetail(token)
}
async function verifyIndustryeVoucherTicket(
context: Awaited<ReturnType<typeof getClaimContext>>,
now: string,
) {
const taskContext = parseTaskContext(context.task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
if (flow.consume.status === 'success') {
return getKuaishouCloudClaimDetail(context.claimToken.token)
}
const industryContext = typeof taskContext === 'object' ? taskContext : {}
const token = String(industryContext.token || '').trim()
const certExpireType = Number(industryContext.certExpireType || 0)
const certActualStartTime = Number(industryContext.certActualStartTime || 0)
const certActualEndTime = Number(industryContext.certActualEndTime || 0)
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
ticket: {
...flow.ticket,
code: '',
status: 'verified',
capturedAt: flow.ticket.capturedAt || now,
capturedBy: flow.ticket.capturedBy || { source: 'send_code_callback' },
verifiedAt: now,
oid: context.order.platform_order_id,
formToken: token,
leftCount: 0,
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code || '',
},
consume: {
...flow.consume,
status: 'pending',
shopId: context.order.shop_id,
shopName: context.order.shop_name,
autoConsumeEnabled: true,
},
certInfo: {
certExpireType,
certActualStartTime,
certActualEndTime,
},
},
}
const taskWithTicket = (await updateTask(context.task.id, {
claim_token: context.claimToken.token,
claim_expires_at: context.claimToken.expired_at,
context_json: JSON.stringify(nextContext),
claimed_at: context.task.claimed_at || now,
task_status: TASK_STATUS.WAITING_BINDING,
user_action_status: 'pending_claim',
last_error: '',
updated_at: now,
})) || {
...context.task,
context_json: JSON.stringify(nextContext),
claimed_at: context.task.claimed_at || now,
task_status: TASK_STATUS.WAITING_BINDING,
user_action_status: 'pending_claim',
last_error: '',
updated_at: now,
}
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
await prepareKuaishouCloudFulfillmentTask(taskWithTicket, {
source: 'send_code_callback',
actor: { source: 'send_code_callback' },
})
}
await createTaskEvent(
context.task.id,
'kuaishou_cloud_ticket_verified',
{
ticketCodeMasked: '',
source: 'send_code_callback',
industryVoucher: true,
verifiedAt: now,
},
now,
)
return getKuaishouCloudClaimDetail(context.claimToken.token)
}
export async function getKuaishouCloudClaimGuideAssetPath(filename: unknown) {
const normalized = String(filename || '').trim()
if (!ALLOWED_GUIDE_FILES.has(normalized)) {
@@ -261,8 +357,18 @@ export async function getKuaishouCloudClaimDetail(token: unknown) {
const context = await getClaimContext(token)
let task = context.task
const executorKey = String(task.executor_key || '').trim()
if (executorKey === 'kuaishou-industry') {
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
if (flow.consume.status !== 'success') {
const now = nowIso()
await verifyIndustryeVoucherTicket(context, now).catch(() => null)
}
}
if (
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
executorKey === 'kuaishou_ct_assisted' &&
!isKuaishouCloudMockTask(task)
) {
task = (await syncKuaishouCloudRoleInfo(task)) || task