- 后端:claim 详情分流 affiliate_dash 分支 + 详情轮询刷新 bind-result - 输 UID 触发 bind(拿 bindUuid/二维码, task→waiting_binding) - 新增 POST /claim/:token/affiliate-dash/submit(submit→redeeming + 事件) - 前端:ClaimAffiliateDashSteps(二维码/绑定状态/mismatch警告/提交发货) - claim-snapshot/claim-poll 按 flowType 分流(step2 绑定中/step3 已绑定待提交/step4 结果) - 前后端 typecheck + 后端 212 测试通过
649 lines
21 KiB
TypeScript
649 lines
21 KiB
TypeScript
import { findClaimTokenByToken, updateClaimToken } from '../../repositories/claim-token-repo.js'
|
|
import { getOrderItemById } from '../../repositories/order-item-repo.js'
|
|
import { getOrderById } from '../../repositories/order-repo.js'
|
|
import { findTaskByClaimTokenId, updateTask } from '../../repositories/task-repo.js'
|
|
import { createHttpError } from '../../utils/http.js'
|
|
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
|
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
|
|
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 { 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'
|
|
|
|
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
|
type ClaimContext = {
|
|
claimToken: ClaimTokenRow
|
|
task: TaskRow
|
|
order: OrderRow
|
|
orderItem: OrderItemRow
|
|
}
|
|
|
|
export async function getClaimContext(token: unknown) {
|
|
const normalized = String(token || '').trim()
|
|
|
|
if (!normalized) {
|
|
throw createHttpError('缺少领取 token', {
|
|
statusCode: 400,
|
|
errorCode: 'missing_claim_token',
|
|
})
|
|
}
|
|
|
|
const claimToken = await findClaimTokenByToken(normalized)
|
|
|
|
if (!claimToken) {
|
|
throw createHttpError('领取链接无效或不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'claim_token_not_found',
|
|
})
|
|
}
|
|
|
|
const task = await findTaskByClaimTokenId(claimToken.id)
|
|
|
|
if (!task) {
|
|
throw createHttpError('领取任务不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'claim_task_not_found',
|
|
})
|
|
}
|
|
|
|
if (claimToken.status !== 'active') {
|
|
throw createHttpError('领取链接当前不可用', {
|
|
statusCode: 410,
|
|
errorCode: 'claim_token_inactive',
|
|
})
|
|
}
|
|
|
|
if (claimToken.expired_at && new Date(claimToken.expired_at).getTime() <= Date.now()) {
|
|
const expiredContext = await expireClaimContext(claimToken, task)
|
|
|
|
throw createHttpError('领取链接已过期', {
|
|
statusCode: 410,
|
|
errorCode: 'claim_token_expired',
|
|
context: expiredContext,
|
|
})
|
|
}
|
|
|
|
const [order, orderItem] = await Promise.all([
|
|
getOrderById(task.order_id),
|
|
getOrderItemById(task.order_item_id),
|
|
])
|
|
|
|
if (!order || !orderItem) {
|
|
throw createHttpError('领取任务关联订单不完整', {
|
|
statusCode: 500,
|
|
errorCode: 'claim_order_incomplete',
|
|
})
|
|
}
|
|
|
|
return {
|
|
claimToken,
|
|
task,
|
|
order,
|
|
orderItem,
|
|
}
|
|
}
|
|
|
|
export function buildClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
|
if (String(task.executor_key || '').trim() === 'kuaishou_feifei') {
|
|
return buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem })
|
|
}
|
|
|
|
if (String(task.executor_key || '').trim() === '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,
|
|
}
|
|
}
|
|
|
|
function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
|
const context = parseTaskContext(task)
|
|
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
|
const flow = normalizeAffiliateDashFlow(context.affiliateDash)
|
|
const productTitle = String(
|
|
flow.productName || orderItem.sku_name || orderItem.sku_code || '',
|
|
).trim()
|
|
const 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),
|
|
}],
|
|
}
|
|
|
|
return {
|
|
tokenStatus: claimToken.status,
|
|
claimUrl: buildClaimUrl(claimToken.token),
|
|
flowType: 'affiliate_dash',
|
|
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: productTitle,
|
|
quantity: orderItem.quantity,
|
|
},
|
|
product,
|
|
session: null as null,
|
|
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,
|
|
}
|
|
}
|
|
|
|
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
|
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),
|
|
}],
|
|
}
|
|
|
|
return {
|
|
tokenStatus: claimToken.status,
|
|
claimUrl: buildClaimUrl(claimToken.token),
|
|
flowType: 'kuaishou_feifei',
|
|
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: product.title,
|
|
quantity: orderItem.quantity,
|
|
},
|
|
product,
|
|
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,
|
|
},
|
|
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),
|
|
}]
|
|
|
|
return {
|
|
title: displaySkuName,
|
|
skuCode: String(orderItem.sku_code || '').trim(),
|
|
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
|
isBundle: normalizedDeliveryItems.length > 1,
|
|
items: normalizedDeliveryItems,
|
|
}
|
|
}
|
|
|
|
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 })
|
|
}
|
|
|
|
return Array.from(merged.values())
|
|
}
|
|
|
|
function resolveClaimKuaishouCloudSource(task: TaskRow): JsonObject {
|
|
if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
|
return {}
|
|
}
|
|
|
|
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') {
|
|
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(),
|
|
},
|
|
}
|
|
}
|
|
|
|
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, {
|
|
status: TASK_STATUS.EXPIRED,
|
|
updated_at: now,
|
|
})
|
|
|
|
let nextTask = task
|
|
|
|
if (!isTaskFinalStatus(task.task_status)) {
|
|
nextTask = await updateTask(task.id, {
|
|
task_status: TASK_STATUS.EXPIRED,
|
|
user_action_status: TASK_STATUS.EXPIRED,
|
|
last_error: '领取链接已过期',
|
|
updated_at: now,
|
|
}) || task
|
|
}
|
|
|
|
return {
|
|
claimToken: nextClaimToken,
|
|
task: nextTask,
|
|
}
|
|
}
|