修复 affiliate-dash 重复绑定与提交前校验:UID 未变复用绑定防重复生成二维码、提交前强制刷新绑定状态、UID 仅允许纯数字且限 15 位

This commit is contained in:
yml2213
2026-08-06 23:46:02 +08:00
parent 8756a196ae
commit 17acd65f28
6 changed files with 61 additions and 13 deletions
@@ -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)
@@ -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',
})
@@ -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) ||
@@ -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')
@@ -18,7 +18,7 @@ export function ClaimUidStep({
1
</Typography.Title>
<p className="claim-muted claim-uid-step-desc">
15
</p>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
@@ -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}
/>
<Button type="primary" size="large" loading={submitting} onClick={onSubmit} block>
@@ -50,9 +50,9 @@ export function UidEditRow({
<Input
size="large"
value={uidInput}
maxLength={64}
maxLength={15}
placeholder="游戏编号"
onChange={(event) => onChange(event.target.value)}
onChange={(event) => onChange(event.target.value.replace(/\D/g, '').slice(0, 15))}
onPressEnter={onSubmit}
/>
<Tooltip title="游戏编号填错时,修改为正确编号后点击「更新 UID」即可">