refactor backend route and fulfillment modules
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
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
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user