修复 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.uidMatched, true)
|
||||||
assert.equal(summary.compatibilityMode, 'uid')
|
assert.equal(summary.compatibilityMode, 'uid')
|
||||||
assert.equal(summary.boundUid, '10001')
|
assert.equal(summary.boundUid, '10001')
|
||||||
|
assert.equal(summary.displaySource, 'live')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('buildClaimIdentityAdminSummary marks legacy without uid', () => {
|
test('buildClaimIdentityAdminSummary marks legacy without uid', () => {
|
||||||
@@ -97,4 +98,36 @@ test('buildClaimIdentityAdminSummary marks legacy without uid', () => {
|
|||||||
assert.equal(summary.ready, false)
|
assert.equal(summary.ready, false)
|
||||||
assert.equal(summary.compatibilityMode, 'legacy_no_uid')
|
assert.equal(summary.compatibilityMode, 'legacy_no_uid')
|
||||||
assert.equal(summary.uidMatched, null)
|
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 } = {},
|
options: { flowLike?: unknown; taskRoleId?: unknown; taskRoleName?: unknown } = {},
|
||||||
): ClaimIdentityAdminSummary {
|
): ClaimIdentityAdminSummary {
|
||||||
const identity = getClaimIdentityFromContext(context)
|
const identity = getClaimIdentityFromContext(context)
|
||||||
const boundUid = resolveBoundRoleUid(options.flowLike) || normalizeClaimUid(options.taskRoleId)
|
|
||||||
const flow = isPlainObject(options.flowLike) ? options.flowLike : {}
|
const flow = isPlainObject(options.flowLike) ? options.flowLike : {}
|
||||||
const binding = isPlainObject(flow.binding) ? flow.binding : {}
|
const binding = isPlainObject(flow.binding) ? flow.binding : {}
|
||||||
const role = isPlainObject(flow.role) ? flow.role : {}
|
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 || '',
|
role.name || binding.roleName || options.taskRoleName || '',
|
||||||
).trim()
|
).trim()
|
||||||
|
const boundUid = useShipped ? shippedUid : liveBoundUid
|
||||||
|
const boundRoleName = useShipped
|
||||||
|
? shippedRoleName || liveBoundRoleName
|
||||||
|
: liveBoundRoleName
|
||||||
const ready = Boolean(identity.expectedUid)
|
const ready = Boolean(identity.expectedUid)
|
||||||
const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null
|
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 {
|
return {
|
||||||
expectedUid: identity.expectedUid,
|
expectedUid: identity.expectedUid,
|
||||||
@@ -159,13 +205,11 @@ export function buildClaimIdentityAdminSummary(
|
|||||||
boundRoleName,
|
boundRoleName,
|
||||||
uidMatched,
|
uidMatched,
|
||||||
compatibilityMode: ready ? ('uid' as const) : ('legacy_no_uid' as const),
|
compatibilityMode: ready ? ('uid' as const) : ('legacy_no_uid' as const),
|
||||||
note: ready
|
note,
|
||||||
? uidMatched === true
|
displaySource: useShipped ? 'shipped' : 'live',
|
||||||
? 'UID 已匹配,可继续履约'
|
shippedUid,
|
||||||
: uidMatched === false
|
shippedRoleName,
|
||||||
? '绑定角色 ID 与填写 UID 不一致'
|
shippedAt,
|
||||||
: '已填 UID,等待绑定识别'
|
|
||||||
: '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,21 @@ import {
|
|||||||
prepareFulfillmentBinding,
|
prepareFulfillmentBinding,
|
||||||
refreshFulfillmentRole,
|
refreshFulfillmentRole,
|
||||||
} from '../fulfillment/executors/registry.js'
|
} 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 type { TaskRow } from '../../types/repository/rows.js'
|
||||||
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
|
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
|
||||||
|
|
||||||
export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
|
export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
|
||||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
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) {
|
if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) {
|
||||||
// ticket verified 或绑定尚未 ready:补 prepare(覆盖提前核销后未取号场景)
|
// ticket verified 或绑定尚未 ready:补 prepare(覆盖提前核销后未取号场景)
|
||||||
if (
|
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 { nowIso } from '../../../utils/time.js'
|
||||||
import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js'
|
import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js'
|
||||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||||
|
import { getClaimIdentityFromTask } from '../../claim/claim-identity.js'
|
||||||
import {
|
import {
|
||||||
|
buildKuaishouCloudShippedSnapshot,
|
||||||
isIndustryEVoucherTask,
|
isIndustryEVoucherTask,
|
||||||
isKuaishouCloudTask,
|
isKuaishouCloudTask,
|
||||||
maskCode,
|
maskCode,
|
||||||
@@ -148,6 +150,17 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
'cloudtentacles 发货成功',
|
'cloudtentacles 发货成功',
|
||||||
).trim()
|
).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 = {
|
const nextContext = {
|
||||||
...syncedTaskContext,
|
...syncedTaskContext,
|
||||||
kuaishouCloudFulfillment: {
|
kuaishouCloudFulfillment: {
|
||||||
@@ -169,6 +182,8 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
note: dispatchSummary,
|
note: dispatchSummary,
|
||||||
items: dispatchResults,
|
items: dispatchResults,
|
||||||
},
|
},
|
||||||
|
// 冻结发货身份:后续 claim 轮询/过期换号不得改写展示与匹配依据
|
||||||
|
shipped: syncedFlow.shipped || shippedSnapshot,
|
||||||
purchase: {
|
purchase: {
|
||||||
...syncedFlow.purchase,
|
...syncedFlow.purchase,
|
||||||
usedKnapsack: stockResult.usedKnapsack,
|
usedKnapsack: stockResult.usedKnapsack,
|
||||||
@@ -209,11 +224,15 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
skuId: syncedFlow.binding.skuId,
|
skuId: syncedFlow.binding.skuId,
|
||||||
vnId: syncedFlow.binding.vnId,
|
vnId: syncedFlow.binding.vnId,
|
||||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||||
|
expectedUid: shippedSnapshot.expectedUid,
|
||||||
|
roleId: shippedSnapshot.roleId,
|
||||||
|
roleName: shippedSnapshot.roleName,
|
||||||
sendType: firstDispatchResult.sendType,
|
sendType: firstDispatchResult.sendType,
|
||||||
note: dispatchSummary,
|
note: dispatchSummary,
|
||||||
deliveryItems,
|
deliveryItems,
|
||||||
dispatchResults,
|
dispatchResults,
|
||||||
stockResult,
|
stockResult,
|
||||||
|
shipped: shippedSnapshot,
|
||||||
actor,
|
actor,
|
||||||
},
|
},
|
||||||
now,
|
now,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
|
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
|
||||||
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.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 }
|
export type { JsonObject }
|
||||||
|
|
||||||
@@ -91,6 +96,19 @@ export type KuaishouCloudFlow = {
|
|||||||
note: string
|
note: string
|
||||||
items: unknown[]
|
items: unknown[]
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 发货成功瞬间冻结的身份快照。
|
||||||
|
* claim 轮询 / 过期换号不得覆盖;后台 UID 匹配以本快照为准。
|
||||||
|
*/
|
||||||
|
shipped: {
|
||||||
|
expectedUid: string
|
||||||
|
roleId: string
|
||||||
|
roleName: string
|
||||||
|
vnId: number
|
||||||
|
vnPhone: string
|
||||||
|
vnKey: string
|
||||||
|
dispatchedAt: string | null
|
||||||
|
} | null
|
||||||
returnNumber: {
|
returnNumber: {
|
||||||
status: string
|
status: string
|
||||||
returnedAt: unknown
|
returnedAt: unknown
|
||||||
@@ -125,6 +143,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
|
|||||||
const consume = asJsonObject(source.consume)
|
const consume = asJsonObject(source.consume)
|
||||||
const ticket = asJsonObject(source.ticket)
|
const ticket = asJsonObject(source.ticket)
|
||||||
const rebind = asJsonObject(source.rebind)
|
const rebind = asJsonObject(source.rebind)
|
||||||
|
const shippedSource = asJsonObject(source.shipped)
|
||||||
const deliveryItems = normalizeDeliveryItems(source, binding)
|
const deliveryItems = normalizeDeliveryItems(source, binding)
|
||||||
|
|
||||||
const roleName = String(role.name || binding.roleName || '').trim()
|
const roleName = String(role.name || binding.roleName || '').trim()
|
||||||
@@ -211,6 +230,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
|
|||||||
note: String(dispatch.note || '').trim(),
|
note: String(dispatch.note || '').trim(),
|
||||||
items: Array.isArray(dispatch.items) ? dispatch.items : [],
|
items: Array.isArray(dispatch.items) ? dispatch.items : [],
|
||||||
},
|
},
|
||||||
|
shipped: normalizeKuaishouCloudShippedSnapshot(shippedSource),
|
||||||
returnNumber: {
|
returnNumber: {
|
||||||
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||||
returnedAt: returnNumber.returnedAt || null,
|
returnedAt: returnNumber.returnedAt || null,
|
||||||
@@ -238,6 +258,85 @@ export function hasKuaishouCloudDefaultRoleSnapshot(flowLike: unknown) {
|
|||||||
return Boolean(flow.role.defaultName || flow.role.defaultRid)
|
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,
|
isKuaishouCloudTask,
|
||||||
hasKuaishouCloudCustomerRole,
|
hasKuaishouCloudCustomerRole,
|
||||||
hasKuaishouCloudDefaultRoleSnapshot,
|
hasKuaishouCloudDefaultRoleSnapshot,
|
||||||
|
isKuaishouCloudDispatchSucceeded,
|
||||||
|
isKuaishouCloudBindingMutationFrozen,
|
||||||
|
buildKuaishouCloudShippedSnapshot,
|
||||||
|
normalizeKuaishouCloudShippedSnapshot,
|
||||||
maskCode,
|
maskCode,
|
||||||
maskPhone,
|
maskPhone,
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
@@ -20,6 +24,7 @@ export {
|
|||||||
export type {
|
export type {
|
||||||
KuaishouCloudDeliveryItem,
|
KuaishouCloudDeliveryItem,
|
||||||
KuaishouCloudFlow,
|
KuaishouCloudFlow,
|
||||||
|
KuaishouCloudShippedSnapshot,
|
||||||
} from './domain.js'
|
} from './domain.js'
|
||||||
export { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
|
export { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
|
||||||
export { ensureTaskClaimLink } from './ensure-claim-link.js'
|
export { ensureTaskClaimLink } from './ensure-claim-link.js'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js
|
|||||||
import {
|
import {
|
||||||
KUAISHOU_CLOUD_FIXED_VN_KEY,
|
KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||||
isKuaishouCloudBindUrlFresh,
|
isKuaishouCloudBindUrlFresh,
|
||||||
|
isKuaishouCloudBindingMutationFrozen,
|
||||||
isKuaishouCloudTask,
|
isKuaishouCloudTask,
|
||||||
maskPhone,
|
maskPhone,
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
@@ -65,6 +66,16 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
|
|||||||
const claimLinkState = await ensureTaskClaimLink(task)
|
const claimLinkState = await ensureTaskClaimLink(task)
|
||||||
const source = String(options.source || 'system').trim() || 'system'
|
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 (
|
if (
|
||||||
!force &&
|
!force &&
|
||||||
flow.binding.prepareStatus === 'ready' &&
|
flow.binding.prepareStatus === 'ready' &&
|
||||||
@@ -373,6 +384,13 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
|||||||
const allowSkipBack = options.allowSkipBack !== false
|
const allowSkipBack = options.allowSkipBack !== false
|
||||||
const preferReuseVn = options.preferReuseVn !== 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) {
|
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||||
throw createHttpError('当前任务缺少可刷新绑定链接的虚拟号信息', {
|
throw createHttpError('当前任务缺少可刷新绑定链接的虚拟号信息', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { nowIso } from '../../../utils/time.js'
|
|||||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||||
import {
|
import {
|
||||||
isKuaishouCloudBindUrlFresh,
|
isKuaishouCloudBindUrlFresh,
|
||||||
|
isKuaishouCloudBindingMutationFrozen,
|
||||||
isKuaishouCloudTask,
|
isKuaishouCloudTask,
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
normalizeKuaishouCloudRoleInfo,
|
normalizeKuaishouCloudRoleInfo,
|
||||||
@@ -31,6 +32,15 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json
|
|||||||
const taskContext = parseTaskContext(task)
|
const taskContext = parseTaskContext(task)
|
||||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||||
|
|
||||||
|
// 已发货:禁止 probe 触发过期换号 / 覆盖角色
|
||||||
|
if (isKuaishouCloudBindingMutationFrozen(task, flow)) {
|
||||||
|
return {
|
||||||
|
task,
|
||||||
|
flow,
|
||||||
|
probe: null as null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!flow.binding.bindUrl) {
|
if (!flow.binding.bindUrl) {
|
||||||
return {
|
return {
|
||||||
task,
|
task,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtua
|
|||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import { nowIso } from '../../../utils/time.js'
|
import { nowIso } from '../../../utils/time.js'
|
||||||
import {
|
import {
|
||||||
|
isKuaishouCloudBindingMutationFrozen,
|
||||||
isKuaishouCloudTask,
|
isKuaishouCloudTask,
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
normalizeKuaishouCloudRoleInfo,
|
normalizeKuaishouCloudRoleInfo,
|
||||||
@@ -28,6 +29,19 @@ export async function refreshKuaishouCloudTaskRoleInfo(task: TaskRow, options: J
|
|||||||
const taskContext = parseTaskContext(task)
|
const taskContext = parseTaskContext(task)
|
||||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
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) {
|
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||||
throw createHttpError('当前任务还没有可查询的绑定角色信息,请先准备绑定资源', {
|
throw createHttpError('当前任务还没有可查询的绑定角色信息,请先准备绑定资源', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export type ClaimIdentityAdminSummary = ClaimIdentityPayload & {
|
|||||||
uidMatched: boolean | null
|
uidMatched: boolean | null
|
||||||
compatibilityMode: 'uid' | 'legacy_no_uid'
|
compatibilityMode: 'uid' | 'legacy_no_uid'
|
||||||
note: string
|
note: string
|
||||||
|
/** 展示依据:shipped=发货快照;live=当前绑定 */
|
||||||
|
displaySource: 'shipped' | 'live'
|
||||||
|
shippedUid: string
|
||||||
|
shippedRoleName: string
|
||||||
|
shippedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** context_json 中 claimIdentity 的宽松写入形态(兼容历史 uid 字段) */
|
/** context_json 中 claimIdentity 的宽松写入形态(兼容历史 uid 字段) */
|
||||||
|
|||||||
@@ -715,6 +715,11 @@ function FulfillmentOverviewPanel({
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="绑定角色 ID">
|
<Descriptions.Item label="绑定角色 ID">
|
||||||
{claimIdentity?.boundUid || detail.task.roleId || '-'}
|
{claimIdentity?.boundUid || detail.task.roleId || '-'}
|
||||||
|
{claimIdentity?.displaySource === 'shipped' ? (
|
||||||
|
<Tag color="blue" style={{ marginLeft: 6 }}>
|
||||||
|
发货快照
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="绑定角色名">
|
<Descriptions.Item label="绑定角色名">
|
||||||
{claimIdentity?.boundRoleName || detail.task.roleName || '-'}
|
{claimIdentity?.boundRoleName || detail.task.roleName || '-'}
|
||||||
@@ -725,6 +730,17 @@ function FulfillmentOverviewPanel({
|
|||||||
<Descriptions.Item label="兼容模式">
|
<Descriptions.Item label="兼容模式">
|
||||||
{claimIdentity?.compatibilityMode === 'legacy_no_uid' ? '旧单无 UID' : 'UID 闸门'}
|
{claimIdentity?.compatibilityMode === 'legacy_no_uid' ? '旧单无 UID' : 'UID 闸门'}
|
||||||
</Descriptions.Item>
|
</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}>
|
<Descriptions.Item label="说明" span={3}>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
{claimIdentity?.note || '暂无 UID 信息'}
|
{claimIdentity?.note || '暂无 UID 信息'}
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export interface AdminClaimIdentitySummary {
|
|||||||
uidMatched: boolean | null
|
uidMatched: boolean | null
|
||||||
compatibilityMode: 'uid' | 'legacy_no_uid'
|
compatibilityMode: 'uid' | 'legacy_no_uid'
|
||||||
note: string
|
note: string
|
||||||
|
/** shipped=发货快照;live=当前绑定 */
|
||||||
|
displaySource?: 'shipped' | 'live'
|
||||||
|
shippedUid?: string
|
||||||
|
shippedRoleName?: string
|
||||||
|
shippedAt?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminTaskDetail {
|
export interface AdminTaskDetail {
|
||||||
|
|||||||
Reference in New Issue
Block a user