- syncAffiliateDashTaskStatus 新增 snapshotOnly:submit 拿到 delivered 后直接用本地快照收敛任务为 REDEEMED,不等平台回调(此前要等 delivered notify 落地,约 9 秒) - 新增终态保护:已 REDEEMED/MANUAL_REVIEW/CLOSED/FAILED 的任务,非 delivered 状态不再降级(轮询时平台详情短暂返回 paid/delivering 会把成功任务打回 REDEEMING) - submitAffiliateDashClaim:result.status=delivered 时重新读任务并 snapshotOnly 收敛
648 lines
20 KiB
TypeScript
648 lines
20 KiB
TypeScript
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||
import { getTaskById, updateTask } from '../../repositories/task-repo.js'
|
||
import { createHttpError } from '../../utils/http.js'
|
||
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
|
||
import { nowIso } from '../../utils/time.js'
|
||
import type { JsonObject } from '../../types/json.js'
|
||
import { TASK_STATUS } from '../../domain/task-status.js'
|
||
import {
|
||
confirmFulfillmentRole,
|
||
prepareFulfillmentBinding,
|
||
rebindFulfillmentRole,
|
||
redeemFulfillmentTask,
|
||
} from '../fulfillment/executors/registry.js'
|
||
import {
|
||
isKuaishouCloudMockTask,
|
||
normalizeKuaishouCloudFlow,
|
||
} from '../fulfillment/kuaishou-cloud/index.js'
|
||
import { syncKuaishouFeifeiTaskStatus } from '../fulfillment/kuaishou-feifei/index.js'
|
||
import {
|
||
normalizeAffiliateDashFlow,
|
||
syncAffiliateDashTaskStatus,
|
||
} from '../fulfillment/affiliate-dash/index.js'
|
||
import {
|
||
bindAffiliateDashDelivery,
|
||
getAffiliateDashBindResult,
|
||
submitAffiliateDashDelivery,
|
||
} from '../platforms/affiliate-dash/order-service.js'
|
||
import {
|
||
assertValidClaimUid,
|
||
canUpdateClaimUid,
|
||
getClaimIdentityFromTask,
|
||
} from './claim-identity.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'
|
||
|
||
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
|
||
|
||
function assertLewanClaimTask(task: TaskRow) {
|
||
if (String(task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||
throw createHttpError('当前领取链接不是 kuaishou-lewan 客户领取流程', {
|
||
statusCode: 409,
|
||
errorCode: 'claim_not_kuaishou_cloud',
|
||
})
|
||
}
|
||
}
|
||
|
||
async function requireExecutorAction<T>(
|
||
result: Promise<T | null> | T | null,
|
||
errorMessage: string,
|
||
errorCode: string,
|
||
): Promise<NonNullable<T>> {
|
||
const value = await result
|
||
if (!value) {
|
||
throw createHttpError(errorMessage, {
|
||
statusCode: 409,
|
||
errorCode,
|
||
})
|
||
}
|
||
return value
|
||
}
|
||
|
||
function isKuaishouCloudBindingReady(flow: ReturnType<typeof normalizeKuaishouCloudFlow>) {
|
||
return flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
|
||
}
|
||
|
||
/**
|
||
* 确保 Cloud 绑定资源已准备(取号 + 绑链)。
|
||
* 提前核销后仍可能未 prepare,不能因 consume.success 跳过。
|
||
*/
|
||
async function ensureKuaishouCloudBindingPrepared(
|
||
task: TaskRow,
|
||
options: { source: string; actorSource?: string } = { source: 'system_auto_prepare_binding' },
|
||
): Promise<TaskRow> {
|
||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||
if (isKuaishouCloudBindingReady(flow)) {
|
||
return task
|
||
}
|
||
|
||
try {
|
||
const prepared = await prepareFulfillmentBinding(task, {
|
||
source: options.source,
|
||
actor: { source: options.actorSource || options.source },
|
||
})
|
||
return prepared?.task || task
|
||
} catch {
|
||
return task
|
||
}
|
||
}
|
||
|
||
async function verifyIndustryVoucherTicket(
|
||
context: Awaited<ReturnType<typeof getClaimContext>>,
|
||
now: string,
|
||
): Promise<ClaimDetailPayload> {
|
||
const taskContext = parseTaskContext(context.task)
|
||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||
|
||
// 提前核销后 consume 已是 success,但仍可能未准备绑定资源;禁止整段短路。
|
||
// 注意:不要再调 getKuaishouCloudClaimDetail,避免 prepare 失败时递归。
|
||
if (flow.consume.status === 'success') {
|
||
const task = await ensureKuaishouCloudBindingPrepared(context.task, {
|
||
source: 'claim_after_early_consume_prepare',
|
||
actorSource: 'system',
|
||
})
|
||
return buildClaimDetailPayload({
|
||
claimToken: context.claimToken,
|
||
task,
|
||
order: context.order,
|
||
orderItem: context.orderItem,
|
||
})
|
||
}
|
||
|
||
const industryContext = typeof taskContext === 'object' ? taskContext : {}
|
||
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(
|
||
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: voucherCode,
|
||
status: 'verified',
|
||
capturedAt: flow.ticket.capturedAt || now,
|
||
capturedBy: flow.ticket.capturedBy || { source: 'send_code_callback' },
|
||
verifiedAt: now,
|
||
oid: voucherContext.oid || context.order.platform_order_id,
|
||
formToken: token,
|
||
leftCount: voucherContext.status === 'CONSUMED' ? 0 : 1,
|
||
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code || '',
|
||
},
|
||
consume: {
|
||
...flow.consume,
|
||
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,
|
||
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)
|
||
let taskAfterPrepare = taskWithTicket
|
||
if (!isKuaishouCloudBindingReady(preparedFlow)) {
|
||
taskAfterPrepare = await ensureKuaishouCloudBindingPrepared(taskWithTicket, {
|
||
source: 'send_code_callback',
|
||
actorSource: 'send_code_callback',
|
||
})
|
||
}
|
||
|
||
await createTaskEvent(
|
||
context.task.id,
|
||
'kuaishou_cloud_ticket_verified',
|
||
{
|
||
ticketCodeMasked: '',
|
||
source: 'send_code_callback',
|
||
industryVoucher: true,
|
||
verifiedAt: now,
|
||
},
|
||
now,
|
||
)
|
||
|
||
return buildClaimDetailPayload({
|
||
claimToken: context.claimToken,
|
||
task: taskAfterPrepare,
|
||
order: context.order,
|
||
orderItem: context.orderItem,
|
||
})
|
||
}
|
||
|
||
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 === 'affiliate_dash') {
|
||
task = (await syncAffiliateDashTaskStatus(task)) || task
|
||
task = (await refreshAffiliateDashBindState(task)) || task
|
||
}
|
||
|
||
if (executorKey === 'kuaishou-industry') {
|
||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||
// 已核销也要走 verify:内部会补 prepare 绑定资源
|
||
if (flow.consume.status !== 'success' || !isKuaishouCloudBindingReady(flow)) {
|
||
const now = nowIso()
|
||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
|
||
.catch(() => null)
|
||
if (prepared) {
|
||
return prepared
|
||
}
|
||
}
|
||
}
|
||
|
||
if (executorKey === 'kuaishou_ct_assisted') {
|
||
const taskContext = parseTaskContext(task)
|
||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||
const needsIndustryVoucherPrepare =
|
||
hasUsableIndustryVoucher(taskContext) &&
|
||
(
|
||
flow.ticket.status !== 'verified' ||
|
||
!isKuaishouCloudBindingReady(flow)
|
||
)
|
||
|
||
if (needsIndustryVoucherPrepare) {
|
||
const now = nowIso()
|
||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
|
||
.catch(() => null)
|
||
if (prepared) {
|
||
return prepared
|
||
}
|
||
}
|
||
|
||
// 兜底:不依赖 UID;凭证已确认但绑定未就绪时自动补 prepare(覆盖提前核销场景)
|
||
if (!isKuaishouCloudMockTask(task)) {
|
||
const latestFlow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||
if (
|
||
(latestFlow.ticket.status === 'verified' || hasUsableIndustryVoucher(parseTaskContext(task))) &&
|
||
!isKuaishouCloudBindingReady(latestFlow)
|
||
) {
|
||
task = await ensureKuaishouCloudBindingPrepared(task, {
|
||
source: 'claim_page_auto_binding_prepare',
|
||
actorSource: 'system',
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 已提交 UID 的 lewan 任务:详情查询时顺带刷新角色,便于前端轮询匹配状态
|
||
if (
|
||
executorKey === 'kuaishou_ct_assisted' &&
|
||
!isKuaishouCloudMockTask(task) &&
|
||
getClaimIdentityFromTask(task).expectedUid
|
||
) {
|
||
task = (await syncKuaishouCloudRoleInfo(task)) || task
|
||
}
|
||
|
||
return buildClaimDetailPayload({
|
||
claimToken: context.claimToken,
|
||
task,
|
||
order: context.order,
|
||
orderItem: context.orderItem,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 统一领取 Step1:提交游戏 UID。
|
||
*/
|
||
export async function submitClaimUid(
|
||
token: unknown,
|
||
payload: { uid?: unknown } = {},
|
||
): Promise<ClaimDetailPayload> {
|
||
const context = await getClaimContext(token)
|
||
const expectedUid = assertValidClaimUid(payload.uid)
|
||
const now = nowIso()
|
||
|
||
if (!canUpdateClaimUid(context.task)) {
|
||
throw createHttpError('当前任务状态不可修改 UID', {
|
||
statusCode: 409,
|
||
errorCode: 'claim_uid_locked',
|
||
})
|
||
}
|
||
|
||
const taskContext = parseTaskContext(context.task)
|
||
const previous = getClaimIdentityFromTask(context.task)
|
||
const nextContext = {
|
||
...taskContext,
|
||
claimIdentity: {
|
||
expectedUid,
|
||
submittedAt: previous.expectedUid === expectedUid && previous.submittedAt
|
||
? previous.submittedAt
|
||
: now,
|
||
source: 'claim_page',
|
||
},
|
||
}
|
||
|
||
let updatedTask =
|
||
(await updateTask(context.task.id, {
|
||
context_json: JSON.stringify(nextContext),
|
||
claimed_at: context.task.claimed_at || now,
|
||
last_error: '',
|
||
updated_at: now,
|
||
})) || {
|
||
...context.task,
|
||
context_json: JSON.stringify(nextContext),
|
||
claimed_at: context.task.claimed_at || now,
|
||
updated_at: now,
|
||
}
|
||
|
||
if (previous.expectedUid !== expectedUid) {
|
||
await createTaskEvent(
|
||
context.task.id,
|
||
'claim_uid_submitted',
|
||
{
|
||
expectedUid,
|
||
previousUid: previous.expectedUid || '',
|
||
source: 'claim_page',
|
||
},
|
||
now,
|
||
)
|
||
}
|
||
|
||
const executorKey = String(updatedTask.executor_key || '').trim()
|
||
if (executorKey === 'kuaishou_ct_assisted' && !isKuaishouCloudMockTask(updatedTask)) {
|
||
try {
|
||
const prepared = await prepareFulfillmentBinding(updatedTask, {
|
||
source: 'claim_page_submit_uid',
|
||
actor: { source: 'claim_page' },
|
||
})
|
||
if (prepared?.task) {
|
||
updatedTask = prepared.task
|
||
}
|
||
} catch {
|
||
// 绑定资源准备失败时仍保留 UID,详情页可继续重试/轮询
|
||
}
|
||
}
|
||
|
||
if (executorKey === 'affiliate_dash') {
|
||
updatedTask = (await bindAffiliateDashClaimForTask(updatedTask, expectedUid)) || updatedTask
|
||
}
|
||
|
||
return getKuaishouCloudClaimDetail(token)
|
||
}
|
||
|
||
/**
|
||
* affiliate_dash 领取 Step2:提交发货(绑定完成后)。
|
||
*/
|
||
export async function submitAffiliateDashClaim(
|
||
token: unknown,
|
||
payload: { gameAccount?: unknown; bindUuid?: unknown } = {},
|
||
): Promise<ClaimDetailPayload> {
|
||
const context = await getClaimContext(token)
|
||
const task = context.task
|
||
const executorKey = String(task.executor_key || '').trim()
|
||
if (executorKey !== 'affiliate_dash') {
|
||
throw createHttpError('当前领取链接不是 affiliate-dash 领取流程', {
|
||
statusCode: 409,
|
||
errorCode: 'claim_not_affiliate_dash',
|
||
})
|
||
}
|
||
|
||
const taskContext = parseTaskContextValue(task)
|
||
const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash)
|
||
if (!flow.orderNo) {
|
||
throw createHttpError('affiliate-dash 订单尚未创建', {
|
||
statusCode: 409,
|
||
errorCode: 'affiliate_dash_order_missing',
|
||
})
|
||
}
|
||
|
||
const gameAccount = String(payload.gameAccount || flow.gameAccount || '').trim()
|
||
const bindUuid = String(payload.bindUuid || flow.bindUuid || '').trim()
|
||
if (!gameAccount || !bindUuid) {
|
||
throw createHttpError('缺少绑定账号或 bindUuid', {
|
||
statusCode: 400,
|
||
errorCode: 'affiliate_dash_submit_missing',
|
||
})
|
||
}
|
||
|
||
const now = nowIso()
|
||
const isMock = Boolean(flow.mock?.enabled)
|
||
const result = isMock
|
||
? {
|
||
status: 'delivered',
|
||
message: '开发 mock 已模拟提交发货',
|
||
providerOrderNo: `MOCKAD-${flow.orderNo}`,
|
||
}
|
||
: await submitAffiliateDashDelivery({
|
||
orderNo: flow.orderNo,
|
||
gameAccount,
|
||
bindUuid,
|
||
})
|
||
|
||
const nextFlow = {
|
||
...flow,
|
||
gameAccount,
|
||
bindUuid,
|
||
submitStatus: result.status,
|
||
orderStatus: result.status || flow.orderStatus,
|
||
}
|
||
|
||
await updateTask(task.id, {
|
||
task_status: TASK_STATUS.REDEEMING,
|
||
context_json: JSON.stringify({
|
||
...taskContext,
|
||
affiliateDash: nextFlow,
|
||
}),
|
||
result_code: 'affiliate_dash_submitted',
|
||
result_message: result.message || 'affiliate-dash 已提交发货',
|
||
updated_at: now,
|
||
})
|
||
await createTaskEvent(
|
||
task.id,
|
||
'affiliate_dash_submitted',
|
||
{
|
||
orderNo: flow.orderNo,
|
||
submitStatus: result.status,
|
||
providerOrderNo: result.providerOrderNo,
|
||
},
|
||
now,
|
||
)
|
||
|
||
// 平台已返回 delivered:立即用本地快照收敛任务为成功,不等平台回调(避免结果页等数秒)。
|
||
// delivered 回调到达时 sync 幂等短路(已 REDEEMED 且 delivered 仍会刷新,无副作用)。
|
||
if (result.status === 'delivered') {
|
||
const refreshedTask = await getTaskById(task.id)
|
||
if (refreshedTask) {
|
||
await syncAffiliateDashTaskStatus(refreshedTask, { snapshotOnly: true })
|
||
}
|
||
}
|
||
|
||
return getKuaishouCloudClaimDetail(token)
|
||
}
|
||
|
||
/**
|
||
* 提交 UID 后触发 affiliate-dash 绑定(bind),把 bind_uuid / 二维码写回上下文,
|
||
* task → waiting_binding(link_generated → waiting_binding 合法)。
|
||
*/
|
||
async function bindAffiliateDashClaimForTask(task: TaskRow, gameAccount: string) {
|
||
const taskContext = parseTaskContextValue(task)
|
||
const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash)
|
||
if (!flow.orderNo || !gameAccount) {
|
||
return task
|
||
}
|
||
|
||
const now = nowIso()
|
||
const bindResult = await bindAffiliateDashDelivery({
|
||
orderNo: flow.orderNo,
|
||
gameAccount,
|
||
})
|
||
const nextFlow = {
|
||
...flow,
|
||
bindUuid: bindResult.bindUuid,
|
||
bindUrl: bindResult.bindUrl,
|
||
qrUrl: bindResult.qrUrl,
|
||
gameAccount,
|
||
}
|
||
|
||
const updatedTask = await updateTask(task.id, {
|
||
task_status: TASK_STATUS.WAITING_BINDING,
|
||
context_json: JSON.stringify({
|
||
...taskContext,
|
||
affiliateDash: nextFlow,
|
||
}),
|
||
updated_at: now,
|
||
})
|
||
await createTaskEvent(
|
||
task.id,
|
||
'affiliate_dash_bind_created',
|
||
{
|
||
orderNo: flow.orderNo,
|
||
bindUuid: bindResult.bindUuid,
|
||
gameAccount,
|
||
},
|
||
now,
|
||
)
|
||
|
||
return updatedTask || task
|
||
}
|
||
|
||
/**
|
||
* 轮询绑定状态:详情查询时若有 bind_uuid,拉取 affiliate-dash bind-result 实时刷新
|
||
* bound / 绑定账号 / mismatch 到上下文(可重入,拉取失败不阻塞详情)。
|
||
*/
|
||
async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | null> {
|
||
const taskContext = parseTaskContextValue(task)
|
||
const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash)
|
||
if (flow.mock?.enabled) {
|
||
// dev-mock:不请求 affiliate-dash 平台,绑定状态以 context 为准
|
||
return task
|
||
}
|
||
if (!flow.orderNo || !flow.bindUuid) {
|
||
return task
|
||
}
|
||
|
||
try {
|
||
const result = await getAffiliateDashBindResult({
|
||
orderNo: flow.orderNo,
|
||
bindUuid: flow.bindUuid,
|
||
})
|
||
const nextFlow = {
|
||
...flow,
|
||
bound: result.bound,
|
||
boundAccount: result.gameAccount || flow.boundAccount,
|
||
gameChannel: result.gameChannel || flow.gameChannel,
|
||
bindMismatch: result.mismatch,
|
||
}
|
||
const now = nowIso()
|
||
|
||
return (
|
||
(await updateTask(task.id, {
|
||
context_json: JSON.stringify({
|
||
...taskContext,
|
||
affiliateDash: nextFlow,
|
||
}),
|
||
updated_at: now,
|
||
})) || task
|
||
)
|
||
} catch {
|
||
// 绑定状态拉取失败:保持现状,前端可继续轮询
|
||
return task
|
||
}
|
||
}
|
||
|
||
export async function rebindKuaishouCloudClaimRole(token: unknown) {
|
||
const context = await getClaimContext(token)
|
||
assertLewanClaimTask(context.task)
|
||
|
||
await requireExecutorAction(
|
||
rebindFulfillmentRole(context.task, {
|
||
source: 'claim_page_rebind_role',
|
||
actor: { source: 'claim_page' },
|
||
}),
|
||
'当前领取链接不支持换绑角色',
|
||
'claim_rebind_not_supported',
|
||
)
|
||
|
||
return getKuaishouCloudClaimDetail(token)
|
||
}
|
||
|
||
export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||
const context = await getClaimContext(token)
|
||
assertLewanClaimTask(context.task)
|
||
|
||
await requireExecutorAction(
|
||
confirmFulfillmentRole(context.task, {
|
||
source: 'claim_page_role_confirm',
|
||
actor: { source: 'claim_page' },
|
||
errorCodePrefix: 'claim_kuaishou_cloud',
|
||
forceProbe: true,
|
||
}),
|
||
'当前领取链接不支持确认角色',
|
||
'claim_confirm_not_supported',
|
||
)
|
||
|
||
return getKuaishouCloudClaimDetail(token)
|
||
}
|
||
|
||
/**
|
||
* 领取页一键兑换:鉴权后交给 lewan 履约引擎 redeem 用例。
|
||
*/
|
||
export async function redeemKuaishouCloudClaim(token: unknown) {
|
||
const context = await getClaimContext(token)
|
||
assertLewanClaimTask(context.task)
|
||
|
||
await requireExecutorAction(
|
||
redeemFulfillmentTask(context.task, {
|
||
source: 'claim_page_redeem',
|
||
actor: { source: 'claim_page' },
|
||
autoFinalize: true,
|
||
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
|
||
}),
|
||
'当前领取链接不支持一键兑换',
|
||
'claim_redeem_not_supported',
|
||
)
|
||
|
||
return getKuaishouCloudClaimDetail(token)
|
||
}
|
||
|
||
function parseTaskContext(task: Partial<TaskRow> | null | undefined): JsonObject {
|
||
return parseTaskContextValue(task)
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|