Files
order_site/apps/backend/src/services/fulfillment/kuaishou-cloud/index.ts
T
2026-05-26 22:56:41 +08:00

884 lines
25 KiB
TypeScript

import { createTaskEvent } from "../../../repositories/task-event-repo.js";
import { updateTask } from "../../../repositories/task-repo.js";
import { buildClaimUrl, createTaskClaimToken } from "../../claim/claim-service.js";
import {
buyCloudtentaclesSku,
getCloudtentaclesAsset,
listCloudtentaclesSku,
} from "../../platforms/cloudtentacles/catalog-service.js";
import { getCloudtentaclesKnapsack } from "../../platforms/cloudtentacles/knapsack-service.js";
import {
backCloudtentaclesVirtualNumber,
getCloudtentaclesBindInfo,
probeCloudtentaclesBindUrl,
} from "../../platforms/cloudtentacles/virtual-number-service.js";
import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/helpers.js";
import {
notifyKuaishouCloudAssetNotEnough,
notifyKuaishouCloudBindUrlRefreshFailed,
} from "../../notification/domain-notifications.js";
import { createHttpError } from "../../../utils/http.js";
import { nowIso } from "../../../utils/time.js";
import {
KUAISHOU_CLOUD_FIXED_VN_KEY,
isKuaishouCloudBindUrlFresh,
isKuaishouCloudTask,
maskPhone,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
resolveKuaishouCloudBindUrlExpiresAt,
type JsonObject,
} from "./domain.js";
import {
prepareKuaishouCloudBindResourceWithFallback,
resolveKuaishouCloudBindingResources,
resolveKuaishouCloudVnKeyCandidates,
} from "./binding-resources.js";
import {
resolvePersistedCloudtentaclesContextBySourceKeys,
} from "./cloudtentacles-context.js";
import {
getTaskClaimExpiresAt,
isClaimExpired,
normalizeActor,
parseTaskContext,
} from "./task-context.js";
import type { TaskRow } from "../../../types/repository/rows.js";
type CloudDeliveryPlanItem = {
cloudSkuId: number;
cloudSkuName: string;
quantity: number;
skuItem: JsonObject | null;
knapsackItem: JsonObject | null;
knapsackCount: number;
missingCount: number;
};
export {
isKuaishouCloudBindUrlFresh,
isKuaishouCloudTask,
maskCode,
maskPhone,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
resolveKuaishouCloudBindUrlExpiresAt,
} from "./domain.js";
export {
resolvePersistedCloudtentaclesContextBySourceKeys,
} from "./cloudtentacles-context.js";
export async function ensureTaskClaimLink(task: TaskRow) {
const tokenStatus = String(task?.primary_claim_token_status || "").trim();
const token = String(
task?.primary_claim_token || task?.claim_token || ""
).trim();
const expiredAt = getTaskClaimExpiresAt(task);
if (tokenStatus === "active" && token && !isClaimExpired(expiredAt)) {
return {
token,
expiredAt,
claimUrl: buildClaimUrl(token),
};
}
const claimToken = await createTaskClaimToken(task.id);
return {
token: claimToken.token,
expiredAt: claimToken.expired_at,
claimUrl: claimToken.claimUrl,
};
}
export async function prepareKuaishouCloudFulfillmentTask(
task: TaskRow,
options: JsonObject = {}
) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError("当前任务不是快手 Cloud 履约任务", {
statusCode: 409,
errorCode: "kuaishou_cloud_task_invalid",
});
}
const now = nowIso();
const actor = normalizeActor(options.actor);
const force = options.force === true;
const taskContext = parseTaskContext(task);
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
const claimLinkState = await ensureTaskClaimLink(task);
if (
!force &&
flow.binding.prepareStatus === "ready" &&
flow.binding.vnId > 0 &&
flow.binding.vnPhone &&
flow.binding.bindUrl
) {
if (!isKuaishouCloudBindUrlFresh(flow)) {
return refreshKuaishouCloudTaskBindUrl(task, {
source: options.source || "system_refresh_expired_bind_url",
actor,
claimLinkState,
});
}
let readyTask = task;
if (String(task.task_status || "").trim() !== "waiting_binding") {
readyTask = (await updateTask(task.id, {
task_status: "waiting_binding",
claim_token: claimLinkState.token || task.claim_token || "",
claim_expires_at:
claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
updated_at: now,
})) || task;
}
return {
task: readyTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow,
};
}
const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys(
flow.binding.cloudSourceKeys
);
const [knapsack, skuList] = await Promise.all([
getCloudtentaclesKnapsack(cloudContext),
listCloudtentaclesSku(cloudContext),
]);
const resolvedBinding = resolveKuaishouCloudBindingResources(flow, {
skuItems: Array.isArray(skuList.items) ? skuList.items : [],
knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
});
const vnKeyCandidates = resolveKuaishouCloudVnKeyCandidates({
flow,
binding: resolvedBinding,
});
if (!resolvedBinding.skuId || vnKeyCandidates.length === 0) {
throw createHttpError("当前任务缺少可用 cloud 资源,无法自动准备绑定", {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_binding_config",
});
}
const flowWithResolvedBinding = {
...flow,
binding: {
...flow.binding,
skuId: resolvedBinding.skuId,
skuName: resolvedBinding.skuName,
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
},
};
const deliveryPlan = resolveKuaishouCloudDeliveryPlan(flowWithResolvedBinding, {
skuItems: Array.isArray(skuList.items) ? skuList.items : [],
knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
});
if (deliveryPlan.items.length === 0) {
throw createHttpError("当前任务缺少 cloud 发货物品配置", {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_delivery_items",
});
}
let usedKnapsack = deliveryPlan.items.every((item) => item.missingCount <= 0);
let purchaseTriggered = false;
let assetBefore = 0;
let assetAfter = 0;
if (!usedKnapsack) {
if (!flowWithResolvedBinding.purchase.autoBuyEnabled) {
throw createHttpError("背包中没有现成库存,且当前配置未开启自动购买", {
statusCode: 409,
errorCode: "kuaishou_cloud_auto_buy_disabled",
});
}
const asset = await getCloudtentaclesAsset(cloudContext);
const missingSku = deliveryPlan.items.find(
(item) => item.missingCount > 0 && !item.skuItem
);
if (missingSku) {
throw createHttpError(
`cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`,
{
statusCode: 404,
errorCode: "kuaishou_cloud_sku_not_found",
}
);
}
assetBefore = Number(asset.asset || 0) || 0;
const targetPrice = deliveryPlan.items.reduce(
(sum, item) =>
sum + Math.max(0, item.missingCount) * Number(item.skuItem?.price || 0),
0
);
const requiredAsset =
targetPrice + flowWithResolvedBinding.purchase.minAssetReserve;
if (assetBefore < requiredAsset) {
await notifyKuaishouCloudAssetNotEnough({
task,
flow: flowWithResolvedBinding,
assetBefore,
requiredAsset,
skuName: deliveryPlan.items
.filter((item) => item.missingCount > 0)
.map((item) => item.cloudSkuName || `SKU ${item.cloudSkuId}`)
.join("、"),
});
throw createHttpError(
`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`,
{
statusCode: 409,
errorCode: "kuaishou_cloud_asset_not_enough",
}
);
}
for (const item of deliveryPlan.items) {
if (item.missingCount <= 0) {
continue;
}
await buyCloudtentaclesSku({
...cloudContext,
id: item.cloudSkuId,
count: item.missingCount,
});
}
purchaseTriggered = true;
const assetResult = await getCloudtentaclesAsset(cloudContext);
assetAfter = Number(assetResult.asset || 0) || 0;
}
const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
cloudContext,
vnKeyCandidates,
});
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flowWithResolvedBinding,
binding: {
...flowWithResolvedBinding.binding,
resolvedSourceKey: cloudContext.resolvedSourceKey,
vnKey: preparedBinding.vnKey,
prepareStatus: "ready",
vnId: preparedBinding.vnId,
vnPhone: preparedBinding.vnPhone,
bindUrl: preparedBinding.bindUrl,
bindPreparedAt: now,
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
bindProbeAt: null as null,
bindProbeStatus: "pending",
bindProbeMessage: "",
roleName: "",
roleId: "",
},
role: {
status: "pending",
name: "",
rid: "",
refreshedAt: null as null,
errorMessage: "",
rawInfo: null as null,
},
purchase: {
...flowWithResolvedBinding.purchase,
usedKnapsack,
purchaseTriggered,
assetBefore,
assetAfter,
items: deliveryPlan.items.map((item) => ({
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
requiredCount: item.quantity,
knapsackCount: item.knapsackCount,
purchasedCount: Math.max(0, item.missingCount),
})),
purchaseAt: purchaseTriggered
? now
: flowWithResolvedBinding.purchase.purchaseAt,
},
},
};
const updatedTask = await updateTask(task.id, {
task_status: "waiting_binding",
user_action_status: "pending_claim",
claim_token: claimLinkState.token || task.claim_token || "",
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
role_id: "",
role_name: "",
last_error: "",
context_json: JSON.stringify(nextContext),
updated_at: now,
});
await createTaskEvent(
task.id,
"kuaishou_cloud_binding_prepared",
{
source: String(options.source || "system").trim() || "system",
skuId: flowWithResolvedBinding.binding.skuId,
skuName: flowWithResolvedBinding.binding.skuName,
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
purchaseTriggered,
usedKnapsack,
deliveryItems: deliveryPlan.items.map((item) => ({
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
quantity: item.quantity,
knapsackCount: item.knapsackCount,
purchasedCount: Math.max(0, item.missingCount),
})),
resolvedByName: resolvedBinding.resolvedByName,
actor,
},
now
);
return {
task: updatedTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
};
}
export async function refreshKuaishouCloudTaskBindUrl(
task: TaskRow,
options: JsonObject = {}
) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError("当前任务不是快手 Cloud 履约任务", {
statusCode: 409,
errorCode: "kuaishou_cloud_task_invalid",
});
}
const now = nowIso();
const actor = normalizeActor(options.actor);
const taskContext = parseTaskContext(task);
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError("当前任务缺少可刷新绑定链接的虚拟号信息", {
statusCode: 409,
errorCode: "kuaishou_cloud_missing_bind_url_context",
});
}
const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
]);
const oldVnKey = flow.binding.vnKey;
const oldVnId = flow.binding.vnId;
const oldVnPhone = flow.binding.vnPhone;
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: oldVnKey,
id: oldVnId,
});
await createTaskEvent(
task.id,
"kuaishou_cloud_expired_bind_number_returned",
{
source: String(options.source || "system").trim() || "system",
vnKey: oldVnKey,
vnId: oldVnId,
vnPhoneMasked: maskPhone(oldVnPhone),
actor,
},
now
);
let preparedBinding;
try {
preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
cloudContext,
vnKeyCandidates: resolveKuaishouCloudVnKeyCandidates({
flow,
binding: flow.binding,
}),
});
if (!String(preparedBinding.bindUrl || "").trim()) {
throw createHttpError("cloudtentacles 未返回新的绑定链接", {
statusCode: 502,
errorCode: "kuaishou_cloud_empty_bind_url",
});
}
} catch (error) {
const failedTask = await markKuaishouCloudBindUrlRefreshFailed(task, {
taskContext,
flow,
now,
actor,
error,
});
return {
task: failedTask,
claimUrl: buildClaimUrl(
String(task.primary_claim_token || task.claim_token || "")
),
token: String(task.primary_claim_token || task.claim_token || ""),
flow: normalizeKuaishouCloudFlow(
parseTaskContext(failedTask).kuaishouCloudFulfillment
),
};
}
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
prepareStatus: "ready",
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhone: preparedBinding.vnPhone,
bindUrl: preparedBinding.bindUrl,
bindPreparedAt: now,
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
bindProbeAt: null as null,
bindProbeStatus: "pending",
bindProbeMessage: "",
roleName:
String(task.task_status || "").trim() === "role_confirmed"
? flow.binding.roleName
: "",
roleId:
String(task.task_status || "").trim() === "role_confirmed"
? flow.binding.roleId
: "",
},
role:
String(task.task_status || "").trim() === "role_confirmed"
? flow.role
: {
status: "pending",
name: "",
rid: "",
refreshedAt: null,
errorMessage: "",
rawInfo: null,
},
},
};
const claimLinkState =
options.claimLinkState || (await ensureTaskClaimLink(task));
const updatedTask = await updateTask(task.id, {
task_status:
String(task.task_status || "").trim() === "role_confirmed"
? task.task_status
: "waiting_binding",
claim_token: claimLinkState.token || task.claim_token || "",
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
role_id:
String(task.task_status || "").trim() === "role_confirmed"
? task.role_id
: "",
role_name:
String(task.task_status || "").trim() === "role_confirmed"
? task.role_name
: "",
last_error: "",
context_json: JSON.stringify(nextContext),
updated_at: now,
});
await createTaskEvent(
task.id,
"kuaishou_cloud_bind_url_refreshed",
{
source: String(options.source || "system").trim() || "system",
oldVnId,
oldVnPhoneMasked: maskPhone(oldVnPhone),
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
actor,
},
now
);
return {
task: updatedTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
};
}
export async function probeKuaishouCloudTaskBindUrl(
task: TaskRow,
options: JsonObject = {}
) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError("当前任务不是快手 Cloud 履约任务", {
statusCode: 409,
errorCode: "kuaishou_cloud_task_invalid",
});
}
const now = nowIso();
const taskContext = parseTaskContext(task);
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
if (!flow.binding.bindUrl) {
return {
task,
flow,
probe: null as null,
};
}
const probeIntervalMs =
Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) *
1000;
if (
!options.force &&
flow.binding.bindProbeAt &&
Date.now() - Date.parse(flow.binding.bindProbeAt) < probeIntervalMs
) {
return {
task,
flow,
probe: null as null,
};
}
const probe = await probeCloudtentaclesBindUrl({
bindUrl: flow.binding.bindUrl,
});
if (probe.expired || !isKuaishouCloudBindUrlFresh(flow)) {
const refreshed = await refreshKuaishouCloudTaskBindUrl(task, {
source: options.source || "claim_page_bind_url_expired",
actor: options.actor || { source: "system" },
});
return {
...refreshed,
probe,
};
}
const probeRoleInfo = normalizeKuaishouCloudRoleInfo(probe.roleInfo);
const hasRoleInfo = Boolean(probeRoleInfo.name || probeRoleInfo.rid);
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
bindProbeAt: now,
bindProbeStatus: probe.valid ? "valid" : "invalid",
bindProbeMessage: String(probe.message || probe.reason || "").trim(),
roleName: hasRoleInfo ? probeRoleInfo.name : flow.binding.roleName,
roleId: hasRoleInfo ? probeRoleInfo.rid : flow.binding.roleId,
},
role: {
...flow.role,
status: hasRoleInfo ? "ready" : flow.role.status,
name: hasRoleInfo ? probeRoleInfo.name : flow.role.name,
rid: hasRoleInfo ? probeRoleInfo.rid : flow.role.rid,
refreshedAt: hasRoleInfo ? now : flow.role.refreshedAt,
errorMessage: hasRoleInfo ? "" : flow.role.errorMessage,
rawInfo: hasRoleInfo ? probeRoleInfo.rawInfo : flow.role.rawInfo,
},
},
};
const updatedTask = await updateTask(task.id, {
role_id: hasRoleInfo ? probeRoleInfo.rid : task.role_id,
role_name: hasRoleInfo ? probeRoleInfo.name : task.role_name,
context_json: JSON.stringify(nextContext),
updated_at: now,
});
return {
task: updatedTask,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
probe,
};
}
/**
* @param {any} task
* @param {{ taskContext?: any, flow?: any, now?: string, actor?: any, error?: unknown }} [input]
*/
async function markKuaishouCloudBindUrlRefreshFailed(
task: TaskRow,
{ taskContext, flow, now, actor, error }: JsonObject = {}
) {
const errorMessage =
error instanceof Error
? error.message
: String(error || "新绑定链接准备失败");
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
prepareStatus: "pending",
vnId: 0,
vnPhone: "",
bindUrl: "",
bindPreparedAt: null,
bindExpiresAt: null,
bindProbeAt: now,
bindProbeStatus: "refresh_failed",
bindProbeMessage: errorMessage,
roleName: "",
roleId: "",
},
role: {
status: "pending",
name: "",
rid: "",
refreshedAt: now,
errorMessage:
"绑定链接已过期,旧号码已退还,新链接准备失败,请稍后刷新或联系客服处理",
rawInfo: null,
},
},
};
const updatedTask = await updateTask(task.id, {
task_status: "pending_binding_prepare",
role_id: "",
role_name: "",
last_error: `绑定链接过期,旧号码已退还,新链接准备失败:${errorMessage}`,
context_json: JSON.stringify(nextContext),
updated_at: now,
});
await createTaskEvent(
task.id,
"kuaishou_cloud_bind_url_refresh_failed",
{
source: "system_refresh_expired_bind_url",
errorMessage,
actor,
},
now
);
await notifyKuaishouCloudBindUrlRefreshFailed({
task: updatedTask,
errorMessage,
});
return updatedTask;
}
export async function refreshKuaishouCloudTaskRoleInfo(
task: TaskRow,
options: JsonObject = {}
) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError("当前任务不是快手 Cloud 履约任务", {
statusCode: 409,
errorCode: "kuaishou_cloud_task_invalid",
});
}
const now = nowIso();
const actor = normalizeActor(options.actor);
const taskContext = parseTaskContext(task);
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError(
"当前任务还没有可查询的绑定角色信息,请先准备绑定资源",
{
statusCode: 409,
errorCode: "kuaishou_cloud_missing_bind_info_context",
}
);
}
if (flow.binding.bindUrl) {
const probed = await probeKuaishouCloudTaskBindUrl(task, {
source: options.source || "system_role_refresh_bind_url_probe",
actor,
force: options.forceProbe === true,
});
const probedFlow = normalizeKuaishouCloudFlow(
parseTaskContext(probed.task).kuaishouCloudFulfillment
);
if (probed.probe?.expired) {
return {
task: probed.task,
roleInfo: normalizeKuaishouCloudRoleInfo(null),
flow: probedFlow,
};
}
if (
(probed.probe === null || probed.probe?.valid) &&
(probedFlow.binding.roleName || probedFlow.binding.roleId)
) {
return {
task: probed.task,
roleInfo: {
name: probedFlow.binding.roleName,
rid: probedFlow.binding.roleId,
rawInfo: probedFlow.role.rawInfo,
},
flow: probedFlow,
};
}
}
const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
]);
const bindInfoResult = await getCloudtentaclesBindInfo({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
});
const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo);
const hasRoleInfo = Boolean(bindInfo.name || bindInfo.rid);
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
roleName: hasRoleInfo ? bindInfo.name : "",
roleId: hasRoleInfo ? bindInfo.rid : "",
},
role: {
status: hasRoleInfo ? "ready" : "pending",
name: hasRoleInfo ? bindInfo.name : "",
rid: hasRoleInfo ? bindInfo.rid : "",
refreshedAt: now,
errorMessage: hasRoleInfo
? ""
: "当前还没有查询到角色信息,请完成绑定后稍等片刻再试",
rawInfo: bindInfo.rawInfo,
},
},
};
const updatedTask = await updateTask(task.id, {
role_id: hasRoleInfo ? bindInfo.rid : "",
role_name: hasRoleInfo ? bindInfo.name : "",
context_json: JSON.stringify(nextContext),
updated_at: now,
});
if (options.recordEvent !== false) {
await createTaskEvent(
task.id,
"kuaishou_cloud_role_info_refreshed",
{
source: String(options.source || "system").trim() || "system",
roleName: bindInfo.name,
roleId: bindInfo.rid,
vnId: flow.binding.vnId,
actor,
},
now
);
}
return {
task: updatedTask,
roleInfo: bindInfo,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
};
}
function resolveKuaishouCloudDeliveryPlan(
flow: JsonObject,
{
skuItems = [],
knapsackItems = [],
}: { skuItems?: unknown[]; knapsackItems?: unknown[] } = {}
): { items: CloudDeliveryPlanItem[] } {
const normalizedSkuItems = Array.isArray(skuItems)
? skuItems.filter(isCloudSkuLikeItem)
: [];
const normalizedKnapsackItems = Array.isArray(knapsackItems)
? knapsackItems.filter(isCloudSkuLikeItem)
: [];
const deliveryItems = Array.isArray(flow.deliveryItems)
? flow.deliveryItems
: [];
return {
items: deliveryItems
.map((item: unknown) => {
const source = item && typeof item === "object" ? item as JsonObject : {};
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0;
const quantity = Number(source.quantity || 1) || 1;
if (!cloudSkuId || !Number.isInteger(quantity) || quantity <= 0) {
return null;
}
const skuItem =
normalizedSkuItems.find(
(sku) => Number(sku.id || 0) === cloudSkuId
) || null;
const knapsackItem =
normalizedKnapsackItems.find(
(sku) => Number(sku.id || 0) === cloudSkuId
) || null;
const knapsackCount = Math.max(0, Number(knapsackItem?.count || 0) || 0);
const cloudSkuName = String(
source.cloudSkuName ||
source.skuName ||
skuItem?.name ||
knapsackItem?.name ||
""
).trim();
return {
cloudSkuId,
cloudSkuName,
quantity,
skuItem,
knapsackItem,
knapsackCount,
missingCount: Math.max(0, quantity - knapsackCount),
};
})
.filter((item): item is CloudDeliveryPlanItem => Boolean(item)),
};
}
function isCloudSkuLikeItem(item: unknown): item is JsonObject {
const current = item && typeof item === "object" ? item as JsonObject : {};
return Number(current.id || 0) > 0;
}
export {
dispatchKuaishouCloudFulfillmentTask,
returnKuaishouCloudFulfillmentTask,
} from "./task-finalization.js";