import fs from 'node:fs' import path from 'node:path' import { PROJECT_ROOT } from '../../config/runtime.js' import { createTaskEvent } from '../../repositories/task-event-repo.js' import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../repositories/task-repo.js' import { createHttpError } from '../../utils/http.js' import { nowIso } from '../../utils/time.js' import { getKuaishouEticketSourceConfig, resolveKuaishouEticketShopConfig, } from '../platforms/kuaishou-eticket/source-config-service.js' import { queryKuaishouEticketConsumeDetail } from '../platforms/kuaishou-eticket/consume-service.js' import { dispatchKuaishouCloudFulfillmentTask, maskCode, normalizeKuaishouCloudFlow, prepareKuaishouCloudFulfillmentTask, refreshKuaishouCloudTaskRoleInfo, } from '../fulfillment/kuaishou-cloud/index.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' const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs') const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png']) type JsonObject = Record export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: JsonObject = {}) { const context = await getClaimContext(token) const now = nowIso() if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') { throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', { statusCode: 409, errorCode: 'claim_not_kuaishou_cloud', }) } const flow = normalizeKuaishouCloudFlow(parseTaskContext(context.task).kuaishouCloudFulfillment) const ticketCode = String(payload.ticketCode || payload.eTicketId || '').trim() if (!ticketCode) { throw createHttpError('请先粘贴快手小店核销码', { statusCode: 400, errorCode: 'claim_kuaishou_cloud_missing_ticket_code', }) } const shopId = String(flow.consume.shopId || context.order.shop_id || '').trim() const shopConfig = resolveKuaishouEticketShopConfig({ shopId, shopName: String(context.order.shop_name || '').trim(), }) if (!shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || '').trim()) { throw createHttpError('这笔订单对应的快手小店还没有配置可用 Cookie,请联系客服处理', { statusCode: 409, errorCode: 'claim_kuaishou_cloud_shop_cookie_missing', }) } const eticketSource = getKuaishouEticketSourceConfig() const detailResult = await queryKuaishouEticketConsumeDetail({ baseUrl: eticketSource.baseUrl, cookie: shopConfig.cookie, eTicketId: ticketCode, }) if (!detailResult.ok || detailResult.alreadyConsumed || !detailResult.detail) { throw createHttpError(detailResult.errorMessage || '核销码校验失败,请确认是否复制完整', { statusCode: 409, errorCode: detailResult.alreadyConsumed ? 'claim_kuaishou_cloud_ticket_already_consumed' : 'claim_kuaishou_cloud_ticket_invalid', }) } const nextContext = { ...parseTaskContext(context.task), kuaishouCloudFulfillment: { ...flow, ticket: { ...flow.ticket, code: detailResult.eTicketId || ticketCode, status: 'verified', capturedAt: flow.ticket.capturedAt || now, capturedBy: flow.ticket.capturedBy || { source: 'claim_page', }, verifiedAt: now, oid: String(detailResult.detail.oid || '').trim(), formToken: String(detailResult.detail.formToken || '').trim(), leftCount: Number(detailResult.detail.leftCount || 0) || 0, goodsTitle: String(detailResult.goods?.itemTitle || '').trim(), }, consume: { ...flow.consume, shopId, }, }, } const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment) if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) { await prepareKuaishouCloudFulfillmentTask({ ...context.task, context_json: JSON.stringify(nextContext), }, { source: 'claim_page_ticket_verified', actor: { source: 'claim_page' }, }) } else { await updateTask(context.task.id, { claim_token: context.claimToken.token, claim_expires_at: context.claimToken.expired_at, context_json: JSON.stringify(nextContext), last_error: '', updated_at: now, }) } if (!context.task.claimed_at) { await updateTask(context.task.id, { claimed_at: now, updated_at: now, }) } await createTaskEvent(context.task.id, 'kuaishou_cloud_ticket_verified', { ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode), shopId, shopName: String(context.order.shop_name || '').trim(), goodsTitle: String(detailResult.goods?.itemTitle || '').trim(), }, now) return getKuaishouCloudClaimDetail(token) } export async function getKuaishouCloudClaimGuideAssetPath(filename: unknown) { const normalized = String(filename || '').trim() if (!ALLOWED_GUIDE_FILES.has(normalized)) { throw createHttpError('指引图片不存在', { statusCode: 404, errorCode: 'claim_kuaishou_cloud_asset_not_found', }) } const filePath = path.resolve(KUAISHOU_CLOUD_GUIDE_DIR, normalized) if (!filePath.startsWith(KUAISHOU_CLOUD_GUIDE_DIR) || !fs.existsSync(filePath)) { throw createHttpError('指引图片不存在', { statusCode: 404, errorCode: 'claim_kuaishou_cloud_asset_not_found', }) } return filePath } export async function getKuaishouCloudClaimDetail(token: unknown) { const context = await getClaimContext(token) let task = context.task if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') { task = await syncKuaishouCloudRoleInfo(task) || task } return buildClaimDetailPayload({ claimToken: context.claimToken, task, order: context.order, orderItem: context.orderItem, }) } function parseTaskContext(task: Partial | null | undefined): JsonObject { const rawValue = task?.context_json if (!rawValue) { return {} } if (typeof rawValue === 'object') { return rawValue } try { return JSON.parse(String(rawValue || '{}')) } catch { return {} } } export async function confirmKuaishouCloudClaimRole(token: unknown) { const context = await getClaimContext(token) const now = nowIso() if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') { throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', { statusCode: 409, errorCode: 'claim_not_kuaishou_cloud', }) } if (['role_confirmed', 'redeeming', 'dispatched_pending_return', 'completed', 'manual_review'].includes(String(context.task.task_status || '').trim())) { return getKuaishouCloudClaimDetail(token) } const refreshed = await refreshKuaishouCloudTaskRoleInfo(context.task, { source: 'claim_page_role_confirm', actor: { source: 'claim_page' }, recordEvent: false, forceProbe: true, }) const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment) if (flow.ticket.status !== 'verified') { throw createHttpError('请先验证核销码', { statusCode: 409, errorCode: 'claim_kuaishou_cloud_ticket_not_verified', }) } if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) { throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', { statusCode: 409, errorCode: 'claim_kuaishou_cloud_role_not_ready', }) } await updateTask(context.task.id, { task_status: 'role_confirmed', role_id: flow.binding.roleId, role_name: flow.binding.roleName, user_action_status: 'role_confirmed', role_confirmed_at: now, last_error: '', updated_at: now, }) await createTaskEvent(context.task.id, 'kuaishou_cloud_role_confirmed', { vnPhone: flow.binding.vnPhone, roleName: flow.binding.roleName, roleId: flow.binding.roleId, }, now) return getKuaishouCloudClaimDetail(token) } export async function redeemKuaishouCloudClaim(token: unknown) { const context = await getClaimContext(token) const now = nowIso() if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') { throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', { statusCode: 409, errorCode: 'claim_not_kuaishou_cloud', }) } const currentStatus = String(context.task.task_status || '').trim() if (['redeeming', 'dispatched_pending_return', 'completed', 'manual_review'].includes(currentStatus)) { return getKuaishouCloudClaimDetail(token) } if (currentStatus !== 'role_confirmed') { throw createHttpError('请先确认角色信息', { statusCode: 409, errorCode: 'claim_kuaishou_cloud_role_not_confirmed', }) } const lockedTask = await updateTaskStatusIfCurrent(context.task.id, 'role_confirmed', { task_status: 'redeeming', user_action_status: 'not_required', last_error: '', updated_at: now, }) if (!lockedTask) { return getKuaishouCloudClaimDetail(token) } try { await dispatchKuaishouCloudFulfillmentTask(lockedTask, { source: 'claim_page_redeem', actor: { source: 'claim_page' }, autoFinalize: true, }) } catch (error) { const latestTask = await getTaskById(lockedTask.id) if (latestTask && String(latestTask.task_status || '').trim() !== 'redeeming') { return getKuaishouCloudClaimDetail(token) } const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理' await updateTask(lockedTask.id, { task_status: 'manual_review', user_action_status: 'not_required', last_error: message, result_code: 'kuaishou_cloud_redeem_failed', result_message: message, updated_at: nowIso(), }) await createTaskEvent(lockedTask.id, 'kuaishou_cloud_redeem_failed', { source: 'claim_page_redeem', errorMessage: message, }, nowIso()) throw error } return getKuaishouCloudClaimDetail(token) }