refactor(fulfillment): 移除飞飞/乐玩通道,仅保留 affiliate_dash
新单只路由 affiliate_dash(未命中走 manual_dispatch);历史飞飞/乐玩任务保留只读 stub,避免订单重同步失败。同步清理 cloudtentacles 调度、Admin 写路径与相关 env。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,6 @@ import test from 'node:test'
|
||||
|
||||
import {
|
||||
appendUidToUrl,
|
||||
assertLewanAutoFulfillmentUidReady,
|
||||
assertValidClaimUid,
|
||||
buildClaimIdentityAdminSummary,
|
||||
isClaimUidMatched,
|
||||
@@ -56,39 +55,6 @@ test('appendUidToUrl overwrites existing 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(
|
||||
{
|
||||
|
||||
@@ -108,38 +108,6 @@ 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 } = {},
|
||||
|
||||
@@ -8,13 +8,18 @@ import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { TASK_STATUS, isTaskFinalStatus } from '../../domain/task-status.js'
|
||||
import { buildClaimUrl } from './claim-service.js'
|
||||
import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js'
|
||||
import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import { buildClaimIdentityPayload } from './claim-identity.js'
|
||||
import { normalizeAffiliateDashFlow } from '../fulfillment/affiliate-dash/index.js'
|
||||
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
||||
|
||||
const RETIRED_CHANNEL_MESSAGE: Record<string, string> = {
|
||||
kuaishou_feifei: '飞飞履约渠道已下线,本链接仅可查看历史信息,无法继续领取',
|
||||
kuaishou_ct_assisted: '乐玩履约渠道已下线,本链接仅可查看历史信息,无法继续领取',
|
||||
}
|
||||
|
||||
type ClaimContext = {
|
||||
claimToken: ClaimTokenRow
|
||||
task: TaskRow
|
||||
@@ -88,73 +93,17 @@ export async function getClaimContext(token: unknown) {
|
||||
}
|
||||
|
||||
export function buildClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
if (String(task.executor_key || '').trim() === 'kuaishou_feifei') {
|
||||
return buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem })
|
||||
}
|
||||
const executorKey = String(task.executor_key || '').trim()
|
||||
|
||||
if (String(task.executor_key || '').trim() === 'affiliate_dash') {
|
||||
if (executorKey === 'affiliate_dash') {
|
||||
return buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem })
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(taskContext.claimIdentity)
|
||||
const kuaishouCloudSource = resolveClaimKuaishouCloudSource(task)
|
||||
const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order)
|
||||
const displaySkuName = resolveClaimOrderItemDisplaySkuName(
|
||||
orderItem,
|
||||
kuaishouCloudFulfillment,
|
||||
kuaishouCloudSource,
|
||||
)
|
||||
const product = buildClaimProductPayload(displaySkuName, orderItem, kuaishouCloudSource)
|
||||
|
||||
return {
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: 'kuaishou_cloud',
|
||||
claimIdentity,
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
executorKey: task.executor_key || '',
|
||||
requiresSupportReview: false,
|
||||
expiresAt: claimToken.expired_at,
|
||||
claimedAt: task.claimed_at,
|
||||
roleConfirmedAt: task.role_confirmed_at,
|
||||
redeemedAt: task.redeemed_at,
|
||||
loginType: task.login_type,
|
||||
lastError: task.last_error,
|
||||
runtimeSessionId: task.runtime_session_id,
|
||||
},
|
||||
order: {
|
||||
orderId: order.id,
|
||||
platform: order.platform,
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
totalAmount: formatFenToAmount(order.total_amount),
|
||||
totalAmountFen: normalizeFen(order.total_amount),
|
||||
currency: order.currency,
|
||||
},
|
||||
orderItem: {
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
skuName: displaySkuName,
|
||||
quantity: orderItem.quantity,
|
||||
},
|
||||
product,
|
||||
session: null as null,
|
||||
kuaishouCloudFulfillment,
|
||||
kuaishouFeifei: null as null,
|
||||
result: task.redeemed_at
|
||||
? {
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
: null,
|
||||
if (executorKey === 'kuaishou_feifei' || executorKey === 'kuaishou_ct_assisted') {
|
||||
return buildRetiredClaimDetailPayload({ claimToken, task, order, orderItem })
|
||||
}
|
||||
|
||||
return buildGenericClaimDetailPayload({ claimToken, task, order, orderItem })
|
||||
}
|
||||
|
||||
function buildAffiliateDashClaimDetailPayload({
|
||||
@@ -186,32 +135,12 @@ function buildAffiliateDashClaimDetailPayload({
|
||||
return {
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: 'affiliate_dash',
|
||||
flowType: 'affiliate_dash' as const,
|
||||
channelOffline: false,
|
||||
channelOfflineMessage: '',
|
||||
claimIdentity,
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
executorKey: task.executor_key || '',
|
||||
requiresSupportReview: false,
|
||||
expiresAt: claimToken.expired_at,
|
||||
claimedAt: task.claimed_at,
|
||||
roleConfirmedAt: task.role_confirmed_at,
|
||||
redeemedAt: task.redeemed_at,
|
||||
loginType: task.login_type,
|
||||
lastError: task.last_error,
|
||||
runtimeSessionId: task.runtime_session_id,
|
||||
},
|
||||
order: {
|
||||
orderId: order.id,
|
||||
platform: order.platform,
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
totalAmount: formatFenToAmount(order.total_amount),
|
||||
totalAmountFen: normalizeFen(order.total_amount),
|
||||
currency: order.currency,
|
||||
},
|
||||
task: buildClaimTaskPayload(task, claimToken),
|
||||
order: buildClaimOrderPayload(order),
|
||||
orderItem: {
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
@@ -223,313 +152,140 @@ function buildAffiliateDashClaimDetailPayload({
|
||||
kuaishouCloudFulfillment: null as null,
|
||||
kuaishouFeifei: null as null,
|
||||
affiliateDash: flow,
|
||||
result: task.redeemed_at
|
||||
? {
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
: null,
|
||||
result: buildClaimResultPayload(task),
|
||||
}
|
||||
}
|
||||
|
||||
function buildKuaishouFeifeiClaimDetailPayload({
|
||||
claimToken,
|
||||
task,
|
||||
order,
|
||||
orderItem,
|
||||
}: ClaimContext) {
|
||||
function buildRetiredClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
const executorKey = String(task.executor_key || '').trim()
|
||||
const context = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||
const expectedUid = getClaimIdentityFromContext(context).expectedUid
|
||||
const flow = mapClaimKuaishouFeifeiFulfillment(context.kuaishouFeifei, expectedUid)
|
||||
const product = {
|
||||
title: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
isBundle: false,
|
||||
items: [
|
||||
{
|
||||
cloudSkuId: 0,
|
||||
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
},
|
||||
],
|
||||
}
|
||||
const productTitle = String(orderItem.sku_name || orderItem.sku_code || '').trim()
|
||||
const offlineMessage =
|
||||
RETIRED_CHANNEL_MESSAGE[executorKey] || '该履约渠道已下线,本链接仅可查看历史信息'
|
||||
|
||||
return {
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: 'kuaishou_feifei',
|
||||
flowType: executorKey === 'kuaishou_feifei' ? ('kuaishou_feifei' as const) : ('kuaishou_cloud' as const),
|
||||
channelOffline: true,
|
||||
channelOfflineMessage: offlineMessage,
|
||||
claimIdentity,
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
executorKey: task.executor_key || '',
|
||||
requiresSupportReview: false,
|
||||
expiresAt: claimToken.expired_at,
|
||||
claimedAt: task.claimed_at,
|
||||
roleConfirmedAt: task.role_confirmed_at,
|
||||
redeemedAt: task.redeemed_at,
|
||||
loginType: task.login_type,
|
||||
lastError: task.last_error,
|
||||
runtimeSessionId: task.runtime_session_id,
|
||||
},
|
||||
order: {
|
||||
orderId: order.id,
|
||||
platform: order.platform,
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
totalAmount: formatFenToAmount(order.total_amount),
|
||||
totalAmountFen: normalizeFen(order.total_amount),
|
||||
currency: order.currency,
|
||||
},
|
||||
task: buildClaimTaskPayload(task, claimToken),
|
||||
order: buildClaimOrderPayload(order),
|
||||
orderItem: {
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
skuName: product.title,
|
||||
skuName: productTitle,
|
||||
quantity: orderItem.quantity,
|
||||
},
|
||||
product,
|
||||
product: {
|
||||
title: productTitle,
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
isBundle: false,
|
||||
items: [
|
||||
{
|
||||
cloudSkuId: 0,
|
||||
name: productTitle,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
},
|
||||
],
|
||||
},
|
||||
session: null as null,
|
||||
kuaishouCloudFulfillment: null as null,
|
||||
kuaishouFeifei: flow,
|
||||
result: task.redeemed_at
|
||||
? {
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
function mapClaimKuaishouFeifeiFulfillment(value: unknown, expectedUid = '') {
|
||||
const source = isPlainObject(value) ? value : {}
|
||||
const h5 = isPlainObject(source.h5) ? source.h5 : {}
|
||||
const entryUrl = String(h5.entryUrl || '').trim()
|
||||
const rechargeUrl = String(h5.rechargeUrl || '').trim()
|
||||
const h5UrlWithUid = resolveKuaishouFeifeiH5UrlWithUid(source, expectedUid)
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_feifei',
|
||||
productCode: String(source.productCode || '').trim(),
|
||||
productName: String(source.productName || '').trim(),
|
||||
platformOrderNo: String(source.platformOrderNo || '').trim(),
|
||||
orderNo: String(source.orderNo || '').trim(),
|
||||
rechargeStatus: Number(source.rechargeStatus || 0) || 0,
|
||||
rechargeStatusLabel: String(source.rechargeStatusLabel || '').trim(),
|
||||
rechargeResultMessage: String(source.rechargeResultMessage || '').trim(),
|
||||
claimUrl: String(source.claimUrl || '').trim(),
|
||||
consumeStatus: String(source.consumeStatus || 'pending').trim(),
|
||||
h5: {
|
||||
entryUrl,
|
||||
rechargeUrl,
|
||||
kuaishouFeifei: null as null,
|
||||
affiliateDash: null as null,
|
||||
result: buildClaimResultPayload(task) || {
|
||||
resultCode: 'channel_offline',
|
||||
resultMessage: offlineMessage,
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
},
|
||||
h5UrlWithUid: expectedUid ? h5UrlWithUid : '',
|
||||
lastSyncedAt: source.lastSyncedAt || null,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveClaimOrderItemDisplaySkuName(
|
||||
orderItem: OrderItemRow,
|
||||
kuaishouCloudFulfillment: JsonObject | null,
|
||||
kuaishouCloudSource: JsonObject = {},
|
||||
) {
|
||||
const fulfillment = isPlainObject(kuaishouCloudFulfillment) ? kuaishouCloudFulfillment : {}
|
||||
const binding = isPlainObject(fulfillment.binding) ? fulfillment.binding : {}
|
||||
const ticket = isPlainObject(fulfillment.ticket) ? fulfillment.ticket : {}
|
||||
const skuCode = String(orderItem.sku_code || '').trim()
|
||||
const rawSkuName = String(orderItem.sku_name || '').trim()
|
||||
const candidates = [
|
||||
kuaishouCloudSource.internalSkuName,
|
||||
rawSkuName,
|
||||
ticket.goodsTitle,
|
||||
binding.skuName,
|
||||
skuCode,
|
||||
]
|
||||
|
||||
for (const value of candidates) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized && !isOrderItemIdentifierName(normalized, skuCode)) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return rawSkuName || skuCode
|
||||
}
|
||||
|
||||
function buildClaimProductPayload(
|
||||
displaySkuName: string,
|
||||
orderItem: OrderItemRow,
|
||||
kuaishouCloudSource: JsonObject = {},
|
||||
) {
|
||||
const binding = isPlainObject(kuaishouCloudSource.binding) ? kuaishouCloudSource.binding : {}
|
||||
const deliveryItems = normalizeClaimDeliveryItems(kuaishouCloudSource.deliveryItems, binding)
|
||||
.map((item) => ({
|
||||
cloudSkuId: item.cloudSkuId,
|
||||
name: item.cloudSkuName,
|
||||
quantity: item.quantity,
|
||||
}))
|
||||
.filter((item) => item.name)
|
||||
const normalizedDeliveryItems =
|
||||
deliveryItems.length > 0
|
||||
? mergeClaimProductItems(deliveryItems)
|
||||
: [
|
||||
{
|
||||
cloudSkuId: 0,
|
||||
name: displaySkuName,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
},
|
||||
]
|
||||
function buildGenericClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
const context = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||
const productTitle = String(orderItem.sku_name || orderItem.sku_code || '').trim()
|
||||
|
||||
return {
|
||||
title: displaySkuName,
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
isBundle: normalizedDeliveryItems.length > 1,
|
||||
items: normalizedDeliveryItems,
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: String(task.executor_key || 'manual_dispatch').trim() || 'manual_dispatch',
|
||||
channelOffline: false,
|
||||
channelOfflineMessage: '',
|
||||
claimIdentity,
|
||||
task: buildClaimTaskPayload(task, claimToken),
|
||||
order: buildClaimOrderPayload(order),
|
||||
orderItem: {
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
skuName: productTitle,
|
||||
quantity: orderItem.quantity,
|
||||
},
|
||||
product: {
|
||||
title: productTitle,
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
isBundle: false,
|
||||
items: [
|
||||
{
|
||||
cloudSkuId: 0,
|
||||
name: productTitle,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
},
|
||||
],
|
||||
},
|
||||
session: null as null,
|
||||
kuaishouCloudFulfillment: null as null,
|
||||
kuaishouFeifei: null as null,
|
||||
affiliateDash: null as null,
|
||||
result: buildClaimResultPayload(task),
|
||||
}
|
||||
}
|
||||
|
||||
function mergeClaimProductItems(
|
||||
items: Array<{ cloudSkuId: number; name: string; quantity: number }>,
|
||||
) {
|
||||
const merged = new Map<string, { cloudSkuId: number; name: string; quantity: number }>()
|
||||
|
||||
for (const item of items) {
|
||||
const key = item.cloudSkuId > 0 ? `id:${item.cloudSkuId}` : `name:${item.name}`
|
||||
const existing = merged.get(key)
|
||||
if (existing) {
|
||||
existing.quantity += item.quantity
|
||||
existing.name = existing.name || item.name
|
||||
continue
|
||||
}
|
||||
|
||||
merged.set(key, { ...item })
|
||||
function buildClaimTaskPayload(task: TaskRow, claimToken: ClaimTokenRow) {
|
||||
return {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
executorKey: task.executor_key || '',
|
||||
requiresSupportReview: false,
|
||||
expiresAt: claimToken.expired_at,
|
||||
claimedAt: task.claimed_at,
|
||||
roleConfirmedAt: task.role_confirmed_at,
|
||||
redeemedAt: task.redeemed_at,
|
||||
loginType: task.login_type,
|
||||
lastError: task.last_error,
|
||||
runtimeSessionId: task.runtime_session_id,
|
||||
}
|
||||
|
||||
return Array.from(merged.values())
|
||||
}
|
||||
|
||||
function resolveClaimKuaishouCloudSource(task: TaskRow): JsonObject {
|
||||
if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
return {}
|
||||
function buildClaimOrderPayload(order: OrderRow) {
|
||||
return {
|
||||
orderId: order.id,
|
||||
platform: order.platform,
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
totalAmount: formatFenToAmount(order.total_amount),
|
||||
totalAmountFen: normalizeFen(order.total_amount),
|
||||
currency: order.currency,
|
||||
}
|
||||
|
||||
const source = parseTaskContext(task).kuaishouCloudFulfillment
|
||||
return isPlainObject(source) ? source : {}
|
||||
}
|
||||
|
||||
export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow) {
|
||||
if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
function buildClaimResultPayload(task: TaskRow) {
|
||||
if (!task.redeemed_at) {
|
||||
return null
|
||||
}
|
||||
|
||||
const source = parseTaskContext(task).kuaishouCloudFulfillment
|
||||
if (!source || typeof source !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const binding = asJsonObject(source.binding)
|
||||
const role = asJsonObject(source.role)
|
||||
const purchase = asJsonObject(source.purchase)
|
||||
const ticket = asJsonObject(source.ticket)
|
||||
const dispatch = asJsonObject(source.dispatch)
|
||||
const returnNumber = asJsonObject(source.returnNumber)
|
||||
const consume = asJsonObject(source.consume)
|
||||
const roleName = String(role.name || binding.roleName || '').trim()
|
||||
const roleId = String(role.rid || binding.roleId || '').trim()
|
||||
const defaultRoleName = String(role.defaultName || role.defaultRoleName || '').trim()
|
||||
const defaultRoleId = String(role.defaultRid || role.defaultRoleId || '').trim()
|
||||
const isDefaultRole =
|
||||
Boolean(roleName || roleId) &&
|
||||
isSameClaimKuaishouCloudRole(
|
||||
{ name: roleName, rid: roleId },
|
||||
{ name: defaultRoleName, rid: defaultRoleId },
|
||||
)
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_cloud',
|
||||
shopId: String(order?.shop_id || '').trim(),
|
||||
shopName: String(order?.shop_name || '').trim(),
|
||||
guideImages: [],
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
cloudSourceKeys: Array.isArray(binding.cloudSourceKeys)
|
||||
? binding.cloudSourceKeys
|
||||
.map((value: unknown) => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
|
||||
skuId: Number(binding.skuId || 0) || 0,
|
||||
skuName: String(binding.skuName || '').trim(),
|
||||
vnKey: String(binding.vnKey || '').trim(),
|
||||
vnId: Number(binding.vnId || 0) || 0,
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt: binding.bindPreparedAt || null,
|
||||
bindExpiresAt: binding.bindExpiresAt || null,
|
||||
bindProbeAt: binding.bindProbeAt || null,
|
||||
bindProbeStatus: String(binding.bindProbeStatus || '').trim(),
|
||||
bindProbeMessage: String(binding.bindProbeMessage || '').trim(),
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
roleName: String(binding.roleName || '').trim(),
|
||||
roleId: String(binding.roleId || '').trim(),
|
||||
},
|
||||
role: {
|
||||
status: String(role.status || 'pending').trim() || 'pending',
|
||||
name: roleName,
|
||||
rid: roleId,
|
||||
refreshedAt: role.refreshedAt || null,
|
||||
errorMessage: String(role.errorMessage || '').trim(),
|
||||
defaultName: defaultRoleName,
|
||||
defaultRid: defaultRoleId,
|
||||
defaultCapturedAt: role.defaultCapturedAt || null,
|
||||
defaultCaptureStatus: String(role.defaultCaptureStatus || '').trim(),
|
||||
defaultErrorMessage: String(role.defaultErrorMessage || '').trim(),
|
||||
isDefaultRole,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: purchase.usedKnapsack === true,
|
||||
purchaseTriggered: purchase.purchaseTriggered === true,
|
||||
assetBefore: Number(purchase.assetBefore || 0) || 0,
|
||||
assetAfter: Number(purchase.assetAfter || 0) || 0,
|
||||
purchaseAt: purchase.purchaseAt || null,
|
||||
},
|
||||
dispatch: {
|
||||
status: String(dispatch.status || 'pending').trim() || 'pending',
|
||||
dispatchAt: dispatch.dispatchAt || null,
|
||||
failedAt: dispatch.failedAt || null,
|
||||
failedStage: String(dispatch.failedStage || '').trim(),
|
||||
errorCode: String(dispatch.errorCode || '').trim(),
|
||||
errorMessage: String(dispatch.errorMessage || '').trim(),
|
||||
note: String(dispatch.note || '').trim(),
|
||||
},
|
||||
returnNumber: {
|
||||
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||
returnedAt: returnNumber.returnedAt || null,
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
shopName: String(consume.shopName || '').trim(),
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,115 +293,6 @@ function parseTaskContext(task: TaskRow): JsonObject {
|
||||
return parseTaskContextValue(task)
|
||||
}
|
||||
|
||||
function isOrderItemIdentifierName(value: string, skuCode: string) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isClaimPlaceholderSkuName(normalized)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const normalizedSkuCode = String(skuCode || '').trim()
|
||||
if (normalizedSkuCode && normalized === normalizedSkuCode && /^\d{8,}$/.test(normalizedSkuCode)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return /^\d{8,}$/.test(normalized)
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
function isSameClaimKuaishouCloudRole(
|
||||
left: { name?: unknown; rid?: unknown },
|
||||
right: { name?: unknown; rid?: unknown },
|
||||
) {
|
||||
const leftRid = String(left.rid || '').trim()
|
||||
const rightRid = String(right.rid || '').trim()
|
||||
if (leftRid && rightRid) {
|
||||
return leftRid === rightRid
|
||||
}
|
||||
|
||||
const leftName = normalizeClaimRoleNameForCompare(left.name)
|
||||
const rightName = normalizeClaimRoleNameForCompare(right.name)
|
||||
return Boolean(leftName && rightName && leftName === rightName)
|
||||
}
|
||||
|
||||
function normalizeClaimRoleNameForCompare(value: unknown) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
|
||||
const rawItems = Array.isArray(value) ? value : []
|
||||
const items = rawItems
|
||||
.map((item) => normalizeClaimDeliveryItem(item))
|
||||
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
|
||||
Boolean(item),
|
||||
)
|
||||
|
||||
if (items.length > 0) {
|
||||
return mergeClaimDeliveryItems(items)
|
||||
}
|
||||
|
||||
const cloudSkuId = Number(binding.skuId || 0) || 0
|
||||
if (!cloudSkuId) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(binding.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeClaimDeliveryItem(value: unknown) {
|
||||
const source = isPlainObject(value) ? value : {}
|
||||
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
|
||||
|
||||
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const quantity = Number(source.quantity || 1) || 1
|
||||
return {
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
|
||||
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeClaimDeliveryItems(
|
||||
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>,
|
||||
) {
|
||||
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>()
|
||||
|
||||
for (const item of items) {
|
||||
const existing = merged.get(item.cloudSkuId)
|
||||
if (existing) {
|
||||
existing.quantity += item.quantity
|
||||
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
|
||||
continue
|
||||
}
|
||||
|
||||
merged.set(item.cloudSkuId, { ...item })
|
||||
}
|
||||
|
||||
return Array.from(merged.values())
|
||||
}
|
||||
|
||||
function isClaimPlaceholderSkuName(value: string) {
|
||||
return ['测试链接专用', '商品领取', '测试商品'].includes(String(value || '').trim())
|
||||
}
|
||||
|
||||
async function expireClaimContext(claimToken: ClaimTokenRow, task: TaskRow) {
|
||||
const now = nowIso()
|
||||
const nextClaimToken = await updateClaimToken(claimToken.id, {
|
||||
|
||||
@@ -5,17 +5,6 @@ import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { TASK_STATUS } from '../../domain/task-status.js'
|
||||
import {
|
||||
confirmFulfillmentRole,
|
||||
prepareFulfillmentBinding,
|
||||
rebindFulfillmentRole,
|
||||
redeemFulfillmentTask,
|
||||
} from '../fulfillment/executors/registry.js'
|
||||
import {
|
||||
isKuaishouCloudMockTask,
|
||||
normalizeKuaishouCloudFlow,
|
||||
} from '../fulfillment/kuaishou-cloud/index.js'
|
||||
import { syncKuaishouFeifeiTaskStatus } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import {
|
||||
normalizeAffiliateDashFlow,
|
||||
syncAffiliateDashTaskStatus,
|
||||
@@ -31,79 +20,23 @@ import {
|
||||
getClaimIdentityFromTask,
|
||||
} from './claim-identity.js'
|
||||
import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim-context.js'
|
||||
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
|
||||
|
||||
function assertLewanClaimTask(task: TaskRow) {
|
||||
if (String(task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
throw createHttpError('当前领取链接不是 kuaishou-lewan 客户领取流程', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
const RETIRED_EXECUTOR_KEYS = new Set(['kuaishou_feifei', 'kuaishou_ct_assisted'])
|
||||
|
||||
function isRetiredExecutor(executorKey: unknown) {
|
||||
return RETIRED_EXECUTOR_KEYS.has(String(executorKey || '').trim())
|
||||
}
|
||||
|
||||
async function requireExecutorAction<T>(
|
||||
result: Promise<T | null> | T | null,
|
||||
errorMessage: string,
|
||||
errorCode: string,
|
||||
): Promise<NonNullable<T>> {
|
||||
const value = await result
|
||||
if (!value) {
|
||||
throw createHttpError(errorMessage, {
|
||||
statusCode: 409,
|
||||
errorCode,
|
||||
})
|
||||
}
|
||||
return value
|
||||
}
|
||||
export async function getKuaishouCloudClaimDetail(token: unknown): Promise<ClaimDetailPayload> {
|
||||
const context = await getClaimContext(token)
|
||||
let task = context.task
|
||||
const executorKey = String(task.executor_key || '').trim()
|
||||
|
||||
function isKuaishouCloudBindingReady(flow: ReturnType<typeof normalizeKuaishouCloudFlow>) {
|
||||
return (
|
||||
flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 Cloud 绑定资源已准备(取号 + 绑链)。
|
||||
* 提前核销后仍可能未 prepare,不能因 consume.success 跳过。
|
||||
*/
|
||||
async function ensureKuaishouCloudBindingPrepared(
|
||||
task: TaskRow,
|
||||
options: { source: string; actorSource?: string } = { source: 'system_auto_prepare_binding' },
|
||||
): Promise<TaskRow> {
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||||
if (isKuaishouCloudBindingReady(flow)) {
|
||||
return task
|
||||
}
|
||||
|
||||
try {
|
||||
const prepared = await prepareFulfillmentBinding(task, {
|
||||
source: options.source,
|
||||
actor: { source: options.actorSource || options.source },
|
||||
})
|
||||
return prepared?.task || task
|
||||
} catch {
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyIndustryVoucherTicket(
|
||||
context: Awaited<ReturnType<typeof getClaimContext>>,
|
||||
now: string,
|
||||
): Promise<ClaimDetailPayload> {
|
||||
const taskContext = parseTaskContext(context.task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
// 提前核销后 consume 已是 success,但仍可能未准备绑定资源;禁止整段短路。
|
||||
// 注意:不要再调 getKuaishouCloudClaimDetail,避免 prepare 失败时递归。
|
||||
if (flow.consume.status === 'success') {
|
||||
const task = await ensureKuaishouCloudBindingPrepared(context.task, {
|
||||
source: 'claim_after_early_consume_prepare',
|
||||
actorSource: 'system',
|
||||
})
|
||||
// 已下线渠道:只读详情,不主动同步上游
|
||||
if (isRetiredExecutor(executorKey)) {
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task,
|
||||
@@ -112,127 +45,6 @@ async function verifyIndustryVoucherTicket(
|
||||
})
|
||||
}
|
||||
|
||||
const industryContext = typeof taskContext === 'object' ? taskContext : {}
|
||||
const voucherContext = normalizeIndustryVoucherContext(industryContext.kuaishouIndustryVoucher)
|
||||
if (voucherContext.status === 'DESTROYED') {
|
||||
throw createHttpError('当前电子凭证已销毁,无法继续领取', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_industry_voucher_destroyed',
|
||||
})
|
||||
}
|
||||
|
||||
const token = String(voucherContext.token || industryContext.token || '').trim()
|
||||
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
||||
const certExpireType = Number(industryContext.certExpireType || 0)
|
||||
const certActualStartTime = Number(
|
||||
voucherContext.validStartTime || industryContext.certActualStartTime || 0,
|
||||
)
|
||||
const certActualEndTime = Number(
|
||||
voucherContext.validEndTime || industryContext.certActualEndTime || 0,
|
||||
)
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouIndustryVoucher: {
|
||||
...voucherContext,
|
||||
oid: voucherContext.oid || context.order.platform_order_id,
|
||||
token,
|
||||
eticketId: voucherCode,
|
||||
voucherCode,
|
||||
status: voucherContext.status || 'UNUSED',
|
||||
verifiedAt: voucherContext.verifiedAt || now,
|
||||
},
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
ticket: {
|
||||
...flow.ticket,
|
||||
code: voucherCode,
|
||||
status: 'verified',
|
||||
capturedAt: flow.ticket.capturedAt || now,
|
||||
capturedBy: flow.ticket.capturedBy || { source: 'send_code_callback' },
|
||||
verifiedAt: now,
|
||||
oid: voucherContext.oid || context.order.platform_order_id,
|
||||
formToken: token,
|
||||
leftCount: voucherContext.status === 'CONSUMED' ? 0 : 1,
|
||||
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code || '',
|
||||
},
|
||||
consume: {
|
||||
...flow.consume,
|
||||
status: voucherContext.status === 'CONSUMED' ? 'success' : 'pending',
|
||||
shopId: context.order.shop_id,
|
||||
shopName: context.order.shop_name,
|
||||
autoConsumeEnabled: true,
|
||||
consumedAt:
|
||||
voucherContext.status === 'CONSUMED'
|
||||
? voucherContext.consumedAt || flow.consume.consumedAt || now
|
||||
: flow.consume.consumedAt,
|
||||
},
|
||||
certInfo: {
|
||||
certExpireType,
|
||||
certActualStartTime,
|
||||
certActualEndTime,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const taskWithTicket = (await updateTask(context.task.id, {
|
||||
claim_token: context.claimToken.token,
|
||||
claim_expires_at: context.claimToken.expired_at,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
task_status: TASK_STATUS.WAITING_BINDING,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})) || {
|
||||
...context.task,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
task_status: TASK_STATUS.WAITING_BINDING,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
}
|
||||
|
||||
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
||||
let taskAfterPrepare = taskWithTicket
|
||||
if (!isKuaishouCloudBindingReady(preparedFlow)) {
|
||||
taskAfterPrepare = await ensureKuaishouCloudBindingPrepared(taskWithTicket, {
|
||||
source: 'send_code_callback',
|
||||
actorSource: 'send_code_callback',
|
||||
})
|
||||
}
|
||||
|
||||
await createTaskEvent(
|
||||
context.task.id,
|
||||
'kuaishou_cloud_ticket_verified',
|
||||
{
|
||||
ticketCodeMasked: '',
|
||||
source: 'send_code_callback',
|
||||
industryVoucher: true,
|
||||
verifiedAt: now,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task: taskAfterPrepare,
|
||||
order: context.order,
|
||||
orderItem: context.orderItem,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getKuaishouCloudClaimDetail(token: unknown): Promise<ClaimDetailPayload> {
|
||||
const context = await getClaimContext(token)
|
||||
let task = context.task
|
||||
|
||||
const executorKey = String(task.executor_key || '').trim()
|
||||
|
||||
if (executorKey === 'kuaishou_feifei') {
|
||||
task = (await syncKuaishouFeifeiTaskStatus(task)) || task
|
||||
}
|
||||
|
||||
if (executorKey === 'affiliate_dash') {
|
||||
task = (await syncAffiliateDashTaskStatus(task)) || task
|
||||
if (shouldRefreshAffiliateDashBindState(task)) {
|
||||
@@ -240,64 +52,6 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
}
|
||||
}
|
||||
|
||||
if (executorKey === 'kuaishou-industry') {
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||||
// 已核销也要走 verify:内部会补 prepare 绑定资源
|
||||
if (flow.consume.status !== 'success' || !isKuaishouCloudBindingReady(flow)) {
|
||||
const now = nowIso()
|
||||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
|
||||
context,
|
||||
now,
|
||||
).catch(() => null)
|
||||
if (prepared) {
|
||||
return prepared
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (executorKey === 'kuaishou_ct_assisted') {
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
const needsIndustryVoucherPrepare =
|
||||
hasUsableIndustryVoucher(taskContext) &&
|
||||
(flow.ticket.status !== 'verified' || !isKuaishouCloudBindingReady(flow))
|
||||
|
||||
if (needsIndustryVoucherPrepare) {
|
||||
const now = nowIso()
|
||||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
|
||||
context,
|
||||
now,
|
||||
).catch(() => null)
|
||||
if (prepared) {
|
||||
return prepared
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:不依赖 UID;凭证已确认但绑定未就绪时自动补 prepare(覆盖提前核销场景)
|
||||
if (!isKuaishouCloudMockTask(task)) {
|
||||
const latestFlow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||||
if (
|
||||
(latestFlow.ticket.status === 'verified' ||
|
||||
hasUsableIndustryVoucher(parseTaskContext(task))) &&
|
||||
!isKuaishouCloudBindingReady(latestFlow)
|
||||
) {
|
||||
task = await ensureKuaishouCloudBindingPrepared(task, {
|
||||
source: 'claim_page_auto_binding_prepare',
|
||||
actorSource: 'system',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 已提交 UID 的 lewan 任务:详情查询时顺带刷新角色,便于前端轮询匹配状态
|
||||
if (
|
||||
executorKey === 'kuaishou_ct_assisted' &&
|
||||
!isKuaishouCloudMockTask(task) &&
|
||||
getClaimIdentityFromTask(task).expectedUid
|
||||
) {
|
||||
task = (await syncKuaishouCloudRoleInfo(task)) || task
|
||||
}
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
claimToken: context.claimToken,
|
||||
task,
|
||||
@@ -308,12 +62,22 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
|
||||
/**
|
||||
* 统一领取 Step1:提交游戏 UID。
|
||||
* 已下线渠道仅允许查看,禁止再改 UID。
|
||||
*/
|
||||
export async function submitClaimUid(
|
||||
token: unknown,
|
||||
payload: { uid?: unknown } = {},
|
||||
): Promise<ClaimDetailPayload> {
|
||||
const context = await getClaimContext(token)
|
||||
const executorKey = String(context.task.executor_key || '').trim()
|
||||
|
||||
if (isRetiredExecutor(executorKey)) {
|
||||
throw createHttpError('该履约渠道已下线,无法继续提交 UID', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_channel_offline',
|
||||
})
|
||||
}
|
||||
|
||||
const expectedUid = assertValidClaimUid(payload.uid)
|
||||
const now = nowIso()
|
||||
|
||||
@@ -326,7 +90,6 @@ export async function submitClaimUid(
|
||||
|
||||
const taskContext = parseTaskContext(context.task)
|
||||
const previous = getClaimIdentityFromTask(context.task)
|
||||
const executorKey = String(context.task.executor_key || '').trim()
|
||||
if (executorKey === 'affiliate_dash') {
|
||||
return submitAffiliateDashClaimUid(context, expectedUid, previous, taskContext, now, token)
|
||||
}
|
||||
@@ -336,17 +99,12 @@ export async function submitClaimUid(
|
||||
claimIdentity: buildClaimIdentityContext(previous, expectedUid, now),
|
||||
}
|
||||
|
||||
let updatedTask = (await updateTask(context.task.id, {
|
||||
await updateTask(context.task.id, {
|
||||
context_json: JSON.stringify(nextContext),
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})) || {
|
||||
...context.task,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
updated_at: now,
|
||||
}
|
||||
})
|
||||
|
||||
await createClaimUidSubmittedEventIfChanged(
|
||||
context.task.id,
|
||||
@@ -355,20 +113,6 @@ export async function submitClaimUid(
|
||||
now,
|
||||
)
|
||||
|
||||
if (executorKey === 'kuaishou_ct_assisted' && !isKuaishouCloudMockTask(updatedTask)) {
|
||||
try {
|
||||
const prepared = await prepareFulfillmentBinding(updatedTask, {
|
||||
source: 'claim_page_submit_uid',
|
||||
actor: { source: 'claim_page' },
|
||||
})
|
||||
if (prepared?.task) {
|
||||
updatedTask = prepared.task
|
||||
}
|
||||
} catch {
|
||||
// 绑定资源准备失败时仍保留 UID,详情页可继续重试/轮询
|
||||
}
|
||||
}
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
@@ -586,8 +330,6 @@ export async function submitAffiliateDashClaim(
|
||||
const latestFlow = flow
|
||||
|
||||
if (!isMock && flow.bindUuid) {
|
||||
// 提交前不再额外请求 bind-result:平台侧曾出现 GET 后 submit 的 bind_verify 异常。
|
||||
// 这里仅使用详情轮询写入的本地快照做强拦截,真正提交仍交给平台二次校验。
|
||||
if (isAffiliateDashBindSnapshotStale(flow, expectedGameAccount)) {
|
||||
throw createHttpError('绑定二维码与当前 UID 不一致,请更新 UID 后重新绑定', {
|
||||
statusCode: 409,
|
||||
@@ -664,8 +406,6 @@ export async function submitAffiliateDashClaim(
|
||||
now,
|
||||
)
|
||||
|
||||
// 平台已返回 delivered:立即用本地快照收敛任务为成功,不等平台回调(避免结果页等数秒)。
|
||||
// delivered 回调到达时 sync 幂等短路(已 REDEEMED 且 delivered 仍会刷新,无副作用)。
|
||||
if (result.status === 'delivered') {
|
||||
const refreshedTask = await getTaskById(task.id)
|
||||
if (refreshedTask) {
|
||||
@@ -789,7 +529,6 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | n
|
||||
const taskContext = parseTaskContextValue(task)
|
||||
const flow = normalizeAffiliateDashFlow(taskContext.affiliateDash)
|
||||
if (flow.mock?.enabled) {
|
||||
// dev-mock:不请求 affiliate-dash 平台,绑定状态以 context 为准
|
||||
return task
|
||||
}
|
||||
if (!flow.orderNo || !flow.bindUuid) {
|
||||
@@ -817,8 +556,6 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | n
|
||||
bindUuid: flow.bindUuid,
|
||||
})
|
||||
const boundAccount = result.bound ? result.gameAccount || flow.boundAccount : ''
|
||||
// 本地强制校验:绑定返回的游戏账号与第一步填写的 UID 不一致时置 mismatch
|
||||
// (不依赖平台 mismatch 字段,防止平台未标记但实际账号不一致的情况)。
|
||||
const localMismatch = Boolean(
|
||||
boundAccount &&
|
||||
expectedGameAccount &&
|
||||
@@ -848,7 +585,6 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | n
|
||||
})) || task
|
||||
)
|
||||
} catch {
|
||||
// 绑定状态拉取失败:保持现状,前端可继续轮询
|
||||
return task
|
||||
}
|
||||
}
|
||||
@@ -859,91 +595,6 @@ function normalizeUid(value: unknown) {
|
||||
.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
export async function rebindKuaishouCloudClaimRole(token: unknown) {
|
||||
const context = await getClaimContext(token)
|
||||
assertLewanClaimTask(context.task)
|
||||
|
||||
await requireExecutorAction(
|
||||
rebindFulfillmentRole(context.task, {
|
||||
source: 'claim_page_rebind_role',
|
||||
actor: { source: 'claim_page' },
|
||||
}),
|
||||
'当前领取链接不支持换绑角色',
|
||||
'claim_rebind_not_supported',
|
||||
)
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||
const context = await getClaimContext(token)
|
||||
assertLewanClaimTask(context.task)
|
||||
|
||||
await requireExecutorAction(
|
||||
confirmFulfillmentRole(context.task, {
|
||||
source: 'claim_page_role_confirm',
|
||||
actor: { source: 'claim_page' },
|
||||
errorCodePrefix: 'claim_kuaishou_cloud',
|
||||
forceProbe: true,
|
||||
}),
|
||||
'当前领取链接不支持确认角色',
|
||||
'claim_confirm_not_supported',
|
||||
)
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取页一键兑换:鉴权后交给 lewan 履约引擎 redeem 用例。
|
||||
*/
|
||||
export async function redeemKuaishouCloudClaim(token: unknown) {
|
||||
const context = await getClaimContext(token)
|
||||
assertLewanClaimTask(context.task)
|
||||
|
||||
await requireExecutorAction(
|
||||
redeemFulfillmentTask(context.task, {
|
||||
source: 'claim_page_redeem',
|
||||
actor: { source: 'claim_page' },
|
||||
autoFinalize: true,
|
||||
errorCodePrefix: 'claim_kuaishou_cloud_redeem',
|
||||
}),
|
||||
'当前领取链接不支持一键兑换',
|
||||
'claim_redeem_not_supported',
|
||||
)
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
function parseTaskContext(task: Partial<TaskRow> | null | undefined): JsonObject {
|
||||
return parseTaskContextValue(task)
|
||||
}
|
||||
|
||||
function hasUsableIndustryVoucher(context: JsonObject = {}) {
|
||||
const voucher = normalizeIndustryVoucherContext(context.kuaishouIndustryVoucher)
|
||||
if (!voucher.voucherCode && !voucher.eticketId) {
|
||||
return false
|
||||
}
|
||||
|
||||
return voucher.status !== 'DESTROYED'
|
||||
}
|
||||
|
||||
function normalizeIndustryVoucherContext(value: unknown): JsonObject {
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
const status = String(source.status || 'UNUSED')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
|
||||
return {
|
||||
...source,
|
||||
oid: String(source.oid || '').trim(),
|
||||
token: String(source.token || '').trim(),
|
||||
eticketId: String(source.eticketId || source.voucherCode || '').trim(),
|
||||
voucherCode: String(source.voucherCode || source.eticketId || '').trim(),
|
||||
status: status === 'CONSUMED' || status === 'DESTROYED' ? status : 'UNUSED',
|
||||
validStartTime: Number(source.validStartTime || 0) || 0,
|
||||
validEndTime: Number(source.validEndTime || 0) || 0,
|
||||
verifiedAt: source.verifiedAt || null,
|
||||
consumedAt: source.consumedAt || null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import {
|
||||
prepareFulfillmentBinding,
|
||||
refreshFulfillmentRole,
|
||||
} from '../fulfillment/executors/registry.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 (
|
||||
flow.ticket.status === 'verified' ||
|
||||
flow.binding.prepareStatus !== 'ready' ||
|
||||
!flow.binding.bindUrl
|
||||
) {
|
||||
try {
|
||||
const prepared = await prepareFulfillmentBinding(task, {
|
||||
source: 'claim_page_retry_binding_prepare',
|
||||
actor: { source: 'system' },
|
||||
})
|
||||
return prepared?.task || task
|
||||
} catch {
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
try {
|
||||
// 只走 refresh:内部会按间隔 probe 绑链,必要时再查 bind_info
|
||||
// 不再 forceProbe + 前置 probe,避免同轮重复打上游
|
||||
const result = await refreshFulfillmentRole(task, {
|
||||
source: 'claim_page_polling',
|
||||
actor: { source: 'system' },
|
||||
recordEvent: false,
|
||||
forceProbe: false,
|
||||
})
|
||||
return result?.task || task
|
||||
} catch {
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
function parseTaskContext(task: Partial<TaskRow> | null | undefined) {
|
||||
return parseTaskContextValue(task)
|
||||
}
|
||||
Reference in New Issue
Block a user