import { getOrderItemById } from '../../../repositories/order-item-repo.js' import { getOrderById } from '../../../repositories/order-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js' import { updateTask } from '../../../repositories/task-repo.js' import { createHttpError } from '../../../utils/http.js' import { logIntegration } from '../../../utils/logger.js' import { parseTaskContext } from '../../../utils/task-json.js' import { nowIso } from '../../../utils/time.js' import { TASK_STATUS } from '../../../domain/task-status.js' import { appendUidToUrl, getClaimIdentityFromContext } from '../../claim/claim-identity.js' import type { JsonObject } from '../../../types/json.js' import { createKuaishouFeifeiOrder, queryKuaishouFeifeiOrder, } from '../../platforms/kuaishou-feifei/order-service.js' import { consumeKuaishouIndustryVouchersForTask } from '../../platforms/kuaishou-industry/voucher-service.js' import { buildKuaishouIndustryVoucherContext } from '../../platforms/kuaishou-industry/voucher-binding-service.js' import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js' import type { TaskRow } from '../../../types/repository/rows.js' export function isKuaishouFeifeiTask(task: Partial | null | undefined) { return String(task?.executor_key || '').trim() === 'kuaishou_feifei' } export async function prepareKuaishouFeifeiTask(task: TaskRow) { if (!isKuaishouFeifeiTask(task)) { throw createHttpError('当前任务不是 kuaishou-feifei 履约任务', { statusCode: 409, errorCode: 'kuaishou_feifei_task_invalid', }) } const now = nowIso() const taskContext = parseTaskContext(task) const flow = normalizeKuaishouFeifeiFlow(taskContext.kuaishouFeifei) const existingClaimUrl = resolveKuaishouFeifeiClaimUrl(flow) if (flow.orderNo && existingClaimUrl) { return updateTask(task.id, { task_status: TASK_STATUS.LINK_GENERATED, user_action_status: 'pending_claim', context_json: JSON.stringify({ ...taskContext, kuaishouFeifei: flow, }), updated_at: now, }) } if (!flow.productCode) { throw createHttpError('kuaishou-feifei 商品编码缺失,请检查商品规则配置', { statusCode: 409, errorCode: 'kuaishou_feifei_product_code_missing', }) } const platformOrderNo = flow.platformOrderNo || buildFeifeiPlatformOrderNo(task) const platformAmount = await resolveKuaishouFeifeiTaskPlatformAmount(task) const orderInput: Parameters[0] = { platformOrderNo, productCode: flow.productCode, platformBuyNum: 1, } if (platformAmount > 0) { orderInput.platformAmount = platformAmount } const order = await createKuaishouFeifeiOrder(orderInput) const feifeiClaimUrl = resolveKuaishouFeifeiOrderClaimUrl(order) if (!feifeiClaimUrl) { throw createHttpError('kuaishou-feifei 未返回领取链接', { statusCode: 502, errorCode: 'kuaishou_feifei_missing_claim_url', }) } const nextFlow = mergeKuaishouFeifeiOrder(flow, order, { platformOrderNo, claimUrl: feifeiClaimUrl, syncedAt: now, }) const updatedTask = await updateTask(task.id, { task_status: TASK_STATUS.LINK_GENERATED, user_action_status: 'pending_claim', result_code: 'kuaishou_feifei_order_created', result_message: order.rechargeStatusLabel || 'kuaishou-feifei 订单已创建', context_json: JSON.stringify({ ...taskContext, kuaishouFeifei: nextFlow, }), updated_at: now, }) await createTaskEvent( task.id, 'kuaishou_feifei_order_created', { platformOrderNo, orderNo: order.orderNo, productCode: flow.productCode, platformAmount, rechargeStatus: order.rechargeStatus, rechargeStatusLabel: order.rechargeStatusLabel, rechargeUrl: order.h5.rechargeUrl, }, now, ) return updatedTask } export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) { if (!isKuaishouFeifeiTask(task)) { return task } const taskContext = parseTaskContext(task) const flow = normalizeKuaishouFeifeiFlow(taskContext.kuaishouFeifei) if (!flow.platformOrderNo && !flow.orderNo) { return task } const now = nowIso() const order = await queryKuaishouFeifeiOrder({ platformOrderNo: flow.platformOrderNo, orderNo: flow.orderNo, }) let nextTaskStatus = task.task_status let deliveryStatus = task.delivery_status let redeemedAt = task.redeemed_at let resultCode = task.result_code let resultMessage = task.result_message let lastError = task.last_error const nextFlow = mergeKuaishouFeifeiOrder(flow, order, { syncedAt: now, claimUrl: resolveKuaishouFeifeiOrderClaimUrl(order) || resolveKuaishouFeifeiClaimUrl(flow), }) let nextIndustryVoucher = taskContext.kuaishouIndustryVoucher if (order.rechargeStatus === 30) { const consumeResult = await consumeKuaishouIndustryVouchersForTask(task, { source: 'kuaishou_feifei_completed', consumeTime: Date.now(), }) const consumedVoucher = consumeResult.consumed[0] || null if (consumeResult.ok || consumeResult.vouchers.length === 0) { nextTaskStatus = TASK_STATUS.COMPLETED deliveryStatus = 'delivered' redeemedAt = redeemedAt || now resultCode = 'kuaishou_feifei_completed' resultMessage = order.rechargeStatusLabel || 'kuaishou-feifei 履约成功' lastError = '' nextFlow.consumeStatus = consumeResult.vouchers.length > 0 ? 'success' : 'not_required' if (consumedVoucher) { nextIndustryVoucher = buildKuaishouIndustryVoucherContext( consumedVoucher, taskContext.kuaishouIndustryVoucher, now, ) } logIntegration('[kuaishou-feifei]', '飞飞履约完成,行业电子凭证核销处理完成', { taskId: task.id, platformOrderNo: nextFlow.platformOrderNo, orderNo: nextFlow.orderNo, voucherCount: consumeResult.vouchers.length, consumedCount: consumeResult.consumed.length, consumeStatus: nextFlow.consumeStatus, }) } else { nextTaskStatus = TASK_STATUS.MANUAL_REVIEW resultCode = 'kuaishou_feifei_industry_consume_failed' resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理' lastError = resultMessage nextFlow.consumeStatus = 'failed' logIntegration( '[kuaishou-feifei]', '飞飞履约完成,但行业电子凭证核销失败', { taskId: task.id, platformOrderNo: nextFlow.platformOrderNo, orderNo: nextFlow.orderNo, voucherCount: consumeResult.vouchers.length, failedCount: consumeResult.failed.length, errorMessage: resultMessage, }, { level: 'warn' }, ) } } else if ([40, 50, 60].includes(order.rechargeStatus)) { nextTaskStatus = order.rechargeStatus === 60 ? TASK_STATUS.CLOSED : TASK_STATUS.MANUAL_REVIEW resultCode = `kuaishou_feifei_status_${order.rechargeStatus}` resultMessage = order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常' lastError = resultMessage } const updatedTask = await updateTask(task.id, { task_status: nextTaskStatus, delivery_status: deliveryStatus, redeemed_at: redeemedAt, result_code: resultCode, result_message: resultMessage, last_error: lastError, context_json: JSON.stringify({ ...taskContext, kuaishouFeifei: nextFlow, kuaishouIndustryVoucher: nextIndustryVoucher, }), updated_at: now, }) return updatedTask || task } /** normalizeKuaishouFeifeiFlow 出口类型 */ export type KuaishouFeifeiFlow = { flowType: 'kuaishou_feifei' productCode: string productName: string platformOrderNo: string orderNo: string rechargeStatus: number rechargeStatusLabel: string rechargeResultMessage: string claimUrl: string consumeStatus: string shortLink: ReturnType h5: { entryUrl: string rechargeUrl: string } lastSyncedAt: unknown raw: unknown } export function normalizeKuaishouFeifeiFlow(value: unknown): KuaishouFeifeiFlow { const source = value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} const h5 = source.h5 && typeof source.h5 === 'object' ? (source.h5 as JsonObject) : {} 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(), // 历史字段保留读取,新流程不再生成 shortLink shortLink: normalizeShortLinkPayload(source.shortLink), h5: { entryUrl: String(h5.entryUrl || '').trim(), rechargeUrl: String(h5.rechargeUrl || '').trim(), }, lastSyncedAt: source.lastSyncedAt || null, raw: source.raw && typeof source.raw === 'object' ? source.raw : null, } } /** 平台原始 H5 链接(不拼 uid、不用短链)。 */ export function resolveKuaishouFeifeiClaimUrl(value: unknown) { const flow = normalizeKuaishouFeifeiFlow(value) return resolveKuaishouFeifeiDirectClaimUrl(flow) } /** 在已有 expectedUid 时返回拼好 uid 的 H5 链接。 */ export function resolveKuaishouFeifeiH5UrlWithUid(value: unknown, expectedUid?: unknown) { const directUrl = resolveKuaishouFeifeiClaimUrl(value) if (!directUrl) { return '' } const uid = String(expectedUid || '').trim() if (!uid) { return directUrl } return appendUidToUrl(directUrl, uid) } export function resolveKuaishouFeifeiH5UrlWithTaskUid( task: Partial | null | undefined, flowValue?: unknown, ) { const context = parseTaskContext(task) const identity = getClaimIdentityFromContext(context) const flow = flowValue === undefined ? context.kuaishouFeifei : flowValue return resolveKuaishouFeifeiH5UrlWithUid(flow, identity.expectedUid) } function mergeKuaishouFeifeiOrder( flow: KuaishouFeifeiFlow, order: Awaited>, patch: { platformOrderNo?: string claimUrl?: string syncedAt: string }, ) { return { ...flow, productCode: order.productCode || flow.productCode, productName: order.productName || flow.productName, platformOrderNo: order.platformOrderNo || patch.platformOrderNo || flow.platformOrderNo, orderNo: order.orderNo || flow.orderNo, rechargeStatus: order.rechargeStatus, rechargeStatusLabel: order.rechargeStatusLabel, rechargeResultMessage: order.rechargeResultMessage, claimUrl: patch.claimUrl || flow.claimUrl, shortLink: flow.shortLink, h5: { entryUrl: order.h5.entryUrl || flow.h5.entryUrl, rechargeUrl: order.h5.rechargeUrl || flow.h5.rechargeUrl, }, lastSyncedAt: patch.syncedAt, raw: order.raw, } } function resolveKuaishouFeifeiDirectClaimUrl(flow: KuaishouFeifeiFlow) { const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl if (h5ClaimUrl) { return h5ClaimUrl } // 兼容历史 shortLink 中的原始 target if (flow.shortLink?.targetUrl) { return flow.shortLink.targetUrl } return isLocalClaimUrl(flow.claimUrl) ? '' : flow.claimUrl } function resolveKuaishouFeifeiOrderClaimUrl( order: Awaited>, ) { return String(order.h5.rechargeUrl || order.h5.entryUrl || '').trim() } function isLocalClaimUrl(value: unknown) { const url = String(value || '').trim() return !/^https?:\/\//i.test(url) || url.includes('/#/claim/') } export function buildFeifeiPlatformOrderNo(task: TaskRow) { const platformOrderId = String(task.platform_order_id || '').trim() if (platformOrderId && platformOrderId.length <= 64) { return platformOrderId } const taskNo = String(task.task_no || '').trim() if (taskNo && taskNo.length <= 64) { return taskNo } return `OS-FEIFEI-${task.id}` } export async function resolveKuaishouFeifeiTaskPlatformAmount(task: TaskRow) { const [order, orderItem] = await Promise.all([ getOrderById(task.order_id), getOrderItemById(task.order_item_id), ]) return resolveKuaishouFeifeiPlatformAmount({ totalAmountFen: order?.total_amount, quantity: orderItem?.quantity, }) } export function resolveKuaishouFeifeiPlatformAmount({ totalAmountFen, quantity, }: { totalAmountFen?: unknown quantity?: unknown } = {}) { const totalFen = normalizePositiveFen(totalAmountFen) if (totalFen <= 0) { return 0 } const normalizedQuantity = Math.max(1, Math.trunc(Number(quantity || 1)) || 1) return Number((totalFen / normalizedQuantity / 100).toFixed(2)) } function normalizePositiveFen(value: unknown) { const parsed = Number(value) return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0 }