import { findClaimTokenByToken, updateClaimToken } from '../../repositories/claim-token-repo.js' import { releaseReservedInventoryItem } from '../../repositories/inventory-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 { buildClaimUrl } from './claim-service.js' export const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed']) export const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide' 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 }) { const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order) return { tokenStatus: claimToken.status, claimUrl: buildClaimUrl(claimToken.token), flowType: 'kuaishou_cloud', 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, browserSessionId: task.browser_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: orderItem.sku_name, quantity: orderItem.quantity, }, session: null, kuaishouCloudFulfillment, result: task.redeemed_at ? { resultCode: String(task.result_code || ''), resultMessage: String(task.result_message || ''), screenshotReady: false, screenshotUrl: '', } : null, } } export function mapClaimKuaishouCloudFulfillment(task, order) { 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 = source.binding && typeof source.binding === 'object' ? source.binding : {} const role = source.role && typeof source.role === 'object' ? source.role : {} const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {} const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {} const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {} const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {} const consume = source.consume && typeof source.consume === 'object' ? source.consume : {} return { flowType: 'kuaishou_cloud', shopId: String(order?.shop_id || '').trim(), shopName: String(order?.shop_name || '').trim(), guideImages: [ `${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/1.png`, `${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/2.png`, `${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/3.png`, ], 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', cloudSourceKey: String(binding.cloudSourceKey || '').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: String(role.name || binding.roleName || '').trim(), rid: String(role.rid || binding.roleId || '').trim(), refreshedAt: role.refreshedAt || null, errorMessage: String(role.errorMessage || '').trim(), }, 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, 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) { return parseTaskContextValue(task) } async function expireClaimContext(claimToken, task) { const now = nowIso() const nextClaimToken = await updateClaimToken(claimToken.id, { status: 'expired', updated_at: now, }) let nextTask = task if (!CLAIM_TERMINAL_STATUSES.has(String(task.task_status || '')) && task.task_status !== 'redeemed') { if (task.primary_inventory_item_id) { await releaseReservedInventoryItem(task.primary_inventory_item_id, now) } nextTask = await updateTask(task.id, { task_status: 'expired', inventory_status: 'pending', user_action_status: 'expired', last_error: '领取链接已过期,预占库存项已释放', updated_at: now, }) } return { claimToken: nextClaimToken, task: nextTask, } }