领取/发货链路优化
This commit is contained in:
@@ -2,8 +2,6 @@ import { createTaskEvent } from "../../../repositories/task-event-repo.js";
|
|||||||
import { updateTask } from "../../../repositories/task-repo.js";
|
import { updateTask } from "../../../repositories/task-repo.js";
|
||||||
import { buildClaimUrl, createTaskClaimToken } from "../../claim/claim-service.js";
|
import { buildClaimUrl, createTaskClaimToken } from "../../claim/claim-service.js";
|
||||||
import {
|
import {
|
||||||
buyCloudtentaclesSku,
|
|
||||||
getCloudtentaclesAsset,
|
|
||||||
listCloudtentaclesSku,
|
listCloudtentaclesSku,
|
||||||
} from "../../platforms/cloudtentacles/catalog-service.js";
|
} from "../../platforms/cloudtentacles/catalog-service.js";
|
||||||
import { getCloudtentaclesKnapsack } from "../../platforms/cloudtentacles/knapsack-service.js";
|
import { getCloudtentaclesKnapsack } from "../../platforms/cloudtentacles/knapsack-service.js";
|
||||||
@@ -14,7 +12,6 @@ import {
|
|||||||
} from "../../platforms/cloudtentacles/virtual-number-service.js";
|
} from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||||
import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/helpers.js";
|
import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/helpers.js";
|
||||||
import {
|
import {
|
||||||
notifyKuaishouCloudAssetNotEnough,
|
|
||||||
notifyKuaishouCloudBindUrlRefreshFailed,
|
notifyKuaishouCloudBindUrlRefreshFailed,
|
||||||
} from "../../notification/domain-notifications.js";
|
} from "../../notification/domain-notifications.js";
|
||||||
import { createHttpError } from "../../../utils/http.js";
|
import { createHttpError } from "../../../utils/http.js";
|
||||||
@@ -189,77 +186,27 @@ export async function prepareKuaishouCloudFulfillmentTask(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let usedKnapsack = deliveryPlan.items.every((item) => item.missingCount <= 0);
|
const usedKnapsack = deliveryPlan.items.every((item) => item.missingCount <= 0);
|
||||||
let purchaseTriggered = false;
|
if (!usedKnapsack && !flowWithResolvedBinding.purchase.autoBuyEnabled) {
|
||||||
let assetBefore = 0;
|
|
||||||
let assetAfter = 0;
|
|
||||||
|
|
||||||
if (!usedKnapsack) {
|
|
||||||
if (!flowWithResolvedBinding.purchase.autoBuyEnabled) {
|
|
||||||
throw createHttpError("背包中没有现成库存,且当前配置未开启自动购买", {
|
throw createHttpError("背包中没有现成库存,且当前配置未开启自动购买", {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
errorCode: "kuaishou_cloud_auto_buy_disabled",
|
errorCode: "kuaishou_cloud_auto_buy_disabled",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const asset = await getCloudtentaclesAsset(cloudContext);
|
|
||||||
const missingSku = deliveryPlan.items.find(
|
const missingSku = deliveryPlan.items.find(
|
||||||
(item) => item.missingCount > 0 && !item.skuItem
|
(item) => item.missingCount > 0 && !item.skuItem
|
||||||
);
|
);
|
||||||
if (missingSku) {
|
if (missingSku) {
|
||||||
throw createHttpError(
|
throw createHttpError(`cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`, {
|
||||||
`cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`,
|
|
||||||
{
|
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
errorCode: "kuaishou_cloud_sku_not_found",
|
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);
|
const purchaseTriggered = false;
|
||||||
assetAfter = Number(assetResult.asset || 0) || 0;
|
const assetBefore = 0;
|
||||||
}
|
const assetAfter = 0;
|
||||||
|
|
||||||
const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
|
const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
|
||||||
cloudContext,
|
cloudContext,
|
||||||
@@ -305,11 +252,9 @@ export async function prepareKuaishouCloudFulfillmentTask(
|
|||||||
cloudSkuName: item.cloudSkuName,
|
cloudSkuName: item.cloudSkuName,
|
||||||
requiredCount: item.quantity,
|
requiredCount: item.quantity,
|
||||||
knapsackCount: item.knapsackCount,
|
knapsackCount: item.knapsackCount,
|
||||||
purchasedCount: Math.max(0, item.missingCount),
|
purchasedCount: 0,
|
||||||
})),
|
})),
|
||||||
purchaseAt: purchaseTriggered
|
purchaseAt: flowWithResolvedBinding.purchase.purchaseAt,
|
||||||
? now
|
|
||||||
: flowWithResolvedBinding.purchase.purchaseAt,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -343,7 +288,7 @@ export async function prepareKuaishouCloudFulfillmentTask(
|
|||||||
cloudSkuName: item.cloudSkuName,
|
cloudSkuName: item.cloudSkuName,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
knapsackCount: item.knapsackCount,
|
knapsackCount: item.knapsackCount,
|
||||||
purchasedCount: Math.max(0, item.missingCount),
|
purchasedCount: 0,
|
||||||
})),
|
})),
|
||||||
resolvedByName: resolvedBinding.resolvedByName,
|
resolvedByName: resolvedBinding.resolvedByName,
|
||||||
actor,
|
actor,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { buildDispatchStockItems } from "./task-finalization.js";
|
||||||
|
|
||||||
|
test("buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口", () => {
|
||||||
|
const result = buildDispatchStockItems(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
cloudSkuId: 101,
|
||||||
|
cloudSkuName: "",
|
||||||
|
quantity: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
skuItems: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
name: "套装-暗影哥特",
|
||||||
|
price: 88,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
knapsackItems: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
name: "套装-暗影哥特",
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{
|
||||||
|
cloudSkuId: 101,
|
||||||
|
cloudSkuName: "套装-暗影哥特",
|
||||||
|
quantity: 3,
|
||||||
|
requiredCount: 3,
|
||||||
|
knapsackCount: 1,
|
||||||
|
purchasedCount: 2,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildDispatchStockItems 背包足够时不需要补买", () => {
|
||||||
|
const result = buildDispatchStockItems(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
cloudSkuId: 202,
|
||||||
|
cloudSkuName: "套装-浪漫天命",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
knapsackItems: [
|
||||||
|
{
|
||||||
|
id: 202,
|
||||||
|
name: "套装-浪漫天命",
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result[0]?.knapsackCount, 5);
|
||||||
|
assert.equal(result[0]?.purchasedCount, 0);
|
||||||
|
});
|
||||||
@@ -4,8 +4,17 @@ import { updateTask } from "../../../repositories/task-repo.js";
|
|||||||
import { createHttpError } from "../../../utils/http.js";
|
import { createHttpError } from "../../../utils/http.js";
|
||||||
import { nowIso } from "../../../utils/time.js";
|
import { nowIso } from "../../../utils/time.js";
|
||||||
import { TASK_STATUS, normalizeTaskStatus, type TaskStatus } from "../../../domain/task-status.js";
|
import { TASK_STATUS, normalizeTaskStatus, type TaskStatus } from "../../../domain/task-status.js";
|
||||||
import { notifyKuaishouCloudConsumeFailed } from "../../notification/domain-notifications.js";
|
import {
|
||||||
import { useCloudtentaclesSku } from "../../platforms/cloudtentacles/catalog-service.js";
|
notifyKuaishouCloudAssetNotEnough,
|
||||||
|
notifyKuaishouCloudConsumeFailed,
|
||||||
|
} from "../../notification/domain-notifications.js";
|
||||||
|
import {
|
||||||
|
buyCloudtentaclesSku,
|
||||||
|
getCloudtentaclesAsset,
|
||||||
|
listCloudtentaclesSku,
|
||||||
|
useCloudtentaclesSku,
|
||||||
|
} from "../../platforms/cloudtentacles/catalog-service.js";
|
||||||
|
import { getCloudtentaclesKnapsack } from "../../platforms/cloudtentacles/knapsack-service.js";
|
||||||
import { backCloudtentaclesVirtualNumber } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
import { backCloudtentaclesVirtualNumber } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||||
import { consumeKuaishouEticket } from "../../platforms/kuaishou-eticket/consume-service.js";
|
import { consumeKuaishouEticket } from "../../platforms/kuaishou-eticket/consume-service.js";
|
||||||
import { isKuaishouEticketMockTicketCode } from "../../platforms/kuaishou-eticket/mock-ticket-service.js";
|
import { isKuaishouEticketMockTicketCode } from "../../platforms/kuaishou-eticket/mock-ticket-service.js";
|
||||||
@@ -38,6 +47,22 @@ type DispatchResultItem = DispatchDeliveryItem & {
|
|||||||
responseMessage: string;
|
responseMessage: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DispatchStockItem = DispatchDeliveryItem & {
|
||||||
|
requiredCount: number;
|
||||||
|
knapsackCount: number;
|
||||||
|
purchasedCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DispatchStockResult = {
|
||||||
|
usedKnapsack: boolean;
|
||||||
|
purchaseTriggered: boolean;
|
||||||
|
assetBefore: number;
|
||||||
|
assetAfter: number;
|
||||||
|
items: DispatchStockItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloudSkuDispatchLocks = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
export async function dispatchKuaishouCloudFulfillmentTask(
|
export async function dispatchKuaishouCloudFulfillmentTask(
|
||||||
task: TaskRow,
|
task: TaskRow,
|
||||||
options: JsonObject = {}
|
options: JsonObject = {}
|
||||||
@@ -103,26 +128,15 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatchResults: DispatchResultItem[] = [];
|
const { dispatchResults, stockResult } = await withCloudSkuDispatchLocks(
|
||||||
for (const item of deliveryItems) {
|
resolveCloudSkuDispatchLockKeys(cloudContext.resolvedSourceKey, deliveryItems),
|
||||||
for (let index = 0; index < item.quantity; index += 1) {
|
() => prepareStockAndDispatch({
|
||||||
const dispatchResult = await useCloudtentaclesSku({
|
task,
|
||||||
...cloudContext,
|
flow: syncedFlow,
|
||||||
id: item.cloudSkuId,
|
cloudContext,
|
||||||
virtualNumberId: syncedFlow.binding.vnId,
|
deliveryItems,
|
||||||
phone: syncedFlow.binding.vnPhone,
|
})
|
||||||
});
|
);
|
||||||
dispatchResults.push({
|
|
||||||
cloudSkuId: item.cloudSkuId,
|
|
||||||
cloudSkuName: item.cloudSkuName,
|
|
||||||
unitIndex: index + 1,
|
|
||||||
quantity: item.quantity,
|
|
||||||
sendType: Number(dispatchResult.sendType || 0) || 0,
|
|
||||||
note: String(dispatchResult.note || "").trim(),
|
|
||||||
responseMessage: String(dispatchResult.responseMessage || "").trim(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const firstDispatchResult: DispatchResultItem = dispatchResults[0] || {
|
const firstDispatchResult: DispatchResultItem = dispatchResults[0] || {
|
||||||
cloudSkuId: syncedFlow.binding.skuId,
|
cloudSkuId: syncedFlow.binding.skuId,
|
||||||
cloudSkuName: syncedFlow.binding.skuName,
|
cloudSkuName: syncedFlow.binding.skuName,
|
||||||
@@ -160,6 +174,17 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
note: dispatchSummary,
|
note: dispatchSummary,
|
||||||
items: dispatchResults,
|
items: dispatchResults,
|
||||||
},
|
},
|
||||||
|
purchase: {
|
||||||
|
...syncedFlow.purchase,
|
||||||
|
usedKnapsack: stockResult.usedKnapsack,
|
||||||
|
purchaseTriggered: stockResult.purchaseTriggered,
|
||||||
|
assetBefore: stockResult.assetBefore,
|
||||||
|
assetAfter: stockResult.assetAfter,
|
||||||
|
purchaseAt: stockResult.purchaseTriggered
|
||||||
|
? now
|
||||||
|
: syncedFlow.purchase.purchaseAt,
|
||||||
|
items: stockResult.items,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -193,6 +218,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
note: dispatchSummary,
|
note: dispatchSummary,
|
||||||
deliveryItems,
|
deliveryItems,
|
||||||
dispatchResults,
|
dispatchResults,
|
||||||
|
stockResult,
|
||||||
actor,
|
actor,
|
||||||
},
|
},
|
||||||
now
|
now
|
||||||
@@ -222,6 +248,184 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function prepareStockAndDispatch({
|
||||||
|
task,
|
||||||
|
flow,
|
||||||
|
cloudContext,
|
||||||
|
deliveryItems,
|
||||||
|
}: {
|
||||||
|
task: TaskRow;
|
||||||
|
flow: JsonObject;
|
||||||
|
cloudContext: JsonObject;
|
||||||
|
deliveryItems: DispatchDeliveryItem[];
|
||||||
|
}) {
|
||||||
|
const [knapsack, skuList] = await Promise.all([
|
||||||
|
getCloudtentaclesKnapsack(cloudContext),
|
||||||
|
listCloudtentaclesSku(cloudContext),
|
||||||
|
]);
|
||||||
|
const skuItems = Array.isArray(skuList.items) ? skuList.items : [];
|
||||||
|
const knapsackItems = Array.isArray(knapsack.items) ? knapsack.items : [];
|
||||||
|
const stockItems = buildDispatchStockItems(deliveryItems, {
|
||||||
|
skuItems,
|
||||||
|
knapsackItems,
|
||||||
|
});
|
||||||
|
const missingItems = stockItems.filter((item) => item.purchasedCount > 0);
|
||||||
|
|
||||||
|
let assetBefore = 0;
|
||||||
|
let assetAfter = 0;
|
||||||
|
let purchaseTriggered = false;
|
||||||
|
|
||||||
|
if (missingItems.length > 0) {
|
||||||
|
if (flow.purchase?.autoBuyEnabled === false) {
|
||||||
|
throw createHttpError("背包中没有现成库存,且当前配置未开启自动购买", {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: "kuaishou_cloud_auto_buy_disabled",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingSku = missingItems.find((item) => !findCloudSkuItem(skuItems, item.cloudSkuId));
|
||||||
|
if (missingSku) {
|
||||||
|
throw createHttpError(`cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`, {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: "kuaishou_cloud_sku_not_found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = await getCloudtentaclesAsset(cloudContext);
|
||||||
|
assetBefore = Number(asset.asset || 0) || 0;
|
||||||
|
const targetPrice = missingItems.reduce((sum, item) => {
|
||||||
|
const skuItem = findCloudSkuItem(skuItems, item.cloudSkuId);
|
||||||
|
return sum + item.purchasedCount * Number(skuItem?.price || 0);
|
||||||
|
}, 0);
|
||||||
|
const requiredAsset = targetPrice + Number(flow.purchase?.minAssetReserve || 0);
|
||||||
|
|
||||||
|
if (assetBefore < requiredAsset) {
|
||||||
|
await notifyKuaishouCloudAssetNotEnough({
|
||||||
|
task,
|
||||||
|
flow,
|
||||||
|
assetBefore,
|
||||||
|
requiredAsset,
|
||||||
|
skuName: missingItems
|
||||||
|
.map((item) => item.cloudSkuName || `SKU ${item.cloudSkuId}`)
|
||||||
|
.join("、"),
|
||||||
|
});
|
||||||
|
throw createHttpError(`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: "kuaishou_cloud_asset_not_enough",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of missingItems) {
|
||||||
|
await buyCloudtentaclesSku({
|
||||||
|
...cloudContext,
|
||||||
|
id: item.cloudSkuId,
|
||||||
|
count: item.purchasedCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseTriggered = true;
|
||||||
|
const assetResult = await getCloudtentaclesAsset(cloudContext);
|
||||||
|
assetAfter = Number(assetResult.asset || 0) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatchResults: DispatchResultItem[] = [];
|
||||||
|
for (const item of deliveryItems) {
|
||||||
|
for (let index = 0; index < item.quantity; index += 1) {
|
||||||
|
const dispatchResult = await useCloudtentaclesSku({
|
||||||
|
...cloudContext,
|
||||||
|
id: item.cloudSkuId,
|
||||||
|
virtualNumberId: flow.binding.vnId,
|
||||||
|
phone: flow.binding.vnPhone,
|
||||||
|
});
|
||||||
|
dispatchResults.push({
|
||||||
|
cloudSkuId: item.cloudSkuId,
|
||||||
|
cloudSkuName: item.cloudSkuName,
|
||||||
|
unitIndex: index + 1,
|
||||||
|
quantity: item.quantity,
|
||||||
|
sendType: Number(dispatchResult.sendType || 0) || 0,
|
||||||
|
note: String(dispatchResult.note || "").trim(),
|
||||||
|
responseMessage: String(dispatchResult.responseMessage || "").trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispatchResults,
|
||||||
|
stockResult: {
|
||||||
|
usedKnapsack: stockItems.every((item) => item.purchasedCount <= 0),
|
||||||
|
purchaseTriggered,
|
||||||
|
assetBefore,
|
||||||
|
assetAfter,
|
||||||
|
items: stockItems,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDispatchStockItems(
|
||||||
|
deliveryItems: DispatchDeliveryItem[],
|
||||||
|
{ skuItems = [], knapsackItems = [] }: { skuItems?: JsonObject[]; knapsackItems?: JsonObject[] } = {}
|
||||||
|
): DispatchStockItem[] {
|
||||||
|
return deliveryItems.map((item) => {
|
||||||
|
const knapsackItem = findCloudSkuItem(knapsackItems, item.cloudSkuId);
|
||||||
|
const skuItem = findCloudSkuItem(skuItems, item.cloudSkuId);
|
||||||
|
const knapsackCount = Math.max(0, Number(knapsackItem?.count || 0) || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
cloudSkuName: item.cloudSkuName || String(skuItem?.name || knapsackItem?.name || "").trim(),
|
||||||
|
requiredCount: item.quantity,
|
||||||
|
knapsackCount,
|
||||||
|
purchasedCount: Math.max(0, item.quantity - knapsackCount),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCloudSkuItem(items: JsonObject[], cloudSkuId: number) {
|
||||||
|
return items.find((item) => Number(item.id || 0) === cloudSkuId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCloudSkuDispatchLockKeys(sourceKey: unknown, deliveryItems: DispatchDeliveryItem[]) {
|
||||||
|
const normalizedSourceKey = String(sourceKey || "default").trim() || "default";
|
||||||
|
return deliveryItems.map((item) => `${normalizedSourceKey}:${item.cloudSkuId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withCloudSkuDispatchLocks<T>(keys: string[], callback: () => Promise<T>): Promise<T> {
|
||||||
|
const normalizedKeys = [...new Set(keys.map((key) => String(key || "").trim()).filter(Boolean))].sort();
|
||||||
|
|
||||||
|
async function run(index: number): Promise<T> {
|
||||||
|
const key = normalizedKeys[index];
|
||||||
|
if (!key) {
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return withCloudSkuDispatchLock(key, () => run(index + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return run(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withCloudSkuDispatchLock<T>(key: string, callback: () => Promise<T>): Promise<T> {
|
||||||
|
const previous = cloudSkuDispatchLocks.get(key) || Promise.resolve();
|
||||||
|
let release: () => void = () => {};
|
||||||
|
const current = new Promise<void>((resolve) => {
|
||||||
|
release = resolve;
|
||||||
|
});
|
||||||
|
const queued = previous.catch(() => {}).then(() => current);
|
||||||
|
|
||||||
|
cloudSkuDispatchLocks.set(key, queued);
|
||||||
|
|
||||||
|
await previous.catch(() => {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await callback();
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
if (cloudSkuDispatchLocks.get(key) === queued) {
|
||||||
|
cloudSkuDispatchLocks.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDispatchDeliveryItems(flow: JsonObject): DispatchDeliveryItem[] {
|
function resolveDispatchDeliveryItems(flow: JsonObject): DispatchDeliveryItem[] {
|
||||||
const rawItems = Array.isArray(flow.deliveryItems) ? flow.deliveryItems : [];
|
const rawItems = Array.isArray(flow.deliveryItems) ? flow.deliveryItems : [];
|
||||||
const items = rawItems
|
const items = rawItems
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export async function cloudtentaclesRequest(pathname: unknown, options: JsonObje
|
|||||||
const statusCode = Number(options.businessErrorStatusCode || 400)
|
const statusCode = Number(options.businessErrorStatusCode || 400)
|
||||||
const errorCode = String(options.businessErrorCode || 'cloudtentacles_business_error')
|
const errorCode = String(options.businessErrorCode || 'cloudtentacles_business_error')
|
||||||
|
|
||||||
if (statusCode === 401 || isCloudtentaclesExpiredMessage(message)) {
|
if (isCloudtentaclesExpiredMessage(message)) {
|
||||||
await notifyCloudtentaclesAuthExpired({
|
await notifyCloudtentaclesAuthExpired({
|
||||||
pathname: normalizedPathname,
|
pathname: normalizedPathname,
|
||||||
errorCode,
|
errorCode,
|
||||||
|
|||||||
Reference in New Issue
Block a user