feat(claim): 配合方案B支持买家纠错扫码UID自愈绑定,自建页面增加确认更换提示与快捷填入
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
|||||||
canReuseAffiliateDashBindSnapshot,
|
canReuseAffiliateDashBindSnapshot,
|
||||||
isAffiliateDashBindSnapshotStale,
|
isAffiliateDashBindSnapshotStale,
|
||||||
isAffiliateDashSnapshotFresh,
|
isAffiliateDashSnapshotFresh,
|
||||||
|
isRockAffiliateDashSku,
|
||||||
|
shouldRefuseRockRebind,
|
||||||
shouldRefreshAffiliateDashBindStateForSnapshot,
|
shouldRefreshAffiliateDashBindStateForSnapshot,
|
||||||
} from './kuaishou-cloud-claim-service.js'
|
} from './kuaishou-cloud-claim-service.js'
|
||||||
|
|
||||||
@@ -123,6 +125,81 @@ test('canReuseAffiliateDashBindSnapshot reuses verified matching bound account',
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('rock rebind is refused after mismatch or after a successful bind changes uid', () => {
|
||||||
|
assert.equal(isRockAffiliateDashSku('rock_black_tshirt'), true)
|
||||||
|
assert.equal(isRockAffiliateDashSku('top_black_elite_trainer'), false)
|
||||||
|
assert.equal(
|
||||||
|
shouldRefuseRockRebind({
|
||||||
|
sku: 'rock_black_tshirt',
|
||||||
|
bindUuid: 'bind-1',
|
||||||
|
bindMismatch: true,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
// 方案 B:买家改回扫码真实账号时放行自愈,改第三个账号时拒绝
|
||||||
|
assert.equal(
|
||||||
|
shouldRefuseRockRebind(
|
||||||
|
{
|
||||||
|
sku: 'rock_black_tshirt',
|
||||||
|
bindUuid: 'bind-1',
|
||||||
|
boundAccount: '3454648872',
|
||||||
|
bindMismatch: true,
|
||||||
|
},
|
||||||
|
'3454648872',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
shouldRefuseRockRebind(
|
||||||
|
{
|
||||||
|
sku: 'rock_black_tshirt',
|
||||||
|
bindUuid: 'bind-1',
|
||||||
|
boundAccount: '3454648872',
|
||||||
|
bindMismatch: true,
|
||||||
|
},
|
||||||
|
'9999999999',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
shouldRefuseRockRebind(
|
||||||
|
{
|
||||||
|
sku: 'rock_black_tshirt',
|
||||||
|
bindUuid: 'bind-1',
|
||||||
|
bound: true,
|
||||||
|
boundAccount: '111',
|
||||||
|
expectedGameAccount: '111',
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
shouldRefuseRockRebind(
|
||||||
|
{
|
||||||
|
sku: 'rock_black_tshirt',
|
||||||
|
bindUuid: 'bind-1',
|
||||||
|
bound: false,
|
||||||
|
bindMismatch: false,
|
||||||
|
expectedGameAccount: '111',
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
shouldRefuseRockRebind(
|
||||||
|
{
|
||||||
|
sku: 'skin_other',
|
||||||
|
bindUuid: 'bind-1',
|
||||||
|
bindMismatch: true,
|
||||||
|
},
|
||||||
|
'222',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('canReuseAffiliateDashBindSnapshot refuses mismatched bound account', () => {
|
test('canReuseAffiliateDashBindSnapshot refuses mismatched bound account', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
canReuseAffiliateDashBindSnapshot(
|
canReuseAffiliateDashBindSnapshot(
|
||||||
|
|||||||
@@ -203,6 +203,13 @@ async function submitAffiliateDashClaimUid(
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldRefuseRockRebind(flow, expectedUid)) {
|
||||||
|
throw createHttpError(ROCK_CLAIM_TOKEN_LOCKED_MESSAGE, {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'affiliate_dash_rock_token_locked',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!flow.orderNo) {
|
if (!flow.orderNo) {
|
||||||
await updateTask(context.task.id, {
|
await updateTask(context.task.id, {
|
||||||
...baseTaskPatch,
|
...baseTaskPatch,
|
||||||
@@ -472,6 +479,40 @@ function resolveAffiliateDashRecordedBindUid(flow: AffiliateDashFlowSnapshot) {
|
|||||||
return normalizeUid(flow.expectedGameAccount || flow.gameAccount)
|
return normalizeUid(flow.expectedGameAccount || flow.gameAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ROCK_CLAIM_TOKEN_LOCKED_MESSAGE =
|
||||||
|
'该订单提货链接已绑定失败,不能更换新的提货号,请联系客服处理'
|
||||||
|
|
||||||
|
export function isRockAffiliateDashSku(sku: unknown) {
|
||||||
|
return String(sku || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.startsWith('rock_')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rock 一单一号:绑定失败禁止换第三方新号;但如果买家纠正为当前扫码角色的 UID,允许放行自愈。 */
|
||||||
|
export function shouldRefuseRockRebind(flowLike: unknown, nextUid?: unknown) {
|
||||||
|
const flow = normalizeAffiliateDashFlow(flowLike)
|
||||||
|
if (!isRockAffiliateDashSku(flow.sku)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const next = normalizeUid(nextUid)
|
||||||
|
const boundAccount = normalizeUid(flow.boundAccount)
|
||||||
|
|
||||||
|
// 方案 B:如果买家确认修改为当前扫码反查到的角色 UID,放行自愈
|
||||||
|
if (flow.bindMismatch) {
|
||||||
|
if (next && boundAccount && next === boundAccount) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flow.bound) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const current = normalizeUid(flow.boundAccount || flow.expectedGameAccount || flow.gameAccount)
|
||||||
|
return Boolean(next && current && next !== current)
|
||||||
|
}
|
||||||
|
|
||||||
export function isAffiliateDashBindSnapshotStale(flowLike: unknown, expectedUid: unknown): boolean {
|
export function isAffiliateDashBindSnapshotStale(flowLike: unknown, expectedUid: unknown): boolean {
|
||||||
const flow = normalizeAffiliateDashFlow(flowLike)
|
const flow = normalizeAffiliateDashFlow(flowLike)
|
||||||
const expected = normalizeUid(expectedUid)
|
const expected = normalizeUid(expectedUid)
|
||||||
@@ -577,6 +618,18 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | n
|
|||||||
const expectedGameAccount = resolveAffiliateDashExpectedGameAccount(task, flow)
|
const expectedGameAccount = resolveAffiliateDashExpectedGameAccount(task, flow)
|
||||||
if (isAffiliateDashBindSnapshotStale(flow, expectedGameAccount)) {
|
if (isAffiliateDashBindSnapshotStale(flow, expectedGameAccount)) {
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
if (shouldRefuseRockRebind(flow, expectedGameAccount)) {
|
||||||
|
return (
|
||||||
|
(await updateTask(task.id, {
|
||||||
|
context_json: JSON.stringify({
|
||||||
|
...taskContext,
|
||||||
|
affiliateDash: flow,
|
||||||
|
}),
|
||||||
|
last_error: ROCK_CLAIM_TOKEN_LOCKED_MESSAGE,
|
||||||
|
updated_at: now,
|
||||||
|
})) || task
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
(await updateTask(task.id, {
|
(await updateTask(task.id, {
|
||||||
context_json: JSON.stringify({
|
context_json: JSON.stringify({
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ export function AffiliateDashClaimPanel({
|
|||||||
affiliateDash.bindMismatch ||
|
affiliateDash.bindMismatch ||
|
||||||
(bound && boundAccount && expectedAccount && !boundAccountMatched),
|
(bound && boundAccount && expectedAccount && !boundAccountMatched),
|
||||||
)
|
)
|
||||||
|
const rockTokenLocked = Boolean(
|
||||||
|
bindMismatch &&
|
||||||
|
String(affiliateDash.sku || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.startsWith('rock_'),
|
||||||
|
)
|
||||||
const canSubmit = Boolean(bound && boundAccountMatched && !bindMismatch)
|
const canSubmit = Boolean(bound && boundAccountMatched && !bindMismatch)
|
||||||
const displayBoundAccount = boundAccount || '-'
|
const displayBoundAccount = boundAccount || '-'
|
||||||
const roleName = String(affiliateDash.roleName || '').trim()
|
const roleName = String(affiliateDash.roleName || '').trim()
|
||||||
@@ -62,11 +68,13 @@ export function AffiliateDashClaimPanel({
|
|||||||
? skipScanAuthorized
|
? skipScanAuthorized
|
||||||
? '账号已在平台授权,确认无误后提交发货(无需再次扫码)'
|
? '账号已在平台授权,确认无误后提交发货(无需再次扫码)'
|
||||||
: '账号绑定成功,确认无误后提交发货'
|
: '账号绑定成功,确认无误后提交发货'
|
||||||
: bindMismatch
|
: rockTokenLocked
|
||||||
? '绑定账号与填写 UID 不一致,请核对 UID 后用正确账号重新绑定'
|
? '提货链接已绑定失败,不能更换新的提货号,请联系客服处理'
|
||||||
: bound
|
: bindMismatch
|
||||||
? '已扫码,系统正在确认绑定账号'
|
? '绑定账号与填写 UID 不一致,请核对 UID 后用正确账号重新绑定'
|
||||||
: '打开绑定链接完成绑定,系统将自动确认'}
|
: bound
|
||||||
|
? '已扫码,系统正在确认绑定账号'
|
||||||
|
: '打开绑定链接完成绑定,系统将自动确认'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -92,7 +100,39 @@ export function AffiliateDashClaimPanel({
|
|||||||
type="warning"
|
type="warning"
|
||||||
showIcon
|
showIcon
|
||||||
message="绑定账号与填写 UID 不一致"
|
message="绑定账号与填写 UID 不一致"
|
||||||
description={`当前识别到 ${displayBoundAccount},请使用你填写的 UID(${expectedAccount || '-'})对应账号重新扫码绑定。`}
|
description={
|
||||||
|
<div>
|
||||||
|
<p style={{ margin: '0 0 8px 0' }}>
|
||||||
|
当前微信/QQ扫码识别到角色:
|
||||||
|
<strong>
|
||||||
|
{roleName ? `「${roleName}」` : ''} ({displayBoundAccount})
|
||||||
|
</strong>
|
||||||
|
, 与您填写的 UID({expectedAccount || '-'})不一致。
|
||||||
|
</p>
|
||||||
|
{boundAccount ? (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 6 }}>
|
||||||
|
如确系本人账号,请修改上方 UID 为扫码账号({boundAccount})并点击更新确认绑定:
|
||||||
|
</Typography.Text>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
disabled={submittingUid}
|
||||||
|
onClick={() => {
|
||||||
|
onUidChange(boundAccount)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入扫码 UID({boundAccount})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p style={{ margin: 0, color: 'rgba(0, 0, 0, 0.45)' }}>
|
||||||
|
如需更换微信号请联系客服处理。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -186,7 +226,11 @@ export function AffiliateDashClaimPanel({
|
|||||||
{hasBindQr ? (
|
{hasBindQr ? (
|
||||||
<div style={{ marginBottom: 16, textAlign: 'center' }}>
|
<div style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 8 }}>
|
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 8 }}>
|
||||||
如需换号可重新扫码绑定
|
{String(affiliateDash.sku || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.startsWith('rock_')
|
||||||
|
? '请使用填写的 UID 对应账号扫码。绑错后不能更换提货号,需联系客服'
|
||||||
|
: '如需换号可重新扫码绑定'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<img
|
<img
|
||||||
src={qrCodeDataUrl}
|
src={qrCodeDataUrl}
|
||||||
|
|||||||
Reference in New Issue
Block a user