新增 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
+11
View File
@@ -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_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)
@@ -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,
@@ -0,0 +1,161 @@
import { CheckCircleOutlined, LinkOutlined } from '@ant-design/icons'
import { Alert, Button, Card, Typography } from 'antd'
import type {
ClaimAffiliateDashFlowInfo,
ClaimOrderInfo,
ClaimProductInfo,
} from '@/types/claim'
import type { ClaimSnapshot } from './claim-snapshot'
import { InfoTile, UidEditRow } from './claim-shared'
export function AffiliateDashClaimPanel({
affiliateDash,
expectedUid,
order,
product,
taskLastError,
qrCodeDataUrl,
submittingUid,
submittingSubmit,
uidInput,
onUidChange,
onResubmitUid,
onSubmit,
onOpenBindUrl,
}: {
affiliateDash: ClaimAffiliateDashFlowInfo
expectedUid: string
order: ClaimOrderInfo | null
product: ClaimProductInfo | null
taskLastError?: string
qrCodeDataUrl: string
submittingUid: boolean
submittingSubmit: boolean
uidInput: string
onUidChange: (value: string) => void
onResubmitUid: () => void
onSubmit: () => void
onOpenBindUrl: () => void
}) {
const bindReady = Boolean(affiliateDash.bindUrl || affiliateDash.qrUrl)
const bound = Boolean(affiliateDash.bound)
const bindMismatch = Boolean(affiliateDash.bindMismatch)
return (
<Card className="claim-content-card">
<div className="claim-feifei-main">
<span className="claim-feifei-label">affiliate-dash</span>
<Typography.Title level={2}>
{bound ? '第 3 步:确认并提交发货' : '第 2 步:绑定领取账号'}
</Typography.Title>
<p>{bound ? '账号绑定成功,确认无误后提交发货' : '打开绑定链接完成绑定,系统将自动确认'}</p>
</div>
<UidEditRow
uidInput={uidInput}
submitting={submittingUid}
onChange={onUidChange}
onSubmit={onResubmitUid}
/>
<div className="claim-info-grid">
<InfoTile label="填写 UID" value={expectedUid || '-'} />
<InfoTile label="订单号" value={order?.platformOrderId || '-'} />
<InfoTile label="affiliate-dash 单号" value={affiliateDash.orderNo || '-'} />
<InfoTile label="领取商品" value={product?.title || affiliateDash.productName || '-'} />
<InfoTile label="金额" value={`${affiliateDash.amount} ${affiliateDash.currency}`.trim() || '-'} />
</div>
{taskLastError ? (
<Alert
type="warning"
showIcon
message="上次操作遇到问题"
description={taskLastError}
style={{ marginBottom: 16 }}
/>
) : null}
{bindMismatch ? (
<Alert
type="warning"
showIcon
message="绑定账号与填写 UID 不一致"
description={`已绑定 ${affiliateDash.boundAccount || '-'},请确认绑定的正是你填写的 UID${expectedUid || '-'}`}
style={{ marginBottom: 16 }}
/>
) : null}
{bound ? (
<Alert
type="success"
showIcon
icon={<CheckCircleOutlined />}
message={`已绑定账号:${affiliateDash.boundAccount || affiliateDash.gameAccount || '-'}`}
description={affiliateDash.gameChannel ? `渠道:${affiliateDash.gameChannel}` : undefined}
style={{ marginBottom: 16 }}
/>
) : null}
{!bound ? (
<div className="claim-bind-area" style={{ marginBottom: 16 }}>
{qrCodeDataUrl ? (
<img
src={qrCodeDataUrl}
alt="绑定二维码"
style={{ width: 180, height: 180, borderRadius: 8 }}
/>
) : null}
{bindReady ? (
<Button
type="primary"
icon={<LinkOutlined />}
onClick={onOpenBindUrl}
style={{ marginLeft: 16 }}
>
</Button>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
) : (
<Button
type="primary"
size="large"
loading={submittingSubmit}
disabled={submittingSubmit}
onClick={onSubmit}
style={{ marginBottom: 16 }}
>
</Button>
)}
</Card>
)
}
export function AffiliateDashResultStep({
snapshot,
}: {
snapshot: ClaimSnapshot
}) {
const affiliateDash = snapshot.affiliateDash
return (
<Card className="claim-content-card claim-result-card">
<div className="claim-result-title">{snapshot.resultTitle}</div>
<div className="claim-result-description">{snapshot.resultDescription}</div>
{affiliateDash ? (
<div className="claim-info-grid" style={{ marginTop: 24 }}>
<InfoTile label="领取商品" value={affiliateDash.productName || '-'} />
<InfoTile label="affiliate-dash 单号" value={affiliateDash.orderNo || '-'} />
<InfoTile label="订单状态" value={affiliateDash.orderStatus || '-'} />
<InfoTile label="发货状态" value={affiliateDash.submitStatus || '-'} />
<InfoTile label="金额" value={`${affiliateDash.amount} ${affiliateDash.currency}`.trim() || '-'} />
</div>
) : null}
</Card>
)
}
+51 -2
View File
@@ -10,10 +10,12 @@ import {
fetchClaimDetail,
rebindKuaishouCloudClaimRole,
redeemKuaishouCloudClaim,
submitAffiliateDashClaim,
submitClaimUid,
} from '@/services/claim'
import type { ClaimDetailData } from '@/types/claim'
import { AffiliateDashClaimPanel, AffiliateDashResultStep } from './ClaimAffiliateDashSteps'
import { FeifeiClaimPanel, FeifeiResultStep } from './ClaimFeifeiSteps'
import { ClaimHeaderCard } from './ClaimHeaderCard'
import { ClaimResultStep, KuaishouCloudClaimSteps } from './ClaimLewanSteps'
@@ -32,6 +34,7 @@ export default function ClaimPage() {
const [confirmingRole, setConfirmingRole] = useState(false)
const [rebindingRole, setRebindingRole] = useState(false)
const [redeeming, setRedeeming] = useState(false)
const [submittingSubmit, setSubmittingSubmit] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [detail, setDetail] = useState<ClaimDetailData | null>(null)
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('')
@@ -81,7 +84,11 @@ export default function ClaimPage() {
setUidInput(expectedUid)
}
await generateQRCode(
String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim(),
String(
nextDetail.kuaishouCloudFulfillment?.binding.bindUrl ||
nextDetail.affiliateDash?.bindUrl ||
'',
).trim(),
)
},
[generateQRCode],
@@ -172,6 +179,28 @@ export default function ClaimPage() {
}
}
async function handleSubmitAffiliateDash() {
const affiliateDash = snapshot.affiliateDash
const uid = uidInput.trim() || affiliateDash?.gameAccount || ''
const bindUuid = affiliateDash?.bindUuid || ''
if (!uid || !bindUuid) {
showError('绑定尚未完成,请先绑定账号')
return
}
setSubmittingSubmit(true)
try {
const response = await submitAffiliateDashClaim(token, { gameAccount: uid, bindUuid })
await applyDetail(response.data)
showSuccess('发货已提交')
} catch (error) {
showError(error instanceof Error ? error.message : '提交发货失败')
} finally {
setSubmittingSubmit(false)
}
}
async function refreshRole() {
setRefreshingRole(true)
try {
@@ -263,7 +292,9 @@ export default function ClaimPage() {
}
function openBindUrl(useCurrentPage = false) {
const bindUrl = String(snapshot.flow?.binding.bindUrl || '').trim()
const bindUrl = String(
snapshot.flow?.binding.bindUrl || snapshot.affiliateDash?.bindUrl || '',
).trim()
if (!bindUrl) {
showError('绑定链接还没准备好,请稍后刷新')
return
@@ -337,9 +368,27 @@ export default function ClaimPage() {
) : snapshot.hasRedeemResult ? (
snapshot.isFeifeiFlow ? (
<FeifeiResultStep snapshot={snapshot} />
) : snapshot.isAffiliateDashFlow ? (
<AffiliateDashResultStep snapshot={snapshot} />
) : (
<ClaimResultStep snapshot={snapshot} />
)
) : snapshot.isAffiliateDashFlow && snapshot.affiliateDash ? (
<AffiliateDashClaimPanel
affiliateDash={snapshot.affiliateDash}
expectedUid={snapshot.expectedUid}
order={snapshot.order}
product={snapshot.product}
taskLastError={snapshot.task?.lastError}
qrCodeDataUrl={qrCodeDataUrl}
submittingUid={submittingUid}
submittingSubmit={submittingSubmit}
uidInput={uidInput}
onUidChange={setUidInput}
onResubmitUid={() => void handleSubmitUid()}
onSubmit={() => void handleSubmitAffiliateDash()}
onOpenBindUrl={openBindUrl}
/>
) : snapshot.isFeifeiFlow && snapshot.feifei ? (
<FeifeiClaimPanel
feifei={snapshot.feifei}
@@ -65,6 +65,13 @@ export function resolveNextPollDelayMs(
return FEIFEI_POLL_MS
}
if (nextSnapshot.isAffiliateDashFlow) {
if (!nextSnapshot.affiliateDash?.bound) {
return BINDING_PREPARE_POLL_MS
}
return FEIFEI_POLL_MS
}
if (nextSnapshot.currentStep === 2) {
if (!nextSnapshot.isBindingPrepared) {
return BINDING_PREPARE_POLL_MS
@@ -6,6 +6,7 @@ import {
normalizeTaskStatus,
} from '@/domain/task-status'
import type {
ClaimAffiliateDashFlowInfo,
ClaimDetailData,
ClaimKuaishouCloudFlowInfo,
ClaimKuaishouFeifeiFlowInfo,
@@ -20,6 +21,7 @@ export function createClaimSnapshot(
) {
const flow = detail?.kuaishouCloudFulfillment || null
const feifei = detail?.kuaishouFeifei || null
const affiliateDash = detail?.affiliateDash || null
const order = detail?.order || null
const orderItem = detail?.orderItem || null
const product = detail?.product || null
@@ -31,6 +33,7 @@ export function createClaimSnapshot(
const roleName = flow?.role.name || flow?.binding.roleName || ''
const roleId = flow?.role.rid || flow?.binding.roleId || ''
const isFeifeiFlow = detail?.flowType === 'kuaishou_feifei'
const isAffiliateDashFlow = detail?.flowType === 'affiliate_dash'
const isBindUrlExpired = isDateExpired(flow?.binding.bindExpiresAt || '')
const isBindingPrepared =
flow?.binding.prepareStatus === 'ready' &&
@@ -54,25 +57,33 @@ export function createClaimSnapshot(
const isRedeemFailed =
normalizedStatus === TASK_STATUS.MANUAL_REVIEW ||
normalizedStatus === TASK_STATUS.FAILED ||
normalizedStatus === TASK_STATUS.CLOSED ||
String(flow?.dispatch.status || '').trim() === 'failed' ||
(isFeifeiFlow && [40, 50].includes(Number(feifei?.rechargeStatus || 0)))
(isFeifeiFlow && [40, 50].includes(Number(feifei?.rechargeStatus || 0))) ||
(isAffiliateDashFlow &&
['ship_failed', 'cancelled'].includes(String(affiliateDash?.orderStatus || '').trim()))
const isCompleted =
isKuaishouCloudCompletedStatus(task?.status) ||
(isFeifeiFlow && Number(feifei?.rechargeStatus || 0) === 30)
const hasRedeemResult =
isDispatched ||
hasKuaishouCloudRedeemResultStatus(task?.status) ||
(isFeifeiFlow && [30, 40, 50, 60].includes(Number(feifei?.rechargeStatus || 0)))
(isFeifeiFlow && [30, 40, 50, 60].includes(Number(feifei?.rechargeStatus || 0))) ||
(isAffiliateDashFlow &&
['delivered', 'ship_failed', 'cancelled'].includes(String(affiliateDash?.orderStatus || '').trim()))
const currentStep = resolveCurrentStep({
hasExpectedUid,
hasRedeemResult,
isRoleConfirmed,
isFeifeiFlow,
isUidMatched,
isAffiliateDashFlow,
affiliateDash,
})
const progressText = resolveProgressText({
feifei,
isFeifeiFlow,
isAffiliateDashFlow,
affiliateDash,
hasExpectedUid,
expectedUid,
hasRedeemResult,
@@ -80,11 +91,18 @@ export function createClaimSnapshot(
isBindingPrepared,
isUidMatched,
})
const resultTitle = resolveResultTitle({ isRedeemFailed, isCompleted, isDispatched })
const resultTitle = resolveResultTitle({
isRedeemFailed,
isCompleted,
isDispatched,
isAffiliateDashFlow,
affiliateDash,
})
const resultDescription = resolveResultDescription({
detail,
flow,
feifei,
affiliateDash,
task,
isRedeemFailed,
isCompleted,
@@ -98,6 +116,7 @@ export function createClaimSnapshot(
return {
flow,
feifei,
affiliateDash,
order,
orderItem,
product,
@@ -108,6 +127,7 @@ export function createClaimSnapshot(
roleName,
roleId,
isFeifeiFlow,
isAffiliateDashFlow,
isBindUrlExpired,
isBindingPrepared,
isBindingPreparing,
@@ -161,6 +181,8 @@ function resolveCurrentStep(options: {
hasRedeemResult: boolean
isRoleConfirmed: boolean
isFeifeiFlow: boolean
isAffiliateDashFlow: boolean
affiliateDash: ClaimAffiliateDashFlowInfo | null
isUidMatched?: boolean
}) {
if (!options.hasExpectedUid) {
@@ -169,6 +191,12 @@ function resolveCurrentStep(options: {
if (options.hasRedeemResult) {
return 4
}
if (options.isAffiliateDashFlow) {
if (!options.affiliateDash?.bound) {
return 2
}
return 3
}
if (!options.isFeifeiFlow && options.isRoleConfirmed) {
return 3
}
@@ -177,7 +205,9 @@ function resolveCurrentStep(options: {
function resolveProgressText(options: {
feifei: ClaimKuaishouFeifeiFlowInfo | null
affiliateDash: ClaimAffiliateDashFlowInfo | null
isFeifeiFlow: boolean
isAffiliateDashFlow: boolean
hasExpectedUid: boolean
expectedUid: string
hasRedeemResult: boolean
@@ -188,6 +218,17 @@ function resolveProgressText(options: {
if (!options.hasExpectedUid) {
return '请填写游戏编号'
}
if (options.isAffiliateDashFlow) {
if (options.hasRedeemResult) {
return '发货结果已生成'
}
if (!options.affiliateDash?.bound) {
return options.affiliateDash?.bindUrl || options.affiliateDash?.qrUrl
? '请扫码绑定账号'
: '绑定链接生成中,请稍候'
}
return '已绑定,请提交发货'
}
if (options.isFeifeiFlow) {
return options.feifei?.rechargeStatusLabel || `UID ${options.expectedUid},请打开领取链接`
}
@@ -210,9 +251,11 @@ function resolveResultTitle(options: {
isRedeemFailed: boolean
isCompleted: boolean
isDispatched: boolean
isAffiliateDashFlow: boolean
affiliateDash: ClaimAffiliateDashFlowInfo | null
}) {
if (options.isRedeemFailed) {
return '兑换遇到问题'
return '发货遇到问题'
}
if (options.isCompleted) {
return '兑换成功'
@@ -220,6 +263,9 @@ function resolveResultTitle(options: {
if (options.isDispatched) {
return '兑换请求已提交'
}
if (options.isAffiliateDashFlow && options.affiliateDash?.orderStatus === 'delivered') {
return '发货已完成'
}
return '结果已记录'
}
@@ -227,6 +273,7 @@ function resolveResultDescription(options: {
detail: ClaimDetailData | null
flow: ClaimKuaishouCloudFlowInfo | null
feifei: ClaimKuaishouFeifeiFlowInfo | null
affiliateDash: ClaimAffiliateDashFlowInfo | null
task: ClaimDetailData['task'] | null
isRedeemFailed: boolean
isCompleted: boolean
@@ -236,16 +283,21 @@ function resolveResultDescription(options: {
options.task?.lastError ||
options.flow?.dispatch.errorMessage ||
options.feifei?.rechargeResultMessage ||
options.affiliateDash?.failureReason ||
options.detail?.result?.resultMessage ||
'',
).trim()
return message || '当前兑换流程需要客服处理,后续结果请以后台任务界面为准。'
return message || '当前流程需要客服处理,后续结果请以后台任务界面为准。'
}
if (options.isCompleted) {
return '当前兑换流程已经完成。'
}
if (options.affiliateDash) {
return '你的发货请求已经提交。后续结果会由系统保存在后台任务界面。'
}
return '你的兑换请求已经提交。后续结果会由系统保存在后台任务界面。'
}
+7
View File
@@ -9,6 +9,13 @@ export function submitClaimUid(token: string, uid: string) {
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/uid`, { uid })
}
export function submitAffiliateDashClaim(
token: string,
payload: { gameAccount: string; bindUuid: string },
) {
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/affiliate-dash/submit`, payload)
}
export function confirmKuaishouCloudClaimRole(token: string) {
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
}
+29 -1
View File
@@ -162,10 +162,37 @@ export interface ClaimKuaishouFeifeiFlowInfo {
lastSyncedAt: string | null
}
export interface ClaimAffiliateDashFlowInfo {
flowType: 'affiliate_dash'
sku: string
productName: string
orderNo: string
clientOrderNo: string
orderStatus: string
canShip: boolean
cannotShipReason: string
providerOrderNo: string
failureReason: string
amount: number
currency: string
bindUuid: string
bindUrl: string
qrUrl: string
gameAccount: string
expectedGameAccount: string
bindMismatch: boolean
bound: boolean
boundAccount: string
gameChannel: string
submitStatus: string
consumeStatus: string
lastSyncedAt: string | null
}
export interface ClaimDetailData {
tokenStatus: ClaimTokenStatus
claimUrl: string
flowType: 'kuaishou_cloud' | 'kuaishou_ct_assisted' | 'kuaishou_feifei' | (string & {})
flowType: 'kuaishou_cloud' | 'kuaishou_ct_assisted' | 'kuaishou_feifei' | 'affiliate_dash' | (string & {})
claimIdentity: ClaimIdentityInfo | null
task: ClaimTaskInfo
order: ClaimOrderInfo
@@ -174,5 +201,6 @@ export interface ClaimDetailData {
session: unknown | null
kuaishouCloudFulfillment: ClaimKuaishouCloudFlowInfo | null
kuaishouFeifei: ClaimKuaishouFeifeiFlowInfo | null
affiliateDash: ClaimAffiliateDashFlowInfo | null
result: ClaimResultInfo | null
}
@@ -493,3 +493,33 @@ order_site 处理要求:
- 未命中映射的商品 → 不选 affiliate_dash,回退 ✅
**验证**:前后端 typecheck 通过;后端全量 212 测试通过。
---
## 18. 阶段 4 落地记录(v2.4 · 已完成)
产出文件:
**后端(`apps/backend/src/`**
| 文件 | 内容 |
| --- | --- |
| `services/claim/kuaishou-cloud-claim-context.ts` | `buildClaimDetailPayload``affiliate_dash` 分支 + `buildAffiliateDashClaimDetailPayload`flowType/affiliateDash 块/product/task/order/orderItem/result |
| `services/claim/kuaishou-cloud-claim-service.ts` | `getKuaishouCloudClaimDetail` affiliate_dash 分支:`syncAffiliateDashTaskStatus` + `refreshAffiliateDashBindState`(有 bindUuid 时拉 bind-result 实时刷新 bound/账号/mismatch,可重入);`submitClaimUid` affiliate_dash 分支 → `bindAffiliateDashClaimForTask`bind 拿 bindUuid/二维码,task → WAITING_BINDING);新增 `submitAffiliateDashClaim`submit → REDEEMING + task event |
| `services/fulfillment/affiliate-dash/index.ts` | `AffiliateDashFlow``bound/boundAccount/gameChannel` 字段(bind-result 状态) |
| `routes/claims.ts` | 新增 `POST /:token/affiliate-dash/submit`claimWriteRateLimit |
**前端(`apps/frontend/src/`**
| 文件 | 内容 |
| --- | --- |
| `types/claim.ts` | `ClaimAffiliateDashFlowInfo` + `ClaimDetailData.affiliateDash` |
| `services/claim.ts` | `submitAffiliateDashClaim(token, {gameAccount, bindUuid})` |
| `pages/claim/claim-snapshot.ts` | `isAffiliateDashFlow`、hasRedeemResult/currentStep/progressText/resultTitle/resultDescription 的 affiliate_dash 分支(step: 无UID=1/绑定中=2/已绑定待提交=3/结果=4) |
| `pages/claim/claim-poll.ts` | affiliate_dash 轮询(未绑定用 BINDING_PREPARE_POLL_MS,已绑定用 FEIFEI_POLL_MS |
| `pages/claim/ClaimAffiliateDashSteps.tsx`(新) | `AffiliateDashClaimPanel`(二维码/绑定链接、bound 状态、mismatch 警告、提交发货按钮)+ `AffiliateDashResultStep` |
| `pages/claim/ClaimPage.tsx` | 分发分支 + `handleSubmitAffiliateDash` + `openBindUrl`/二维码生成支持 affiliateDash.bindUrl |
**验证**:前后端 typecheck 通过;后端全量 212 测试通过;claim 详情分流脚本验证(flowType=affiliate_dash、affiliateDash 块完整、product 正确、其他 flow 为 null)。
**领取闭环状态迁移(与 §8 状态机一致)**:输 UID → submitClaimUidbind 触发,task `link_generated → waiting_binding`)→ 轮询详情(bind-result 刷新 bound)→ 提交发货(submitAffiliateDashClaimtask `waiting_binding → redeeming`)→ 回调 delivered → redeemed + 核销。