- 后端: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 测试通过
313 lines
8.8 KiB
TypeScript
313 lines
8.8 KiB
TypeScript
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()
|
||
}
|