新增 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:
@@ -6,6 +6,7 @@ import {
|
|||||||
getKuaishouCloudClaimDetail,
|
getKuaishouCloudClaimDetail,
|
||||||
rebindKuaishouCloudClaimRole,
|
rebindKuaishouCloudClaimRole,
|
||||||
redeemKuaishouCloudClaim,
|
redeemKuaishouCloudClaim,
|
||||||
|
submitAffiliateDashClaim,
|
||||||
submitClaimUid,
|
submitClaimUid,
|
||||||
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||||
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.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(
|
router.post(
|
||||||
'/:token/kuaishou-cloud/confirm-role',
|
'/:token/kuaishou-cloud/confirm-role',
|
||||||
claimWriteRateLimit,
|
claimWriteRateLimit,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TASK_STATUS, isTaskFinalStatus } from '../../domain/task-status.js'
|
|||||||
import { buildClaimUrl } from './claim-service.js'
|
import { buildClaimUrl } from './claim-service.js'
|
||||||
import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js'
|
import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js'
|
||||||
import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.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 type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
import { asJsonObject, type JsonObject } from '../../types/json.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 })
|
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 taskContext = parseTaskContext(task)
|
||||||
const claimIdentity = buildClaimIdentityPayload(taskContext.claimIdentity)
|
const claimIdentity = buildClaimIdentityPayload(taskContext.claimIdentity)
|
||||||
const kuaishouCloudSource = resolveClaimKuaishouCloudSource(task)
|
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) {
|
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||||
const context = parseTaskContext(task)
|
const context = parseTaskContext(task)
|
||||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ import {
|
|||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
} from '../fulfillment/kuaishou-cloud/index.js'
|
} from '../fulfillment/kuaishou-cloud/index.js'
|
||||||
import { syncKuaishouFeifeiTaskStatus } from '../fulfillment/kuaishou-feifei/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 {
|
import {
|
||||||
assertValidClaimUid,
|
assertValidClaimUid,
|
||||||
canUpdateClaimUid,
|
canUpdateClaimUid,
|
||||||
@@ -221,6 +230,11 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
|||||||
task = (await syncKuaishouFeifeiTaskStatus(task)) || task
|
task = (await syncKuaishouFeifeiTaskStatus(task)) || task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (executorKey === 'affiliate_dash') {
|
||||||
|
task = (await syncAffiliateDashTaskStatus(task)) || task
|
||||||
|
task = (await refreshAffiliateDashBindState(task)) || task
|
||||||
|
}
|
||||||
|
|
||||||
if (executorKey === 'kuaishou-industry') {
|
if (executorKey === 'kuaishou-industry') {
|
||||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||||||
// 已核销也要走 verify:内部会补 prepare 绑定资源
|
// 已核销也要走 verify:内部会补 prepare 绑定资源
|
||||||
@@ -357,9 +371,173 @@ export async function submitClaimUid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (executorKey === 'affiliate_dash') {
|
||||||
|
updatedTask = (await bindAffiliateDashClaimForTask(updatedTask, expectedUid)) || updatedTask
|
||||||
|
}
|
||||||
|
|
||||||
return getKuaishouCloudClaimDetail(token)
|
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_binding(link_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) {
|
export async function rebindKuaishouCloudClaimRole(token: unknown) {
|
||||||
const context = await getClaimContext(token)
|
const context = await getClaimContext(token)
|
||||||
assertLewanClaimTask(context.task)
|
assertLewanClaimTask(context.task)
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ export type AffiliateDashFlow = {
|
|||||||
gameAccount: string
|
gameAccount: string
|
||||||
expectedGameAccount: string
|
expectedGameAccount: string
|
||||||
bindMismatch: boolean
|
bindMismatch: boolean
|
||||||
|
/** 是否已在 affiliate-dash 侧完成绑定(bind-result.bound) */
|
||||||
|
bound: boolean
|
||||||
|
boundAccount: string
|
||||||
|
gameChannel: string
|
||||||
submitStatus: string
|
submitStatus: string
|
||||||
consumeStatus: string
|
consumeStatus: string
|
||||||
lastSyncedAt: unknown
|
lastSyncedAt: unknown
|
||||||
@@ -270,6 +274,9 @@ export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow {
|
|||||||
gameAccount: String(source.gameAccount || '').trim(),
|
gameAccount: String(source.gameAccount || '').trim(),
|
||||||
expectedGameAccount: String(source.expectedGameAccount || '').trim(),
|
expectedGameAccount: String(source.expectedGameAccount || '').trim(),
|
||||||
bindMismatch: Boolean(source.bindMismatch),
|
bindMismatch: Boolean(source.bindMismatch),
|
||||||
|
bound: Boolean(source.bound),
|
||||||
|
boundAccount: String(source.boundAccount || '').trim(),
|
||||||
|
gameChannel: String(source.gameChannel || '').trim(),
|
||||||
submitStatus: String(source.submitStatus || '').trim(),
|
submitStatus: String(source.submitStatus || '').trim(),
|
||||||
consumeStatus: String(source.consumeStatus || 'pending').trim(),
|
consumeStatus: String(source.consumeStatus || 'pending').trim(),
|
||||||
lastSyncedAt: source.lastSyncedAt || null,
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,10 +10,12 @@ import {
|
|||||||
fetchClaimDetail,
|
fetchClaimDetail,
|
||||||
rebindKuaishouCloudClaimRole,
|
rebindKuaishouCloudClaimRole,
|
||||||
redeemKuaishouCloudClaim,
|
redeemKuaishouCloudClaim,
|
||||||
|
submitAffiliateDashClaim,
|
||||||
submitClaimUid,
|
submitClaimUid,
|
||||||
} from '@/services/claim'
|
} from '@/services/claim'
|
||||||
import type { ClaimDetailData } from '@/types/claim'
|
import type { ClaimDetailData } from '@/types/claim'
|
||||||
|
|
||||||
|
import { AffiliateDashClaimPanel, AffiliateDashResultStep } from './ClaimAffiliateDashSteps'
|
||||||
import { FeifeiClaimPanel, FeifeiResultStep } from './ClaimFeifeiSteps'
|
import { FeifeiClaimPanel, FeifeiResultStep } from './ClaimFeifeiSteps'
|
||||||
import { ClaimHeaderCard } from './ClaimHeaderCard'
|
import { ClaimHeaderCard } from './ClaimHeaderCard'
|
||||||
import { ClaimResultStep, KuaishouCloudClaimSteps } from './ClaimLewanSteps'
|
import { ClaimResultStep, KuaishouCloudClaimSteps } from './ClaimLewanSteps'
|
||||||
@@ -32,6 +34,7 @@ export default function ClaimPage() {
|
|||||||
const [confirmingRole, setConfirmingRole] = useState(false)
|
const [confirmingRole, setConfirmingRole] = useState(false)
|
||||||
const [rebindingRole, setRebindingRole] = useState(false)
|
const [rebindingRole, setRebindingRole] = useState(false)
|
||||||
const [redeeming, setRedeeming] = useState(false)
|
const [redeeming, setRedeeming] = useState(false)
|
||||||
|
const [submittingSubmit, setSubmittingSubmit] = useState(false)
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
const [detail, setDetail] = useState<ClaimDetailData | null>(null)
|
const [detail, setDetail] = useState<ClaimDetailData | null>(null)
|
||||||
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('')
|
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('')
|
||||||
@@ -81,7 +84,11 @@ export default function ClaimPage() {
|
|||||||
setUidInput(expectedUid)
|
setUidInput(expectedUid)
|
||||||
}
|
}
|
||||||
await generateQRCode(
|
await generateQRCode(
|
||||||
String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim(),
|
String(
|
||||||
|
nextDetail.kuaishouCloudFulfillment?.binding.bindUrl ||
|
||||||
|
nextDetail.affiliateDash?.bindUrl ||
|
||||||
|
'',
|
||||||
|
).trim(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[generateQRCode],
|
[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() {
|
async function refreshRole() {
|
||||||
setRefreshingRole(true)
|
setRefreshingRole(true)
|
||||||
try {
|
try {
|
||||||
@@ -263,7 +292,9 @@ export default function ClaimPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openBindUrl(useCurrentPage = false) {
|
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) {
|
if (!bindUrl) {
|
||||||
showError('绑定链接还没准备好,请稍后刷新')
|
showError('绑定链接还没准备好,请稍后刷新')
|
||||||
return
|
return
|
||||||
@@ -337,9 +368,27 @@ export default function ClaimPage() {
|
|||||||
) : snapshot.hasRedeemResult ? (
|
) : snapshot.hasRedeemResult ? (
|
||||||
snapshot.isFeifeiFlow ? (
|
snapshot.isFeifeiFlow ? (
|
||||||
<FeifeiResultStep snapshot={snapshot} />
|
<FeifeiResultStep snapshot={snapshot} />
|
||||||
|
) : snapshot.isAffiliateDashFlow ? (
|
||||||
|
<AffiliateDashResultStep snapshot={snapshot} />
|
||||||
) : (
|
) : (
|
||||||
<ClaimResultStep 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 ? (
|
) : snapshot.isFeifeiFlow && snapshot.feifei ? (
|
||||||
<FeifeiClaimPanel
|
<FeifeiClaimPanel
|
||||||
feifei={snapshot.feifei}
|
feifei={snapshot.feifei}
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ export function resolveNextPollDelayMs(
|
|||||||
return FEIFEI_POLL_MS
|
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.currentStep === 2) {
|
||||||
if (!nextSnapshot.isBindingPrepared) {
|
if (!nextSnapshot.isBindingPrepared) {
|
||||||
return BINDING_PREPARE_POLL_MS
|
return BINDING_PREPARE_POLL_MS
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
normalizeTaskStatus,
|
normalizeTaskStatus,
|
||||||
} from '@/domain/task-status'
|
} from '@/domain/task-status'
|
||||||
import type {
|
import type {
|
||||||
|
ClaimAffiliateDashFlowInfo,
|
||||||
ClaimDetailData,
|
ClaimDetailData,
|
||||||
ClaimKuaishouCloudFlowInfo,
|
ClaimKuaishouCloudFlowInfo,
|
||||||
ClaimKuaishouFeifeiFlowInfo,
|
ClaimKuaishouFeifeiFlowInfo,
|
||||||
@@ -20,6 +21,7 @@ export function createClaimSnapshot(
|
|||||||
) {
|
) {
|
||||||
const flow = detail?.kuaishouCloudFulfillment || null
|
const flow = detail?.kuaishouCloudFulfillment || null
|
||||||
const feifei = detail?.kuaishouFeifei || null
|
const feifei = detail?.kuaishouFeifei || null
|
||||||
|
const affiliateDash = detail?.affiliateDash || null
|
||||||
const order = detail?.order || null
|
const order = detail?.order || null
|
||||||
const orderItem = detail?.orderItem || null
|
const orderItem = detail?.orderItem || null
|
||||||
const product = detail?.product || null
|
const product = detail?.product || null
|
||||||
@@ -31,6 +33,7 @@ export function createClaimSnapshot(
|
|||||||
const roleName = flow?.role.name || flow?.binding.roleName || ''
|
const roleName = flow?.role.name || flow?.binding.roleName || ''
|
||||||
const roleId = flow?.role.rid || flow?.binding.roleId || ''
|
const roleId = flow?.role.rid || flow?.binding.roleId || ''
|
||||||
const isFeifeiFlow = detail?.flowType === 'kuaishou_feifei'
|
const isFeifeiFlow = detail?.flowType === 'kuaishou_feifei'
|
||||||
|
const isAffiliateDashFlow = detail?.flowType === 'affiliate_dash'
|
||||||
const isBindUrlExpired = isDateExpired(flow?.binding.bindExpiresAt || '')
|
const isBindUrlExpired = isDateExpired(flow?.binding.bindExpiresAt || '')
|
||||||
const isBindingPrepared =
|
const isBindingPrepared =
|
||||||
flow?.binding.prepareStatus === 'ready' &&
|
flow?.binding.prepareStatus === 'ready' &&
|
||||||
@@ -54,25 +57,33 @@ export function createClaimSnapshot(
|
|||||||
const isRedeemFailed =
|
const isRedeemFailed =
|
||||||
normalizedStatus === TASK_STATUS.MANUAL_REVIEW ||
|
normalizedStatus === TASK_STATUS.MANUAL_REVIEW ||
|
||||||
normalizedStatus === TASK_STATUS.FAILED ||
|
normalizedStatus === TASK_STATUS.FAILED ||
|
||||||
|
normalizedStatus === TASK_STATUS.CLOSED ||
|
||||||
String(flow?.dispatch.status || '').trim() === 'failed' ||
|
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 =
|
const isCompleted =
|
||||||
isKuaishouCloudCompletedStatus(task?.status) ||
|
isKuaishouCloudCompletedStatus(task?.status) ||
|
||||||
(isFeifeiFlow && Number(feifei?.rechargeStatus || 0) === 30)
|
(isFeifeiFlow && Number(feifei?.rechargeStatus || 0) === 30)
|
||||||
const hasRedeemResult =
|
const hasRedeemResult =
|
||||||
isDispatched ||
|
isDispatched ||
|
||||||
hasKuaishouCloudRedeemResultStatus(task?.status) ||
|
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({
|
const currentStep = resolveCurrentStep({
|
||||||
hasExpectedUid,
|
hasExpectedUid,
|
||||||
hasRedeemResult,
|
hasRedeemResult,
|
||||||
isRoleConfirmed,
|
isRoleConfirmed,
|
||||||
isFeifeiFlow,
|
isFeifeiFlow,
|
||||||
isUidMatched,
|
isAffiliateDashFlow,
|
||||||
|
affiliateDash,
|
||||||
})
|
})
|
||||||
const progressText = resolveProgressText({
|
const progressText = resolveProgressText({
|
||||||
feifei,
|
feifei,
|
||||||
isFeifeiFlow,
|
isFeifeiFlow,
|
||||||
|
isAffiliateDashFlow,
|
||||||
|
affiliateDash,
|
||||||
hasExpectedUid,
|
hasExpectedUid,
|
||||||
expectedUid,
|
expectedUid,
|
||||||
hasRedeemResult,
|
hasRedeemResult,
|
||||||
@@ -80,11 +91,18 @@ export function createClaimSnapshot(
|
|||||||
isBindingPrepared,
|
isBindingPrepared,
|
||||||
isUidMatched,
|
isUidMatched,
|
||||||
})
|
})
|
||||||
const resultTitle = resolveResultTitle({ isRedeemFailed, isCompleted, isDispatched })
|
const resultTitle = resolveResultTitle({
|
||||||
|
isRedeemFailed,
|
||||||
|
isCompleted,
|
||||||
|
isDispatched,
|
||||||
|
isAffiliateDashFlow,
|
||||||
|
affiliateDash,
|
||||||
|
})
|
||||||
const resultDescription = resolveResultDescription({
|
const resultDescription = resolveResultDescription({
|
||||||
detail,
|
detail,
|
||||||
flow,
|
flow,
|
||||||
feifei,
|
feifei,
|
||||||
|
affiliateDash,
|
||||||
task,
|
task,
|
||||||
isRedeemFailed,
|
isRedeemFailed,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
@@ -98,6 +116,7 @@ export function createClaimSnapshot(
|
|||||||
return {
|
return {
|
||||||
flow,
|
flow,
|
||||||
feifei,
|
feifei,
|
||||||
|
affiliateDash,
|
||||||
order,
|
order,
|
||||||
orderItem,
|
orderItem,
|
||||||
product,
|
product,
|
||||||
@@ -108,6 +127,7 @@ export function createClaimSnapshot(
|
|||||||
roleName,
|
roleName,
|
||||||
roleId,
|
roleId,
|
||||||
isFeifeiFlow,
|
isFeifeiFlow,
|
||||||
|
isAffiliateDashFlow,
|
||||||
isBindUrlExpired,
|
isBindUrlExpired,
|
||||||
isBindingPrepared,
|
isBindingPrepared,
|
||||||
isBindingPreparing,
|
isBindingPreparing,
|
||||||
@@ -161,6 +181,8 @@ function resolveCurrentStep(options: {
|
|||||||
hasRedeemResult: boolean
|
hasRedeemResult: boolean
|
||||||
isRoleConfirmed: boolean
|
isRoleConfirmed: boolean
|
||||||
isFeifeiFlow: boolean
|
isFeifeiFlow: boolean
|
||||||
|
isAffiliateDashFlow: boolean
|
||||||
|
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||||
isUidMatched?: boolean
|
isUidMatched?: boolean
|
||||||
}) {
|
}) {
|
||||||
if (!options.hasExpectedUid) {
|
if (!options.hasExpectedUid) {
|
||||||
@@ -169,6 +191,12 @@ function resolveCurrentStep(options: {
|
|||||||
if (options.hasRedeemResult) {
|
if (options.hasRedeemResult) {
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
|
if (options.isAffiliateDashFlow) {
|
||||||
|
if (!options.affiliateDash?.bound) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 3
|
||||||
|
}
|
||||||
if (!options.isFeifeiFlow && options.isRoleConfirmed) {
|
if (!options.isFeifeiFlow && options.isRoleConfirmed) {
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
@@ -177,7 +205,9 @@ function resolveCurrentStep(options: {
|
|||||||
|
|
||||||
function resolveProgressText(options: {
|
function resolveProgressText(options: {
|
||||||
feifei: ClaimKuaishouFeifeiFlowInfo | null
|
feifei: ClaimKuaishouFeifeiFlowInfo | null
|
||||||
|
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||||
isFeifeiFlow: boolean
|
isFeifeiFlow: boolean
|
||||||
|
isAffiliateDashFlow: boolean
|
||||||
hasExpectedUid: boolean
|
hasExpectedUid: boolean
|
||||||
expectedUid: string
|
expectedUid: string
|
||||||
hasRedeemResult: boolean
|
hasRedeemResult: boolean
|
||||||
@@ -188,6 +218,17 @@ function resolveProgressText(options: {
|
|||||||
if (!options.hasExpectedUid) {
|
if (!options.hasExpectedUid) {
|
||||||
return '请填写游戏编号'
|
return '请填写游戏编号'
|
||||||
}
|
}
|
||||||
|
if (options.isAffiliateDashFlow) {
|
||||||
|
if (options.hasRedeemResult) {
|
||||||
|
return '发货结果已生成'
|
||||||
|
}
|
||||||
|
if (!options.affiliateDash?.bound) {
|
||||||
|
return options.affiliateDash?.bindUrl || options.affiliateDash?.qrUrl
|
||||||
|
? '请扫码绑定账号'
|
||||||
|
: '绑定链接生成中,请稍候'
|
||||||
|
}
|
||||||
|
return '已绑定,请提交发货'
|
||||||
|
}
|
||||||
if (options.isFeifeiFlow) {
|
if (options.isFeifeiFlow) {
|
||||||
return options.feifei?.rechargeStatusLabel || `UID ${options.expectedUid},请打开领取链接`
|
return options.feifei?.rechargeStatusLabel || `UID ${options.expectedUid},请打开领取链接`
|
||||||
}
|
}
|
||||||
@@ -210,9 +251,11 @@ function resolveResultTitle(options: {
|
|||||||
isRedeemFailed: boolean
|
isRedeemFailed: boolean
|
||||||
isCompleted: boolean
|
isCompleted: boolean
|
||||||
isDispatched: boolean
|
isDispatched: boolean
|
||||||
|
isAffiliateDashFlow: boolean
|
||||||
|
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||||
}) {
|
}) {
|
||||||
if (options.isRedeemFailed) {
|
if (options.isRedeemFailed) {
|
||||||
return '兑换遇到问题'
|
return '发货遇到问题'
|
||||||
}
|
}
|
||||||
if (options.isCompleted) {
|
if (options.isCompleted) {
|
||||||
return '兑换成功'
|
return '兑换成功'
|
||||||
@@ -220,6 +263,9 @@ function resolveResultTitle(options: {
|
|||||||
if (options.isDispatched) {
|
if (options.isDispatched) {
|
||||||
return '兑换请求已提交'
|
return '兑换请求已提交'
|
||||||
}
|
}
|
||||||
|
if (options.isAffiliateDashFlow && options.affiliateDash?.orderStatus === 'delivered') {
|
||||||
|
return '发货已完成'
|
||||||
|
}
|
||||||
return '结果已记录'
|
return '结果已记录'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +273,7 @@ function resolveResultDescription(options: {
|
|||||||
detail: ClaimDetailData | null
|
detail: ClaimDetailData | null
|
||||||
flow: ClaimKuaishouCloudFlowInfo | null
|
flow: ClaimKuaishouCloudFlowInfo | null
|
||||||
feifei: ClaimKuaishouFeifeiFlowInfo | null
|
feifei: ClaimKuaishouFeifeiFlowInfo | null
|
||||||
|
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||||
task: ClaimDetailData['task'] | null
|
task: ClaimDetailData['task'] | null
|
||||||
isRedeemFailed: boolean
|
isRedeemFailed: boolean
|
||||||
isCompleted: boolean
|
isCompleted: boolean
|
||||||
@@ -236,16 +283,21 @@ function resolveResultDescription(options: {
|
|||||||
options.task?.lastError ||
|
options.task?.lastError ||
|
||||||
options.flow?.dispatch.errorMessage ||
|
options.flow?.dispatch.errorMessage ||
|
||||||
options.feifei?.rechargeResultMessage ||
|
options.feifei?.rechargeResultMessage ||
|
||||||
|
options.affiliateDash?.failureReason ||
|
||||||
options.detail?.result?.resultMessage ||
|
options.detail?.result?.resultMessage ||
|
||||||
'',
|
'',
|
||||||
).trim()
|
).trim()
|
||||||
return message || '当前兑换流程需要客服处理,后续结果请以后台任务界面为准。'
|
return message || '当前流程需要客服处理,后续结果请以后台任务界面为准。'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.isCompleted) {
|
if (options.isCompleted) {
|
||||||
return '当前兑换流程已经完成。'
|
return '当前兑换流程已经完成。'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.affiliateDash) {
|
||||||
|
return '你的发货请求已经提交。后续结果会由系统保存在后台任务界面。'
|
||||||
|
}
|
||||||
|
|
||||||
return '你的兑换请求已经提交。后续结果会由系统保存在后台任务界面。'
|
return '你的兑换请求已经提交。后续结果会由系统保存在后台任务界面。'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ export function submitClaimUid(token: string, uid: string) {
|
|||||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/uid`, { uid })
|
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) {
|
export function confirmKuaishouCloudClaimRole(token: string) {
|
||||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,10 +162,37 @@ export interface ClaimKuaishouFeifeiFlowInfo {
|
|||||||
lastSyncedAt: string | null
|
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 {
|
export interface ClaimDetailData {
|
||||||
tokenStatus: ClaimTokenStatus
|
tokenStatus: ClaimTokenStatus
|
||||||
claimUrl: string
|
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
|
claimIdentity: ClaimIdentityInfo | null
|
||||||
task: ClaimTaskInfo
|
task: ClaimTaskInfo
|
||||||
order: ClaimOrderInfo
|
order: ClaimOrderInfo
|
||||||
@@ -174,5 +201,6 @@ export interface ClaimDetailData {
|
|||||||
session: unknown | null
|
session: unknown | null
|
||||||
kuaishouCloudFulfillment: ClaimKuaishouCloudFlowInfo | null
|
kuaishouCloudFulfillment: ClaimKuaishouCloudFlowInfo | null
|
||||||
kuaishouFeifei: ClaimKuaishouFeifeiFlowInfo | null
|
kuaishouFeifei: ClaimKuaishouFeifeiFlowInfo | null
|
||||||
|
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||||
result: ClaimResultInfo | null
|
result: ClaimResultInfo | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,3 +493,33 @@ order_site 处理要求:
|
|||||||
- 未命中映射的商品 → 不选 affiliate_dash,回退 ✅
|
- 未命中映射的商品 → 不选 affiliate_dash,回退 ✅
|
||||||
|
|
||||||
**验证**:前后端 typecheck 通过;后端全量 212 测试通过。
|
**验证**:前后端 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 → submitClaimUid(bind 触发,task `link_generated → waiting_binding`)→ 轮询详情(bind-result 刷新 bound)→ 提交发货(submitAffiliateDashClaim,task `waiting_binding → redeeming`)→ 回调 delivered → redeemed + 核销。
|
||||||
|
|||||||
Reference in New Issue
Block a user