新增 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:
yml2213
2026-08-05 15:56:51 +08:00
parent 3284fed7a1
commit 5ef1ed6018
11 changed files with 614 additions and 9 deletions
@@ -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_bindinglink_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)