新增 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
@@ -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
}