修复 lewan 发货后 claim 轮询换号污染绑定与 UID 展示

发货成功时写入 shipped 身份快照,并冻结绑定资源变更;后台 UID 匹配优先使用发货快照,避免误报不一致。
This commit is contained in:
yml2213
2026-07-16 15:52:54 +08:00
parent be7347e600
commit 1216f252de
13 changed files with 384 additions and 11 deletions
@@ -90,6 +90,7 @@ test('buildClaimIdentityAdminSummary reports match state', () => {
assert.equal(summary.uidMatched, true)
assert.equal(summary.compatibilityMode, 'uid')
assert.equal(summary.boundUid, '10001')
assert.equal(summary.displaySource, 'live')
})
test('buildClaimIdentityAdminSummary marks legacy without uid', () => {
@@ -97,4 +98,36 @@ test('buildClaimIdentityAdminSummary marks legacy without uid', () => {
assert.equal(summary.ready, false)
assert.equal(summary.compatibilityMode, 'legacy_no_uid')
assert.equal(summary.uidMatched, null)
assert.equal(summary.displaySource, 'live')
})
test('buildClaimIdentityAdminSummary prefers shipped snapshot after dispatch pollution', () => {
const summary = buildClaimIdentityAdminSummary(
{
claimIdentity: { expectedUid: '3781015014', submittedAt: 't', source: 'claim_page' },
},
{
flowLike: {
dispatch: { status: 'success', dispatchAt: '2026-07-16T06:25:13.000Z' },
shipped: {
expectedUid: '3781015014',
roleId: '3781015014',
roleName: 'l迈',
vnId: 189363,
vnPhone: '18641685924',
dispatchedAt: '2026-07-16T06:25:13.000Z',
},
// 发货后换号污染:当前绑定已变成别的角色
binding: { roleId: '3805904834', roleName: '他爆炸' },
role: { rid: '3805904834', name: '他爆炸' },
},
},
)
assert.equal(summary.displaySource, 'shipped')
assert.equal(summary.boundUid, '3781015014')
assert.equal(summary.boundRoleName, 'l迈')
assert.equal(summary.uidMatched, true)
assert.equal(summary.shippedUid, '3781015014')
assert.match(summary.note, /已发货/)
assert.match(summary.note, /当前虚拟号角色已变化/)
})
@@ -140,15 +140,61 @@ export function buildClaimIdentityAdminSummary(
options: { flowLike?: unknown; taskRoleId?: unknown; taskRoleName?: unknown } = {},
): ClaimIdentityAdminSummary {
const identity = getClaimIdentityFromContext(context)
const boundUid = resolveBoundRoleUid(options.flowLike) || normalizeClaimUid(options.taskRoleId)
const flow = isPlainObject(options.flowLike) ? options.flowLike : {}
const binding = isPlainObject(flow.binding) ? flow.binding : {}
const role = isPlainObject(flow.role) ? flow.role : {}
const boundRoleName = String(
const shipped = isPlainObject(flow.shipped) ? flow.shipped : null
const dispatch = isPlainObject(flow.dispatch) ? flow.dispatch : {}
const shippedUid = normalizeClaimUid(shipped?.roleId || shipped?.expectedUid || '')
const shippedRoleName = String(shipped?.roleName || '').trim()
const shippedAt = shipped?.dispatchedAt ? String(shipped.dispatchedAt) : null
const dispatchSucceeded = String(dispatch.status || '').trim() === 'success'
// 已发货优先用快照,避免 claim 轮询换号后误报 UID 不一致
const useShipped = Boolean(shippedUid && (dispatchSucceeded || shippedAt))
const liveBoundUid =
resolveBoundRoleUid(options.flowLike) || normalizeClaimUid(options.taskRoleId)
const liveBoundRoleName = String(
role.name || binding.roleName || options.taskRoleName || '',
).trim()
const boundUid = useShipped ? shippedUid : liveBoundUid
const boundRoleName = useShipped
? shippedRoleName || liveBoundRoleName
: liveBoundRoleName
const ready = Boolean(identity.expectedUid)
const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null
const liveMismatched =
useShipped &&
liveBoundUid &&
shippedUid &&
!isClaimUidMatched(shippedUid, liveBoundUid)
let note = '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货'
if (ready) {
if (useShipped) {
if (uidMatched === true) {
note = liveMismatched
? `已发货:道具发至 UID ${shippedUid}${shippedRoleName ? `${shippedRoleName}` : ''};当前虚拟号角色已变化,请勿以当前绑定为准`
: `已发货:道具发至 UID ${shippedUid}${shippedRoleName ? `${shippedRoleName}` : ''},与填写 UID 一致`
} else if (uidMatched === false) {
note = `已发货快照角色 ID${boundUid})与填写 UID${identity.expectedUid})不一致,请核对事件日志`
} else {
note = '已发货,但快照缺少角色 ID'
}
} else if (dispatchSucceeded) {
note =
uidMatched === true
? '已发货:当前绑定角色与填写 UID 一致(无发货快照,建议以事件日志为准)'
: uidMatched === false
? '已发货,但当前绑定角色与填写 UID 不一致(可能为发货后换号污染;请核对 dispatch 事件中的 roleId'
: '已发货,等待核对绑定角色'
} else if (uidMatched === true) {
note = 'UID 已匹配,可继续履约'
} else if (uidMatched === false) {
note = '绑定角色 ID 与填写 UID 不一致'
} else {
note = '已填 UID,等待绑定识别'
}
}
return {
expectedUid: identity.expectedUid,
@@ -159,13 +205,11 @@ export function buildClaimIdentityAdminSummary(
boundRoleName,
uidMatched,
compatibilityMode: ready ? ('uid' as const) : ('legacy_no_uid' as const),
note: ready
? uidMatched === true
? 'UID 已匹配,可继续履约'
: uidMatched === false
? '绑定角色 ID 与填写 UID 不一致'
: '已填 UID,等待绑定识别'
: '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货',
note,
displaySource: useShipped ? 'shipped' : 'live',
shippedUid,
shippedRoleName,
shippedAt,
}
}
@@ -2,13 +2,21 @@ import {
prepareFulfillmentBinding,
refreshFulfillmentRole,
} from '../fulfillment/executors/registry.js'
import { normalizeKuaishouCloudFlow } from '../fulfillment/kuaishou-cloud/index.js'
import {
isKuaishouCloudBindingMutationFrozen,
normalizeKuaishouCloudFlow,
} from '../fulfillment/kuaishou-cloud/index.js'
import type { TaskRow } from '../../types/repository/rows.js'
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
// 已发货/兑换中:禁止 claim 轮询换号或覆盖角色(脏单 status 可能已回退 waiting_binding
if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
return task
}
if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) {
// ticket verified 或绑定尚未 ready:补 prepare(覆盖提前核销后未取号场景)
if (