增加履约高级配置

This commit is contained in:
yml2213
2026-05-26 22:56:41 +08:00
parent 5377f8893d
commit e8010728a4
18 changed files with 858 additions and 62 deletions
@@ -106,6 +106,7 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', ()
cloudSourceKeys: ['account_a', 'account_b'], cloudSourceKeys: ['account_a', 'account_b'],
cloudSkuId: 0, cloudSkuId: 0,
cloudSkuName: '', cloudSkuName: '',
deliveryItems: [],
vnKey: '', vnKey: '',
autoBuyEnabled: true, autoBuyEnabled: true,
minAssetReserve: 0, minAssetReserve: 0,
@@ -62,6 +62,7 @@ export function mapAdminCloudtentaclesSession(session: JsonObject = {}) {
} }
export function mapAdminKuaishouCloudFulfillmentItem(item: JsonObject = {}) { export function mapAdminKuaishouCloudFulfillmentItem(item: JsonObject = {}) {
const deliveryItems = normalizeDeliveryItems(item);
return { return {
id: String(item.id || "").trim(), id: String(item.id || "").trim(),
enabled: item.enabled !== false, enabled: item.enabled !== false,
@@ -82,6 +83,7 @@ export function mapAdminKuaishouCloudFulfillmentItem(item: JsonObject = {}) {
: [], : [],
cloudSkuId: Number(item.cloudSkuId || 0) || 0, cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || "").trim(), cloudSkuName: String(item.cloudSkuName || "").trim(),
deliveryItems,
vnKey: String(item.vnKey || "").trim(), vnKey: String(item.vnKey || "").trim(),
autoBuyEnabled: item.autoBuyEnabled !== false, autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: Number(item.minAssetReserve || 0) || 0, minAssetReserve: Number(item.minAssetReserve || 0) || 0,
@@ -101,3 +103,42 @@ export function mapAdminKuaishouCloudFulfillmentSource(config: JsonObject = {})
), ),
}; };
} }
function normalizeDeliveryItems(item: JsonObject = {}) {
const rawItems = Array.isArray(item.deliveryItems) ? item.deliveryItems : [];
const normalizedItems = rawItems
.map((value) => normalizeDeliveryItem(value))
.filter(Boolean);
if (normalizedItems.length > 0) {
return normalizedItems;
}
const cloudSkuId = Number(item.cloudSkuId || 0) || 0;
if (cloudSkuId <= 0) {
return [];
}
return [
{
cloudSkuId,
cloudSkuName: String(item.cloudSkuName || "").trim(),
quantity: 1,
},
];
}
function normalizeDeliveryItem(value: unknown) {
const source = value && typeof value === "object" ? value as JsonObject : {};
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0;
if (cloudSkuId <= 0) {
return null;
}
const quantity = Number(source.quantity || 1) || 1;
return {
cloudSkuId,
cloudSkuName: String(source.cloudSkuName || source.skuName || "").trim(),
quantity: Math.max(1, Math.round(quantity)),
};
}
@@ -35,6 +35,7 @@ export function normalizeKuaishouCloudFlow(value: unknown) {
source.consume && typeof source.consume === "object" ? source.consume : {}; source.consume && typeof source.consume === "object" ? source.consume : {};
const ticket = const ticket =
source.ticket && typeof source.ticket === "object" ? source.ticket : {}; source.ticket && typeof source.ticket === "object" ? source.ticket : {};
const deliveryItems = normalizeDeliveryItems(source, binding);
const roleName = String(role.name || binding.roleName || "").trim(); const roleName = String(role.name || binding.roleName || "").trim();
const roleId = String(role.rid || binding.roleId || "").trim(); const roleId = String(role.rid || binding.roleId || "").trim();
@@ -48,6 +49,7 @@ export function normalizeKuaishouCloudFlow(value: unknown) {
configId: String(source.configId || "").trim(), configId: String(source.configId || "").trim(),
internalSkuCode: String(source.internalSkuCode || "").trim(), internalSkuCode: String(source.internalSkuCode || "").trim(),
internalSkuName: String(source.internalSkuName || "").trim(), internalSkuName: String(source.internalSkuName || "").trim(),
deliveryItems,
ticket: { ticket: {
code: String(ticket.code || "").trim(), code: String(ticket.code || "").trim(),
status: String(ticket.status || "pending").trim() || "pending", status: String(ticket.status || "pending").trim() || "pending",
@@ -100,6 +102,7 @@ export function normalizeKuaishouCloudFlow(value: unknown) {
assetBefore: Number(purchase.assetBefore || 0) || 0, assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0, assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null, purchaseAt: purchase.purchaseAt || null,
items: Array.isArray(purchase.items) ? purchase.items : [],
}, },
dispatch: { dispatch: {
status: String(dispatch.status || "pending").trim() || "pending", status: String(dispatch.status || "pending").trim() || "pending",
@@ -107,6 +110,7 @@ export function normalizeKuaishouCloudFlow(value: unknown) {
dispatchBy: dispatch.dispatchBy || null, dispatchBy: dispatch.dispatchBy || null,
sendType: Number(dispatch.sendType || 0) || 0, sendType: Number(dispatch.sendType || 0) || 0,
note: String(dispatch.note || "").trim(), note: String(dispatch.note || "").trim(),
items: Array.isArray(dispatch.items) ? dispatch.items : [],
}, },
returnNumber: { returnNumber: {
status: String(returnNumber.status || "pending").trim() || "pending", status: String(returnNumber.status || "pending").trim() || "pending",
@@ -126,6 +130,13 @@ export function normalizeKuaishouCloudFlow(value: unknown) {
}; };
} }
export function normalizeKuaishouCloudDeliveryItems(value: unknown) {
const source: JsonObject = value && typeof value === "object" ? value as JsonObject : {};
const binding =
source.binding && typeof source.binding === "object" ? source.binding : {};
return normalizeDeliveryItems(source, binding);
}
export function normalizeKuaishouCloudRoleInfo(value: unknown) { export function normalizeKuaishouCloudRoleInfo(value: unknown) {
const rawInfo: JsonObject | null = value && typeof value === "object" ? value as JsonObject : null; const rawInfo: JsonObject | null = value && typeof value === "object" ? value as JsonObject : null;
const nestedBindInfo = const nestedBindInfo =
@@ -204,3 +215,61 @@ export function normalizeStringArray(value: unknown) {
} }
return []; return [];
} }
function normalizeDeliveryItems(source: JsonObject, binding: JsonObject) {
const rawItems = Array.isArray(source.deliveryItems) ? source.deliveryItems : [];
const items = rawItems
.map((item) => normalizeDeliveryItem(item))
.filter(Boolean) as Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>;
if (items.length > 0) {
return mergeDeliveryItems(items);
}
const skuId = Number(binding.skuId || 0) || 0;
if (!skuId) {
return [];
}
return [
{
cloudSkuId: skuId,
cloudSkuName: String(binding.skuName || "").trim(),
quantity: 1,
},
];
}
function normalizeDeliveryItem(value: unknown) {
const source = value && typeof value === "object" ? value as JsonObject : {};
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0;
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
return null;
}
const quantity = Number(source.quantity || 1) || 1;
return {
cloudSkuId,
cloudSkuName: String(source.cloudSkuName || source.skuName || "").trim(),
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
};
}
function mergeDeliveryItems(
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>
) {
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>();
for (const item of items) {
const existing = merged.get(item.cloudSkuId);
if (existing) {
existing.quantity += item.quantity;
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName;
continue;
}
merged.set(item.cloudSkuId, { ...item });
}
return Array.from(merged.values());
}
@@ -45,6 +45,16 @@ import {
} from "./task-context.js"; } from "./task-context.js";
import type { TaskRow } from "../../../types/repository/rows.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 { export {
isKuaishouCloudBindUrlFresh, isKuaishouCloudBindUrlFresh,
isKuaishouCloudTask, isKuaishouCloudTask,
@@ -167,8 +177,18 @@ export async function prepareKuaishouCloudFulfillmentTask(
}, },
}; };
const knapsackItem = resolvedBinding.knapsackItem; const deliveryPlan = resolveKuaishouCloudDeliveryPlan(flowWithResolvedBinding, {
let usedKnapsack = Number(knapsackItem?.count || 0) > 0; 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 purchaseTriggered = false;
let assetBefore = 0; let assetBefore = 0;
let assetAfter = 0; let assetAfter = 0;
@@ -182,11 +202,12 @@ export async function prepareKuaishouCloudFulfillmentTask(
} }
const asset = await getCloudtentaclesAsset(cloudContext); const asset = await getCloudtentaclesAsset(cloudContext);
const targetSku = resolvedBinding.skuItem; const missingSku = deliveryPlan.items.find(
(item) => item.missingCount > 0 && !item.skuItem
if (!targetSku) { );
if (missingSku) {
throw createHttpError( throw createHttpError(
`cloudtentacles 未找到 SKU ${flowWithResolvedBinding.binding.skuId}`, `cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`,
{ {
statusCode: 404, statusCode: 404,
errorCode: "kuaishou_cloud_sku_not_found", errorCode: "kuaishou_cloud_sku_not_found",
@@ -195,7 +216,11 @@ export async function prepareKuaishouCloudFulfillmentTask(
} }
assetBefore = Number(asset.asset || 0) || 0; assetBefore = Number(asset.asset || 0) || 0;
const targetPrice = Number(targetSku.price || 0) || 0; const targetPrice = deliveryPlan.items.reduce(
(sum, item) =>
sum + Math.max(0, item.missingCount) * Number(item.skuItem?.price || 0),
0
);
const requiredAsset = const requiredAsset =
targetPrice + flowWithResolvedBinding.purchase.minAssetReserve; targetPrice + flowWithResolvedBinding.purchase.minAssetReserve;
if (assetBefore < requiredAsset) { if (assetBefore < requiredAsset) {
@@ -204,7 +229,10 @@ export async function prepareKuaishouCloudFulfillmentTask(
flow: flowWithResolvedBinding, flow: flowWithResolvedBinding,
assetBefore, assetBefore,
requiredAsset, requiredAsset,
skuName: targetSku.name, skuName: deliveryPlan.items
.filter((item) => item.missingCount > 0)
.map((item) => item.cloudSkuName || `SKU ${item.cloudSkuId}`)
.join("、"),
}); });
throw createHttpError( throw createHttpError(
`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, `余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`,
@@ -215,11 +243,17 @@ export async function prepareKuaishouCloudFulfillmentTask(
); );
} }
await buyCloudtentaclesSku({ for (const item of deliveryPlan.items) {
...cloudContext, if (item.missingCount <= 0) {
id: flowWithResolvedBinding.binding.skuId, continue;
count: 1, }
});
await buyCloudtentaclesSku({
...cloudContext,
id: item.cloudSkuId,
count: item.missingCount,
});
}
purchaseTriggered = true; purchaseTriggered = true;
const assetResult = await getCloudtentaclesAsset(cloudContext); const assetResult = await getCloudtentaclesAsset(cloudContext);
@@ -265,6 +299,13 @@ export async function prepareKuaishouCloudFulfillmentTask(
purchaseTriggered, purchaseTriggered,
assetBefore, assetBefore,
assetAfter, 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 purchaseAt: purchaseTriggered
? now ? now
: flowWithResolvedBinding.purchase.purchaseAt, : flowWithResolvedBinding.purchase.purchaseAt,
@@ -296,6 +337,13 @@ export async function prepareKuaishouCloudFulfillmentTask(
vnPhoneMasked: maskPhone(preparedBinding.vnPhone), vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
purchaseTriggered, purchaseTriggered,
usedKnapsack, 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, resolvedByName: resolvedBinding.resolvedByName,
actor, actor,
}, },
@@ -766,6 +814,69 @@ export async function refreshKuaishouCloudTaskRoleInfo(
}; };
} }
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 { export {
dispatchKuaishouCloudFulfillmentTask, dispatchKuaishouCloudFulfillmentTask,
returnKuaishouCloudFulfillmentTask, returnKuaishouCloudFulfillmentTask,
@@ -23,6 +23,19 @@ import { syncKuaishouCloudRoleInfoBeforeDispatch } from "./dispatch-role-sync.js
import { normalizeActor, parseTaskContext } from "./task-context.js"; import { normalizeActor, parseTaskContext } from "./task-context.js";
import type { TaskRow } from "../../../types/repository/rows.js"; import type { TaskRow } from "../../../types/repository/rows.js";
type DispatchDeliveryItem = {
cloudSkuId: number;
cloudSkuName: string;
quantity: number;
};
type DispatchResultItem = DispatchDeliveryItem & {
unitIndex: number;
sendType: number;
note: string;
responseMessage: string;
};
export async function dispatchKuaishouCloudFulfillmentTask( export async function dispatchKuaishouCloudFulfillmentTask(
task: TaskRow, task: TaskRow,
options: JsonObject = {} options: JsonObject = {}
@@ -80,12 +93,51 @@ export async function dispatchKuaishouCloudFulfillmentTask(
const syncedFlow = synced.flow; const syncedFlow = synced.flow;
const syncedTaskContext = synced.taskContext; const syncedTaskContext = synced.taskContext;
const dispatchResult = await useCloudtentaclesSku({ const deliveryItems = resolveDispatchDeliveryItems(syncedFlow);
...cloudContext, if (deliveryItems.length === 0) {
id: syncedFlow.binding.skuId, throw createHttpError("当前任务缺少 cloud 发货物品配置", {
virtualNumberId: syncedFlow.binding.vnId, statusCode: 409,
phone: syncedFlow.binding.vnPhone, errorCode: "kuaishou_cloud_missing_delivery_items",
}); });
}
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: syncedFlow.binding.vnId,
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] || {
cloudSkuId: syncedFlow.binding.skuId,
cloudSkuName: syncedFlow.binding.skuName,
quantity: 1,
unitIndex: 1,
sendType: 0,
note: "",
responseMessage: "",
};
const dispatchSummary =
dispatchResults.length > 1
? `cloudtentacles 发货成功,共 ${dispatchResults.length}`
: String(
firstDispatchResult.responseMessage ||
firstDispatchResult.note ||
"cloudtentacles 发货成功"
).trim();
const nextContext = { const nextContext = {
...syncedTaskContext, ...syncedTaskContext,
@@ -102,12 +154,9 @@ export async function dispatchKuaishouCloudFulfillmentTask(
status: "success", status: "success",
dispatchAt: now, dispatchAt: now,
dispatchBy: actor, dispatchBy: actor,
sendType: Number(dispatchResult.sendType || 0) || 0, sendType: Number(firstDispatchResult.sendType || 0) || 0,
note: String( note: dispatchSummary,
dispatchResult.note || items: dispatchResults,
dispatchResult.responseMessage ||
"cloudtentacles 发货成功"
).trim(),
}, },
}, },
}; };
@@ -116,11 +165,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
task_status: "dispatched_pending_return", task_status: "dispatched_pending_return",
delivery_status: "delivered", delivery_status: "delivered",
result_code: "kuaishou_cloud_dispatched", result_code: "kuaishou_cloud_dispatched",
result_message: String( result_message: dispatchSummary,
dispatchResult.responseMessage ||
dispatchResult.note ||
"cloudtentacles 发货成功"
).trim(),
user_action_status: "not_required", user_action_status: "not_required",
last_error: "", last_error: "",
context_json: JSON.stringify(nextContext), context_json: JSON.stringify(nextContext),
@@ -142,8 +187,10 @@ export async function dispatchKuaishouCloudFulfillmentTask(
skuId: syncedFlow.binding.skuId, skuId: syncedFlow.binding.skuId,
vnId: syncedFlow.binding.vnId, vnId: syncedFlow.binding.vnId,
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone), vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
sendType: dispatchResult.sendType, sendType: firstDispatchResult.sendType,
note: dispatchResult.note, note: dispatchSummary,
deliveryItems,
dispatchResults,
actor, actor,
}, },
now now
@@ -173,6 +220,43 @@ export async function dispatchKuaishouCloudFulfillmentTask(
}; };
} }
function resolveDispatchDeliveryItems(flow: JsonObject): DispatchDeliveryItem[] {
const rawItems = Array.isArray(flow.deliveryItems) ? flow.deliveryItems : [];
const items = rawItems
.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 (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
return null;
}
return {
cloudSkuId,
cloudSkuName: String(source.cloudSkuName || source.skuName || "").trim(),
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
};
})
.filter((item): item is DispatchDeliveryItem => Boolean(item));
if (items.length > 0) {
return items;
}
const fallbackSkuId = Number(flow?.binding?.skuId || 0) || 0;
if (!fallbackSkuId) {
return [];
}
return [
{
cloudSkuId: fallbackSkuId,
cloudSkuName: String(flow?.binding?.skuName || "").trim(),
quantity: 1,
},
];
}
export async function returnKuaishouCloudFulfillmentTask( export async function returnKuaishouCloudFulfillmentTask(
task: TaskRow, task: TaskRow,
options: JsonObject = {} options: JsonObject = {}
@@ -131,6 +131,12 @@ export async function syncDeliveryTasksForOrderWithDeps(
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles) const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume) const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume)
const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop) const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop)
const deliveryItems = normalizeCloudDeliveryItems(cloudtentaclesConfig)
const primaryDeliveryItem = deliveryItems[0] || {
cloudSkuId: Number(cloudtentaclesConfig.skuId || 0) || 0,
cloudSkuName: String(cloudtentaclesConfig.skuName || '').trim(),
quantity: 1,
}
for (let index = 0; index < quantity; index += 1) { for (let index = 0; index < quantity; index += 1) {
const createdAt = getNowIso() const createdAt = getNowIso()
@@ -172,6 +178,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
configId: String(fulfillmentConfig.configId || '').trim(), configId: String(fulfillmentConfig.configId || '').trim(),
internalSkuCode: item.sku_code, internalSkuCode: item.sku_code,
internalSkuName: item.sku_name, internalSkuName: item.sku_name,
deliveryItems,
ticket: { ticket: {
code: '', code: '',
status: 'pending', status: 'pending',
@@ -186,8 +193,8 @@ export async function syncDeliveryTasksForOrderWithDeps(
binding: { binding: {
prepareStatus: 'pending', prepareStatus: 'pending',
cloudSourceKeys: normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys), cloudSourceKeys: normalizeStringArray(cloudtentaclesConfig.cloudSourceKeys),
skuId: Number(cloudtentaclesConfig.skuId || 0) || 0, skuId: primaryDeliveryItem.cloudSkuId,
skuName: String(cloudtentaclesConfig.skuName || '').trim(), skuName: primaryDeliveryItem.cloudSkuName,
vnKey: '1', vnKey: '1',
vnId: 0, vnId: 0,
vnPhone: '', vnPhone: '',
@@ -394,6 +401,68 @@ function normalizeStringArray(value: unknown): string[] {
) )
} }
function normalizeCloudDeliveryItems(value: JsonObject): Array<{
cloudSkuId: number
cloudSkuName: string
quantity: number
}> {
const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : []
const items = rawItems
.map((item) => normalizeCloudDeliveryItem(item))
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } => Boolean(item))
if (items.length > 0) {
return mergeCloudDeliveryItems(items)
}
const cloudSkuId = Number(value.skuId || 0) || 0
if (!cloudSkuId) {
return []
}
return [{
cloudSkuId,
cloudSkuName: String(value.skuName || '').trim(),
quantity: 1,
}]
}
function normalizeCloudDeliveryItem(value: unknown) {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
return null
}
const quantity = Number(source.quantity || 1) || 1
return {
cloudSkuId,
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
}
}
function mergeCloudDeliveryItems(
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>,
) {
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>()
for (const item of items) {
const existing = merged.get(item.cloudSkuId)
if (existing) {
existing.quantity += item.quantity
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
continue
}
merged.set(item.cloudSkuId, { ...item })
}
return Array.from(merged.values())
}
function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow { function isTaskRow(task: TaskRow | DeliveryTaskRow | null | undefined): task is TaskRow {
return Boolean(task && Number(task.id || 0) > 0) return Boolean(task && Number(task.id || 0) > 0)
} }
@@ -32,6 +32,13 @@ test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => {
profileKey: 'kuaishou_ct_assisted', profileKey: 'kuaishou_ct_assisted',
enabled: true, enabled: true,
priority: 100, priority: 100,
deliveryItems: [
{
cloudSkuId: 28,
cloudSkuName: '',
quantity: 1,
},
],
config: { config: {
flowType: 'kuaishou_cloud_fulfillment', flowType: 'kuaishou_cloud_fulfillment',
configId: '', configId: '',
@@ -39,6 +46,13 @@ test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => {
cloudSourceKeys: ['account_a', 'account_b'], cloudSourceKeys: ['account_a', 'account_b'],
skuId: 28, skuId: 28,
skuName: '', skuName: '',
deliveryItems: [
{
cloudSkuId: 28,
cloudSkuName: '',
quantity: 1,
},
],
vnKey: '1', vnKey: '1',
autoBuyEnabled: true, autoBuyEnabled: true,
minAssetReserve: 0, minAssetReserve: 0,
@@ -63,3 +77,32 @@ test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => {
], ],
) )
}) })
test('mapKuaishouCloudFulfillmentItemsToBindings maps package delivery items', () => {
const [binding] = mapKuaishouCloudFulfillmentItemsToBindings({
items: [
{
enabled: true,
internalSkuCode: 'package-1',
internalSkuName: '套餐 1',
externalSkuCode: 'P001',
cloudSourceKeys: ['account_a'],
deliveryItems: [
{ cloudSkuId: 73, cloudSkuName: '物品 A', quantity: 1 },
{ cloudSkuId: 76, cloudSkuName: '物品 B', quantity: 50 },
],
},
],
})
assert.deepEqual(binding?.deliveryItems, [
{ cloudSkuId: 73, cloudSkuName: '物品 A', quantity: 1 },
{ cloudSkuId: 76, cloudSkuName: '物品 B', quantity: 50 },
])
assert.deepEqual(binding?.config.cloudtentacles.deliveryItems, [
{ cloudSkuId: 73, cloudSkuName: '物品 A', quantity: 1 },
{ cloudSkuId: 76, cloudSkuName: '物品 B', quantity: 50 },
])
assert.equal(binding?.config.cloudtentacles.skuId, 73)
assert.equal(binding?.config.cloudtentacles.skuName, '物品 A')
})
@@ -12,6 +12,12 @@ const KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH = path.join(
type JsonObject = Record<string, any>; type JsonObject = Record<string, any>;
type DeliveryItem = {
cloudSkuId: number;
cloudSkuName: string;
quantity: number;
};
export function getKuaishouCloudFulfillmentFilePath() { export function getKuaishouCloudFulfillmentFilePath() {
return KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH; return KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH;
} }
@@ -42,13 +48,19 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config: JsonObject =
profileKey: "kuaishou_ct_assisted", profileKey: "kuaishou_ct_assisted",
enabled: item.enabled !== false, enabled: item.enabled !== false,
priority: normalizePriority(item.priority), priority: normalizePriority(item.priority),
deliveryItems: normalizeDeliveryItems(item),
config: { config: {
flowType: "kuaishou_cloud_fulfillment", flowType: "kuaishou_cloud_fulfillment",
configId: String(item.id || "").trim(), configId: String(item.id || "").trim(),
cloudtentacles: { cloudtentacles: {
cloudSourceKeys: normalizeStringArray(item.cloudSourceKeys), cloudSourceKeys: normalizeStringArray(item.cloudSourceKeys),
skuId: normalizePositiveInteger(item.cloudSkuId), skuId:
skuName: String(item.cloudSkuName || "").trim(), normalizeDeliveryItems(item)[0]?.cloudSkuId ||
normalizePositiveInteger(item.cloudSkuId),
skuName:
normalizeDeliveryItems(item)[0]?.cloudSkuName ||
String(item.cloudSkuName || "").trim(),
deliveryItems: normalizeDeliveryItems(item),
vnKey: "1", vnKey: "1",
autoBuyEnabled: item.autoBuyEnabled !== false, autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: normalizeNonNegativeInteger(item.minAssetReserve, 0), minAssetReserve: normalizeNonNegativeInteger(item.minAssetReserve, 0),
@@ -109,6 +121,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) {
const internalSkuCode = String(rawValue.internalSkuCode || "").trim(); const internalSkuCode = String(rawValue.internalSkuCode || "").trim();
const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId); const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId);
const deliveryItems = normalizeDeliveryItems(rawValue);
const cloudSourceKeys = normalizeStringArray(rawValue.cloudSourceKeys); const cloudSourceKeys = normalizeStringArray(rawValue.cloudSourceKeys);
const externalSkuCode = String(rawValue.externalSkuCode || "").trim(); const externalSkuCode = String(rawValue.externalSkuCode || "").trim();
const externalItemId = String(rawValue.externalItemId || "").trim(); const externalItemId = String(rawValue.externalItemId || "").trim();
@@ -128,7 +141,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) {
return null; return null;
} }
if (!cloudSkuId) { if (deliveryItems.length === 0) {
return null; return null;
} }
@@ -157,8 +170,11 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) {
rawValue.resolvedSkuName || rawValue.internalSkuName || "" rawValue.resolvedSkuName || rawValue.internalSkuName || ""
).trim(), ).trim(),
cloudSourceKeys, cloudSourceKeys,
cloudSkuId, cloudSkuId: deliveryItems[0]?.cloudSkuId || cloudSkuId,
cloudSkuName: String(rawValue.cloudSkuName || "").trim(), cloudSkuName:
deliveryItems[0]?.cloudSkuName ||
String(rawValue.cloudSkuName || "").trim(),
deliveryItems,
vnKey: "1", vnKey: "1",
autoBuyEnabled: rawValue.autoBuyEnabled !== false, autoBuyEnabled: rawValue.autoBuyEnabled !== false,
minAssetReserve: normalizeNonNegativeInteger(rawValue.minAssetReserve, 0), minAssetReserve: normalizeNonNegativeInteger(rawValue.minAssetReserve, 0),
@@ -185,6 +201,64 @@ function normalizeNonNegativeInteger(value: unknown, fallback: number) {
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback; return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
} }
function normalizeDeliveryItems(value: JsonObject): DeliveryItem[] {
const rawItems = Array.isArray(value.deliveryItems) ? value.deliveryItems : [];
const items = rawItems
.map((item: unknown) => normalizeDeliveryItem(item))
.filter(Boolean) as DeliveryItem[];
if (items.length > 0) {
return mergeDeliveryItems(items);
}
const cloudSkuId = normalizePositiveInteger(value.cloudSkuId);
if (!cloudSkuId) {
return [];
}
return [
{
cloudSkuId,
cloudSkuName: String(value.cloudSkuName || "").trim(),
quantity: 1,
},
];
}
function normalizeDeliveryItem(value: unknown): DeliveryItem | null {
if (!isPlainObject(value)) {
return null;
}
const cloudSkuId = normalizePositiveInteger(value.cloudSkuId || value.skuId);
if (!cloudSkuId) {
return null;
}
return {
cloudSkuId,
cloudSkuName: String(value.cloudSkuName || value.skuName || "").trim(),
quantity: normalizePositiveInteger(value.quantity) || 1,
};
}
function mergeDeliveryItems(items: DeliveryItem[]): DeliveryItem[] {
const merged = new Map<number, DeliveryItem>();
for (const item of items) {
const existing = merged.get(item.cloudSkuId);
if (existing) {
existing.quantity += item.quantity;
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName;
continue;
}
merged.set(item.cloudSkuId, { ...item });
}
return Array.from(merged.values());
}
function normalizePriority(value: unknown) { function normalizePriority(value: unknown) {
const parsed = Number(value); const parsed = Number(value);
if (!Number.isFinite(parsed)) { if (!Number.isFinite(parsed)) {
+1
View File
@@ -66,6 +66,7 @@ export type {
AdminCloudtentaclesSkuListResult, AdminCloudtentaclesSkuListResult,
AdminCloudtentaclesDeliveryRecordItem, AdminCloudtentaclesDeliveryRecordItem,
AdminCloudtentaclesDeliveryRecordListResult, AdminCloudtentaclesDeliveryRecordListResult,
AdminKuaishouCloudDeliveryItem,
AdminKuaishouCloudFulfillmentItem, AdminKuaishouCloudFulfillmentItem,
AdminKuaishouCloudFulfillmentConfig, AdminKuaishouCloudFulfillmentConfig,
} from './platform-config' } from './platform-config'
@@ -49,6 +49,7 @@ export type {
} from './cloudtentacles' } from './cloudtentacles'
export type { export type {
AdminKuaishouCloudDeliveryItem,
AdminKuaishouCloudFulfillmentItem, AdminKuaishouCloudFulfillmentItem,
AdminKuaishouCloudFulfillmentConfig, AdminKuaishouCloudFulfillmentConfig,
} from './kuaishou-cloud-fulfillment' } from './kuaishou-cloud-fulfillment'
@@ -1,3 +1,9 @@
export interface AdminKuaishouCloudDeliveryItem {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
export interface AdminKuaishouCloudFulfillmentItem { export interface AdminKuaishouCloudFulfillmentItem {
id: string id: string
enabled: boolean enabled: boolean
@@ -14,6 +20,7 @@ export interface AdminKuaishouCloudFulfillmentItem {
cloudSourceKeys: string[] cloudSourceKeys: string[]
cloudSkuId: number cloudSkuId: number
cloudSkuName: string cloudSkuName: string
deliveryItems: AdminKuaishouCloudDeliveryItem[]
vnKey: string vnKey: string
autoBuyEnabled: boolean autoBuyEnabled: boolean
minAssetReserve: number minAssetReserve: number
@@ -429,6 +429,34 @@
padding: var(--space-4); padding: var(--space-4);
} }
.delivery-items-editor {
display: grid;
gap: var(--space-3);
padding: var(--space-4);
}
.delivery-items-head {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: center;
color: var(--text-primary);
font-weight: 700;
}
.delivery-item-row {
display: grid;
grid-template-columns: minmax(220px, 1fr) minmax(160px, 0.7fr) 132px 48px;
gap: var(--space-3);
align-items: center;
}
.delivery-item-sku,
.delivery-item-name,
.delivery-item-count {
width: 100%;
}
.field-wide { .field-wide {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
@@ -501,6 +529,7 @@
.rule-editor-main, .rule-editor-main,
.strategy-grid, .strategy-grid,
.mapping-editor, .mapping-editor,
.delivery-item-row,
.mapping-grid, .mapping-grid,
.mapping-extra { .mapping-extra {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -13,6 +13,25 @@ const config = useKuaishouCloudConfig()
const ninetyone = useKuaishouCloudNinetyone(config) const ninetyone = useKuaishouCloudNinetyone(config)
const cloudSku = useKuaishouCloudSku(config.cloudtentaclesSourceOptions) const cloudSku = useKuaishouCloudSku(config.cloudtentaclesSourceOptions)
function handlePrimaryCloudSkuSelected(item: (typeof config.items.value)[number], value: number | string | undefined) {
cloudSku.handleCloudSkuSelected(item, value)
config.syncDeliveryItemsFromPrimaryCloudSku(item)
}
function handleDeliveryItemCloudSkuSelected(
item: (typeof config.items.value)[number],
index: number,
value: number | string | undefined,
) {
const deliveryItem = item.deliveryItems[index]
if (!deliveryItem) {
return
}
cloudSku.handleDeliveryItemCloudSkuSelected(item, deliveryItem, value)
config.syncPrimaryCloudSkuFromDeliveryItems(item)
}
onMounted(config.loadConfigs) onMounted(config.loadConfigs)
</script> </script>
@@ -61,6 +80,7 @@ onMounted(config.loadConfigs)
:get-cloud-source-summary="config.getCloudSourceSummary" :get-cloud-source-summary="config.getCloudSourceSummary"
:get-cloud-sku-summary="config.getCloudSkuSummary" :get-cloud-sku-summary="config.getCloudSkuSummary"
:get-consume-shop-summary="config.getConsumeShopSummary" :get-consume-shop-summary="config.getConsumeShopSummary"
:get-delivery-plan-summary="config.getDeliveryPlanSummary"
:get-cloud-sku-options="cloudSku.getCloudSkuOptions" :get-cloud-sku-options="cloudSku.getCloudSkuOptions"
:format-cloud-sku-option-label="cloudSku.formatCloudSkuOptionLabel" :format-cloud-sku-option-label="cloudSku.formatCloudSkuOptionLabel"
:get-kuaishou-consume-shop-options="config.getKuaishouConsumeShopOptions" :get-kuaishou-consume-shop-options="config.getKuaishouConsumeShopOptions"
@@ -74,12 +94,15 @@ onMounted(config.loadConfigs)
@add-item="config.addItem()" @add-item="config.addItem()"
@save-configs="config.saveConfigs()" @save-configs="config.saveConfigs()"
@toggle-collapsed="config.toggleCollapsed($event)" @toggle-collapsed="config.toggleCollapsed($event)"
@handle-cloud-sku-selected="(item, value) => cloudSku.handleCloudSkuSelected(item, value)" @handle-cloud-sku-selected="handlePrimaryCloudSkuSelected"
@handle-delivery-item-cloud-sku-selected="handleDeliveryItemCloudSkuSelected"
@handle-cloud-sku-dropdown-visible=" @handle-cloud-sku-dropdown-visible="
(item, visible) => cloudSku.handleCloudSkuDropdownVisible(item, visible) (item, visible) => cloudSku.handleCloudSkuDropdownVisible(item, visible)
" "
@handle-kuaishou-consume-shop-selected="config.handleKuaishouConsumeShopSelected($event)" @handle-kuaishou-consume-shop-selected="config.handleKuaishouConsumeShopSelected($event)"
@handle-cloud-source-keys-changed="config.handleCloudSourceKeysChanged($event)" @handle-cloud-source-keys-changed="config.handleCloudSourceKeysChanged($event)"
@add-delivery-item="config.addDeliveryItem($event)"
@remove-delivery-item="(item, index) => config.removeDeliveryItem(item, index)"
@remove-item="config.removeItem($event)" @remove-item="config.removeItem($event)"
/> />
</template> </template>
@@ -19,6 +19,7 @@ defineProps<{
getCloudSourceSummary: (item: EditableItem) => string getCloudSourceSummary: (item: EditableItem) => string
getCloudSkuSummary: (item: EditableItem) => string getCloudSkuSummary: (item: EditableItem) => string
getConsumeShopSummary: (item: EditableItem) => string getConsumeShopSummary: (item: EditableItem) => string
getDeliveryPlanSummary: (item: EditableItem) => string
getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[] getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[]
formatCloudSkuOptionLabel: (sku: AdminCloudtentaclesSkuItem) => string formatCloudSkuOptionLabel: (sku: AdminCloudtentaclesSkuItem) => string
getKuaishouConsumeShopOptions: () => AdminKuaishouEticketShopConfigItem[] getKuaishouConsumeShopOptions: () => AdminKuaishouEticketShopConfigItem[]
@@ -28,11 +29,21 @@ defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
toggleCollapsed: [] toggleCollapsed: []
handleCloudSkuSelected: [value: number | string | undefined] handleCloudSkuSelected: [value: number | string | undefined]
handleDeliveryItemCloudSkuSelected: [index: number, value: number | string | undefined]
handleCloudSkuDropdownVisible: [visible: boolean] handleCloudSkuDropdownVisible: [visible: boolean]
handleKuaishouConsumeShopSelected: [] handleKuaishouConsumeShopSelected: []
handleCloudSourceKeysChanged: [] handleCloudSourceKeysChanged: []
addDeliveryItem: []
removeDeliveryItem: [index: number]
remove: [] remove: []
}>() }>()
function isAdvancedDeliveryConfigured(item: EditableItem) {
return (
item.deliveryItems.length > 1 ||
item.deliveryItems.some((deliveryItem) => Number(deliveryItem.quantity || 1) > 1)
)
}
</script> </script>
<template> <template>
@@ -151,6 +162,7 @@ const emit = defineEmits<{
v-model="item.cloudSkuId" v-model="item.cloudSkuId"
class="text-input" class="text-input"
placeholder="请选择 cloud SKU" placeholder="请选择 cloud SKU"
:disabled="isAdvancedDeliveryConfigured(item)"
@change="emit('handleCloudSkuSelected', item.cloudSkuId)" @change="emit('handleCloudSkuSelected', item.cloudSkuId)"
@visible-change="emit('handleCloudSkuDropdownVisible', $event)" @visible-change="emit('handleCloudSkuDropdownVisible', $event)"
> >
@@ -168,6 +180,7 @@ const emit = defineEmits<{
:model-value="item.cloudSkuName" :model-value="item.cloudSkuName"
class="text-input" class="text-input"
placeholder="选择后自动带出" placeholder="选择后自动带出"
disabled
readonly readonly
/> />
</el-form-item> </el-form-item>
@@ -229,6 +242,61 @@ const emit = defineEmits<{
</el-form> </el-form>
</section> </section>
<details class="advanced-fields">
<summary>
<span>高级履约配置</span>
<small>{{ getDeliveryPlanSummary(item) }}</small>
</summary>
<div class="delivery-items-editor">
<div class="delivery-items-head">
<span>发货物品明细</span>
<el-button size="small" @click="emit('addDeliveryItem')">添加物品</el-button>
</div>
<div
v-for="(deliveryItem, deliveryIndex) in item.deliveryItems"
:key="`${item.localId}-${deliveryIndex}`"
class="delivery-item-row"
>
<el-select
v-model="deliveryItem.cloudSkuId"
class="delivery-item-sku"
placeholder="请选择 cloud SKU"
@change="emit('handleDeliveryItemCloudSkuSelected', deliveryIndex, deliveryItem.cloudSkuId)"
@visible-change="emit('handleCloudSkuDropdownVisible', $event)"
>
<el-option label="请选择 cloud SKU" :value="0" />
<el-option
v-for="sku in getCloudSkuOptions(item)"
:key="sku.id"
:label="formatCloudSkuOptionLabel(sku)"
:value="sku.id"
/>
</el-select>
<el-input
:model-value="deliveryItem.cloudSkuName || '-'"
class="delivery-item-name"
readonly
/>
<el-input-number
v-model="deliveryItem.quantity"
class="delivery-item-count"
:min="1"
:max="999"
controls-position="right"
/>
<el-button
link
type="danger"
:disabled="item.deliveryItems.length <= 1"
@click="emit('removeDeliveryItem', deliveryIndex)"
>
删除
</el-button>
</div>
<small class="field-help">同一个用户只绑定一次角色系统会按这里的物品和数量依次发货</small>
</div>
</details>
<details class="advanced-fields"> <details class="advanced-fields">
<summary> <summary>
<span>高级匹配与备注</span> <span>高级匹配与备注</span>
@@ -24,6 +24,7 @@ defineProps<{
getCloudSourceSummary: (item: EditableItem) => string getCloudSourceSummary: (item: EditableItem) => string
getCloudSkuSummary: (item: EditableItem) => string getCloudSkuSummary: (item: EditableItem) => string
getConsumeShopSummary: (item: EditableItem) => string getConsumeShopSummary: (item: EditableItem) => string
getDeliveryPlanSummary: (item: EditableItem) => string
getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[] getCloudSkuOptions: (item: EditableItem) => AdminCloudtentaclesSkuItem[]
formatCloudSkuOptionLabel: (sku: AdminCloudtentaclesSkuItem) => string formatCloudSkuOptionLabel: (sku: AdminCloudtentaclesSkuItem) => string
getKuaishouConsumeShopOptions: () => AdminKuaishouEticketShopConfigItem[] getKuaishouConsumeShopOptions: () => AdminKuaishouEticketShopConfigItem[]
@@ -41,9 +42,12 @@ const emit = defineEmits<{
saveConfigs: [] saveConfigs: []
toggleCollapsed: [localId: string] toggleCollapsed: [localId: string]
handleCloudSkuSelected: [item: EditableItem, value: number | string | undefined] handleCloudSkuSelected: [item: EditableItem, value: number | string | undefined]
handleDeliveryItemCloudSkuSelected: [item: EditableItem, index: number, value: number | string | undefined]
handleCloudSkuDropdownVisible: [item: EditableItem, visible: boolean] handleCloudSkuDropdownVisible: [item: EditableItem, visible: boolean]
handleKuaishouConsumeShopSelected: [item: EditableItem] handleKuaishouConsumeShopSelected: [item: EditableItem]
handleCloudSourceKeysChanged: [item: EditableItem] handleCloudSourceKeysChanged: [item: EditableItem]
addDeliveryItem: [item: EditableItem]
removeDeliveryItem: [item: EditableItem, index: number]
removeItem: [localId: string] removeItem: [localId: string]
}>() }>()
</script> </script>
@@ -130,15 +134,21 @@ const emit = defineEmits<{
:get-cloud-source-summary="getCloudSourceSummary" :get-cloud-source-summary="getCloudSourceSummary"
:get-cloud-sku-summary="getCloudSkuSummary" :get-cloud-sku-summary="getCloudSkuSummary"
:get-consume-shop-summary="getConsumeShopSummary" :get-consume-shop-summary="getConsumeShopSummary"
:get-delivery-plan-summary="getDeliveryPlanSummary"
:get-cloud-sku-options="getCloudSkuOptions" :get-cloud-sku-options="getCloudSkuOptions"
:format-cloud-sku-option-label="formatCloudSkuOptionLabel" :format-cloud-sku-option-label="formatCloudSkuOptionLabel"
:get-kuaishou-consume-shop-options="getKuaishouConsumeShopOptions" :get-kuaishou-consume-shop-options="getKuaishouConsumeShopOptions"
:format-kuaishou-consume-shop-option="formatKuaishouConsumeShopOption" :format-kuaishou-consume-shop-option="formatKuaishouConsumeShopOption"
@toggle-collapsed="emit('toggleCollapsed', item.localId)" @toggle-collapsed="emit('toggleCollapsed', item.localId)"
@handle-cloud-sku-selected="emit('handleCloudSkuSelected', item, $event)" @handle-cloud-sku-selected="emit('handleCloudSkuSelected', item, $event)"
@handle-delivery-item-cloud-sku-selected="
(index, value) => emit('handleDeliveryItemCloudSkuSelected', item, index, value)
"
@handle-cloud-sku-dropdown-visible="emit('handleCloudSkuDropdownVisible', item, $event)" @handle-cloud-sku-dropdown-visible="emit('handleCloudSkuDropdownVisible', item, $event)"
@handle-kuaishou-consume-shop-selected="emit('handleKuaishouConsumeShopSelected', item)" @handle-kuaishou-consume-shop-selected="emit('handleKuaishouConsumeShopSelected', item)"
@handle-cloud-source-keys-changed="emit('handleCloudSourceKeysChanged', item)" @handle-cloud-source-keys-changed="emit('handleCloudSourceKeysChanged', item)"
@add-delivery-item="emit('addDeliveryItem', item)"
@remove-delivery-item="emit('removeDeliveryItem', item, $event)"
@remove="emit('removeItem', item.localId)" @remove="emit('removeItem', item.localId)"
/> />
</el-card> </el-card>
@@ -8,6 +8,7 @@ import {
saveAdminKuaishouCloudFulfillmentConfig, saveAdminKuaishouCloudFulfillmentConfig,
} from '@/services/admin' } from '@/services/admin'
import type { import type {
AdminKuaishouCloudDeliveryItem,
AdminKuaishouCloudFulfillmentConfig, AdminKuaishouCloudFulfillmentConfig,
AdminKuaishouCloudFulfillmentItem, AdminKuaishouCloudFulfillmentItem,
AdminKuaishouEticketShopConfigItem, AdminKuaishouEticketShopConfigItem,
@@ -79,6 +80,7 @@ export function useKuaishouCloudConfig() {
cloudSourceKeys: sourceKeys, cloudSourceKeys: sourceKeys,
cloudSkuId: 0, cloudSkuId: 0,
cloudSkuName: '', cloudSkuName: '',
deliveryItems: [],
vnKey: '1', vnKey: '1',
autoBuyEnabled: true, autoBuyEnabled: true,
minAssetReserve: 0, minAssetReserve: 0,
@@ -96,6 +98,8 @@ export function useKuaishouCloudConfig() {
item.kuaishouConsumeShopName, item.kuaishouConsumeShopName,
) )
const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys)
const deliveryItems = normalizeDeliveryItems(item)
const primaryDeliveryItem = deliveryItems[0]
return { return {
localId: crypto.randomUUID(), localId: crypto.randomUUID(),
id: item.id || crypto.randomUUID(), id: item.id || crypto.randomUUID(),
@@ -111,8 +115,9 @@ export function useKuaishouCloudConfig() {
externalSkuName: item.externalSkuName || '', externalSkuName: item.externalSkuName || '',
resolvedSkuName: item.resolvedSkuName || '', resolvedSkuName: item.resolvedSkuName || '',
cloudSourceKeys: sourceKeys, cloudSourceKeys: sourceKeys,
cloudSkuId: Number(item.cloudSkuId || 0) || 0, cloudSkuId: primaryDeliveryItem?.cloudSkuId || Number(item.cloudSkuId || 0) || 0,
cloudSkuName: item.cloudSkuName || '', cloudSkuName: primaryDeliveryItem?.cloudSkuName || item.cloudSkuName || '',
deliveryItems,
vnKey: item.vnKey || '', vnKey: item.vnKey || '',
autoBuyEnabled: item.autoBuyEnabled !== false, autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: Number(item.minAssetReserve || 0) || 0, minAssetReserve: Number(item.minAssetReserve || 0) || 0,
@@ -130,6 +135,8 @@ export function useKuaishouCloudConfig() {
item.kuaishouConsumeShopName, item.kuaishouConsumeShopName,
) )
const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys) const sourceKeys = normalizeCloudSourceKeys(item.cloudSourceKeys)
const deliveryItems = normalizeDeliveryItems(item)
const primaryDeliveryItem = deliveryItems[0]
return { return {
id: item.id.trim() || item.internalSkuCode.trim() || item.localId, id: item.id.trim() || item.internalSkuCode.trim() || item.localId,
enabled: item.enabled, enabled: item.enabled,
@@ -144,8 +151,9 @@ export function useKuaishouCloudConfig() {
externalSkuName: item.externalSkuName.trim(), externalSkuName: item.externalSkuName.trim(),
resolvedSkuName: item.resolvedSkuName.trim(), resolvedSkuName: item.resolvedSkuName.trim(),
cloudSourceKeys: sourceKeys, cloudSourceKeys: sourceKeys,
cloudSkuId: Number(item.cloudSkuId || 0) || 0, cloudSkuId: primaryDeliveryItem?.cloudSkuId || Number(item.cloudSkuId || 0) || 0,
cloudSkuName: item.cloudSkuName.trim(), cloudSkuName: primaryDeliveryItem?.cloudSkuName || item.cloudSkuName.trim(),
deliveryItems,
vnKey: item.vnKey.trim(), vnKey: item.vnKey.trim(),
autoBuyEnabled: item.autoBuyEnabled, autoBuyEnabled: item.autoBuyEnabled,
minAssetReserve: Math.max(0, Number(item.minAssetReserve || 0) || 0), minAssetReserve: Math.max(0, Number(item.minAssetReserve || 0) || 0),
@@ -170,6 +178,64 @@ export function useKuaishouCloudConfig() {
return normalizeCloudSourceKeys([cloudtentaclesSourceOptions.value[0]?.key]) return normalizeCloudSourceKeys([cloudtentaclesSourceOptions.value[0]?.key])
} }
function normalizeDeliveryItems(
item: Pick<EditableItem, 'deliveryItems' | 'cloudSkuId' | 'cloudSkuName'> | AdminKuaishouCloudFulfillmentItem,
): AdminKuaishouCloudDeliveryItem[] {
const rawItems = Array.isArray(item.deliveryItems) ? item.deliveryItems : []
const normalizedItems = rawItems
.map((value) => normalizeDeliveryItem(value))
.filter((value): value is AdminKuaishouCloudDeliveryItem => Boolean(value))
if (normalizedItems.length > 0) {
return mergeDeliveryItems(normalizedItems)
}
const cloudSkuId = Number(item.cloudSkuId || 0) || 0
if (cloudSkuId <= 0) {
return []
}
return [
{
cloudSkuId,
cloudSkuName: String(item.cloudSkuName || '').trim(),
quantity: 1,
},
]
}
function normalizeDeliveryItem(value: unknown): AdminKuaishouCloudDeliveryItem | null {
const source = value && typeof value === 'object' ? (value as Partial<AdminKuaishouCloudDeliveryItem>) : {}
const cloudSkuId = Number(source.cloudSkuId || 0) || 0
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
return null
}
const quantity = Number(source.quantity || 1) || 1
return {
cloudSkuId,
cloudSkuName: String(source.cloudSkuName || '').trim(),
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
}
}
function mergeDeliveryItems(items: AdminKuaishouCloudDeliveryItem[]) {
const merged = new Map<number, AdminKuaishouCloudDeliveryItem>()
for (const item of items) {
const existing = merged.get(item.cloudSkuId)
if (existing) {
existing.quantity += item.quantity
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
continue
}
merged.set(item.cloudSkuId, { ...item })
}
return Array.from(merged.values())
}
// ── validation helpers ──────────────────────────────── // ── validation helpers ────────────────────────────────
function hasExternalMatch( function hasExternalMatch(
@@ -191,6 +257,7 @@ export function useKuaishouCloudConfig() {
item.cloudSourceKeys.length > 0 || item.cloudSourceKeys.length > 0 ||
Number(item.cloudSkuId || 0) > 0 || Number(item.cloudSkuId || 0) > 0 ||
item.cloudSkuName.trim() || item.cloudSkuName.trim() ||
normalizeDeliveryItems(item).length > 0 ||
item.vnKey.trim() || item.vnKey.trim() ||
item.shopId.trim() || item.shopId.trim() ||
item.notes.trim() || item.notes.trim() ||
@@ -209,9 +276,9 @@ export function useKuaishouCloudConfig() {
function isItemComplete(item: EditableItem) { function isItemComplete(item: EditableItem) {
return Boolean( return Boolean(
item.internalSkuCode.trim() && item.internalSkuCode.trim() &&
item.cloudSourceKeys.length > 0 && item.cloudSourceKeys.length > 0 &&
Number(item.cloudSkuId || 0) > 0 && normalizeDeliveryItems(item).length > 0 &&
(!item.autoConsumeAfterDispatch || Boolean(item.kuaishouConsumeShopId.trim())) && (!item.autoConsumeAfterDispatch || Boolean(item.kuaishouConsumeShopId.trim())) &&
hasExternalMatch(item), hasExternalMatch(item),
) )
@@ -232,8 +299,16 @@ export function useKuaishouCloudConfig() {
return { localId: item.localId, message: `${title} 需要选择至少一个 cloud 账号` } return { localId: item.localId, message: `${title} 需要选择至少一个 cloud 账号` }
} }
if (Number(item.cloudSkuId || 0) <= 0) { const deliveryItems = normalizeDeliveryItems(item)
return { localId: item.localId, message: `${title} 需要填写 cloud SKU ID` } if (deliveryItems.length === 0) {
return { localId: item.localId, message: `${title} 需要至少配置 1 个 cloud 发货物品` }
}
const invalidDeliveryItem = deliveryItems.find(
(deliveryItem) => Number(deliveryItem.quantity || 0) <= 0,
)
if (invalidDeliveryItem) {
return { localId: item.localId, message: `${title} 的发货数量必须大于 0` }
} }
if (!hasExternalMatch(item)) { if (!hasExternalMatch(item)) {
@@ -263,7 +338,7 @@ export function useKuaishouCloudConfig() {
item.provider.trim() || '91kaquan', item.provider.trim() || '91kaquan',
item.platform.trim() || 'kuaishou', item.platform.trim() || 'kuaishou',
item.shopId.trim() || '跨店铺', item.shopId.trim() || '跨店铺',
item.cloudSkuId > 0 ? `cloud#${item.cloudSkuId}` : '待填 cloud SKU', getDeliveryPlanSummary(item),
] ]
return parts.join(' / ') return parts.join(' / ')
} }
@@ -367,17 +442,45 @@ export function useKuaishouCloudConfig() {
} }
function getCloudSkuSummary(item: EditableItem) { function getCloudSkuSummary(item: EditableItem) {
if (item.cloudSkuId > 0 && item.cloudSkuName.trim()) { const deliveryItems = normalizeDeliveryItems(item)
return `${item.cloudSkuName.trim()} · #${item.cloudSkuId}` if (deliveryItems.length > 1) {
return `${deliveryItems.length} 个物品 / 共 ${getDeliveryTotalQuantity(item)}`
} }
if (item.cloudSkuId > 0) { const primaryItem = deliveryItems[0]
return `cloud SKU #${item.cloudSkuId}` if (primaryItem?.cloudSkuId && primaryItem.cloudSkuName.trim()) {
const suffix = primaryItem.quantity > 1 ? ` × ${primaryItem.quantity}` : ''
return `${primaryItem.cloudSkuName.trim()} · #${primaryItem.cloudSkuId}${suffix}`
}
if (primaryItem?.cloudSkuId) {
const suffix = primaryItem.quantity > 1 ? ` × ${primaryItem.quantity}` : ''
return `cloud SKU #${primaryItem.cloudSkuId}${suffix}`
} }
return '待选择 cloud SKU' return '待选择 cloud SKU'
} }
function getDeliveryPlanSummary(item: EditableItem) {
const deliveryItems = normalizeDeliveryItems(item)
if (deliveryItems.length === 0) {
return '待填发货物品'
}
if (deliveryItems.length === 1) {
const firstItem = deliveryItems[0]
return firstItem.quantity > 1
? `cloud#${firstItem.cloudSkuId} × ${firstItem.quantity}`
: `cloud#${firstItem.cloudSkuId}`
}
return `${deliveryItems.length} 个物品 / ${getDeliveryTotalQuantity(item)}`
}
function getDeliveryTotalQuantity(item: EditableItem) {
return normalizeDeliveryItems(item).reduce((sum, deliveryItem) => sum + deliveryItem.quantity, 0)
}
// ── collapse state ──────────────────────────────────── // ── collapse state ────────────────────────────────────
function isCollapsed(localId: string) { function isCollapsed(localId: string) {
@@ -472,6 +575,49 @@ export function useKuaishouCloudConfig() {
item.cloudSourceKeys = sourceKeys item.cloudSourceKeys = sourceKeys
item.cloudSkuId = 0 item.cloudSkuId = 0
item.cloudSkuName = '' item.cloudSkuName = ''
item.deliveryItems = []
}
function addDeliveryItem(item: EditableItem) {
item.deliveryItems.push({
cloudSkuId: 0,
cloudSkuName: '',
quantity: 1,
})
}
function removeDeliveryItem(item: EditableItem, index: number) {
item.deliveryItems.splice(index, 1)
syncPrimaryCloudSkuFromDeliveryItems(item)
}
function syncPrimaryCloudSkuFromDeliveryItems(item: EditableItem) {
const primaryItem = normalizeDeliveryItems(item)[0]
item.cloudSkuId = primaryItem?.cloudSkuId || 0
item.cloudSkuName = primaryItem?.cloudSkuName || ''
}
function syncDeliveryItemsFromPrimaryCloudSku(item: EditableItem) {
if (Number(item.cloudSkuId || 0) <= 0) {
item.deliveryItems = []
return
}
if (item.deliveryItems.length === 0) {
item.deliveryItems.push({
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
quantity: 1,
})
return
}
item.deliveryItems[0] = {
...item.deliveryItems[0],
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
quantity: Math.max(1, Number(item.deliveryItems[0].quantity || 1) || 1),
}
} }
async function focusValidationTarget(localId: string) { async function focusValidationTarget(localId: string) {
@@ -559,6 +705,7 @@ export function useKuaishouCloudConfig() {
getCloudSkuSummary, getCloudSkuSummary,
getCloudSourceSummary, getCloudSourceSummary,
getConsumeShopSummary, getConsumeShopSummary,
getDeliveryPlanSummary,
// consume shops // consume shops
getKuaishouConsumeShopOptions, getKuaishouConsumeShopOptions,
findKuaishouConsumeShop, findKuaishouConsumeShop,
@@ -566,6 +713,10 @@ export function useKuaishouCloudConfig() {
formatKuaishouConsumeShopOption, formatKuaishouConsumeShopOption,
handleKuaishouConsumeShopSelected, handleKuaishouConsumeShopSelected,
handleCloudSourceKeysChanged, handleCloudSourceKeysChanged,
addDeliveryItem,
removeDeliveryItem,
syncPrimaryCloudSkuFromDeliveryItems,
syncDeliveryItemsFromPrimaryCloudSku,
// collapse // collapse
isCollapsed, isCollapsed,
setCollapsed, setCollapsed,
@@ -62,6 +62,7 @@ export function useKuaishouCloudNinetyone(
cloudSourceKeys: sourceKeys, cloudSourceKeys: sourceKeys,
cloudSkuId: 0, cloudSkuId: 0,
cloudSkuName: '', cloudSkuName: '',
deliveryItems: [],
vnKey: '1', vnKey: '1',
autoBuyEnabled: true, autoBuyEnabled: true,
minAssetReserve: 0, minAssetReserve: 0,
@@ -2,7 +2,7 @@ import { ref } from 'vue'
import { showError, showSuccess } from '@/lib/feedback' import { showError, showSuccess } from '@/lib/feedback'
import { fetchAdminCloudtentaclesSkuList } from '@/services/admin' import { fetchAdminCloudtentaclesSkuList } from '@/services/admin'
import type { AdminCloudtentaclesSkuItem } from '@/types/admin' import type { AdminCloudtentaclesSkuItem, AdminKuaishouCloudDeliveryItem } from '@/types/admin'
import type { EditableItem } from './types' import type { EditableItem } from './types'
@@ -86,9 +86,8 @@ export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key
const sourceKey = getSourceKey(item) const sourceKey = getSourceKey(item)
const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || [] const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || []
const keyword = getCloudSkuKeyword(item) const keyword = getCloudSkuKeyword(item)
const scored = catalog return catalog
.map((sku) => ({ sku, score: scoreCloudSkuMatch(sku, keyword) })) .map((sku) => ({ sku, score: scoreCloudSkuMatch(sku, keyword) }))
.filter((entry) => entry.score > 0 || entry.sku.id === Number(item.cloudSkuId || 0))
.sort((left, right) => { .sort((left, right) => {
if (right.score !== left.score) { if (right.score !== left.score) {
return right.score - left.score return right.score - left.score
@@ -97,12 +96,6 @@ export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key
return left.sku.name.localeCompare(right.sku.name, 'zh-CN') return left.sku.name.localeCompare(right.sku.name, 'zh-CN')
}) })
.map((entry) => entry.sku) .map((entry) => entry.sku)
if (scored.length > 0) {
return scored.slice(0, 80)
}
return catalog.slice(0, 80)
} }
function formatCloudSkuOptionLabel(item: AdminCloudtentaclesSkuItem) { function formatCloudSkuOptionLabel(item: AdminCloudtentaclesSkuItem) {
@@ -126,6 +119,25 @@ export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key
item.cloudSkuName = matched?.name || item.cloudSkuName || '' item.cloudSkuName = matched?.name || item.cloudSkuName || ''
} }
function handleDeliveryItemCloudSkuSelected(
item: EditableItem,
deliveryItem: AdminKuaishouCloudDeliveryItem,
value: number | string | undefined,
) {
const skuId = Number(value || 0)
deliveryItem.cloudSkuId = Number.isInteger(skuId) && skuId > 0 ? skuId : 0
if (!deliveryItem.cloudSkuId) {
deliveryItem.cloudSkuName = ''
return
}
const sourceKey = getSourceKey(item)
const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || []
const matched = catalog.find((sku) => sku.id === deliveryItem.cloudSkuId)
deliveryItem.cloudSkuName = matched?.name || deliveryItem.cloudSkuName || ''
}
async function handleCloudSkuDropdownVisible(item: EditableItem, visible: boolean) { async function handleCloudSkuDropdownVisible(item: EditableItem, visible: boolean) {
if (!visible) { if (!visible) {
return return
@@ -185,6 +197,7 @@ export function useKuaishouCloudSku(cloudtentaclesSourceOptions?: { value: { key
getCloudSkuOptions, getCloudSkuOptions,
formatCloudSkuOptionLabel, formatCloudSkuOptionLabel,
handleCloudSkuSelected, handleCloudSkuSelected,
handleDeliveryItemCloudSkuSelected,
handleCloudSkuDropdownVisible, handleCloudSkuDropdownVisible,
refreshCloudSkuCatalog, refreshCloudSkuCatalog,
syncCloudSkuCatalogLoading, syncCloudSkuCatalogLoading,