优化领取页绑定 UID 校验

This commit is contained in:
yml2213
2026-08-05 21:19:09 +08:00
parent 0deb498a05
commit c91dd4d144
5 changed files with 157 additions and 34 deletions
@@ -404,35 +404,86 @@ export async function submitAffiliateDashClaim(
}) })
} }
const gameAccount = String(payload.gameAccount || flow.gameAccount || '').trim() const expectedGameAccount = String(
const bindUuid = String(payload.bindUuid || flow.bindUuid || '').trim() getClaimIdentityFromTask(task).expectedUid || flow.expectedGameAccount || flow.gameAccount || '',
if (!gameAccount || !bindUuid) { ).trim()
const requestedGameAccount = String(payload.gameAccount || '').trim()
const requestedBindUuid = String(payload.bindUuid || '').trim()
const bindUuid = String(flow.bindUuid || '').trim()
if (!expectedGameAccount || !bindUuid) {
throw createHttpError('缺少绑定账号或 bindUuid', { throw createHttpError('缺少绑定账号或 bindUuid', {
statusCode: 400, statusCode: 400,
errorCode: 'affiliate_dash_submit_missing', errorCode: 'affiliate_dash_submit_missing',
}) })
} }
if (requestedGameAccount && normalizeUid(requestedGameAccount) !== normalizeUid(expectedGameAccount)) {
throw createHttpError('提交 UID 与第一步填写 UID 不一致,请先更新 UID 后重新绑定', {
statusCode: 409,
errorCode: 'affiliate_dash_submit_uid_changed',
})
}
if (requestedBindUuid && requestedBindUuid !== bindUuid) {
throw createHttpError('绑定二维码已更新,请刷新页面后重试', {
statusCode: 409,
errorCode: 'affiliate_dash_bind_uuid_stale',
})
}
const now = nowIso() const now = nowIso()
const isMock = Boolean(flow.mock?.enabled) const isMock = Boolean(flow.mock?.enabled)
const gameAccount = expectedGameAccount
let latestFlow = flow
// 强制校验:绑定返回的游戏账号与第一步填写 UID 不一致时禁止提交(防止 uid 输错), // 强制校验:绑定返回的游戏账号与第一步填写 UID 不一致时禁止提交(防止 uid 输错),
// 不依赖平台 mismatch 字段。拉取绑定结果失败时不阻塞(平台提交时仍会校验) // 不依赖请求体 UID,也不依赖平台 mismatch 字段,避免错绑后被前端或手动请求绕过
if (!isMock && flow.bindUuid) { if (!isMock && flow.bindUuid) {
let mismatch = false
try { try {
const bindResult = await getAffiliateDashBindResult({ const bindResult = await getAffiliateDashBindResult({
orderNo: flow.orderNo, orderNo: flow.orderNo,
bindUuid, bindUuid,
}) })
const boundAccount = bindResult.gameAccount const boundAccount = bindResult.gameAccount
mismatch = const bindMismatch =
Boolean(bindResult.mismatch) || Boolean(bindResult.mismatch) ||
Boolean(boundAccount && normalizeUid(boundAccount) !== normalizeUid(gameAccount)) Boolean(boundAccount && normalizeUid(boundAccount) !== normalizeUid(expectedGameAccount))
latestFlow = {
...flow,
bound: bindResult.bound,
boundAccount: boundAccount || flow.boundAccount,
gameChannel: bindResult.gameChannel || flow.gameChannel,
bindMismatch,
}
await updateTask(task.id, {
context_json: JSON.stringify({
...taskContext,
affiliateDash: latestFlow,
}),
updated_at: now,
})
} catch { } catch {
// 拉取失败:保持放行 // 拉取失败:只能使用本地最近一次详情刷新结果,不能无校验放行
} }
if (mismatch) { const latestBoundAccount = latestFlow.boundAccount
const latestMismatch =
Boolean(latestFlow.bindMismatch) ||
Boolean(
latestFlow.bound &&
latestBoundAccount &&
normalizeUid(latestBoundAccount) !== normalizeUid(expectedGameAccount),
)
if (!latestFlow.bound) {
throw createHttpError('绑定尚未完成,请先扫码绑定账号', {
statusCode: 409,
errorCode: 'affiliate_dash_bind_pending',
})
}
if (!latestBoundAccount) {
throw createHttpError('暂时无法确认绑定 UID,请稍后自动刷新后再提交', {
statusCode: 409,
errorCode: 'affiliate_dash_bound_account_missing',
})
}
if (latestMismatch) {
throw createHttpError('绑定账号与填写 UID 不一致,请确认后重新绑定', { throw createHttpError('绑定账号与填写 UID 不一致,请确认后重新绑定', {
statusCode: 409, statusCode: 409,
errorCode: 'affiliate_dash_bind_mismatch', errorCode: 'affiliate_dash_bind_mismatch',
@@ -453,7 +504,7 @@ export async function submitAffiliateDashClaim(
}) })
const nextFlow = { const nextFlow = {
...flow, ...latestFlow,
gameAccount, gameAccount,
bindUuid, bindUuid,
submitStatus: result.status, submitStatus: result.status,
@@ -519,6 +570,7 @@ async function bindAffiliateDashClaimForTask(task: TaskRow, gameAccount: string)
// 重新绑定:清空旧绑定状态,等待用户用新账号扫码完成新绑定 // 重新绑定:清空旧绑定状态,等待用户用新账号扫码完成新绑定
bound: false, bound: false,
boundAccount: '', boundAccount: '',
gameChannel: '',
bindMismatch: false, bindMismatch: false,
} }
@@ -2,6 +2,7 @@ import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icon
import { Alert, Button, Card, Typography } from 'antd' import { Alert, Button, Card, Typography } from 'antd'
import { formatAdminDateTime } from '@/utils/admin-time' import { formatAdminDateTime } from '@/utils/admin-time'
import type { ClaimAffiliateDashFlowInfo } from '@/types/claim' import type { ClaimAffiliateDashFlowInfo } from '@/types/claim'
import { normalizeUid } from './claim-snapshot'
import type { ClaimSnapshot } from './claim-snapshot' import type { ClaimSnapshot } from './claim-snapshot'
import { ClaimProductItems, InfoTile, UidEditRow } from './claim-shared' import { ClaimProductItems, InfoTile, UidEditRow } from './claim-shared'
@@ -29,20 +30,37 @@ export function AffiliateDashClaimPanel({
onSubmit: () => void onSubmit: () => void
}) { }) {
const bound = Boolean(affiliateDash.bound) const bound = Boolean(affiliateDash.bound)
const bindMismatch = Boolean(affiliateDash.bindMismatch) const expectedAccount = String(
expectedUid || affiliateDash.expectedGameAccount || affiliateDash.gameAccount || '',
).trim()
const boundAccount = String(affiliateDash.boundAccount || '').trim()
const boundAccountMatched = Boolean(
bound &&
expectedAccount &&
boundAccount &&
normalizeUid(boundAccount) === normalizeUid(expectedAccount),
)
const bindMismatch = Boolean(
affiliateDash.bindMismatch ||
(bound && boundAccount && expectedAccount && !boundAccountMatched),
)
const canSubmit = Boolean(bound && boundAccountMatched && !bindMismatch)
const displayBoundAccount = boundAccount || '-'
return ( return (
<Card className="claim-content-card"> <Card className="claim-content-card">
<div className="claim-feifei-main"> <div className="claim-feifei-main">
<Typography.Title level={2}> <Typography.Title level={2}>
{bound && !bindMismatch ? '第 3 步:确认并提交发货' : '第 2 步:绑定领取账号'} {canSubmit ? '第 3 步:确认并提交发货' : '第 2 步:绑定领取账号'}
</Typography.Title> </Typography.Title>
<p> <p>
{bound && !bindMismatch {canSubmit
? '账号绑定成功,确认无误后提交发货' ? '账号绑定成功,确认无误后提交发货'
: bindMismatch : bindMismatch
? '绑定账号与填写 UID 不一致,请核对 UID 后用正确账号重新绑定' ? '绑定账号与填写 UID 不一致,请核对 UID 后用正确账号重新绑定'
: '打开绑定链接完成绑定,系统将自动确认'} : bound
? '已扫码,系统正在确认绑定账号'
: '打开绑定链接完成绑定,系统将自动确认'}
</p> </p>
</div> </div>
@@ -68,23 +86,33 @@ export function AffiliateDashClaimPanel({
type="warning" type="warning"
showIcon showIcon
message="绑定账号与填写 UID 不一致" message="绑定账号与填写 UID 不一致"
description={`已绑定 ${affiliateDash.boundAccount || '-'},请确认绑定的正是你填写的 UID${expectedUid || '-'}`} description={`当前识别到 ${displayBoundAccount},请使用你填写的 UID${expectedAccount || '-'}对应账号重新扫码绑定。`}
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
) : null} ) : null}
{bound ? ( {bound && !bindMismatch && !boundAccountMatched ? (
<Alert
type="info"
showIcon
message="已扫码,正在确认绑定账号"
description="系统还没有拿到可校验的绑定 UID,请稍等自动刷新。"
style={{ marginBottom: 16 }}
/>
) : null}
{canSubmit ? (
<Alert <Alert
type="success" type="success"
showIcon showIcon
icon={<CheckCircleOutlined />} icon={<CheckCircleOutlined />}
message={`已绑定账号:${affiliateDash.boundAccount || affiliateDash.gameAccount || '-'}`} message={`已绑定账号:${displayBoundAccount}`}
description={affiliateDash.gameChannel ? `渠道:${affiliateDash.gameChannel}` : undefined} description={affiliateDash.gameChannel ? `渠道:${affiliateDash.gameChannel}` : undefined}
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
) : null} ) : null}
{!bound ? ( {!canSubmit ? (
<div <div
className="claim-bind-area" className="claim-bind-area"
style={{ style={{
@@ -129,14 +157,6 @@ export function AffiliateDashClaimPanel({
V区用V扫 V区用V扫
</span> </span>
</div> </div>
) : bindMismatch ? (
<Alert
type="warning"
showIcon
message="绑定账号与填写 UID 不一致,暂不能提交发货"
description="请核对上方填写的 UID,确认无误后重新绑定;绑定成功且账号一致后,即可提交发货。"
style={{ marginBottom: 16 }}
/>
) : ( ) : (
<Button <Button
type="primary" type="primary"
+15 -2
View File
@@ -181,9 +181,19 @@ export default function ClaimPage() {
async function handleSubmitAffiliateDash() { async function handleSubmitAffiliateDash() {
const affiliateDash = snapshot.affiliateDash const affiliateDash = snapshot.affiliateDash
const uid = uidInput.trim() || affiliateDash?.gameAccount || '' const uid =
snapshot.expectedUid || affiliateDash?.expectedGameAccount || affiliateDash?.gameAccount || ''
const bindUuid = affiliateDash?.bindUuid || '' const bindUuid = affiliateDash?.bindUuid || ''
if (!snapshot.isAffiliateDashReadyToSubmit) {
showError(
snapshot.isAffiliateDashBindMismatch
? '绑定账号与填写 UID 不一致,请用正确账号重新扫码绑定'
: '绑定尚未完成,请先绑定账号',
)
return
}
if (!uid || !bindUuid) { if (!uid || !bindUuid) {
showError('绑定尚未完成,请先绑定账号') showError('绑定尚未完成,请先绑定账号')
return return
@@ -191,7 +201,10 @@ export default function ClaimPage() {
setSubmittingSubmit(true) setSubmittingSubmit(true)
try { try {
const response = await submitAffiliateDashClaim(token, { gameAccount: uid, bindUuid }) const response = await submitAffiliateDashClaim(token, {
gameAccount: uid,
bindUuid,
})
await applyDetail(response.data) await applyDetail(response.data)
showSuccess('发货已提交') showSuccess('发货已提交')
} catch (error) { } catch (error) {
+1 -1
View File
@@ -66,7 +66,7 @@ export function resolveNextPollDelayMs(
} }
if (nextSnapshot.isAffiliateDashFlow) { if (nextSnapshot.isAffiliateDashFlow) {
if (!nextSnapshot.affiliateDash?.bound) { if (!nextSnapshot.isAffiliateDashReadyToSubmit) {
return BINDING_PREPARE_POLL_MS return BINDING_PREPARE_POLL_MS
} }
return FEIFEI_POLL_MS return FEIFEI_POLL_MS
@@ -34,6 +34,27 @@ export function createClaimSnapshot(
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 isAffiliateDashFlow = detail?.flowType === 'affiliate_dash'
const affiliateDashExpectedUid = String(
expectedUid || affiliateDash?.expectedGameAccount || affiliateDash?.gameAccount || '',
).trim()
const affiliateDashBoundAccount = String(affiliateDash?.boundAccount || '').trim()
const isAffiliateDashBound = Boolean(affiliateDash?.bound)
const isAffiliateDashUidMatched = Boolean(
isAffiliateDashBound &&
affiliateDashExpectedUid &&
affiliateDashBoundAccount &&
normalizeUid(affiliateDashExpectedUid) === normalizeUid(affiliateDashBoundAccount),
)
const isAffiliateDashBindMismatch = Boolean(
affiliateDash?.bindMismatch ||
(isAffiliateDashBound &&
affiliateDashExpectedUid &&
affiliateDashBoundAccount &&
!isAffiliateDashUidMatched),
)
const isAffiliateDashReadyToSubmit = Boolean(
isAffiliateDashBound && isAffiliateDashUidMatched && !isAffiliateDashBindMismatch,
)
const isBindUrlExpired = isDateExpired(flow?.binding.bindExpiresAt || '') const isBindUrlExpired = isDateExpired(flow?.binding.bindExpiresAt || '')
const isBindingPrepared = const isBindingPrepared =
flow?.binding.prepareStatus === 'ready' && flow?.binding.prepareStatus === 'ready' &&
@@ -70,14 +91,16 @@ export function createClaimSnapshot(
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 && (isAffiliateDashFlow &&
['delivered', 'ship_failed', 'cancelled'].includes(String(affiliateDash?.orderStatus || '').trim())) ['delivered', 'ship_failed', 'cancelled'].includes(
String(affiliateDash?.orderStatus || '').trim(),
))
const currentStep = resolveCurrentStep({ const currentStep = resolveCurrentStep({
hasExpectedUid, hasExpectedUid,
hasRedeemResult, hasRedeemResult,
isRoleConfirmed, isRoleConfirmed,
isFeifeiFlow, isFeifeiFlow,
isAffiliateDashFlow, isAffiliateDashFlow,
affiliateDash, isAffiliateDashReadyToSubmit,
}) })
const progressText = resolveProgressText({ const progressText = resolveProgressText({
feifei, feifei,
@@ -90,6 +113,8 @@ export function createClaimSnapshot(
isRoleConfirmed, isRoleConfirmed,
isBindingPrepared, isBindingPrepared,
isUidMatched, isUidMatched,
isAffiliateDashReadyToSubmit,
isAffiliateDashBindMismatch,
}) })
const resultTitle = resolveResultTitle({ const resultTitle = resolveResultTitle({
isRedeemFailed, isRedeemFailed,
@@ -128,6 +153,12 @@ export function createClaimSnapshot(
roleId, roleId,
isFeifeiFlow, isFeifeiFlow,
isAffiliateDashFlow, isAffiliateDashFlow,
affiliateDashExpectedUid,
affiliateDashBoundAccount,
isAffiliateDashBound,
isAffiliateDashUidMatched,
isAffiliateDashBindMismatch,
isAffiliateDashReadyToSubmit,
isBindUrlExpired, isBindUrlExpired,
isBindingPrepared, isBindingPrepared,
isBindingPreparing, isBindingPreparing,
@@ -182,8 +213,7 @@ function resolveCurrentStep(options: {
isRoleConfirmed: boolean isRoleConfirmed: boolean
isFeifeiFlow: boolean isFeifeiFlow: boolean
isAffiliateDashFlow: boolean isAffiliateDashFlow: boolean
affiliateDash: ClaimAffiliateDashFlowInfo | null isAffiliateDashReadyToSubmit: boolean
isUidMatched?: boolean
}) { }) {
if (!options.hasExpectedUid) { if (!options.hasExpectedUid) {
return 1 return 1
@@ -192,7 +222,7 @@ function resolveCurrentStep(options: {
return 4 return 4
} }
if (options.isAffiliateDashFlow) { if (options.isAffiliateDashFlow) {
if (!options.affiliateDash?.bound) { if (!options.isAffiliateDashReadyToSubmit) {
return 2 return 2
} }
return 3 return 3
@@ -214,6 +244,8 @@ function resolveProgressText(options: {
isRoleConfirmed: boolean isRoleConfirmed: boolean
isBindingPrepared: boolean isBindingPrepared: boolean
isUidMatched: boolean isUidMatched: boolean
isAffiliateDashReadyToSubmit: boolean
isAffiliateDashBindMismatch: boolean
}) { }) {
if (!options.hasExpectedUid) { if (!options.hasExpectedUid) {
return '请填写游戏编号' return '请填写游戏编号'
@@ -227,6 +259,12 @@ function resolveProgressText(options: {
? '请扫码绑定账号' ? '请扫码绑定账号'
: '绑定链接生成中,请稍候' : '绑定链接生成中,请稍候'
} }
if (options.isAffiliateDashBindMismatch) {
return '绑定 UID 不一致,请重新扫码'
}
if (!options.isAffiliateDashReadyToSubmit) {
return '绑定账号确认中,请稍候'
}
return '已绑定,请提交发货' return '已绑定,请提交发货'
} }
if (options.isFeifeiFlow) { if (options.isFeifeiFlow) {