修复 affiliate-dash 绑定状态一致性
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import {
|
||||||
|
canReuseAffiliateDashBindSnapshot,
|
||||||
|
isAffiliateDashBindSnapshotStale,
|
||||||
|
} from './kuaishou-cloud-claim-service.js'
|
||||||
|
|
||||||
|
test('isAffiliateDashBindSnapshotStale detects bind uuid that belongs to old uid', () => {
|
||||||
|
assert.equal(
|
||||||
|
isAffiliateDashBindSnapshotStale(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-old',
|
||||||
|
bindUrl: 'https://dash.example/bind-old',
|
||||||
|
expectedGameAccount: '111',
|
||||||
|
gameAccount: '111',
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAffiliateDashBindSnapshotStale treats unknown bind uid as stale', () => {
|
||||||
|
assert.equal(
|
||||||
|
isAffiliateDashBindSnapshotStale(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-unknown',
|
||||||
|
bindUrl: 'https://dash.example/bind-unknown',
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAffiliateDashBindSnapshotStale keeps current uid bind snapshot', () => {
|
||||||
|
assert.equal(
|
||||||
|
isAffiliateDashBindSnapshotStale(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-current',
|
||||||
|
bindUrl: 'https://dash.example/bind-current',
|
||||||
|
expectedGameAccount: '222',
|
||||||
|
gameAccount: '222',
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('canReuseAffiliateDashBindSnapshot reuses pending current uid bind', () => {
|
||||||
|
assert.equal(
|
||||||
|
canReuseAffiliateDashBindSnapshot(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-current',
|
||||||
|
bindUrl: 'https://dash.example/bind-current',
|
||||||
|
expectedGameAccount: '222',
|
||||||
|
gameAccount: '222',
|
||||||
|
bound: false,
|
||||||
|
bindMismatch: false,
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('canReuseAffiliateDashBindSnapshot refuses old uid bind', () => {
|
||||||
|
assert.equal(
|
||||||
|
canReuseAffiliateDashBindSnapshot(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-old',
|
||||||
|
bindUrl: 'https://dash.example/bind-old',
|
||||||
|
expectedGameAccount: '111',
|
||||||
|
gameAccount: '111',
|
||||||
|
bound: false,
|
||||||
|
bindMismatch: false,
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('canReuseAffiliateDashBindSnapshot refuses bound snapshot without bound account', () => {
|
||||||
|
assert.equal(
|
||||||
|
canReuseAffiliateDashBindSnapshot(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-current',
|
||||||
|
bindUrl: 'https://dash.example/bind-current',
|
||||||
|
expectedGameAccount: '222',
|
||||||
|
gameAccount: '222',
|
||||||
|
bound: true,
|
||||||
|
boundAccount: '',
|
||||||
|
bindMismatch: false,
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('canReuseAffiliateDashBindSnapshot reuses verified matching bound account', () => {
|
||||||
|
assert.equal(
|
||||||
|
canReuseAffiliateDashBindSnapshot(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-current',
|
||||||
|
bindUrl: 'https://dash.example/bind-current',
|
||||||
|
expectedGameAccount: '222',
|
||||||
|
gameAccount: '222',
|
||||||
|
bound: true,
|
||||||
|
boundAccount: '222',
|
||||||
|
bindMismatch: false,
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('canReuseAffiliateDashBindSnapshot refuses mismatched bound account', () => {
|
||||||
|
assert.equal(
|
||||||
|
canReuseAffiliateDashBindSnapshot(
|
||||||
|
{
|
||||||
|
bindUuid: 'bind-current',
|
||||||
|
bindUrl: 'https://dash.example/bind-current',
|
||||||
|
expectedGameAccount: '222',
|
||||||
|
gameAccount: '222',
|
||||||
|
bound: true,
|
||||||
|
boundAccount: '333',
|
||||||
|
bindMismatch: false,
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -319,44 +319,35 @@ export async function submitClaimUid(
|
|||||||
|
|
||||||
const taskContext = parseTaskContext(context.task)
|
const taskContext = parseTaskContext(context.task)
|
||||||
const previous = getClaimIdentityFromTask(context.task)
|
const previous = getClaimIdentityFromTask(context.task)
|
||||||
|
const executorKey = String(context.task.executor_key || '').trim()
|
||||||
|
if (executorKey === 'affiliate_dash') {
|
||||||
|
return submitAffiliateDashClaimUid(context, expectedUid, previous, taskContext, now, token)
|
||||||
|
}
|
||||||
|
|
||||||
const nextContext = {
|
const nextContext = {
|
||||||
...taskContext,
|
...taskContext,
|
||||||
claimIdentity: {
|
claimIdentity: buildClaimIdentityContext(previous, expectedUid, now),
|
||||||
expectedUid,
|
|
||||||
submittedAt: previous.expectedUid === expectedUid && previous.submittedAt
|
|
||||||
? previous.submittedAt
|
|
||||||
: now,
|
|
||||||
source: 'claim_page',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedTask =
|
let updatedTask = (await updateTask(context.task.id, {
|
||||||
(await updateTask(context.task.id, {
|
context_json: JSON.stringify(nextContext),
|
||||||
context_json: JSON.stringify(nextContext),
|
claimed_at: context.task.claimed_at || now,
|
||||||
claimed_at: context.task.claimed_at || now,
|
last_error: '',
|
||||||
last_error: '',
|
updated_at: now,
|
||||||
updated_at: now,
|
})) || {
|
||||||
})) || {
|
...context.task,
|
||||||
...context.task,
|
context_json: JSON.stringify(nextContext),
|
||||||
context_json: JSON.stringify(nextContext),
|
claimed_at: context.task.claimed_at || now,
|
||||||
claimed_at: context.task.claimed_at || now,
|
updated_at: now,
|
||||||
updated_at: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previous.expectedUid !== expectedUid) {
|
|
||||||
await createTaskEvent(
|
|
||||||
context.task.id,
|
|
||||||
'claim_uid_submitted',
|
|
||||||
{
|
|
||||||
expectedUid,
|
|
||||||
previousUid: previous.expectedUid || '',
|
|
||||||
source: 'claim_page',
|
|
||||||
},
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const executorKey = String(updatedTask.executor_key || '').trim()
|
await createClaimUidSubmittedEventIfChanged(
|
||||||
|
context.task.id,
|
||||||
|
previous.expectedUid,
|
||||||
|
expectedUid,
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
if (executorKey === 'kuaishou_ct_assisted' && !isKuaishouCloudMockTask(updatedTask)) {
|
if (executorKey === 'kuaishou_ct_assisted' && !isKuaishouCloudMockTask(updatedTask)) {
|
||||||
try {
|
try {
|
||||||
const prepared = await prepareFulfillmentBinding(updatedTask, {
|
const prepared = await prepareFulfillmentBinding(updatedTask, {
|
||||||
@@ -371,29 +362,156 @@ export async function submitClaimUid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executorKey === 'affiliate_dash') {
|
return getKuaishouCloudClaimDetail(token)
|
||||||
const dashFlow = normalizeAffiliateDashFlow(parseTaskContext(updatedTask).affiliateDash)
|
}
|
||||||
const uidUnchanged = previous.expectedUid === expectedUid
|
|
||||||
const hasBindUuid = Boolean(String(dashFlow.bindUuid || '').trim())
|
function buildClaimIdentityContext(
|
||||||
const bindMismatch = Boolean(dashFlow.bindMismatch)
|
previous: ReturnType<typeof getClaimIdentityFromTask>,
|
||||||
if (uidUnchanged && hasBindUuid && !bindMismatch) {
|
expectedUid: string,
|
||||||
// UID 未变、已有绑定二维码且当前绑定未报 mismatch:复用现有绑定,避免重复 bind
|
now: string,
|
||||||
// 生成新码覆盖旧码,导致用户扫码的二维码与提交时的 bind_uuid 不一致
|
) {
|
||||||
// (平台 bind_verify 报“账号尚未绑定”)。绑定不一致时仍允许重新 bind 换码。
|
return {
|
||||||
await createTaskEvent(
|
expectedUid,
|
||||||
updatedTask.id,
|
submittedAt:
|
||||||
'affiliate_dash_bind_reused',
|
previous.expectedUid === expectedUid && previous.submittedAt ? previous.submittedAt : now,
|
||||||
{
|
source: 'claim_page',
|
||||||
orderNo: dashFlow.orderNo,
|
|
||||||
bindUuid: dashFlow.bindUuid,
|
|
||||||
gameAccount: expectedUid,
|
|
||||||
},
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
updatedTask = (await bindAffiliateDashClaimForTask(updatedTask, expectedUid)) || updatedTask
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createClaimUidSubmittedEventIfChanged(
|
||||||
|
taskId: number,
|
||||||
|
previousUid: string,
|
||||||
|
expectedUid: string,
|
||||||
|
now: string,
|
||||||
|
) {
|
||||||
|
if (previousUid === expectedUid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createTaskEvent(
|
||||||
|
taskId,
|
||||||
|
'claim_uid_submitted',
|
||||||
|
{
|
||||||
|
expectedUid,
|
||||||
|
previousUid: previousUid || '',
|
||||||
|
source: 'claim_page',
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAffiliateDashClaimUid(
|
||||||
|
context: Awaited<ReturnType<typeof getClaimContext>>,
|
||||||
|
expectedUid: string,
|
||||||
|
previous: ReturnType<typeof getClaimIdentityFromTask>,
|
||||||
|
taskContext: JsonObject,
|
||||||
|
now: string,
|
||||||
|
token: unknown,
|
||||||
|
): Promise<ClaimDetailPayload> {
|
||||||
|
const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash)
|
||||||
|
const claimIdentity = buildClaimIdentityContext(previous, expectedUid, now)
|
||||||
|
const baseTaskPatch = {
|
||||||
|
claimed_at: context.task.claimed_at || now,
|
||||||
|
last_error: '',
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flow.orderNo) {
|
||||||
|
await updateTask(context.task.id, {
|
||||||
|
...baseTaskPatch,
|
||||||
|
context_json: JSON.stringify({
|
||||||
|
...taskContext,
|
||||||
|
claimIdentity,
|
||||||
|
affiliateDash: clearAffiliateDashBindSnapshot(flow, expectedUid),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await createClaimUidSubmittedEventIfChanged(
|
||||||
|
context.task.id,
|
||||||
|
previous.expectedUid,
|
||||||
|
expectedUid,
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
return getKuaishouCloudClaimDetail(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canReuseAffiliateDashBindSnapshot(flow, expectedUid)) {
|
||||||
|
const nextFlow = {
|
||||||
|
...flow,
|
||||||
|
gameAccount: expectedUid,
|
||||||
|
expectedGameAccount: expectedUid,
|
||||||
|
bindMismatch: false,
|
||||||
|
}
|
||||||
|
await updateTask(context.task.id, {
|
||||||
|
...baseTaskPatch,
|
||||||
|
task_status: TASK_STATUS.WAITING_BINDING,
|
||||||
|
context_json: JSON.stringify({
|
||||||
|
...taskContext,
|
||||||
|
claimIdentity,
|
||||||
|
affiliateDash: nextFlow,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await createClaimUidSubmittedEventIfChanged(
|
||||||
|
context.task.id,
|
||||||
|
previous.expectedUid,
|
||||||
|
expectedUid,
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
await createTaskEvent(
|
||||||
|
context.task.id,
|
||||||
|
'affiliate_dash_bind_reused',
|
||||||
|
{
|
||||||
|
orderNo: flow.orderNo,
|
||||||
|
bindUuid: flow.bindUuid,
|
||||||
|
gameAccount: expectedUid,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
return getKuaishouCloudClaimDetail(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindResult = await bindAffiliateDashDelivery({
|
||||||
|
orderNo: flow.orderNo,
|
||||||
|
gameAccount: expectedUid,
|
||||||
|
})
|
||||||
|
const nextFlow = {
|
||||||
|
...flow,
|
||||||
|
bindUuid: bindResult.bindUuid,
|
||||||
|
bindUrl: bindResult.bindUrl,
|
||||||
|
qrUrl: bindResult.qrUrl,
|
||||||
|
gameAccount: expectedUid,
|
||||||
|
expectedGameAccount: expectedUid,
|
||||||
|
// 重新绑定:清空旧绑定状态,等待用户用新账号扫码完成新绑定。
|
||||||
|
bound: false,
|
||||||
|
boundAccount: '',
|
||||||
|
gameChannel: '',
|
||||||
|
bindMismatch: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateTask(context.task.id, {
|
||||||
|
...baseTaskPatch,
|
||||||
|
task_status: TASK_STATUS.WAITING_BINDING,
|
||||||
|
context_json: JSON.stringify({
|
||||||
|
...taskContext,
|
||||||
|
claimIdentity,
|
||||||
|
affiliateDash: nextFlow,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await createClaimUidSubmittedEventIfChanged(
|
||||||
|
context.task.id,
|
||||||
|
previous.expectedUid,
|
||||||
|
expectedUid,
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
await createTaskEvent(
|
||||||
|
context.task.id,
|
||||||
|
'affiliate_dash_bind_created',
|
||||||
|
{
|
||||||
|
orderNo: flow.orderNo,
|
||||||
|
bindUuid: bindResult.bindUuid,
|
||||||
|
gameAccount: expectedUid,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
return getKuaishouCloudClaimDetail(token)
|
return getKuaishouCloudClaimDetail(token)
|
||||||
}
|
}
|
||||||
@@ -425,7 +543,10 @@ export async function submitAffiliateDashClaim(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expectedGameAccount = String(
|
const expectedGameAccount = String(
|
||||||
getClaimIdentityFromTask(task).expectedUid || flow.expectedGameAccount || flow.gameAccount || '',
|
getClaimIdentityFromTask(task).expectedUid ||
|
||||||
|
flow.expectedGameAccount ||
|
||||||
|
flow.gameAccount ||
|
||||||
|
'',
|
||||||
).trim()
|
).trim()
|
||||||
const requestedGameAccount = String(payload.gameAccount || '').trim()
|
const requestedGameAccount = String(payload.gameAccount || '').trim()
|
||||||
const requestedBindUuid = String(payload.bindUuid || '').trim()
|
const requestedBindUuid = String(payload.bindUuid || '').trim()
|
||||||
@@ -436,7 +557,10 @@ export async function submitAffiliateDashClaim(
|
|||||||
errorCode: 'affiliate_dash_submit_missing',
|
errorCode: 'affiliate_dash_submit_missing',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (requestedGameAccount && normalizeUid(requestedGameAccount) !== normalizeUid(expectedGameAccount)) {
|
if (
|
||||||
|
requestedGameAccount &&
|
||||||
|
normalizeUid(requestedGameAccount) !== normalizeUid(expectedGameAccount)
|
||||||
|
) {
|
||||||
throw createHttpError('提交 UID 与第一步填写 UID 不一致,请先更新 UID 后重新绑定', {
|
throw createHttpError('提交 UID 与第一步填写 UID 不一致,请先更新 UID 后重新绑定', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
errorCode: 'affiliate_dash_submit_uid_changed',
|
errorCode: 'affiliate_dash_submit_uid_changed',
|
||||||
@@ -455,15 +579,13 @@ export async function submitAffiliateDashClaim(
|
|||||||
let latestFlow = flow
|
let latestFlow = flow
|
||||||
|
|
||||||
if (!isMock && flow.bindUuid) {
|
if (!isMock && flow.bindUuid) {
|
||||||
// 提交前强制刷新绑定状态:用平台 bind-result 拉取最新 bound/绑定账号/mismatch 并写回本地快照,
|
// 提交前不再额外请求 bind-result:平台侧曾出现 GET 后 submit 的 bind_verify 异常。
|
||||||
// 避免本地轮询快照过期时带着旧绑定直接提交,被平台 bind_verify 拒绝或发错账号。
|
// 这里仅使用详情轮询写入的本地快照做强拦截,真正提交仍交给平台二次校验。
|
||||||
// 校验未通过时直接拦截,不发起 submit(曾观察到 submit 前查询与平台 bind_verify 存在干扰,
|
if (isAffiliateDashBindSnapshotStale(flow, expectedGameAccount)) {
|
||||||
// 拦截在本地可以避免平台侧产生 bind_verify 失败记录)。
|
throw createHttpError('绑定二维码与当前 UID 不一致,请更新 UID 后重新绑定', {
|
||||||
try {
|
statusCode: 409,
|
||||||
const refreshed = (await refreshAffiliateDashBindState(task)) || task
|
errorCode: 'affiliate_dash_bind_uid_stale',
|
||||||
latestFlow = normalizeAffiliateDashFlow(parseTaskContextValue(refreshed).affiliateDash)
|
})
|
||||||
} catch {
|
|
||||||
// 刷新失败:回退到本地最近一次快照继续校验,不阻塞提交;本地快照不满足时下方仍会拦截。
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestBoundAccount = latestFlow.boundAccount
|
const latestBoundAccount = latestFlow.boundAccount
|
||||||
@@ -547,56 +669,76 @@ export async function submitAffiliateDashClaim(
|
|||||||
return getKuaishouCloudClaimDetail(token)
|
return getKuaishouCloudClaimDetail(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type AffiliateDashFlowSnapshot = ReturnType<typeof normalizeAffiliateDashFlow>
|
||||||
* 提交 UID 后触发 affiliate-dash 绑定(bind),把 bind_uuid / 二维码写回上下文,
|
|
||||||
* task → waiting_binding(link_generated → waiting_binding 合法)。
|
function resolveAffiliateDashExpectedGameAccount(
|
||||||
*/
|
task: Partial<TaskRow> | null | undefined,
|
||||||
async function bindAffiliateDashClaimForTask(task: TaskRow, gameAccount: string) {
|
flow: AffiliateDashFlowSnapshot,
|
||||||
const taskContext = parseTaskContextValue(task)
|
) {
|
||||||
const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash)
|
return String(
|
||||||
if (!flow.orderNo || !gameAccount) {
|
getClaimIdentityFromTask(task).expectedUid ||
|
||||||
return task
|
flow.expectedGameAccount ||
|
||||||
|
flow.gameAccount ||
|
||||||
|
'',
|
||||||
|
).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAffiliateDashRecordedBindUid(flow: AffiliateDashFlowSnapshot) {
|
||||||
|
return normalizeUid(flow.expectedGameAccount || flow.gameAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAffiliateDashBindSnapshotStale(flowLike: unknown, expectedUid: unknown): boolean {
|
||||||
|
const flow = normalizeAffiliateDashFlow(flowLike)
|
||||||
|
const expected = normalizeUid(expectedUid)
|
||||||
|
if (!flow.bindUuid || !expected) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = nowIso()
|
const recordedBindUid = resolveAffiliateDashRecordedBindUid(flow)
|
||||||
const bindResult = await bindAffiliateDashDelivery({
|
return !recordedBindUid || recordedBindUid !== expected
|
||||||
orderNo: flow.orderNo,
|
}
|
||||||
gameAccount,
|
|
||||||
})
|
export function canReuseAffiliateDashBindSnapshot(
|
||||||
const nextFlow = {
|
flowLike: unknown,
|
||||||
|
expectedUid: unknown,
|
||||||
|
): boolean {
|
||||||
|
const flow = normalizeAffiliateDashFlow(flowLike)
|
||||||
|
const expected = normalizeUid(expectedUid)
|
||||||
|
if (
|
||||||
|
!expected ||
|
||||||
|
!flow.bindUuid ||
|
||||||
|
!(flow.bindUrl || flow.qrUrl) ||
|
||||||
|
flow.bindMismatch ||
|
||||||
|
isAffiliateDashBindSnapshotStale(flow, expected)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundAccount = normalizeUid(flow.boundAccount)
|
||||||
|
if (boundAccount && boundAccount !== expected) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !flow.bound || Boolean(boundAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAffiliateDashBindSnapshot(
|
||||||
|
flow: AffiliateDashFlowSnapshot,
|
||||||
|
expectedUid: unknown,
|
||||||
|
): AffiliateDashFlowSnapshot {
|
||||||
|
const gameAccount = String(expectedUid || '').trim()
|
||||||
|
return {
|
||||||
...flow,
|
...flow,
|
||||||
bindUuid: bindResult.bindUuid,
|
bindUuid: '',
|
||||||
bindUrl: bindResult.bindUrl,
|
bindUrl: '',
|
||||||
qrUrl: bindResult.qrUrl,
|
qrUrl: '',
|
||||||
gameAccount,
|
gameAccount: gameAccount || flow.gameAccount,
|
||||||
expectedGameAccount: gameAccount,
|
expectedGameAccount: gameAccount || flow.expectedGameAccount,
|
||||||
// 重新绑定:清空旧绑定状态,等待用户用新账号扫码完成新绑定
|
|
||||||
bound: false,
|
bound: false,
|
||||||
boundAccount: '',
|
boundAccount: '',
|
||||||
gameChannel: '',
|
gameChannel: '',
|
||||||
bindMismatch: false,
|
bindMismatch: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -614,24 +756,44 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | n
|
|||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expectedGameAccount = resolveAffiliateDashExpectedGameAccount(task, flow)
|
||||||
|
if (isAffiliateDashBindSnapshotStale(flow, expectedGameAccount)) {
|
||||||
|
const now = nowIso()
|
||||||
|
return (
|
||||||
|
(await updateTask(task.id, {
|
||||||
|
context_json: JSON.stringify({
|
||||||
|
...taskContext,
|
||||||
|
affiliateDash: clearAffiliateDashBindSnapshot(flow, expectedGameAccount),
|
||||||
|
}),
|
||||||
|
last_error: '绑定二维码与当前 UID 不一致,请点击更新 UID 重新生成绑定二维码',
|
||||||
|
updated_at: now,
|
||||||
|
})) || task
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await getAffiliateDashBindResult({
|
const result = await getAffiliateDashBindResult({
|
||||||
orderNo: flow.orderNo,
|
orderNo: flow.orderNo,
|
||||||
bindUuid: flow.bindUuid,
|
bindUuid: flow.bindUuid,
|
||||||
})
|
})
|
||||||
const boundAccount = result.gameAccount || flow.boundAccount
|
const boundAccount = result.bound ? result.gameAccount || flow.boundAccount : ''
|
||||||
// 本地强制校验:绑定返回的游戏账号与第一步填写的 UID 不一致时置 mismatch
|
// 本地强制校验:绑定返回的游戏账号与第一步填写的 UID 不一致时置 mismatch
|
||||||
// (不依赖平台 mismatch 字段,防止平台未标记但实际账号不一致的情况)。
|
// (不依赖平台 mismatch 字段,防止平台未标记但实际账号不一致的情况)。
|
||||||
const localMismatch = Boolean(
|
const localMismatch = Boolean(
|
||||||
boundAccount &&
|
boundAccount &&
|
||||||
flow.expectedGameAccount &&
|
expectedGameAccount &&
|
||||||
normalizeUid(boundAccount) !== normalizeUid(flow.expectedGameAccount),
|
normalizeUid(boundAccount) !== normalizeUid(expectedGameAccount),
|
||||||
)
|
)
|
||||||
const nextFlow = {
|
const nextFlow = {
|
||||||
...flow,
|
...flow,
|
||||||
|
bindUuid: result.bindUuid || flow.bindUuid,
|
||||||
|
bindUrl: result.bindUrl || flow.bindUrl,
|
||||||
|
qrUrl: result.qrUrl || flow.qrUrl,
|
||||||
|
gameAccount: expectedGameAccount || flow.gameAccount,
|
||||||
|
expectedGameAccount: expectedGameAccount || flow.expectedGameAccount,
|
||||||
bound: result.bound,
|
bound: result.bound,
|
||||||
boundAccount,
|
boundAccount,
|
||||||
gameChannel: result.gameChannel || flow.gameChannel,
|
gameChannel: result.bound ? result.gameChannel || flow.gameChannel : '',
|
||||||
bindMismatch: Boolean(result.mismatch) || localMismatch,
|
bindMismatch: Boolean(result.mismatch) || localMismatch,
|
||||||
}
|
}
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
|||||||
Reference in New Issue
Block a user