修复 lewan 发货后 claim 轮询换号污染绑定与 UID 展示
发货成功时写入 shipped 身份快照,并冻结绑定资源变更;后台 UID 匹配优先使用发货快照,避免误报不一致。
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
buildKuaishouCloudShippedSnapshot,
|
||||
isKuaishouCloudBindingMutationFrozen,
|
||||
isKuaishouCloudDispatchSucceeded,
|
||||
normalizeKuaishouCloudFlow,
|
||||
} from './domain.js'
|
||||
|
||||
test('isKuaishouCloudDispatchSucceeded 识别 dispatch.success', () => {
|
||||
assert.equal(
|
||||
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('isKuaishouCloudBindingMutationFrozen:dispatch.success 即使 status=waiting_binding 也冻结', () => {
|
||||
assert.equal(
|
||||
isKuaishouCloudBindingMutationFrozen(
|
||||
{ task_status: TASK_STATUS.WAITING_BINDING },
|
||||
{ dispatch: { status: 'success' } },
|
||||
),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('isKuaishouCloudBindingMutationFrozen:兑换中/已完成冻结', () => {
|
||||
assert.equal(
|
||||
isKuaishouCloudBindingMutationFrozen(
|
||||
{ task_status: TASK_STATUS.REDEEMING },
|
||||
{ dispatch: { status: 'pending' } },
|
||||
),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
isKuaishouCloudBindingMutationFrozen(
|
||||
{ task_status: TASK_STATUS.COMPLETED },
|
||||
{ dispatch: { status: 'pending' } },
|
||||
),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('isKuaishouCloudBindingMutationFrozen:待绑定未发货不冻结', () => {
|
||||
assert.equal(
|
||||
isKuaishouCloudBindingMutationFrozen(
|
||||
{ task_status: TASK_STATUS.WAITING_BINDING },
|
||||
{ dispatch: { status: 'pending' } },
|
||||
),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('normalizeKuaishouCloudFlow 保留 shipped 快照', () => {
|
||||
const flow = normalizeKuaishouCloudFlow({
|
||||
dispatch: { status: 'success' },
|
||||
shipped: {
|
||||
expectedUid: '3781015014',
|
||||
roleId: '3781015014',
|
||||
roleName: 'l迈',
|
||||
vnId: 189363,
|
||||
vnPhone: '18641685924',
|
||||
vnKey: '1',
|
||||
dispatchedAt: '2026-07-16T06:25:13.000Z',
|
||||
},
|
||||
binding: { roleId: '3805904834', roleName: '他爆炸' },
|
||||
})
|
||||
assert.equal(flow.shipped?.roleId, '3781015014')
|
||||
assert.equal(flow.shipped?.expectedUid, '3781015014')
|
||||
assert.equal(flow.binding.roleId, '3805904834')
|
||||
})
|
||||
|
||||
test('buildKuaishouCloudShippedSnapshot 固化发货身份', () => {
|
||||
const snapshot = buildKuaishouCloudShippedSnapshot({
|
||||
expectedUid: '3781015014',
|
||||
roleId: '3781015014',
|
||||
roleName: '安卓-微信-l迈',
|
||||
vnId: 189363,
|
||||
vnPhone: '18641685924',
|
||||
dispatchedAt: 'now',
|
||||
})
|
||||
assert.deepEqual(snapshot, {
|
||||
expectedUid: '3781015014',
|
||||
roleId: '3781015014',
|
||||
roleName: '安卓-微信-l迈',
|
||||
vnId: 189363,
|
||||
vnPhone: '18641685924',
|
||||
vnKey: '1',
|
||||
dispatchedAt: 'now',
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,9 @@ import { createHttpError } from '../../../utils/http.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
import { getClaimIdentityFromTask } from '../../claim/claim-identity.js'
|
||||
import {
|
||||
buildKuaishouCloudShippedSnapshot,
|
||||
isIndustryEVoucherTask,
|
||||
isKuaishouCloudTask,
|
||||
maskCode,
|
||||
@@ -148,6 +150,17 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
'cloudtentacles 发货成功',
|
||||
).trim()
|
||||
|
||||
const claimIdentity = getClaimIdentityFromTask(synced.task || task)
|
||||
const shippedSnapshot = buildKuaishouCloudShippedSnapshot({
|
||||
expectedUid: claimIdentity.expectedUid,
|
||||
roleId: syncedFlow.binding.roleId || syncedFlow.role.rid,
|
||||
roleName: syncedFlow.binding.roleName || syncedFlow.role.name,
|
||||
vnId: syncedFlow.binding.vnId,
|
||||
vnPhone: syncedFlow.binding.vnPhone,
|
||||
vnKey: syncedFlow.binding.vnKey,
|
||||
dispatchedAt: now,
|
||||
})
|
||||
|
||||
const nextContext = {
|
||||
...syncedTaskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
@@ -169,6 +182,8 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
note: dispatchSummary,
|
||||
items: dispatchResults,
|
||||
},
|
||||
// 冻结发货身份:后续 claim 轮询/过期换号不得改写展示与匹配依据
|
||||
shipped: syncedFlow.shipped || shippedSnapshot,
|
||||
purchase: {
|
||||
...syncedFlow.purchase,
|
||||
usedKnapsack: stockResult.usedKnapsack,
|
||||
@@ -209,11 +224,15 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
skuId: syncedFlow.binding.skuId,
|
||||
vnId: syncedFlow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||
expectedUid: shippedSnapshot.expectedUid,
|
||||
roleId: shippedSnapshot.roleId,
|
||||
roleName: shippedSnapshot.roleName,
|
||||
sendType: firstDispatchResult.sendType,
|
||||
note: dispatchSummary,
|
||||
deliveryItems,
|
||||
dispatchResults,
|
||||
stockResult,
|
||||
shipped: shippedSnapshot,
|
||||
actor,
|
||||
},
|
||||
now,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
|
||||
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js'
|
||||
import { asJsonObject, type JsonObject } from '../../../types/json.js'
|
||||
import {
|
||||
isTaskFinalStatus,
|
||||
normalizeTaskStatus,
|
||||
TASK_STATUS,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { asJsonObject, type JsonObject } from '../../../types/json.js'
|
||||
|
||||
export type { JsonObject }
|
||||
|
||||
@@ -91,6 +96,19 @@ export type KuaishouCloudFlow = {
|
||||
note: string
|
||||
items: unknown[]
|
||||
}
|
||||
/**
|
||||
* 发货成功瞬间冻结的身份快照。
|
||||
* claim 轮询 / 过期换号不得覆盖;后台 UID 匹配以本快照为准。
|
||||
*/
|
||||
shipped: {
|
||||
expectedUid: string
|
||||
roleId: string
|
||||
roleName: string
|
||||
vnId: number
|
||||
vnPhone: string
|
||||
vnKey: string
|
||||
dispatchedAt: string | null
|
||||
} | null
|
||||
returnNumber: {
|
||||
status: string
|
||||
returnedAt: unknown
|
||||
@@ -125,6 +143,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
|
||||
const consume = asJsonObject(source.consume)
|
||||
const ticket = asJsonObject(source.ticket)
|
||||
const rebind = asJsonObject(source.rebind)
|
||||
const shippedSource = asJsonObject(source.shipped)
|
||||
const deliveryItems = normalizeDeliveryItems(source, binding)
|
||||
|
||||
const roleName = String(role.name || binding.roleName || '').trim()
|
||||
@@ -211,6 +230,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
|
||||
note: String(dispatch.note || '').trim(),
|
||||
items: Array.isArray(dispatch.items) ? dispatch.items : [],
|
||||
},
|
||||
shipped: normalizeKuaishouCloudShippedSnapshot(shippedSource),
|
||||
returnNumber: {
|
||||
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||
returnedAt: returnNumber.returnedAt || null,
|
||||
@@ -238,6 +258,85 @@ export function hasKuaishouCloudDefaultRoleSnapshot(flowLike: unknown) {
|
||||
return Boolean(flow.role.defaultName || flow.role.defaultRid)
|
||||
}
|
||||
|
||||
export type KuaishouCloudShippedSnapshot = NonNullable<KuaishouCloudFlow['shipped']>
|
||||
|
||||
export function normalizeKuaishouCloudShippedSnapshot(
|
||||
value: unknown,
|
||||
): KuaishouCloudShippedSnapshot | null {
|
||||
const source = asJsonObject(value)
|
||||
const roleId = String(source.roleId || source.rid || '').trim()
|
||||
const expectedUid = String(source.expectedUid || '').trim()
|
||||
const vnId = Number(source.vnId || 0) || 0
|
||||
const dispatchedAt = source.dispatchedAt ? String(source.dispatchedAt) : null
|
||||
|
||||
// 至少要有发货角色或 UID 之一,否则视为无快照
|
||||
if (!roleId && !expectedUid && !vnId && !dispatchedAt) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
expectedUid,
|
||||
roleId,
|
||||
roleName: String(source.roleName || source.name || '').trim(),
|
||||
vnId,
|
||||
vnPhone: String(source.vnPhone || '').trim(),
|
||||
vnKey: String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
dispatchedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function isKuaishouCloudDispatchSucceeded(flowLike: unknown): boolean {
|
||||
const flow = normalizeKuaishouCloudFlow(flowLike)
|
||||
return String(flow.dispatch.status || '').trim() === 'success'
|
||||
}
|
||||
|
||||
/**
|
||||
* 发货后禁止再改绑定资源/角色(claim 轮询换号、刷角色、过期重取等)。
|
||||
* 以 dispatch.success 为准(兼容状态机被污染回 waiting_binding 的脏单)。
|
||||
*/
|
||||
export function isKuaishouCloudBindingMutationFrozen(
|
||||
task: { task_status?: unknown } | null | undefined,
|
||||
flowLike?: unknown,
|
||||
): boolean {
|
||||
const status = normalizeTaskStatus(task?.task_status)
|
||||
if (
|
||||
status === TASK_STATUS.REDEEMING ||
|
||||
status === TASK_STATUS.DISPATCHED_PENDING_RETURN ||
|
||||
status === TASK_STATUS.COMPLETED ||
|
||||
status === TASK_STATUS.REDEEMED ||
|
||||
isTaskFinalStatus(status)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (flowLike !== undefined) {
|
||||
return isKuaishouCloudDispatchSucceeded(flowLike)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function buildKuaishouCloudShippedSnapshot(input: {
|
||||
expectedUid?: unknown
|
||||
roleId?: unknown
|
||||
roleName?: unknown
|
||||
vnId?: unknown
|
||||
vnPhone?: unknown
|
||||
vnKey?: unknown
|
||||
dispatchedAt?: unknown
|
||||
}): KuaishouCloudShippedSnapshot {
|
||||
return {
|
||||
expectedUid: String(input.expectedUid || '').trim(),
|
||||
roleId: String(input.roleId || '').trim(),
|
||||
roleName: String(input.roleName || '').trim(),
|
||||
vnId: Number(input.vnId || 0) || 0,
|
||||
vnPhone: String(input.vnPhone || '').trim(),
|
||||
vnKey:
|
||||
String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
dispatchedAt: input.dispatchedAt ? String(input.dispatchedAt) : null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已有可展示的绑定角色信息(诊断用)。
|
||||
*
|
||||
|
||||
@@ -10,6 +10,10 @@ export {
|
||||
isKuaishouCloudTask,
|
||||
hasKuaishouCloudCustomerRole,
|
||||
hasKuaishouCloudDefaultRoleSnapshot,
|
||||
isKuaishouCloudDispatchSucceeded,
|
||||
isKuaishouCloudBindingMutationFrozen,
|
||||
buildKuaishouCloudShippedSnapshot,
|
||||
normalizeKuaishouCloudShippedSnapshot,
|
||||
maskCode,
|
||||
maskPhone,
|
||||
normalizeKuaishouCloudFlow,
|
||||
@@ -20,6 +24,7 @@ export {
|
||||
export type {
|
||||
KuaishouCloudDeliveryItem,
|
||||
KuaishouCloudFlow,
|
||||
KuaishouCloudShippedSnapshot,
|
||||
} from './domain.js'
|
||||
export { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
|
||||
export { ensureTaskClaimLink } from './ensure-claim-link.js'
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js
|
||||
import {
|
||||
KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
isKuaishouCloudBindUrlFresh,
|
||||
isKuaishouCloudBindingMutationFrozen,
|
||||
isKuaishouCloudTask,
|
||||
maskPhone,
|
||||
normalizeKuaishouCloudFlow,
|
||||
@@ -65,6 +66,16 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
|
||||
const claimLinkState = await ensureTaskClaimLink(task)
|
||||
const source = String(options.source || 'system').trim() || 'system'
|
||||
|
||||
// 已发货:禁止 prepare / 过期换号把状态打回 waiting_binding
|
||||
if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
|
||||
return {
|
||||
task,
|
||||
claimUrl: buildClaimUrl(String(task.primary_claim_token || task.claim_token || '')),
|
||||
token: String(task.primary_claim_token || task.claim_token || ''),
|
||||
flow,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!force &&
|
||||
flow.binding.prepareStatus === 'ready' &&
|
||||
@@ -373,6 +384,13 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
const allowSkipBack = options.allowSkipBack !== false
|
||||
const preferReuseVn = options.preferReuseVn !== false
|
||||
|
||||
if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
|
||||
throw createHttpError('当前任务已经发货,不能刷新或更换绑定链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_bind_url_refresh_after_dispatch_forbidden',
|
||||
})
|
||||
}
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务缺少可刷新绑定链接的虚拟号信息', {
|
||||
statusCode: 409,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
isKuaishouCloudBindUrlFresh,
|
||||
isKuaishouCloudBindingMutationFrozen,
|
||||
isKuaishouCloudTask,
|
||||
normalizeKuaishouCloudFlow,
|
||||
normalizeKuaishouCloudRoleInfo,
|
||||
@@ -31,6 +32,15 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
// 已发货:禁止 probe 触发过期换号 / 覆盖角色
|
||||
if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
|
||||
return {
|
||||
task,
|
||||
flow,
|
||||
probe: null as null,
|
||||
}
|
||||
}
|
||||
|
||||
if (!flow.binding.bindUrl) {
|
||||
return {
|
||||
task,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtua
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import {
|
||||
isKuaishouCloudBindingMutationFrozen,
|
||||
isKuaishouCloudTask,
|
||||
normalizeKuaishouCloudFlow,
|
||||
normalizeKuaishouCloudRoleInfo,
|
||||
@@ -28,6 +29,19 @@ export async function refreshKuaishouCloudTaskRoleInfo(task: TaskRow, options: J
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
// 已发货:禁止 live 刷角色覆盖发货快照(claim 轮询 / 后台误触)
|
||||
if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
|
||||
return {
|
||||
task,
|
||||
roleInfo: {
|
||||
name: flow.shipped?.roleName || flow.binding.roleName || flow.role.name,
|
||||
rid: flow.shipped?.roleId || flow.binding.roleId || flow.role.rid,
|
||||
rawInfo: flow.role.rawInfo,
|
||||
},
|
||||
flow,
|
||||
}
|
||||
}
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务还没有可查询的绑定角色信息,请先准备绑定资源', {
|
||||
statusCode: 409,
|
||||
|
||||
@@ -20,6 +20,11 @@ export type ClaimIdentityAdminSummary = ClaimIdentityPayload & {
|
||||
uidMatched: boolean | null
|
||||
compatibilityMode: 'uid' | 'legacy_no_uid'
|
||||
note: string
|
||||
/** 展示依据:shipped=发货快照;live=当前绑定 */
|
||||
displaySource: 'shipped' | 'live'
|
||||
shippedUid: string
|
||||
shippedRoleName: string
|
||||
shippedAt: string | null
|
||||
}
|
||||
|
||||
/** context_json 中 claimIdentity 的宽松写入形态(兼容历史 uid 字段) */
|
||||
|
||||
@@ -715,6 +715,11 @@ function FulfillmentOverviewPanel({
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="绑定角色 ID">
|
||||
{claimIdentity?.boundUid || detail.task.roleId || '-'}
|
||||
{claimIdentity?.displaySource === 'shipped' ? (
|
||||
<Tag color="blue" style={{ marginLeft: 6 }}>
|
||||
发货快照
|
||||
</Tag>
|
||||
) : null}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="绑定角色名">
|
||||
{claimIdentity?.boundRoleName || detail.task.roleName || '-'}
|
||||
@@ -725,6 +730,17 @@ function FulfillmentOverviewPanel({
|
||||
<Descriptions.Item label="兼容模式">
|
||||
{claimIdentity?.compatibilityMode === 'legacy_no_uid' ? '旧单无 UID' : 'UID 闸门'}
|
||||
</Descriptions.Item>
|
||||
{claimIdentity?.shippedUid ? (
|
||||
<Descriptions.Item label="发货 UID / 角色" span={2}>
|
||||
{claimIdentity.shippedUid}
|
||||
{claimIdentity.shippedRoleName ? ` / ${claimIdentity.shippedRoleName}` : ''}
|
||||
{claimIdentity.shippedAt ? (
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 8 }}>
|
||||
{claimIdentity.shippedAt}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
<Descriptions.Item label="说明" span={3}>
|
||||
<Typography.Text type="secondary">
|
||||
{claimIdentity?.note || '暂无 UID 信息'}
|
||||
|
||||
@@ -69,6 +69,11 @@ export interface AdminClaimIdentitySummary {
|
||||
uidMatched: boolean | null
|
||||
compatibilityMode: 'uid' | 'legacy_no_uid'
|
||||
note: string
|
||||
/** shipped=发货快照;live=当前绑定 */
|
||||
displaySource?: 'shipped' | 'live'
|
||||
shippedUid?: string
|
||||
shippedRoleName?: string
|
||||
shippedAt?: string | null
|
||||
}
|
||||
|
||||
export interface AdminTaskDetail {
|
||||
|
||||
Reference in New Issue
Block a user