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 { nowIso } from "../../../utils/time.js"; import { notifyKuaishouCloudConsumeFailed } from "../../notification/domain-notifications.js"; import { useCloudtentaclesSku } from "../../platforms/cloudtentacles/catalog-service.js"; import { backCloudtentaclesVirtualNumber } from "../../platforms/cloudtentacles/virtual-number-service.js"; import { consumeKuaishouEticket } from "../../platforms/kuaishou-eticket/consume-service.js"; import { getKuaishouEticketSourceConfig, resolveKuaishouEticketShopConfig, } from "../../platforms/kuaishou-eticket/source-config-service.js"; import { isKuaishouCloudTask, maskCode, maskPhone, normalizeKuaishouCloudFlow, type JsonObject, } from "./domain.js"; import { resolvePersistedCloudtentaclesContextWithFallback } from "./cloudtentacles-context.js"; import { normalizeActor, parseTaskContext } from "./task-context.js"; export async function dispatchKuaishouCloudFulfillmentTask( task, options: JsonObject = {} ) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const actor = normalizeActor(options.actor); const now = nowIso(); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); const effectiveSourceKey = flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default"; const cloudContext = resolvePersistedCloudtentaclesContextWithFallback( effectiveSourceKey, flow.binding.cloudSourceKeyFallbacks || [] ); if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) { throw createHttpError( "当前任务还没有准备好绑定资源,请先完成绑定资源准备", { statusCode: 409, errorCode: "kuaishou_cloud_not_prepared", } ); } const ticketCode = String(options.ticketCode || "").trim(); const persistedTicketCode = String(flow.ticket.code || "").trim(); if (!persistedTicketCode && !ticketCode) { throw createHttpError("客户还没有提交有效核销码,暂时不能继续兑换", { statusCode: 409, errorCode: "kuaishou_cloud_missing_ticket_code", }); } if ( flow.dispatch.status === "success" && String(task.task_status || "").trim() === "dispatched_pending_return" ) { return { task, flow }; } const dispatchResult = await useCloudtentaclesSku({ ...cloudContext, id: flow.binding.skuId, virtualNumberId: flow.binding.vnId, phone: flow.binding.vnPhone, }); const nextContext = { ...taskContext, kuaishouCloudFulfillment: { ...flow, ticket: { ...flow.ticket, code: ticketCode || persistedTicketCode, capturedAt: ticketCode ? now : flow.ticket.capturedAt, capturedBy: ticketCode && actor ? actor : flow.ticket.capturedBy, }, dispatch: { ...flow.dispatch, status: "success", dispatchAt: now, dispatchBy: actor, sendType: Number(dispatchResult.sendType || 0) || 0, note: String( dispatchResult.note || dispatchResult.responseMessage || "cloudtentacles 发货成功" ).trim(), }, }, }; let updatedTask = await updateTask(task.id, { task_status: "dispatched_pending_return", delivery_status: "delivered", result_code: "kuaishou_cloud_dispatched", result_message: String( dispatchResult.responseMessage || dispatchResult.note || "cloudtentacles 发货成功" ).trim(), user_action_status: "not_required", last_error: "", context_json: JSON.stringify(nextContext), updated_at: now, }); await createTaskEvent( task.id, "kuaishou_cloud_dispatched", { source: String(options.source || "system").trim() || "system", ticketCodeMasked: maskCode(ticketCode || persistedTicketCode), skuId: flow.binding.skuId, vnId: flow.binding.vnId, vnPhoneMasked: maskPhone(flow.binding.vnPhone), sendType: dispatchResult.sendType, note: dispatchResult.note, actor, }, now ); const shouldAutoFinalize = options.autoFinalize === true && normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment) .returnNumber.autoReturnEnabled === true; if (shouldAutoFinalize) { const finalizeResult = await returnKuaishouCloudFulfillmentTask( updatedTask, { actor, source: options.source || "system_auto_finalize", } ); updatedTask = finalizeResult.task; } return { task: updatedTask, flow: normalizeKuaishouCloudFlow( parseTaskContext(updatedTask).kuaishouCloudFulfillment ), }; } export async function returnKuaishouCloudFulfillmentTask( task, options: JsonObject = {} ) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const actor = normalizeActor(options.actor); const now = nowIso(); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); const effectiveSourceKey = flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default"; const cloudContext = resolvePersistedCloudtentaclesContextWithFallback( effectiveSourceKey, flow.binding.cloudSourceKeyFallbacks || [] ); if (!flow.binding.vnId || !flow.binding.vnKey) { throw createHttpError("当前任务缺少可退还的虚拟号信息", { statusCode: 409, errorCode: "kuaishou_cloud_missing_return_context", }); } if ( flow.returnNumber.status === "success" && ["completed", "manual_review"].includes( String(task.task_status || "").trim() ) ) { return { task, flow }; } await backCloudtentaclesVirtualNumber({ ...cloudContext, key: flow.binding.vnKey, id: flow.binding.vnId, }); const order = await getOrderById(task.order_id); const ticketCode = String(flow.ticket.code || "").trim(); const shopId = String(flow.consume.shopId || order?.shop_id || "").trim(); const shopName = String( flow.consume.shopName || order?.shop_name || "" ).trim(); const eticketSource = getKuaishouEticketSourceConfig(); const shopConfig = resolveKuaishouEticketShopConfig({ shopId, shopName, }); let consumeStatus = "pending"; let consumeErrorMessage = ""; let consumedAt = null; let nextTaskStatus = "completed"; let nextResultCode = "kuaishou_cloud_completed"; let nextResultMessage = "cloudtentacles 发货、退号并完成快手核销"; if (!order) { consumeStatus = "failed"; consumeErrorMessage = "任务关联订单不存在,无法执行快手核销"; } else if (!ticketCode) { consumeStatus = "failed"; consumeErrorMessage = "客户未提交有效核销码,无法执行快手核销"; } else if ( !shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || "").trim() ) { consumeStatus = "failed"; consumeErrorMessage = "订单对应快手小店缺少可用 Cookie,无法执行快手核销"; } else { try { const consumeResult = await consumeKuaishouEticket({ baseUrl: eticketSource.baseUrl, cookie: shopConfig.cookie, eTicketId: ticketCode, oid: String(flow.ticket.oid || "").trim(), formToken: String(flow.ticket.formToken || "").trim(), }); if (consumeResult.consumed) { consumeStatus = "success"; consumedAt = now; } else { consumeStatus = "failed"; consumeErrorMessage = String( consumeResult.errorMessage || "快手核销失败" ).trim(); } } catch (error) { consumeStatus = "failed"; consumeErrorMessage = error instanceof Error ? error.message : "快手核销失败"; } } if (consumeStatus !== "success") { nextTaskStatus = "manual_review"; nextResultCode = "kuaishou_cloud_consume_failed"; nextResultMessage = consumeErrorMessage || "号码已退还,但快手核销未完成,请人工处理"; } const nextContext = { ...taskContext, kuaishouCloudFulfillment: { ...flow, returnNumber: { ...flow.returnNumber, status: "success", returnedAt: now, returnedBy: actor, }, consume: { ...flow.consume, status: consumeStatus, shopId: shopId || flow.consume.shopId, shopName, autoConsumeEnabled: flow.consume.autoConsumeEnabled === true, consumedAt, errorMessage: consumeErrorMessage, }, }, }; const updatedTask = await updateTask(task.id, { task_status: nextTaskStatus, delivery_status: "delivered", result_code: nextResultCode, result_message: nextResultMessage, redeemed_at: consumeStatus === "success" ? now : task.redeemed_at, last_error: consumeErrorMessage, context_json: JSON.stringify(nextContext), updated_at: now, }); await createTaskEvent( task.id, "kuaishou_cloud_number_returned", { source: String(options.source || "system").trim() || "system", vnId: flow.binding.vnId, vnPhoneMasked: maskPhone(flow.binding.vnPhone), actor, }, now ); await createTaskEvent( task.id, consumeStatus === "success" ? "kuaishou_cloud_consumed" : "kuaishou_cloud_consume_failed", { source: String(options.source || "system").trim() || "system", ticketCodeMasked: maskCode(ticketCode), shopId, shopName, consumeStatus, errorMessage: consumeErrorMessage, actor, }, now ); if (consumeStatus !== "success") { await notifyKuaishouCloudConsumeFailed({ task: updatedTask, order, ticketCodeMasked: maskCode(ticketCode), shopId, shopName, errorMessage: consumeErrorMessage, }); } return { task: updatedTask, flow: normalizeKuaishouCloudFlow( parseTaskContext(updatedTask).kuaishouCloudFulfillment ), }; }