diff --git a/apps/backend/data/kuaishou-cloud-fulfillment.json b/apps/backend/data/kuaishou-cloud-fulfillment.json index 9cf0d641..f04b2d49 100644 --- a/apps/backend/data/kuaishou-cloud-fulfillment.json +++ b/apps/backend/data/kuaishou-cloud-fulfillment.json @@ -8,6 +8,33 @@ "provider": "khhao", "platform": "kuaishou", "shopId": "4269276762", + "khhaoShopId": "10", + "internalSkuCode": "套装-浪漫天命", + "internalSkuName": "套装-浪漫天命", + "externalSkuCode": "952", + "externalItemId": "952", + "externalSkuName": "测试2", + "resolvedSkuName": "测试2", + "cloudSourceKey": "default", + "cloudSkuId": 28, + "cloudSkuName": "套装-浪漫天命", + "vnKey": "1", + "autoBuyEnabled": true, + "minAssetReserve": 0, + "autoReturnNumberAfterDispatch": true, + "autoConsumeAfterDispatch": true, + "kuaishouConsumeShopId": "4269276762", + "kuaishouConsumeShopName": "稚嫩游戏交易店", + "notes": "从 khhao 订单 2612400002308936 导入" + }, + { + "id": "套装-浪漫天命", + "enabled": false, + "priority": 100, + "provider": "khhao", + "platform": "kuaishou", + "shopId": "4269276762", + "khhaoShopId": "10", "internalSkuCode": "套装-浪漫天命", "internalSkuName": "测试1", "externalSkuCode": "830", diff --git a/apps/backend/src/services/admin/platform-config/mappers.js b/apps/backend/src/services/admin/platform-config/mappers.js index 3f99c806..ec2c879c 100644 --- a/apps/backend/src/services/admin/platform-config/mappers.js +++ b/apps/backend/src/services/admin/platform-config/mappers.js @@ -121,6 +121,7 @@ export function mapAdminKuaishouCloudFulfillmentItem(item = {}) { provider: String(item.provider || 'khhao').trim() || 'khhao', platform: String(item.platform || 'kuaishou').trim() || 'kuaishou', shopId: String(item.shopId || '').trim(), + khhaoShopId: String(item.khhaoShopId || '').trim(), internalSkuCode: String(item.internalSkuCode || '').trim(), internalSkuName: String(item.internalSkuName || '').trim(), externalSkuCode: String(item.externalSkuCode || '').trim(), diff --git a/apps/backend/src/services/admin/platform-config/mappers.test.js b/apps/backend/src/services/admin/platform-config/mappers.test.js index d34ae1b4..07c64cbc 100644 --- a/apps/backend/src/services/admin/platform-config/mappers.test.js +++ b/apps/backend/src/services/admin/platform-config/mappers.test.js @@ -172,6 +172,7 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', () provider: 'khhao', platform: 'kuaishou', shopId: '', + khhaoShopId: '', internalSkuCode: '', internalSkuName: '', externalSkuCode: '', diff --git a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.js b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.js index 8a8e3bd2..ddb1b172 100644 --- a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.js +++ b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.js @@ -120,55 +120,60 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) { continue } - const matchShopId = resolveBindingMatchShopId(binding) const bindingConfig = resolveBindingRuntimeConfig(binding) - - await upsertSkuFulfillmentBinding({ - skuCode: binding.skuCode, - provider: binding.provider, - platform: binding.platform, - shopId: matchShopId, - profileId: profile.id, - enabled: binding.enabled, - priority: binding.priority, - configJson: JSON.stringify(bindingConfig), - createdAt: timestamp, - updatedAt: timestamp, - }) - + const matchShopIds = resolveBindingMatchShopIds(binding) const match = binding.match || {} const externalSkuName = String(match.externalSkuName || '').trim() const externalItemId = String(match.externalItemId || '').trim() const externalSkuCode = String(match.externalSkuCode || '').trim() - if (!externalSkuName && !externalItemId && !externalSkuCode) { - continue - } + for (const matchShopId of matchShopIds) { + await upsertSkuFulfillmentBinding({ + skuCode: binding.skuCode, + provider: binding.provider, + platform: binding.platform, + shopId: matchShopId, + profileId: profile.id, + enabled: binding.enabled, + priority: binding.priority, + configJson: JSON.stringify(bindingConfig), + createdAt: timestamp, + updatedAt: timestamp, + }) - await upsertProductMatchRule({ - provider: binding.provider, - platform: binding.platform, - shopId: matchShopId, - externalItemId, - externalSkuCode, - externalSkuName, - externalSkuNameNormalized: normalizeProductName(externalSkuName), - resolvedSkuCode: binding.skuCode, - enabled: binding.enabled, - priority: binding.priority, - configJson: JSON.stringify(match.config || {}), - createdAt: timestamp, - updatedAt: timestamp, - }) + if (!externalSkuName && !externalItemId && !externalSkuCode) { + continue + } + + await upsertProductMatchRule({ + provider: binding.provider, + platform: binding.platform, + shopId: matchShopId, + externalItemId, + externalSkuCode, + externalSkuName, + externalSkuNameNormalized: normalizeProductName(externalSkuName), + resolvedSkuCode: binding.skuCode, + enabled: binding.enabled, + priority: binding.priority, + configJson: JSON.stringify(match.config || {}), + createdAt: timestamp, + updatedAt: timestamp, + }) + } } } -function resolveBindingMatchShopId(binding = {}) { +export function resolveBindingMatchShopIds(binding = {}) { if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') { - return String(binding.khhaoShopId || binding.shopId || '').trim() + const candidates = [...new Set([ + String(binding.khhaoShopId || '').trim(), + String(binding.shopId || '').trim(), + ].filter(Boolean))] + return candidates.length > 0 ? candidates : [''] } - return String(binding.shopId || '').trim() + return [String(binding.shopId || '').trim()] } function resolveBindingRuntimeConfig(binding = {}) { diff --git a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.test.js b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.test.js new file mode 100644 index 00000000..6616d81e --- /dev/null +++ b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.test.js @@ -0,0 +1,27 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { resolveBindingMatchShopIds } from './fulfillment-bootstrap-service.js' + +test('resolveBindingMatchShopIds returns dual shop ids for khhao kuaishou bindings', () => { + assert.deepEqual( + resolveBindingMatchShopIds({ + provider: 'khhao', + platform: 'kuaishou', + shopId: '4269276762', + khhaoShopId: '10', + }), + ['10', '4269276762'], + ) +}) + +test('resolveBindingMatchShopIds preserves wildcard rules for empty shop id', () => { + assert.deepEqual( + resolveBindingMatchShopIds({ + provider: 'agiso', + platform: 'xianyu', + shopId: '', + }), + [''], + ) +}) diff --git a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.js b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.js index 6347aef9..52efd9c0 100644 --- a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.js +++ b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.js @@ -33,6 +33,7 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) { provider: String(item.provider || 'khhao').trim() || 'khhao', platform: String(item.platform || 'kuaishou').trim() || 'kuaishou', shopId: String(item.shopId || '').trim(), + khhaoShopId: String(item.khhaoShopId || '').trim(), skuCode: String(item.internalSkuCode || '').trim(), skuName: String(item.internalSkuName || '').trim(), profileKey: 'kuaishou_ct_assisted', @@ -123,6 +124,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) { provider: String(rawValue.provider || 'khhao').trim() || 'khhao', platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou', shopId: String(rawValue.shopId || '').trim(), + khhaoShopId: String(rawValue.khhaoShopId || '').trim(), internalSkuCode, internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode, externalSkuCode, diff --git a/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.js b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.js new file mode 100644 index 00000000..36054c2f --- /dev/null +++ b/apps/backend/src/services/order/kuaishou-cloud-fulfillment-config-service.test.js @@ -0,0 +1,66 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { mapKuaishouCloudFulfillmentItemsToBindings } from './kuaishou-cloud-fulfillment-config-service.js' + +test('mapKuaishouCloudFulfillmentItemsToBindings preserves khhao shop id', () => { + assert.deepEqual( + mapKuaishouCloudFulfillmentItemsToBindings({ + items: [ + { + enabled: true, + provider: 'khhao', + platform: 'kuaishou', + shopId: '4269276762', + khhaoShopId: '10', + internalSkuCode: 'sku-1', + internalSkuName: '内部商品', + externalSkuCode: '952', + externalItemId: '952', + externalSkuName: '测试2', + cloudSkuId: 28, + }, + ], + }), + [ + { + provider: 'khhao', + platform: 'kuaishou', + shopId: '4269276762', + khhaoShopId: '10', + skuCode: 'sku-1', + skuName: '内部商品', + profileKey: 'kuaishou_ct_assisted', + enabled: true, + priority: 100, + config: { + flowType: 'kuaishou_cloud_fulfillment', + configId: '', + cloudtentacles: { + cloudSourceKey: 'default', + skuId: 28, + skuName: '', + vnKey: '1', + autoBuyEnabled: true, + minAssetReserve: 0, + autoReturnNumberAfterDispatch: false, + }, + kuaishouConsume: { + shopId: '', + shopName: '', + autoConsumeAfterDispatch: false, + }, + notes: '', + }, + match: { + externalSkuCode: '952', + externalItemId: '952', + externalSkuName: '测试2', + config: { + resolvedSkuName: '内部商品', + }, + }, + }, + ], + ) +}) diff --git a/apps/backend/src/services/order/order-service.js b/apps/backend/src/services/order/order-service.js index cebfe428..cc9c92d0 100644 --- a/apps/backend/src/services/order/order-service.js +++ b/apps/backend/src/services/order/order-service.js @@ -46,6 +46,7 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = provider: event.provider, platform: event.platform, shopId: event.shopId, + shopIdAliases: event.shopIdAliases, item, })), ) diff --git a/apps/backend/src/services/order/product-match-service.js b/apps/backend/src/services/order/product-match-service.js index c2579569..91d2ec9d 100644 --- a/apps/backend/src/services/order/product-match-service.js +++ b/apps/backend/src/services/order/product-match-service.js @@ -21,6 +21,7 @@ import { resolveProductMatchRule } from '../../repositories/product-match-rule-r * provider?: string * platform?: string * shopId?: string + * shopIdAliases?: string[] * item?: FulfillmentItem * }} params */ @@ -28,12 +29,14 @@ export async function resolveOrderItemForFulfillment({ provider = '', platform = '', shopId = '', + shopIdAliases = [], item = /** @type {FulfillmentItem} */ ({}), }) { const candidate = await resolveConfiguredItemCandidate({ provider, platform, shopId, + shopIdAliases, item, }) const { @@ -89,6 +92,7 @@ export async function resolveOrderItemForFulfillment({ * provider?: string * platform?: string * shopId?: string + * shopIdAliases?: string[] * items?: FulfillmentItem[] * }} params */ @@ -96,6 +100,7 @@ export async function hasConfiguredOrderItems({ provider = '', platform = '', shopId = '', + shopIdAliases = [], items = [], }) { const candidates = await Promise.all( @@ -103,6 +108,7 @@ export async function hasConfiguredOrderItems({ provider, platform, shopId, + shopIdAliases, item, })), ) @@ -135,6 +141,7 @@ async function resolveConfiguredItemCandidate({ provider = '', platform = '', shopId = '', + shopIdAliases = [], item = /** @type {FulfillmentItem} */ ({}), }) { const externalItemId = pickFirstNonEmpty([ @@ -152,28 +159,47 @@ async function resolveConfiguredItemCandidate({ externalSkuCode, ]) const externalSkuNameNormalized = normalizeProductName(externalSkuName) + const shopIdCandidates = resolveShopIdCandidates({ shopId, shopIdAliases }) + let matchedRule = null + let binding = null + + 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 matchedRule = await resolveProductMatchRule({ - provider, - platform, - shopId, - externalItemId, - externalSkuCode, - externalSkuNameNormalized, - }) const resolvedSkuCode = pickFirstNonEmpty([ matchedRule?.resolved_sku_code, externalSkuCode, externalItemId, ]) - const binding = resolvedSkuCode - ? await resolveFulfillmentBinding({ - skuCode: resolvedSkuCode, - provider, - platform, - shopId, - }) - : null return { externalItemId, @@ -187,6 +213,15 @@ async function resolveConfiguredItemCandidate({ } } +export function resolveShopIdCandidates({ shopId = '', shopIdAliases = [] } = {}) { + 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, key) { if (!rawValue) { return '' diff --git a/apps/backend/src/services/order/product-match-service.test.js b/apps/backend/src/services/order/product-match-service.test.js new file mode 100644 index 00000000..a98c0b7d --- /dev/null +++ b/apps/backend/src/services/order/product-match-service.test.js @@ -0,0 +1,18 @@ +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({}), ['']) +}) diff --git a/apps/backend/src/services/order/webhook-service.js b/apps/backend/src/services/order/webhook-service.js index 637748e5..1bdc3713 100644 --- a/apps/backend/src/services/order/webhook-service.js +++ b/apps/backend/src/services/order/webhook-service.js @@ -141,6 +141,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' provider: parsed.provider, platform: parsed.platform, shopId: parsed.shopId, + shopIdAliases: parsed.shopIdAliases, items: parsed.items, }) diff --git a/apps/backend/src/types/admin-write-inputs.js b/apps/backend/src/types/admin-write-inputs.js index 1ec0a77f..cd46ac56 100644 --- a/apps/backend/src/types/admin-write-inputs.js +++ b/apps/backend/src/types/admin-write-inputs.js @@ -75,6 +75,7 @@ export {} * provider?: string * platform?: string * shopId?: string + * khhaoShopId?: string * internalSkuCode?: string * internalSkuName?: string * externalSkuCode?: string diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index b781a2ce..76568ead 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -378,6 +378,7 @@ export interface AdminKuaishouCloudFulfillmentItem { provider: string platform: string shopId: string + khhaoShopId: string internalSkuCode: string internalSkuName: string externalSkuCode: string diff --git a/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue b/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue index 45d7e6e3..37cfca4d 100644 --- a/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue +++ b/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue @@ -99,6 +99,7 @@ function createEmptyItem(): EditableItem { provider: 'khhao', platform: 'kuaishou', shopId: '', + khhaoShopId: '', internalSkuCode: '', internalSkuName: '', externalSkuCode: '', @@ -128,6 +129,7 @@ function mapEditableItem(item: AdminKuaishouCloudFulfillmentItem): EditableItem provider: item.provider || 'khhao', platform: item.platform || 'kuaishou', shopId: item.shopId || '', + khhaoShopId: item.khhaoShopId || '', internalSkuCode: item.internalSkuCode || '', internalSkuName: item.internalSkuName || '', externalSkuCode: item.externalSkuCode || '', @@ -156,6 +158,7 @@ function mapSaveItem(item: EditableItem): AdminKuaishouCloudFulfillmentItem { provider: item.provider.trim() || 'khhao', platform: item.platform.trim() || 'kuaishou', shopId: item.shopId.trim(), + khhaoShopId: item.khhaoShopId.trim(), internalSkuCode: item.internalSkuCode.trim(), internalSkuName: item.internalSkuName.trim(), externalSkuCode: item.externalSkuCode.trim(), @@ -193,6 +196,7 @@ function hasMeaningfulContent(item: EditableItem) { || item.cloudSkuName.trim() || item.vnKey.trim() || item.shopId.trim() + || item.khhaoShopId.trim() || item.notes.trim() || item.provider.trim() !== 'khhao' || item.platform.trim() !== 'kuaishou' @@ -398,6 +402,7 @@ function createItemFromKhhaoOrder(item: AdminKhhaoOrderPreview): EditableItem { provider: item.provider || 'khhao', platform: item.platform || 'kuaishou', shopId: item.kuaishouShopId || item.shopId || '', + khhaoShopId: item.khhaoShopId || item.shopId || '', internalSkuCode: '', internalSkuName: item.itemTitle || '', externalSkuCode: item.skuCode || '', @@ -425,7 +430,10 @@ function findExistingItemFromKhhaoOrder(order: AdminKhhaoOrderPreview) { const orderTitle = String(order.itemTitle || '').trim() return items.value.find((item) => { - const sameShop = !orderShopId || item.shopId.trim() === orderShopId || item.kuaishouConsumeShopId.trim() === orderShopId + const sameShop = !orderShopId + || item.shopId.trim() === orderShopId + || item.khhaoShopId.trim() === String(order.khhaoShopId || '').trim() + || item.kuaishouConsumeShopId.trim() === orderShopId const sameItemId = orderItemId && item.externalItemId.trim() === orderItemId const sameSkuCode = orderSkuCode && item.externalSkuCode.trim() === orderSkuCode const sameTitle = orderTitle && [item.externalSkuName, item.internalSkuName, item.resolvedSkuName].some((value) => value.trim() === orderTitle) @@ -932,6 +940,10 @@ onMounted(loadConfigs) 店铺 ID +