修复 affiliate-dash 重复绑定与提交前校验:UID 未变复用绑定防重复生成二维码、提交前强制刷新绑定状态、UID 仅允许纯数字且限 15 位
This commit is contained in:
@@ -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」即可">
|
||||
|
||||
Reference in New Issue
Block a user