Files
order_site/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts
T

348 lines
10 KiB
TypeScript

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
),
};
}