优化 UID 履约闸门、运营可见性与一键兑换
lewan 发货强制 expectedUid 并取消无 UID 旧路径回落;任务详情展示 UID 匹配态;领取页匹配后可一键兑换;补充 identity 单测并收口短链说明。
This commit is contained in:
@@ -3,7 +3,9 @@ import test from 'node:test'
|
||||
|
||||
import {
|
||||
appendUidToUrl,
|
||||
assertLewanAutoFulfillmentUidReady,
|
||||
assertValidClaimUid,
|
||||
buildClaimIdentityAdminSummary,
|
||||
isClaimUidMatched,
|
||||
normalizeClaimUid,
|
||||
} from './claim-identity.js'
|
||||
@@ -38,3 +40,61 @@ test('appendUidToUrl overwrites existing uid', () => {
|
||||
const next = appendUidToUrl('https://example.com/h5/bind?code=abc&uid=old', 'new-uid')
|
||||
assert.equal(new URL(next).searchParams.get('uid'), 'new-uid')
|
||||
})
|
||||
|
||||
test('assertLewanAutoFulfillmentUidReady rejects legacy task without uid', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertLewanAutoFulfillmentUidReady({
|
||||
context_json: JSON.stringify({ kuaishouCloudFulfillment: {} }),
|
||||
}),
|
||||
/尚未填写游戏 UID/,
|
||||
)
|
||||
})
|
||||
|
||||
test('assertLewanAutoFulfillmentUidReady allows mock without uid', () => {
|
||||
assert.equal(
|
||||
assertLewanAutoFulfillmentUidReady({
|
||||
context_json: JSON.stringify({
|
||||
kuaishouCloudFulfillment: { mock: { enabled: true } },
|
||||
}),
|
||||
}),
|
||||
'',
|
||||
)
|
||||
})
|
||||
|
||||
test('assertLewanAutoFulfillmentUidReady accepts submitted uid', () => {
|
||||
assert.equal(
|
||||
assertLewanAutoFulfillmentUidReady({
|
||||
context_json: JSON.stringify({
|
||||
claimIdentity: { expectedUid: '10001', submittedAt: '2026-07-10T00:00:00.000Z' },
|
||||
kuaishouCloudFulfillment: {},
|
||||
}),
|
||||
}),
|
||||
'10001',
|
||||
)
|
||||
})
|
||||
|
||||
test('buildClaimIdentityAdminSummary reports match state', () => {
|
||||
const summary = buildClaimIdentityAdminSummary(
|
||||
{
|
||||
claimIdentity: { expectedUid: '10001', submittedAt: 't', source: 'claim_page' },
|
||||
},
|
||||
{
|
||||
flowLike: {
|
||||
binding: { roleId: '10001', roleName: '测试角色' },
|
||||
role: { rid: '10001', name: '测试角色' },
|
||||
},
|
||||
},
|
||||
)
|
||||
assert.equal(summary.ready, true)
|
||||
assert.equal(summary.uidMatched, true)
|
||||
assert.equal(summary.compatibilityMode, 'uid')
|
||||
assert.equal(summary.boundUid, '10001')
|
||||
})
|
||||
|
||||
test('buildClaimIdentityAdminSummary marks legacy without uid', () => {
|
||||
const summary = buildClaimIdentityAdminSummary({}, { flowLike: { binding: {} } })
|
||||
assert.equal(summary.ready, false)
|
||||
assert.equal(summary.compatibilityMode, 'legacy_no_uid')
|
||||
assert.equal(summary.uidMatched, null)
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ export function resolveBoundRoleUid(flowLike: unknown): string {
|
||||
export function assertClaimExpectedUidReady(task: Partial<TaskRow> | null | undefined): string {
|
||||
const expectedUid = getClaimIdentityFromTask(task).expectedUid
|
||||
if (!expectedUid) {
|
||||
throw createHttpError('请先填写游戏 UID', {
|
||||
throw createHttpError('请先通过领取页填写游戏 UID(旧单无 UID 不可自动履约)', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_uid_not_submitted',
|
||||
})
|
||||
@@ -99,6 +99,72 @@ export function assertClaimExpectedUidReady(task: Partial<TaskRow> | null | unde
|
||||
return expectedUid
|
||||
}
|
||||
|
||||
/**
|
||||
* lewan 自动发货前:必须已有 expectedUid(不再回落「仅确认角色」旧路径)。
|
||||
* mock 任务可跳过。
|
||||
*/
|
||||
export function assertLewanAutoFulfillmentUidReady(
|
||||
task: Partial<TaskRow> | null | undefined,
|
||||
options: { allowMockSkip?: boolean; errorCodePrefix?: string } = {},
|
||||
): string {
|
||||
const context = parseTaskContext(task)
|
||||
const flow =
|
||||
context.kuaishouCloudFulfillment && typeof context.kuaishouCloudFulfillment === 'object'
|
||||
? (context.kuaishouCloudFulfillment as JsonObject)
|
||||
: {}
|
||||
const mock = flow.mock && typeof flow.mock === 'object' ? (flow.mock as JsonObject) : null
|
||||
if (options.allowMockSkip !== false && mock?.enabled === true) {
|
||||
return getClaimIdentityFromTask(task).expectedUid
|
||||
}
|
||||
|
||||
const expectedUid = getClaimIdentityFromTask(task).expectedUid
|
||||
if (!expectedUid) {
|
||||
const prefix = String(options.errorCodePrefix || 'kuaishou_cloud').trim() || 'kuaishou_cloud'
|
||||
throw createHttpError(
|
||||
'该 lewan 任务尚未填写游戏 UID,请用户打开领取页提交 UID 后再发货(旧单不再支持无 UID 自动发货)',
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${prefix}_expected_uid_required`,
|
||||
},
|
||||
)
|
||||
}
|
||||
return expectedUid
|
||||
}
|
||||
|
||||
export function buildClaimIdentityAdminSummary(
|
||||
context: unknown,
|
||||
options: { flowLike?: unknown; taskRoleId?: unknown; taskRoleName?: unknown } = {},
|
||||
) {
|
||||
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(
|
||||
role.name || binding.roleName || options.taskRoleName || '',
|
||||
).trim()
|
||||
const ready = Boolean(identity.expectedUid)
|
||||
const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null
|
||||
|
||||
return {
|
||||
expectedUid: identity.expectedUid,
|
||||
submittedAt: identity.submittedAt,
|
||||
source: identity.source,
|
||||
ready,
|
||||
boundUid,
|
||||
boundRoleName,
|
||||
uidMatched,
|
||||
compatibilityMode: ready ? ('uid' as const) : ('legacy_no_uid' as const),
|
||||
note: ready
|
||||
? uidMatched === true
|
||||
? 'UID 已匹配,可继续履约'
|
||||
: uidMatched === false
|
||||
? '绑定角色 ID 与填写 UID 不一致'
|
||||
: '已填 UID,等待绑定识别'
|
||||
: '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货',
|
||||
}
|
||||
}
|
||||
|
||||
export function assertBoundUidMatchesExpected(
|
||||
task: Partial<TaskRow> | null | undefined,
|
||||
flowLike: unknown,
|
||||
|
||||
@@ -405,28 +405,70 @@ export async function redeemKuaishouCloudClaim(token: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
const currentStatus = normalizeTaskStatus(context.task.task_status)
|
||||
let workingTask = context.task
|
||||
const currentStatus = normalizeTaskStatus(workingTask.task_status)
|
||||
if (isKuaishouCloudRedeemSettledStatus(currentStatus)) {
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
if (!canRedeemKuaishouCloudClaimStatus(currentStatus)) {
|
||||
throw createHttpError('请先确认角色信息', {
|
||||
throw createHttpError('当前状态不可兑换,请先完成绑定并匹配 UID', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_role_not_confirmed',
|
||||
errorCode: 'claim_kuaishou_cloud_not_ready_for_redeem',
|
||||
})
|
||||
}
|
||||
|
||||
// WAITING_BINDING + UID 匹配:自动升为 ROLE_CONFIRMED,实现一键兑换
|
||||
if (currentStatus === TASK_STATUS.WAITING_BINDING) {
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(workingTask).kuaishouCloudFulfillment,
|
||||
)
|
||||
if (!isKuaishouCloudMockTask(workingTask)) {
|
||||
assertBoundUidMatchesExpected(workingTask, flow, {
|
||||
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
|
||||
})
|
||||
} else {
|
||||
assertClaimExpectedUidReady(workingTask)
|
||||
}
|
||||
|
||||
const confirmed = await updateTask(workingTask.id, {
|
||||
task_status: TASK_STATUS.ROLE_CONFIRMED,
|
||||
role_id: flow.binding.roleId || workingTask.role_id || '',
|
||||
role_name: flow.binding.roleName || workingTask.role_name || '',
|
||||
user_action_status: TASK_STATUS.ROLE_CONFIRMED,
|
||||
role_confirmed_at: now,
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
if (confirmed) {
|
||||
workingTask = confirmed
|
||||
}
|
||||
await createTaskEvent(
|
||||
workingTask.id,
|
||||
'kuaishou_cloud_role_confirmed',
|
||||
{
|
||||
expectedUid: getClaimIdentityFromTask(workingTask).expectedUid,
|
||||
vnPhone: flow.binding.vnPhone,
|
||||
roleName: flow.binding.roleName,
|
||||
roleId: flow.binding.roleId,
|
||||
source: 'claim_page_one_click_redeem',
|
||||
},
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
const flowBeforeRedeem = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(context.task).kuaishouCloudFulfillment,
|
||||
parseTaskContext(workingTask).kuaishouCloudFulfillment,
|
||||
)
|
||||
if (!isKuaishouCloudMockTask(context.task)) {
|
||||
assertBoundUidMatchesExpected(context.task, flowBeforeRedeem, {
|
||||
if (!isKuaishouCloudMockTask(workingTask)) {
|
||||
assertBoundUidMatchesExpected(workingTask, flowBeforeRedeem, {
|
||||
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
|
||||
})
|
||||
} else {
|
||||
assertClaimExpectedUidReady(workingTask)
|
||||
}
|
||||
|
||||
const lockedTask = await updateTaskStatusIfCurrent(context.task.id, TASK_STATUS.ROLE_CONFIRMED, {
|
||||
const lockedTask = await updateTaskStatusIfCurrent(workingTask.id, TASK_STATUS.ROLE_CONFIRMED, {
|
||||
task_status: TASK_STATUS.REDEEMING,
|
||||
user_action_status: 'not_required',
|
||||
last_error: '',
|
||||
|
||||
Reference in New Issue
Block a user