From 17acd65f28ad2b6e2065380d506e9eae27a9045d Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 6 Aug 2026 23:46:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20affiliate-dash=20=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E7=BB=91=E5=AE=9A=E4=B8=8E=E6=8F=90=E4=BA=A4=E5=89=8D?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=EF=BC=9AUID=20=E6=9C=AA=E5=8F=98=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E7=BB=91=E5=AE=9A=E9=98=B2=E9=87=8D=E5=A4=8D=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=BA=8C=E7=BB=B4=E7=A0=81=E3=80=81=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=89=8D=E5=BC=BA=E5=88=B6=E5=88=B7=E6=96=B0=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E3=80=81UID=20=E4=BB=85=E5=85=81=E8=AE=B8?= =?UTF-8?q?=E7=BA=AF=E6=95=B0=E5=AD=97=E4=B8=94=E9=99=90=2015=20=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/claim/claim-identity.test.ts | 15 ++++++++ .../src/services/claim/claim-identity.ts | 7 ++-- .../claim/kuaishou-cloud-claim-service.ts | 37 ++++++++++++++++--- apps/frontend/src/pages/claim/ClaimPage.tsx | 5 +++ .../frontend/src/pages/claim/ClaimUidStep.tsx | 6 +-- .../frontend/src/pages/claim/claim-shared.tsx | 4 +- 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/services/claim/claim-identity.test.ts b/apps/backend/src/services/claim/claim-identity.test.ts index adbc5c46..17125cfc 100644 --- a/apps/backend/src/services/claim/claim-identity.test.ts +++ b/apps/backend/src/services/claim/claim-identity.test.ts @@ -22,6 +22,21 @@ test('assertValidClaimUid rejects empty value', () => { assert.throws(() => assertValidClaimUid(''), /请输入游戏 UID/) }) +test('assertValidClaimUid rejects non-digit uid', () => { + assert.throws(() => assertValidClaimUid('abc123'), /仅支持纯数字/) + assert.throws(() => assertValidClaimUid('166909-256'), /仅支持纯数字/) + assert.throws(() => assertValidClaimUid('166909_256'), /仅支持纯数字/) +}) + +test('assertValidClaimUid rejects 91 order no (16 digits) as uid', () => { + // 91 单号 2621801687456831 为 16 位纯数字,超出 UID 上限,防止误填 + assert.throws(() => assertValidClaimUid('2621801687456831'), /长度不能超过 15/) +}) + +test('assertValidClaimUid accepts uid within 15 digits', () => { + assert.equal(assertValidClaimUid('123456789012345'), '123456789012345') +}) + test('isClaimUidMatched compares normalized values', () => { assert.equal(isClaimUidMatched('166909256', '166909256'), true) assert.equal(isClaimUidMatched('166909256', '999'), false) diff --git a/apps/backend/src/services/claim/claim-identity.ts b/apps/backend/src/services/claim/claim-identity.ts index e771b752..587b4c27 100644 --- a/apps/backend/src/services/claim/claim-identity.ts +++ b/apps/backend/src/services/claim/claim-identity.ts @@ -15,7 +15,8 @@ export type { ClaimIdentityPayload, } from '../../types/claim-identity.js' -const CLAIM_UID_MAX_LENGTH = 64 +// 游戏 UID(角色编号)长度上限:91 单号为 16 位纯数字,防止用户把 91 单号误当 UID 提交。 +const CLAIM_UID_MAX_LENGTH = 15 export function normalizeClaimUid(value: unknown): string { return String(value || '') @@ -39,8 +40,8 @@ export function assertValidClaimUid(value: unknown): string { }) } - if (!/^[A-Za-z0-9_\-]+$/.test(uid)) { - throw createHttpError('UID 仅支持字母、数字、下划线和中划线', { + if (!/^[0-9]+$/.test(uid)) { + throw createHttpError('UID 仅支持纯数字', { statusCode: 400, errorCode: 'claim_uid_invalid', }) diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts index aa17589d..a8d73050 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -372,7 +372,27 @@ export async function submitClaimUid( } if (executorKey === 'affiliate_dash') { - updatedTask = (await bindAffiliateDashClaimForTask(updatedTask, expectedUid)) || updatedTask + const dashFlow = normalizeAffiliateDashFlow(parseTaskContext(updatedTask).affiliateDash) + const uidUnchanged = previous.expectedUid === expectedUid + const hasBindUuid = Boolean(String(dashFlow.bindUuid || '').trim()) + const bindMismatch = Boolean(dashFlow.bindMismatch) + if (uidUnchanged && hasBindUuid && !bindMismatch) { + // UID 未变、已有绑定二维码且当前绑定未报 mismatch:复用现有绑定,避免重复 bind + // 生成新码覆盖旧码,导致用户扫码的二维码与提交时的 bind_uuid 不一致 + // (平台 bind_verify 报“账号尚未绑定”)。绑定不一致时仍允许重新 bind 换码。 + await createTaskEvent( + updatedTask.id, + 'affiliate_dash_bind_reused', + { + orderNo: dashFlow.orderNo, + bindUuid: dashFlow.bindUuid, + gameAccount: expectedUid, + }, + now, + ) + } else { + updatedTask = (await bindAffiliateDashClaimForTask(updatedTask, expectedUid)) || updatedTask + } } return getKuaishouCloudClaimDetail(token) @@ -434,11 +454,18 @@ export async function submitAffiliateDashClaim( const gameAccount = expectedGameAccount let latestFlow = flow - // 提交阶段不再实时调用 affiliate-dash bind-result。 - // 平台日志显示:正常流程在 submit 前额外触发一次 bind-result 后,delivery/submit 会在平台侧 - // bind_verify 阶段偶发/稳定返回“账号尚未绑定”。这里改为使用详情轮询已写入的本地绑定快照, - // 避免 submit 前的额外平台查询改变或干扰平台发货校验链路。 if (!isMock && flow.bindUuid) { + // 提交前强制刷新绑定状态:用平台 bind-result 拉取最新 bound/绑定账号/mismatch 并写回本地快照, + // 避免本地轮询快照过期时带着旧绑定直接提交,被平台 bind_verify 拒绝或发错账号。 + // 校验未通过时直接拦截,不发起 submit(曾观察到 submit 前查询与平台 bind_verify 存在干扰, + // 拦截在本地可以避免平台侧产生 bind_verify 失败记录)。 + try { + const refreshed = (await refreshAffiliateDashBindState(task)) || task + latestFlow = normalizeAffiliateDashFlow(parseTaskContextValue(refreshed).affiliateDash) + } catch { + // 刷新失败:回退到本地最近一次快照继续校验,不阻塞提交;本地快照不满足时下方仍会拦截。 + } + const latestBoundAccount = latestFlow.boundAccount const latestMismatch = Boolean(latestFlow.bindMismatch) || diff --git a/apps/frontend/src/pages/claim/ClaimPage.tsx b/apps/frontend/src/pages/claim/ClaimPage.tsx index 5152db15..8bc6893d 100644 --- a/apps/frontend/src/pages/claim/ClaimPage.tsx +++ b/apps/frontend/src/pages/claim/ClaimPage.tsx @@ -161,6 +161,11 @@ export default function ClaimPage() { }, [loadDetail, resolveNextPollDelay, snapshot, stopPolling]) async function handleSubmitUid() { + // 防并发:提交中忽略重复触发(按钮 loading + 回车 Enter 都可能连续触发) + if (submittingUid) { + return + } + const uid = uidInput.trim() if (!uid) { showError('请输入游戏 UID') diff --git a/apps/frontend/src/pages/claim/ClaimUidStep.tsx b/apps/frontend/src/pages/claim/ClaimUidStep.tsx index 79d7c4fa..9c12c3ff 100644 --- a/apps/frontend/src/pages/claim/ClaimUidStep.tsx +++ b/apps/frontend/src/pages/claim/ClaimUidStep.tsx @@ -18,7 +18,7 @@ export function ClaimUidStep({ 第 1 步:填写游戏编号(数字的那个)

- 请填写游戏内角色编号(纯数字),提交后继续绑定或领取。 + 请填写游戏内角色编号(纯数字,不超过 15 位),提交后继续绑定或领取。

@@ -26,9 +26,9 @@ export function ClaimUidStep({ size="large" placeholder="请输入游戏编号(纯数字)" value={uidInput} - maxLength={64} + maxLength={15} inputMode="numeric" - onChange={(event) => onChange(event.target.value)} + onChange={(event) => onChange(event.target.value.replace(/\D/g, '').slice(0, 15))} onPressEnter={onSubmit} />