删除旧履约配置逻辑
This commit is contained in:
@@ -139,7 +139,6 @@ test('syncDeliveryTasksForOrderWithDeps creates kuaishou cloud task from cloudte
|
||||
|
||||
const result = await syncDeliveryTasksForOrderWithDeps(paidOrder, dynamicOrderItems, {
|
||||
listTasksByOrderId: async () => [],
|
||||
resolveFulfillmentBinding: async () => null,
|
||||
getFulfillmentProfileByKey: async () => ({
|
||||
id: 2,
|
||||
profile_key: 'kuaishou_ct_assisted',
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js'
|
||||
import {
|
||||
getFulfillmentProfileByKey,
|
||||
resolveFulfillmentBinding,
|
||||
} from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
@@ -39,12 +36,6 @@ type DeliveryTaskDeps = {
|
||||
createTask?: typeof createTask
|
||||
listTasksByOrderId?: typeof listTasksByOrderId
|
||||
updateTask?: typeof updateTask
|
||||
resolveFulfillmentBinding?: (input: {
|
||||
skuCode: string
|
||||
provider?: string
|
||||
platform?: string
|
||||
shopId?: string
|
||||
}) => Promise<FulfillmentBindingLike | null>
|
||||
getFulfillmentProfileByKey?: (profileKey: string) => Promise<FulfillmentBindingLike | null>
|
||||
createTaskClaimToken?: (taskId: number | string) => Promise<ClaimTokenLike>
|
||||
notifyTaskAutoManualReview?: (payload: {
|
||||
@@ -84,7 +75,6 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
createTask: createDeliveryTask = createTask,
|
||||
listTasksByOrderId: listTasks = listTasksByOrderId,
|
||||
updateTask: updateDeliveryTask = updateTask,
|
||||
resolveFulfillmentBinding: resolveBinding = resolveFulfillmentBinding,
|
||||
getFulfillmentProfileByKey: getProfileByKey = getFulfillmentProfileByKey,
|
||||
createTaskClaimToken: createClaimToken = createTaskClaimToken,
|
||||
notifyTaskAutoManualReview: notifyManualReview = notifyTaskAutoManualReview,
|
||||
@@ -118,14 +108,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
const tasks: DeliveryTaskRow[] = []
|
||||
|
||||
for (const item of orderItems) {
|
||||
const binding = await resolveBinding({
|
||||
skuCode: item.sku_code,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id,
|
||||
})
|
||||
|
||||
const profile = binding || await resolveDynamicCloudtentaclesProfile(item, getProfileByKey)
|
||||
const profile = await resolveDynamicCloudtentaclesProfile(item, getProfileByKey)
|
||||
|
||||
if (!profile) {
|
||||
continue
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { mapKuaishouCloudFulfillmentItemsToBindings } from './kuaishou-cloud-fulfillment-config-service.js'
|
||||
|
||||
test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => {
|
||||
assert.deepEqual(
|
||||
mapKuaishouCloudFulfillmentItemsToBindings({
|
||||
items: [
|
||||
{
|
||||
enabled: true,
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: '91kaquan',
|
||||
internalSkuCode: 'sku-1',
|
||||
internalSkuName: '内部商品',
|
||||
externalSkuCode: '952',
|
||||
externalItemId: '952',
|
||||
externalSkuName: '测试2',
|
||||
cloudSourceKeys: ['account_a', 'account_b'],
|
||||
cloudSkuId: 28,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[
|
||||
{
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shopId: '91kaquan',
|
||||
skuCode: 'sku-1',
|
||||
skuName: '内部商品',
|
||||
profileKey: 'kuaishou_ct_assisted',
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
deliveryItems: [
|
||||
{
|
||||
cloudSkuId: 28,
|
||||
cloudSkuName: '',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
config: {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: '',
|
||||
cloudtentacles: {
|
||||
cloudSourceKeys: ['account_a', 'account_b'],
|
||||
skuId: 28,
|
||||
skuName: '',
|
||||
deliveryItems: [
|
||||
{
|
||||
cloudSkuId: 28,
|
||||
cloudSkuName: '',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
vnKey: '1',
|
||||
autoBuyEnabled: true,
|
||||
minAssetReserve: 0,
|
||||
autoReturnNumberAfterDispatch: false,
|
||||
},
|
||||
kuaishouConsume: {
|
||||
shopId: '',
|
||||
shopName: '',
|
||||
autoConsumeAfterDispatch: false,
|
||||
},
|
||||
notes: '',
|
||||
},
|
||||
match: {
|
||||
externalSkuCode: '952',
|
||||
externalItemId: '952',
|
||||
externalSkuName: '测试2',
|
||||
config: {
|
||||
resolvedSkuName: '内部商品',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
@@ -1,279 +0,0 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { PROJECT_ROOT } from "../../config/runtime.js";
|
||||
import { readJsonFile, writeJsonFile } from "../../utils/json-file-store.js";
|
||||
import { resolveKuaishouEticketShopConfig } from "../platforms/kuaishou-eticket/source-config-service.js";
|
||||
|
||||
const KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH = path.join(
|
||||
PROJECT_ROOT,
|
||||
"data",
|
||||
"kuaishou-cloud-fulfillment.json"
|
||||
);
|
||||
|
||||
type JsonObject = Record<string, any>;
|
||||
|
||||
type DeliveryItem = {
|
||||
cloudSkuId: number;
|
||||
cloudSkuName: string;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export function getKuaishouCloudFulfillmentFilePath() {
|
||||
return KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH;
|
||||
}
|
||||
|
||||
export function getKuaishouCloudFulfillmentConfig() {
|
||||
return loadKuaishouCloudFulfillmentConfigFromFile();
|
||||
}
|
||||
|
||||
export function saveKuaishouCloudFulfillmentConfig(rawValue: unknown) {
|
||||
return writeJsonFile(
|
||||
KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH,
|
||||
rawValue,
|
||||
normalizeKuaishouCloudFulfillmentConfig
|
||||
);
|
||||
}
|
||||
|
||||
export function mapKuaishouCloudFulfillmentItemsToBindings(config: JsonObject = {}) {
|
||||
const items = Array.isArray(config.items) ? config.items : [];
|
||||
|
||||
return items
|
||||
.filter((item) => item && item.enabled !== false)
|
||||
.map((item) => ({
|
||||
provider: String(item.provider || "91kaquan").trim() || "91kaquan",
|
||||
platform: String(item.platform || "kuaishou").trim() || "kuaishou",
|
||||
shopId: String(item.shopId || "").trim(),
|
||||
skuCode: String(item.internalSkuCode || "").trim(),
|
||||
skuName: String(item.internalSkuName || "").trim(),
|
||||
profileKey: "kuaishou_ct_assisted",
|
||||
enabled: item.enabled !== false,
|
||||
priority: normalizePriority(item.priority),
|
||||
deliveryItems: normalizeDeliveryItems(item),
|
||||
config: {
|
||||
flowType: "kuaishou_cloud_fulfillment",
|
||||
configId: String(item.id || "").trim(),
|
||||
cloudtentacles: {
|
||||
cloudSourceKeys: normalizeStringArray(item.cloudSourceKeys),
|
||||
skuId:
|
||||
normalizeDeliveryItems(item)[0]?.cloudSkuId ||
|
||||
normalizePositiveInteger(item.cloudSkuId),
|
||||
skuName:
|
||||
normalizeDeliveryItems(item)[0]?.cloudSkuName ||
|
||||
String(item.cloudSkuName || "").trim(),
|
||||
deliveryItems: normalizeDeliveryItems(item),
|
||||
vnKey: "1",
|
||||
autoBuyEnabled: item.autoBuyEnabled !== false,
|
||||
minAssetReserve: normalizeNonNegativeInteger(item.minAssetReserve, 0),
|
||||
autoReturnNumberAfterDispatch:
|
||||
item.autoReturnNumberAfterDispatch === true,
|
||||
},
|
||||
kuaishouConsume: {
|
||||
shopId: String(item.kuaishouConsumeShopId || "").trim(),
|
||||
shopName: String(item.kuaishouConsumeShopName || "").trim(),
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||
},
|
||||
notes: String(item.notes || "").trim(),
|
||||
},
|
||||
match: {
|
||||
externalSkuCode: String(item.externalSkuCode || "").trim(),
|
||||
externalItemId: String(item.externalItemId || "").trim(),
|
||||
externalSkuName: String(item.externalSkuName || "").trim(),
|
||||
config: {
|
||||
resolvedSkuName: String(
|
||||
item.resolvedSkuName || item.internalSkuName || ""
|
||||
).trim(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.filter(
|
||||
(item) =>
|
||||
item.skuCode &&
|
||||
(item.match.externalSkuCode ||
|
||||
item.match.externalItemId ||
|
||||
item.match.externalSkuName)
|
||||
);
|
||||
}
|
||||
|
||||
function loadKuaishouCloudFulfillmentConfigFromFile() {
|
||||
return readJsonFile(
|
||||
KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH,
|
||||
() => normalizeKuaishouCloudFulfillmentConfig({}),
|
||||
normalizeKuaishouCloudFulfillmentConfig
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeKuaishouCloudFulfillmentConfig(rawValue: unknown) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {};
|
||||
return {
|
||||
enabled: source.enabled !== false,
|
||||
items: Array.isArray(source.items)
|
||||
? source.items
|
||||
.map((item: unknown) => normalizeKuaishouCloudFulfillmentItem(item))
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeKuaishouCloudFulfillmentItem(rawValue: unknown) {
|
||||
if (!isPlainObject(rawValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const internalSkuCode = String(rawValue.internalSkuCode || "").trim();
|
||||
const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId);
|
||||
const deliveryItems = normalizeDeliveryItems(rawValue);
|
||||
const cloudSourceKeys = normalizeStringArray(rawValue.cloudSourceKeys);
|
||||
const externalSkuCode = String(rawValue.externalSkuCode || "").trim();
|
||||
const externalItemId = String(rawValue.externalItemId || "").trim();
|
||||
const externalSkuName = String(rawValue.externalSkuName || "").trim();
|
||||
const kuaishouConsumeShopId = String(
|
||||
rawValue.kuaishouConsumeShopId || ""
|
||||
).trim();
|
||||
const kuaishouConsumeShopName = String(
|
||||
rawValue.kuaishouConsumeShopName || ""
|
||||
).trim();
|
||||
const kuaishouShopConfig = resolveKuaishouEticketShopConfig({
|
||||
shopId: kuaishouConsumeShopId,
|
||||
shopName: kuaishouConsumeShopName,
|
||||
});
|
||||
|
||||
if (!internalSkuCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (deliveryItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cloudSourceKeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!externalSkuCode && !externalItemId && !externalSkuName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(rawValue.id || internalSkuCode).trim() || internalSkuCode,
|
||||
enabled: rawValue.enabled !== false,
|
||||
priority: normalizePriority(rawValue.priority),
|
||||
provider: String(rawValue.provider || "91kaquan").trim() || "91kaquan",
|
||||
platform: String(rawValue.platform || "kuaishou").trim() || "kuaishou",
|
||||
shopId: String(rawValue.shopId || "").trim(),
|
||||
internalSkuCode,
|
||||
internalSkuName:
|
||||
String(rawValue.internalSkuName || "").trim() || internalSkuCode,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
externalSkuName,
|
||||
resolvedSkuName: String(
|
||||
rawValue.resolvedSkuName || rawValue.internalSkuName || ""
|
||||
).trim(),
|
||||
cloudSourceKeys,
|
||||
cloudSkuId: deliveryItems[0]?.cloudSkuId || cloudSkuId,
|
||||
cloudSkuName:
|
||||
deliveryItems[0]?.cloudSkuName ||
|
||||
String(rawValue.cloudSkuName || "").trim(),
|
||||
deliveryItems,
|
||||
vnKey: "1",
|
||||
autoBuyEnabled: rawValue.autoBuyEnabled !== false,
|
||||
minAssetReserve: normalizeNonNegativeInteger(rawValue.minAssetReserve, 0),
|
||||
autoReturnNumberAfterDispatch:
|
||||
rawValue.autoReturnNumberAfterDispatch === true,
|
||||
autoConsumeAfterDispatch: rawValue.autoConsumeAfterDispatch === true,
|
||||
kuaishouConsumeShopId: String(
|
||||
kuaishouShopConfig?.shopId || kuaishouConsumeShopId
|
||||
).trim(),
|
||||
kuaishouConsumeShopName: String(
|
||||
kuaishouShopConfig?.kshopName || kuaishouConsumeShopName
|
||||
).trim(),
|
||||
notes: String(rawValue.notes || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value: unknown) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function normalizeNonNegativeInteger(value: unknown, fallback: number) {
|
||||
const parsed = Number(value);
|
||||
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) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.round(parsed));
|
||||
}
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Object.prototype.toString.call(value) === "[object Object]";
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v: unknown) => String(v || "").trim()).filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
createOrder,
|
||||
findOrderByPlatformOrderId,
|
||||
findOrderByPlatformOrderIdCandidates,
|
||||
updateOrder,
|
||||
} from '../../repositories/order-repo.js'
|
||||
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
@@ -32,7 +31,6 @@ type SourceOrderEvent = {
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
shopIdAliases?: string[]
|
||||
shopName: string
|
||||
platformOrderId: string
|
||||
orderStatus: string
|
||||
@@ -89,18 +87,12 @@ export async function upsertOrderFromSource(
|
||||
{ sourceLabel = 'source' }: UpsertOrderSourceOptions = {},
|
||||
): Promise<UpsertOrderResult> {
|
||||
const now = nowIso()
|
||||
const exactExisting = await findOrderByPlatformOrderId({
|
||||
const existing = await findOrderByPlatformOrderId({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
const existing = exactExisting || await findOrderByPlatformOrderIdCandidates({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopIds: resolveEventShopIdCandidates(event),
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
|
||||
logIntegration('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, {
|
||||
provider: event.provider,
|
||||
@@ -115,9 +107,7 @@ export async function upsertOrderFromSource(
|
||||
event.items.map((item) => resolveOrderItemForFulfillment({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
item,
|
||||
...(event.shopIdAliases !== undefined ? { shopIdAliases: event.shopIdAliases } : {}),
|
||||
})),
|
||||
)
|
||||
const configuredItems = (resolvedItems as FulfillmentOrderItem[]).filter((item) => item.isConfigured)
|
||||
@@ -214,13 +204,6 @@ export async function upsertOrderFromSource(
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEventShopIdCandidates(event: Pick<SourceOrderEvent, 'shopId' | 'shopIdAliases'>): string[] {
|
||||
return [...new Set([
|
||||
String(event?.shopId || '').trim(),
|
||||
...(Array.isArray(event?.shopIdAliases) ? event.shopIdAliases : []).map((item) => String(item || '').trim()),
|
||||
].filter(Boolean))]
|
||||
}
|
||||
|
||||
const ORDER_STATUS_PRIORITY = {
|
||||
created: 0,
|
||||
paid: 1,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { resolveShopIdCandidates } from './product-match-service.js'
|
||||
|
||||
test('resolveShopIdCandidates merges primary and alias shop ids', () => {
|
||||
assert.deepEqual(
|
||||
resolveShopIdCandidates({
|
||||
shopId: '10',
|
||||
shopIdAliases: ['4269276762', '10', ''],
|
||||
}),
|
||||
['10', '4269276762'],
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveShopIdCandidates preserves wildcard fallback when ids are empty', () => {
|
||||
assert.deepEqual(resolveShopIdCandidates({}), [''])
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { resolveFulfillmentBinding } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { resolveProductMatchRule } from '../../repositories/product-match-rule-repo.js'
|
||||
import {
|
||||
normalizeCloudtentaclesMatchName,
|
||||
resolveCloudtentaclesSkuByProductName,
|
||||
type CloudtentaclesNameMatchResult,
|
||||
} from './cloudtentacles-name-match-service.js'
|
||||
@@ -21,8 +20,6 @@ export type FulfillmentItem = {
|
||||
type ResolveOrderItemForFulfillmentInput = {
|
||||
provider?: string
|
||||
platform?: string
|
||||
shopId?: string
|
||||
shopIdAliases?: string[]
|
||||
item?: FulfillmentItem
|
||||
}
|
||||
|
||||
@@ -35,9 +32,7 @@ type FulfillmentItemCandidate = {
|
||||
externalSkuCode: string
|
||||
externalSkuName: string
|
||||
externalSkuNameNormalized: string
|
||||
matchedRule: Awaited<ReturnType<typeof resolveProductMatchRule>>
|
||||
resolvedSkuCode: string
|
||||
binding: Awaited<ReturnType<typeof resolveFulfillmentBinding>>
|
||||
cloudtentaclesNameMatch: CloudtentaclesNameMatchResult | null
|
||||
isConfigured: boolean
|
||||
}
|
||||
@@ -53,23 +48,14 @@ export type ResolvedFulfillmentItem = FulfillmentItem & {
|
||||
isConfigured: boolean
|
||||
}
|
||||
|
||||
type ShopIdCandidatesInput = {
|
||||
shopId?: string
|
||||
shopIdAliases?: string[]
|
||||
}
|
||||
|
||||
export async function resolveOrderItemForFulfillment({
|
||||
provider = '',
|
||||
platform = '',
|
||||
shopId = '',
|
||||
shopIdAliases = [],
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<ResolvedFulfillmentItem> {
|
||||
const candidate = await resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
shopIdAliases,
|
||||
item,
|
||||
})
|
||||
const {
|
||||
@@ -77,21 +63,16 @@ export async function resolveOrderItemForFulfillment({
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
matchedRule,
|
||||
binding,
|
||||
cloudtentaclesNameMatch,
|
||||
} = candidate
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
matchedRule?.resolved_sku_code,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const resolvedSkuName = pickFirstNonEmpty([
|
||||
cloudtentaclesNameMatch?.cloudSkuName,
|
||||
readConfigValue(matchedRule?.config_json, 'resolvedSkuName'),
|
||||
readConfigValue(matchedRule?.config_json, 'internalProductName'),
|
||||
item.skuName,
|
||||
externalSkuName,
|
||||
resolvedSkuCode,
|
||||
@@ -103,11 +84,7 @@ export async function resolveOrderItemForFulfillment({
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
resolvedSkuCode,
|
||||
matchedProductRuleId: matchedRule ? Number(matchedRule.id) : null,
|
||||
matchedProductRuleBy: String(matchedRule?.matched_by || '').trim(),
|
||||
matchedFulfillmentBindingId: binding ? Number(binding.id) : null,
|
||||
matchedFulfillmentProfileKey: String(binding?.profile_key || '').trim(),
|
||||
matchMode: cloudtentaclesNameMatch?.matchMode || String(matchedRule?.matched_by || '').trim(),
|
||||
matchMode: cloudtentaclesNameMatch?.matchMode || '',
|
||||
cloudtentacles: cloudtentaclesNameMatch
|
||||
? {
|
||||
matchMode: cloudtentaclesNameMatch.matchMode,
|
||||
@@ -141,16 +118,12 @@ export async function resolveOrderItemForFulfillment({
|
||||
export async function hasConfiguredOrderItems({
|
||||
provider = '',
|
||||
platform = '',
|
||||
shopId = '',
|
||||
shopIdAliases = [],
|
||||
items = [],
|
||||
}: HasConfiguredOrderItemsInput): Promise<boolean> {
|
||||
const candidates = await Promise.all(
|
||||
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
shopIdAliases,
|
||||
item,
|
||||
})),
|
||||
)
|
||||
@@ -159,13 +132,7 @@ export async function hasConfiguredOrderItems({
|
||||
}
|
||||
|
||||
export function normalizeProductName(value: unknown): string {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[【】\[\]()()]/g, ' ')
|
||||
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
return normalizeCloudtentaclesMatchName(value)
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values: unknown[]): string {
|
||||
@@ -182,8 +149,6 @@ function pickFirstNonEmpty(values: unknown[]): string {
|
||||
async function resolveConfiguredItemCandidate({
|
||||
provider = '',
|
||||
platform = '',
|
||||
shopId = '',
|
||||
shopIdAliases = [],
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<FulfillmentItemCandidate> {
|
||||
const externalItemId = pickFirstNonEmpty([
|
||||
@@ -201,9 +166,6 @@ async function resolveConfiguredItemCandidate({
|
||||
externalSkuCode,
|
||||
])
|
||||
const externalSkuNameNormalized = normalizeProductName(externalSkuName)
|
||||
const shopIdCandidates = resolveShopIdCandidates({ shopId, shopIdAliases })
|
||||
let matchedRule = null
|
||||
let binding = null
|
||||
|
||||
if (isOpen91KuaishouOrder(provider, platform)) {
|
||||
const cloudtentaclesNameMatch = await resolveCloudtentaclesSkuByProductName(externalSkuName)
|
||||
@@ -218,48 +180,13 @@ async function resolveConfiguredItemCandidate({
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
matchedRule: null,
|
||||
resolvedSkuCode,
|
||||
binding: null,
|
||||
cloudtentaclesNameMatch,
|
||||
isConfigured: Boolean(cloudtentaclesNameMatch),
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidateShopId of shopIdCandidates) {
|
||||
const candidateRule = await resolveProductMatchRule({
|
||||
provider,
|
||||
platform,
|
||||
shopId: candidateShopId,
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuNameNormalized,
|
||||
})
|
||||
if (candidateRule) {
|
||||
matchedRule = candidateRule
|
||||
}
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
candidateRule?.resolved_sku_code,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
binding = resolvedSkuCode
|
||||
? await resolveFulfillmentBinding({
|
||||
skuCode: resolvedSkuCode,
|
||||
provider,
|
||||
platform,
|
||||
shopId: candidateShopId,
|
||||
})
|
||||
: null
|
||||
|
||||
if (binding) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
matchedRule?.resolved_sku_code,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
@@ -269,52 +196,17 @@ async function resolveConfiguredItemCandidate({
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
externalSkuNameNormalized,
|
||||
matchedRule,
|
||||
resolvedSkuCode,
|
||||
binding,
|
||||
cloudtentaclesNameMatch: null,
|
||||
isConfigured: Boolean(binding),
|
||||
isConfigured: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveShopIdCandidates({
|
||||
shopId = '',
|
||||
shopIdAliases = [],
|
||||
}: ShopIdCandidatesInput = {}): string[] {
|
||||
const candidates = [...new Set([
|
||||
String(shopId || '').trim(),
|
||||
...(Array.isArray(shopIdAliases) ? shopIdAliases : []).map((item) => String(item || '').trim()),
|
||||
].filter(Boolean))]
|
||||
|
||||
return candidates.length > 0 ? candidates : ['']
|
||||
}
|
||||
|
||||
function readConfigValue(rawValue: unknown, key: string): string {
|
||||
if (!rawValue) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const parsed = typeof rawValue === 'string' ? safeParseJson(rawValue) : rawValue
|
||||
if (!isPlainObject(parsed)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return String(parsed[key] || '').trim()
|
||||
}
|
||||
|
||||
function safeParseJson(rawValue: unknown): unknown {
|
||||
try {
|
||||
return JSON.parse(String(rawValue || '{}'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
|
||||
return String(provider || '').trim() === '91kaquan'
|
||||
&& String(platform || '').trim() === 'kuaishou'
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user