Files
order_site/apps/frontend/src/pages/claim/claim-snapshot.ts
T
yml2213 5ef1ed6018 新增 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 测试通过
2026-08-05 15:56:51 +08:00

313 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
TASK_STATUS,
hasKuaishouCloudRedeemResultStatus,
isKuaishouCloudCompletedStatus,
isKuaishouCloudRoleConfirmedStatus,
normalizeTaskStatus,
} from '@/domain/task-status'
import type {
ClaimAffiliateDashFlowInfo,
ClaimDetailData,
ClaimKuaishouCloudFlowInfo,
ClaimKuaishouFeifeiFlowInfo,
} from '@/types/claim'
export type ResultVariant = 'success' | 'warning' | 'info'
export type ClaimSnapshot = ReturnType<typeof createClaimSnapshot>
export function createClaimSnapshot(
detail: ClaimDetailData | null,
options: { rebindingRole?: boolean } = {},
) {
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
const task = detail?.task || null
const claimIdentity = detail?.claimIdentity || null
const expectedUid = String(claimIdentity?.expectedUid || '').trim()
const hasExpectedUid = Boolean(expectedUid || claimIdentity?.ready)
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' &&
Boolean(String(flow?.binding.bindUrl || '').trim()) &&
!isBindUrlExpired
const isBindingPreparing = flow?.binding.prepareStatus === 'pending'
const isRoleReady = Boolean(roleName || roleId)
const isUidMatched =
Boolean(expectedUid) && Boolean(roleId) && normalizeUid(expectedUid) === normalizeUid(roleId)
const confirmRoleDisabledReason = resolveConfirmRoleDisabledReason({
isBindingPrepared,
isRoleReady,
isUidMatched,
expectedUid,
roleId,
rebindingRole: Boolean(options.rebindingRole),
})
const isRoleConfirmed = isKuaishouCloudRoleConfirmedStatus(task?.status)
const isDispatched = String(flow?.dispatch.status || '').trim() === 'success'
const normalizedStatus = normalizeTaskStatus(task?.status)
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))) ||
(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))) ||
(isAffiliateDashFlow &&
['delivered', 'ship_failed', 'cancelled'].includes(String(affiliateDash?.orderStatus || '').trim()))
const currentStep = resolveCurrentStep({
hasExpectedUid,
hasRedeemResult,
isRoleConfirmed,
isFeifeiFlow,
isAffiliateDashFlow,
affiliateDash,
})
const progressText = resolveProgressText({
feifei,
isFeifeiFlow,
isAffiliateDashFlow,
affiliateDash,
hasExpectedUid,
expectedUid,
hasRedeemResult,
isRoleConfirmed,
isBindingPrepared,
isUidMatched,
})
const resultTitle = resolveResultTitle({
isRedeemFailed,
isCompleted,
isDispatched,
isAffiliateDashFlow,
affiliateDash,
})
const resultDescription = resolveResultDescription({
detail,
flow,
feifei,
affiliateDash,
task,
isRedeemFailed,
isCompleted,
})
const resultVariant: ResultVariant = isRedeemFailed
? 'warning'
: isCompleted || isDispatched
? 'success'
: 'info'
return {
flow,
feifei,
affiliateDash,
order,
orderItem,
product,
task,
claimIdentity,
expectedUid,
hasExpectedUid,
roleName,
roleId,
isFeifeiFlow,
isAffiliateDashFlow,
isBindUrlExpired,
isBindingPrepared,
isBindingPreparing,
isRoleReady,
isUidMatched,
confirmRoleDisabledReason,
isRoleConfirmed,
isDispatched,
isRedeemFailed,
isCompleted,
hasRedeemResult,
currentStep,
progressText,
resultTitle,
resultDescription,
resultVariant,
}
}
export function normalizeUid(value: unknown) {
return String(value || '')
.trim()
.replace(/\s+/g, '')
}
function resolveConfirmRoleDisabledReason(options: {
isBindingPrepared: boolean
isRoleReady: boolean
isUidMatched: boolean
expectedUid: string
roleId: string
rebindingRole: boolean
}) {
if (!options.isBindingPrepared) {
return '绑定二维码还在准备中,请稍后自动刷新'
}
if (!options.isRoleReady) {
return '请先扫码绑定自己的角色,并刷新角色信息'
}
if (!options.isUidMatched) {
return `当前绑定角色 ID${options.roleId || '-'})与填写 UID${options.expectedUid || '-'})不一致`
}
if (options.rebindingRole) {
return '正在换绑角色,请稍候'
}
return ''
}
function resolveCurrentStep(options: {
hasExpectedUid: boolean
hasRedeemResult: boolean
isRoleConfirmed: boolean
isFeifeiFlow: boolean
isAffiliateDashFlow: boolean
affiliateDash: ClaimAffiliateDashFlowInfo | null
isUidMatched?: boolean
}) {
if (!options.hasExpectedUid) {
return 1
}
if (options.hasRedeemResult) {
return 4
}
if (options.isAffiliateDashFlow) {
if (!options.affiliateDash?.bound) {
return 2
}
return 3
}
if (!options.isFeifeiFlow && options.isRoleConfirmed) {
return 3
}
return 2
}
function resolveProgressText(options: {
feifei: ClaimKuaishouFeifeiFlowInfo | null
affiliateDash: ClaimAffiliateDashFlowInfo | null
isFeifeiFlow: boolean
isAffiliateDashFlow: boolean
hasExpectedUid: boolean
expectedUid: string
hasRedeemResult: boolean
isRoleConfirmed: boolean
isBindingPrepared: boolean
isUidMatched: boolean
}) {
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},请打开领取链接`
}
if (options.hasRedeemResult) {
return '兑换结果已生成'
}
if (options.isRoleConfirmed) {
return '角色已确认,等待兑换'
}
if (!options.isBindingPrepared) {
return '绑定链接准备中,请稍候'
}
if (options.isUidMatched) {
return 'UID 已匹配,请确认兑换'
}
return '请完成扫码绑定并匹配 UID'
}
function resolveResultTitle(options: {
isRedeemFailed: boolean
isCompleted: boolean
isDispatched: boolean
isAffiliateDashFlow: boolean
affiliateDash: ClaimAffiliateDashFlowInfo | null
}) {
if (options.isRedeemFailed) {
return '发货遇到问题'
}
if (options.isCompleted) {
return '兑换成功'
}
if (options.isDispatched) {
return '兑换请求已提交'
}
if (options.isAffiliateDashFlow && options.affiliateDash?.orderStatus === 'delivered') {
return '发货已完成'
}
return '结果已记录'
}
function resolveResultDescription(options: {
detail: ClaimDetailData | null
flow: ClaimKuaishouCloudFlowInfo | null
feifei: ClaimKuaishouFeifeiFlowInfo | null
affiliateDash: ClaimAffiliateDashFlowInfo | null
task: ClaimDetailData['task'] | null
isRedeemFailed: boolean
isCompleted: boolean
}) {
if (options.isRedeemFailed) {
const message = String(
options.task?.lastError ||
options.flow?.dispatch.errorMessage ||
options.feifei?.rechargeResultMessage ||
options.affiliateDash?.failureReason ||
options.detail?.result?.resultMessage ||
'',
).trim()
return message || '当前流程需要客服处理,后续结果请以后台任务界面为准。'
}
if (options.isCompleted) {
return '当前兑换流程已经完成。'
}
if (options.affiliateDash) {
return '你的发货请求已经提交。后续结果会由系统保存在后台任务界面。'
}
return '你的兑换请求已经提交。后续结果会由系统保存在后台任务界面。'
}
function isDateExpired(value: string | null) {
const normalized = String(value || '').trim()
if (!normalized) {
return false
}
const expiresTime = Date.parse(normalized)
return Number.isFinite(expiresTime) && expiresTime <= Date.now()
}