refactor backend route and fulfillment modules

This commit is contained in:
yml
2026-05-21 18:38:04 +08:00
parent 1a9c832c7a
commit efa41ba976
22 changed files with 2441 additions and 2246 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,238 @@
import { createHttpError } from "../../../utils/http.js";
import { normalizeProductName } from "../../order/product-match-service.js";
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
verifyCloudtentaclesLoginCode,
} from "../../platforms/cloudtentacles/virtual-number-service.js";
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js";
export function resolveKuaishouCloudBindingResources(
flow,
{ skuItems = [], knapsackItems = [] } = {}
) {
const normalizedSkuItems = Array.isArray(skuItems)
? skuItems.filter(isCloudSkuLikeItem)
: [];
const normalizedKnapsackItems = Array.isArray(knapsackItems)
? knapsackItems.filter(isCloudSkuLikeItem)
: [];
const currentSkuId = Number(flow?.binding?.skuId || 0) || 0;
const currentSkuName = String(flow?.binding?.skuName || "").trim();
const nameCandidates = collectKuaishouCloudNameCandidates(flow);
const skuItemById =
currentSkuId > 0
? normalizedSkuItems.find(
(item) => Number(item.id || 0) === currentSkuId
) || null
: null;
const knapsackItemById =
currentSkuId > 0
? normalizedKnapsackItems.find(
(item) => Number(item.id || 0) === currentSkuId
) || null
: null;
if (skuItemById || knapsackItemById) {
const matchedItem = skuItemById || knapsackItemById;
return {
skuId: Number(matchedItem?.id || 0) || 0,
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
skuItem: skuItemById,
knapsackItem: knapsackItemById,
resolvedByName: false,
};
}
const matchedSkuItem = findCloudItemByNames(
normalizedSkuItems,
nameCandidates
);
const matchedKnapsackItem = findCloudItemByNames(
normalizedKnapsackItems,
nameCandidates,
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0
);
const matchedItem = matchedSkuItem || matchedKnapsackItem;
return {
skuId: Number(matchedItem?.id || 0) || 0,
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
skuItem: matchedSkuItem,
knapsackItem: matchedKnapsackItem,
resolvedByName: Boolean(matchedItem),
};
}
export function resolveKuaishouCloudVnKeyCandidates(_input = {}) {
return [KUAISHOU_CLOUD_FIXED_VN_KEY];
}
export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonObject = {}) {
const { cloudContext = {}, vnKeyCandidates = [] } = input;
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [];
let lastError = null;
for (const vnKey of candidates) {
let vnId = 0;
let vnPhone = "";
try {
const appointed = await appointCloudtentaclesVirtualNumber({
...cloudContext,
key: vnKey,
});
vnId = Number(appointed.item?.id || 0);
vnPhone = String(appointed.item?.phone || "").trim();
if (!vnId || !vnPhone) {
throw createHttpError("申请虚拟号成功但返回数据不完整", {
statusCode: 502,
errorCode: "kuaishou_cloud_invalid_vn",
});
}
await generateCloudtentaclesLoginCode({
...cloudContext,
key: vnKey,
id: vnId,
});
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
...cloudContext,
key: vnKey,
phone: vnPhone,
});
await verifyCloudtentaclesLoginCode({
...cloudContext,
key: vnKey,
id: vnId,
code: fetchedCode.code,
});
const bindUrlResult = await getCloudtentaclesBindUrl({
...cloudContext,
key: vnKey,
id: vnId,
});
return {
vnKey,
vnId,
vnPhone,
bindUrl: String(bindUrlResult.bindUrl || "").trim(),
};
} catch (error) {
lastError = error;
if (vnId > 0) {
try {
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: vnKey,
id: vnId,
});
} catch {
// 退号失败保留主错误
}
}
if (!isRecoverableKuaishouCloudVnKeyError(error)) {
throw error;
}
}
}
throw (
lastError ||
createHttpError("没有找到可用的 VN Key", {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_binding_config",
})
);
}
function collectKuaishouCloudNameCandidates(flow) {
return Array.from(
new Set(
[
String(flow?.binding?.skuName || "").trim(),
String(flow?.internalSkuName || "").trim(),
String(flow?.internalSkuCode || "").trim(),
].filter(Boolean)
)
);
}
function findCloudItemByNames(items, nameCandidates, preferredId = 0) {
const normalizedItems = Array.isArray(items) ? items : [];
const normalizedNames = nameCandidates
.map((item) => ({
raw: String(item || "").trim(),
normalized: normalizeProductName(item),
}))
.filter((item) => item.raw && item.normalized);
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
return preferredId > 0
? normalizedItems.find((item) => Number(item.id || 0) === preferredId) ||
null
: null;
}
if (preferredId > 0) {
const preferred =
normalizedItems.find((item) => Number(item.id || 0) === preferredId) ||
null;
if (preferred) {
return preferred;
}
}
const exactMatches = normalizedItems.filter((item) => {
const itemName = normalizeProductName(item.name);
return normalizedNames.some(
(candidate) => candidate.normalized === itemName
);
});
if (exactMatches.length > 0) {
return exactMatches[0];
}
const partialMatches = normalizedItems.filter((item) => {
const itemName = normalizeProductName(item.name);
return normalizedNames.some(
(candidate) =>
itemName.includes(candidate.normalized) ||
candidate.normalized.includes(itemName)
);
});
if (partialMatches.length > 0) {
return partialMatches.sort(
(left, right) =>
String(left.name || "").length - String(right.name || "").length
)[0];
}
return null;
}
function isCloudSkuLikeItem(item) {
return Boolean(item) && typeof item === "object" && Number(item.id || 0) > 0;
}
function isRecoverableKuaishouCloudVnKeyError(error) {
const errorCode = String(error?.errorCode || error?.code || "").trim();
const errorMessage = String(error?.message || "").trim();
return (
errorCode === "cloudtentacles_vn_bind_url_failed" &&
errorMessage.includes("不支持的游戏类型")
);
}
@@ -0,0 +1,93 @@
import { createHttpError } from "../../../utils/http.js";
import {
getCloudtentaclesSourceConfig,
getCloudtentaclesSourceByKey,
} from "../../platforms/cloudtentacles/source-config-service.js";
import {
getCloudtentaclesSessionState,
getCloudtentaclesSessionStateByKey,
} from "../../platforms/cloudtentacles/session-state-service.js";
import { normalizeStringArray } from "./domain.js";
export function resolvePersistedCloudtentaclesContext(sourceKey = "default") {
const source =
getCloudtentaclesSourceByKey(sourceKey) || getCloudtentaclesSourceConfig();
const session =
getCloudtentaclesSessionStateByKey(sourceKey) ||
getCloudtentaclesSessionState();
const token = String(session.token || "").trim();
if (!token) {
throw createHttpError(
"当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验",
{
statusCode: 409,
errorCode: "kuaishou_cloud_missing_cloud_token",
}
);
}
return {
baseUrl:
String(session.baseUrl || source.baseUrl || "").trim() ||
"https://123.207.217.176",
token,
deviceId: String(session.deviceId || source.deviceId || "-").trim() || "-",
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
};
}
/**
* 按优先级依次尝试 cloudSourceKey 和 fallbacks 列表中的账号,
* 找到第一个有可用 token 的账号返回其 context。
* 将实际使用的 resolvedSourceKey 也返回,确保后续操作(退号、发货等)
* 使用同一个 sourceKey,防止跨账号操作导致数据不一致。
*/
export function resolvePersistedCloudtentaclesContextWithFallback(
primarySourceKey = "default",
fallbacks = []
) {
const candidates = [
String(primarySourceKey || "default").trim() || "default",
...normalizeStringArray(fallbacks),
];
const uniqueCandidates = [...new Set(candidates)];
let lastError = null;
for (const sourceKey of uniqueCandidates) {
const source = getCloudtentaclesSourceByKey(sourceKey);
const session = getCloudtentaclesSessionStateByKey(sourceKey);
if (!source) continue;
const token = String(session?.token || "").trim();
if (!token) {
lastError = createHttpError(
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
);
continue;
}
return {
baseUrl:
String(session.baseUrl || source.baseUrl || "").trim() ||
"https://123.207.217.176",
token,
deviceId:
String(session.deviceId || source.deviceId || "-").trim() || "-",
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
resolvedSourceKey: sourceKey,
};
}
throw (
lastError ||
createHttpError(
"所有 cloudtentacles 备选账号均不可用,请先到平台配置完成登录校验",
{ statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" }
)
);
}
@@ -0,0 +1,223 @@
import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/shared.js";
export const KUAISHOU_CLOUD_FIXED_VN_KEY = "1";
export type JsonObject = Record<string, any>;
export function isKuaishouCloudTask(task) {
return String(task?.executor_key || "").trim() === "kuaishou_ct_assisted";
}
export function normalizeKuaishouCloudFlow(value) {
const source = value && typeof value === "object" ? value : {};
const binding =
source.binding && typeof source.binding === "object" ? source.binding : {};
const role =
source.role && typeof source.role === "object" ? source.role : {};
const purchase =
source.purchase && typeof source.purchase === "object"
? source.purchase
: {};
const dispatch =
source.dispatch && typeof source.dispatch === "object"
? source.dispatch
: {};
const returnNumber =
source.returnNumber && typeof source.returnNumber === "object"
? source.returnNumber
: {};
const consume =
source.consume && typeof source.consume === "object" ? source.consume : {};
const ticket =
source.ticket && typeof source.ticket === "object" ? source.ticket : {};
const roleName = String(role.name || binding.roleName || "").trim();
const roleId = String(role.rid || binding.roleId || "").trim();
const bindPreparedAt = binding.bindPreparedAt || null;
const bindExpiresAt =
binding.bindExpiresAt ||
resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt);
return {
...source,
configId: String(source.configId || "").trim(),
internalSkuCode: String(source.internalSkuCode || "").trim(),
internalSkuName: String(source.internalSkuName || "").trim(),
ticket: {
code: String(ticket.code || "").trim(),
status: String(ticket.status || "pending").trim() || "pending",
capturedAt: ticket.capturedAt || null,
capturedBy: ticket.capturedBy || null,
verifiedAt: ticket.verifiedAt || null,
oid: String(ticket.oid || "").trim(),
formToken: String(ticket.formToken || "").trim(),
leftCount: Number(ticket.leftCount || 0) || 0,
goodsTitle: String(ticket.goodsTitle || "").trim(),
},
binding: {
prepareStatus:
String(binding.prepareStatus || "pending").trim() || "pending",
cloudSourceKey:
String(binding.cloudSourceKey || "default").trim() || "default",
cloudSourceKeyFallbacks: normalizeStringArray(
binding.cloudSourceKeyFallbacks
),
resolvedSourceKey: String(binding.resolvedSourceKey || "").trim(),
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || "").trim(),
vnKey:
String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() ||
KUAISHOU_CLOUD_FIXED_VN_KEY,
vnId: Number(binding.vnId || 0) || 0,
vnPhone: String(binding.vnPhone || "").trim(),
bindUrl: String(binding.bindUrl || "").trim(),
bindPreparedAt,
bindExpiresAt,
bindProbeAt: binding.bindProbeAt || null,
bindProbeStatus: String(binding.bindProbeStatus || "").trim(),
bindProbeMessage: String(binding.bindProbeMessage || "").trim(),
roleName,
roleId,
},
role: {
status:
String(
role.status || (roleName || roleId ? "ready" : "pending")
).trim() || "pending",
name: roleName,
rid: roleId,
refreshedAt: role.refreshedAt || null,
errorMessage: String(role.errorMessage || "").trim(),
rawInfo:
role.rawInfo && typeof role.rawInfo === "object" ? role.rawInfo : null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
usedKnapsack: purchase.usedKnapsack === true,
purchaseTriggered: purchase.purchaseTriggered === true,
assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null,
},
dispatch: {
status: String(dispatch.status || "pending").trim() || "pending",
dispatchAt: dispatch.dispatchAt || null,
dispatchBy: dispatch.dispatchBy || null,
sendType: Number(dispatch.sendType || 0) || 0,
note: String(dispatch.note || "").trim(),
},
returnNumber: {
status: String(returnNumber.status || "pending").trim() || "pending",
returnedAt: returnNumber.returnedAt || null,
returnedBy: returnNumber.returnedBy || null,
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
},
consume: {
status: String(consume.status || "pending").trim() || "pending",
shopId: String(consume.shopId || "").trim(),
shopName: String(consume.shopName || "").trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
consumedAt: consume.consumedAt || null,
errorMessage: String(consume.errorMessage || "").trim(),
},
notes: String(source.notes || "").trim(),
};
}
export function normalizeKuaishouCloudRoleInfo(value) {
const rawInfo = value && typeof value === "object" ? value : null;
const nestedBindInfo =
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === "object"
? rawInfo.sBindInfo
: null;
const source = nestedBindInfo || rawInfo;
return {
name: String(
source?.name ||
source?.roleName ||
source?.nickname ||
source?.sRoleName ||
""
).trim(),
rid: String(
source?.rid ||
source?.roleId ||
source?.uid ||
source?.sRoleId ||
source?.sUserId ||
""
).trim(),
rawInfo,
};
}
export function maskPhone(value) {
const text = String(value || "").trim();
if (!text) {
return "";
}
if (text.length <= 7) {
return `${text.slice(0, 2)}***${text.slice(-2)}`;
}
return `${text.slice(0, 3)}****${text.slice(-4)}`;
}
export function maskCode(value) {
const text = String(value || "").trim();
if (!text) {
return "";
}
if (text.length <= 8) {
return `${text.slice(0, 2)}***${text.slice(-2)}`;
}
return `${text.slice(0, 4)}****${text.slice(-4)}`;
}
export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) {
const preparedTime = Date.parse(String(preparedAt || ""));
if (!Number.isFinite(preparedTime)) {
return null;
}
const config = resolveCloudtentaclesConfig();
const ttlSeconds = Number(config.bindUrlTtlSeconds || 600);
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString();
}
export function isKuaishouCloudBindUrlFresh(flow, now = new Date()) {
const normalizedFlow = normalizeKuaishouCloudFlow(flow);
if (!normalizedFlow.binding.bindUrl) {
return false;
}
const expiresAt = normalizedFlow.binding.bindExpiresAt;
if (!expiresAt) {
return false;
}
const expiresTime = Date.parse(String(expiresAt || ""));
if (!Number.isFinite(expiresTime)) {
return false;
}
return expiresTime > now.getTime();
}
export function normalizeStringArray(value) {
if (Array.isArray(value)) {
return value.map((v) => String(v || "").trim()).filter(Boolean);
}
if (typeof value === "string") {
return value
.split(",")
.map((v) => v.trim())
.filter(Boolean);
}
return [];
}
@@ -0,0 +1,52 @@
export function normalizeActor(actor) {
if (!actor || typeof actor !== "object") {
return null;
}
const source = String(actor.source || "").trim();
const userId = Number(actor.userId || 0) || 0;
const username = String(actor.username || "").trim();
const role = String(actor.role || "").trim();
if (!source && !userId && !username && !role) {
return null;
}
return {
source,
userId,
username,
role,
};
}
export function parseTaskContext(task) {
const value = task?.context_json;
if (!value) {
return {};
}
if (typeof value === "object") {
return value;
}
try {
return JSON.parse(String(value || "{}"));
} catch {
return {};
}
}
export function getTaskClaimExpiresAt(task) {
return task?.claim_expires_at || task?.primary_claim_expires_at || null;
}
export function isClaimExpired(expiredAt) {
if (!expiredAt) {
return false;
}
const timestamp = new Date(expiredAt).getTime();
return Number.isFinite(timestamp) && timestamp <= Date.now();
}
@@ -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
),
};
}