新增 affiliate_dash 统一领取页流程(阶段 4)
- 后端:claim 详情分流 affiliate_dash 分支 + 详情轮询刷新 bind-result - 输 UID 触发 bind(拿 bindUuid/二维码, task→waiting_binding) - 新增 POST /claim/:token/affiliate-dash/submit(submit→redeeming + 事件) - 前端:ClaimAffiliateDashSteps(二维码/绑定状态/mismatch警告/提交发货) - claim-snapshot/claim-poll 按 flowType 分流(step2 绑定中/step3 已绑定待提交/step4 结果) - 前后端 typecheck + 后端 212 测试通过
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
getKuaishouCloudClaimDetail,
|
||||
rebindKuaishouCloudClaimRole,
|
||||
redeemKuaishouCloudClaim,
|
||||
submitAffiliateDashClaim,
|
||||
submitClaimUid,
|
||||
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js'
|
||||
@@ -43,6 +44,16 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/:token/affiliate-dash/submit',
|
||||
claimWriteRateLimit,
|
||||
createRouteHandler((req) => submitAffiliateDashClaim(req.params.token, req.body || {}), {
|
||||
successMessage: '发货已提交',
|
||||
errorMessage: '提交发货失败',
|
||||
scope: '[claims/:token/affiliate-dash/submit]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/:token/kuaishou-cloud/confirm-role',
|
||||
claimWriteRateLimit,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TASK_STATUS, isTaskFinalStatus } from '../../domain/task-status.js'
|
||||
import { buildClaimUrl } from './claim-service.js'
|
||||
import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js'
|
||||
import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import { normalizeAffiliateDashFlow } from '../fulfillment/affiliate-dash/index.js'
|
||||
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
||||
|
||||
@@ -91,6 +92,10 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
|
||||
return buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem })
|
||||
}
|
||||
|
||||
if (String(task.executor_key || '').trim() === 'affiliate_dash') {
|
||||
return buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem })
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(taskContext.claimIdentity)
|
||||
const kuaishouCloudSource = resolveClaimKuaishouCloudSource(task)
|
||||
@@ -152,6 +157,76 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
|
||||
}
|
||||
}
|
||||
|
||||
function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
const context = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||
const flow = normalizeAffiliateDashFlow(context.affiliateDash)
|
||||
const productTitle = String(
|
||||
flow.productName || orderItem.sku_name || orderItem.sku_code || '',
|
||||
).trim()
|
||||
const product = {
|
||||
title: productTitle,
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
isBundle: false,
|
||||
items: [{
|
||||
cloudSkuId: 0,
|
||||
name: productTitle,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
}],
|
||||
}
|
||||
|
||||
return {
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: 'affiliate_dash',
|
||||
claimIdentity,
|
||||
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: productTitle,
|
||||
quantity: orderItem.quantity,
|
||||
},
|
||||
product,
|
||||
session: null as null,
|
||||
kuaishouCloudFulfillment: null as null,
|
||||
kuaishouFeifei: null as null,
|
||||
affiliateDash: flow,
|
||||
result: task.redeemed_at
|
||||
? {
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
const context = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||
|
||||
@@ -16,6 +16,15 @@ import {
|
||||
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,
|
||||
@@ -221,6 +230,11 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
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 绑定资源
|
||||
@@ -357,9 +371,173 @@ export async function submitClaimUid(
|
||||
}
|
||||
}
|
||||
|
||||
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 result = 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,
|
||||
)
|
||||
|
||||
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.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)
|
||||
|
||||
@@ -240,6 +240,10 @@ export type AffiliateDashFlow = {
|
||||
gameAccount: string
|
||||
expectedGameAccount: string
|
||||
bindMismatch: boolean
|
||||
/** 是否已在 affiliate-dash 侧完成绑定(bind-result.bound) */
|
||||
bound: boolean
|
||||
boundAccount: string
|
||||
gameChannel: string
|
||||
submitStatus: string
|
||||
consumeStatus: string
|
||||
lastSyncedAt: unknown
|
||||
@@ -270,6 +274,9 @@ export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow {
|
||||
gameAccount: String(source.gameAccount || '').trim(),
|
||||
expectedGameAccount: String(source.expectedGameAccount || '').trim(),
|
||||
bindMismatch: Boolean(source.bindMismatch),
|
||||
bound: Boolean(source.bound),
|
||||
boundAccount: String(source.boundAccount || '').trim(),
|
||||
gameChannel: String(source.gameChannel || '').trim(),
|
||||
submitStatus: String(source.submitStatus || '').trim(),
|
||||
consumeStatus: String(source.consumeStatus || 'pending').trim(),
|
||||
lastSyncedAt: source.lastSyncedAt || null,
|
||||
|
||||
Reference in New Issue
Block a user