支持 khhao 快手双店铺ID匹配

This commit is contained in:
yml
2026-05-04 18:52:23 +08:00
parent 42979d9581
commit c0431a6379
14 changed files with 251 additions and 53 deletions
@@ -8,6 +8,33 @@
"provider": "khhao", "provider": "khhao",
"platform": "kuaishou", "platform": "kuaishou",
"shopId": "4269276762", "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": "套装-浪漫天命", "internalSkuCode": "套装-浪漫天命",
"internalSkuName": "测试1", "internalSkuName": "测试1",
"externalSkuCode": "830", "externalSkuCode": "830",
@@ -121,6 +121,7 @@ export function mapAdminKuaishouCloudFulfillmentItem(item = {}) {
provider: String(item.provider || 'khhao').trim() || 'khhao', provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou', platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(), shopId: String(item.shopId || '').trim(),
khhaoShopId: String(item.khhaoShopId || '').trim(),
internalSkuCode: String(item.internalSkuCode || '').trim(), internalSkuCode: String(item.internalSkuCode || '').trim(),
internalSkuName: String(item.internalSkuName || '').trim(), internalSkuName: String(item.internalSkuName || '').trim(),
externalSkuCode: String(item.externalSkuCode || '').trim(), externalSkuCode: String(item.externalSkuCode || '').trim(),
@@ -172,6 +172,7 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', ()
provider: 'khhao', provider: 'khhao',
platform: 'kuaishou', platform: 'kuaishou',
shopId: '', shopId: '',
khhaoShopId: '',
internalSkuCode: '', internalSkuCode: '',
internalSkuName: '', internalSkuName: '',
externalSkuCode: '', externalSkuCode: '',
@@ -120,9 +120,14 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
continue continue
} }
const matchShopId = resolveBindingMatchShopId(binding)
const bindingConfig = resolveBindingRuntimeConfig(binding) const bindingConfig = resolveBindingRuntimeConfig(binding)
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()
for (const matchShopId of matchShopIds) {
await upsertSkuFulfillmentBinding({ await upsertSkuFulfillmentBinding({
skuCode: binding.skuCode, skuCode: binding.skuCode,
provider: binding.provider, provider: binding.provider,
@@ -136,11 +141,6 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
updatedAt: timestamp, updatedAt: timestamp,
}) })
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) { if (!externalSkuName && !externalItemId && !externalSkuCode) {
continue continue
} }
@@ -161,14 +161,19 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
updatedAt: timestamp, updatedAt: timestamp,
}) })
} }
}
} }
function resolveBindingMatchShopId(binding = {}) { export function resolveBindingMatchShopIds(binding = {}) {
if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') { 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 = {}) { function resolveBindingRuntimeConfig(binding = {}) {
@@ -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: '',
}),
[''],
)
})
@@ -33,6 +33,7 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
provider: String(item.provider || 'khhao').trim() || 'khhao', provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou', platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(), shopId: String(item.shopId || '').trim(),
khhaoShopId: String(item.khhaoShopId || '').trim(),
skuCode: String(item.internalSkuCode || '').trim(), skuCode: String(item.internalSkuCode || '').trim(),
skuName: String(item.internalSkuName || '').trim(), skuName: String(item.internalSkuName || '').trim(),
profileKey: 'kuaishou_ct_assisted', profileKey: 'kuaishou_ct_assisted',
@@ -123,6 +124,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
provider: String(rawValue.provider || 'khhao').trim() || 'khhao', provider: String(rawValue.provider || 'khhao').trim() || 'khhao',
platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou', platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(rawValue.shopId || '').trim(), shopId: String(rawValue.shopId || '').trim(),
khhaoShopId: String(rawValue.khhaoShopId || '').trim(),
internalSkuCode, internalSkuCode,
internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode, internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode,
externalSkuCode, externalSkuCode,
@@ -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: '内部商品',
},
},
},
],
)
})
@@ -46,6 +46,7 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } =
provider: event.provider, provider: event.provider,
platform: event.platform, platform: event.platform,
shopId: event.shopId, shopId: event.shopId,
shopIdAliases: event.shopIdAliases,
item, item,
})), })),
) )
@@ -21,6 +21,7 @@ import { resolveProductMatchRule } from '../../repositories/product-match-rule-r
* provider?: string * provider?: string
* platform?: string * platform?: string
* shopId?: string * shopId?: string
* shopIdAliases?: string[]
* item?: FulfillmentItem * item?: FulfillmentItem
* }} params * }} params
*/ */
@@ -28,12 +29,14 @@ export async function resolveOrderItemForFulfillment({
provider = '', provider = '',
platform = '', platform = '',
shopId = '', shopId = '',
shopIdAliases = [],
item = /** @type {FulfillmentItem} */ ({}), item = /** @type {FulfillmentItem} */ ({}),
}) { }) {
const candidate = await resolveConfiguredItemCandidate({ const candidate = await resolveConfiguredItemCandidate({
provider, provider,
platform, platform,
shopId, shopId,
shopIdAliases,
item, item,
}) })
const { const {
@@ -89,6 +92,7 @@ export async function resolveOrderItemForFulfillment({
* provider?: string * provider?: string
* platform?: string * platform?: string
* shopId?: string * shopId?: string
* shopIdAliases?: string[]
* items?: FulfillmentItem[] * items?: FulfillmentItem[]
* }} params * }} params
*/ */
@@ -96,6 +100,7 @@ export async function hasConfiguredOrderItems({
provider = '', provider = '',
platform = '', platform = '',
shopId = '', shopId = '',
shopIdAliases = [],
items = [], items = [],
}) { }) {
const candidates = await Promise.all( const candidates = await Promise.all(
@@ -103,6 +108,7 @@ export async function hasConfiguredOrderItems({
provider, provider,
platform, platform,
shopId, shopId,
shopIdAliases,
item, item,
})), })),
) )
@@ -135,6 +141,7 @@ async function resolveConfiguredItemCandidate({
provider = '', provider = '',
platform = '', platform = '',
shopId = '', shopId = '',
shopIdAliases = [],
item = /** @type {FulfillmentItem} */ ({}), item = /** @type {FulfillmentItem} */ ({}),
}) { }) {
const externalItemId = pickFirstNonEmpty([ const externalItemId = pickFirstNonEmpty([
@@ -152,28 +159,47 @@ async function resolveConfiguredItemCandidate({
externalSkuCode, externalSkuCode,
]) ])
const externalSkuNameNormalized = normalizeProductName(externalSkuName) const externalSkuNameNormalized = normalizeProductName(externalSkuName)
const shopIdCandidates = resolveShopIdCandidates({ shopId, shopIdAliases })
let matchedRule = null
let binding = null
const matchedRule = await resolveProductMatchRule({ for (const candidateShopId of shopIdCandidates) {
const candidateRule = await resolveProductMatchRule({
provider, provider,
platform, platform,
shopId, shopId: candidateShopId,
externalItemId, externalItemId,
externalSkuCode, externalSkuCode,
externalSkuNameNormalized, 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([ const resolvedSkuCode = pickFirstNonEmpty([
matchedRule?.resolved_sku_code, matchedRule?.resolved_sku_code,
externalSkuCode, externalSkuCode,
externalItemId, externalItemId,
]) ])
const binding = resolvedSkuCode
? await resolveFulfillmentBinding({
skuCode: resolvedSkuCode,
provider,
platform,
shopId,
})
: null
return { return {
externalItemId, 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) { function readConfigValue(rawValue, key) {
if (!rawValue) { if (!rawValue) {
return '' return ''
@@ -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({}), [''])
})
@@ -141,6 +141,7 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = ''
provider: parsed.provider, provider: parsed.provider,
platform: parsed.platform, platform: parsed.platform,
shopId: parsed.shopId, shopId: parsed.shopId,
shopIdAliases: parsed.shopIdAliases,
items: parsed.items, items: parsed.items,
}) })
@@ -75,6 +75,7 @@ export {}
* provider?: string * provider?: string
* platform?: string * platform?: string
* shopId?: string * shopId?: string
* khhaoShopId?: string
* internalSkuCode?: string * internalSkuCode?: string
* internalSkuName?: string * internalSkuName?: string
* externalSkuCode?: string * externalSkuCode?: string
+1
View File
@@ -378,6 +378,7 @@ export interface AdminKuaishouCloudFulfillmentItem {
provider: string provider: string
platform: string platform: string
shopId: string shopId: string
khhaoShopId: string
internalSkuCode: string internalSkuCode: string
internalSkuName: string internalSkuName: string
externalSkuCode: string externalSkuCode: string
@@ -99,6 +99,7 @@ function createEmptyItem(): EditableItem {
provider: 'khhao', provider: 'khhao',
platform: 'kuaishou', platform: 'kuaishou',
shopId: '', shopId: '',
khhaoShopId: '',
internalSkuCode: '', internalSkuCode: '',
internalSkuName: '', internalSkuName: '',
externalSkuCode: '', externalSkuCode: '',
@@ -128,6 +129,7 @@ function mapEditableItem(item: AdminKuaishouCloudFulfillmentItem): EditableItem
provider: item.provider || 'khhao', provider: item.provider || 'khhao',
platform: item.platform || 'kuaishou', platform: item.platform || 'kuaishou',
shopId: item.shopId || '', shopId: item.shopId || '',
khhaoShopId: item.khhaoShopId || '',
internalSkuCode: item.internalSkuCode || '', internalSkuCode: item.internalSkuCode || '',
internalSkuName: item.internalSkuName || '', internalSkuName: item.internalSkuName || '',
externalSkuCode: item.externalSkuCode || '', externalSkuCode: item.externalSkuCode || '',
@@ -156,6 +158,7 @@ function mapSaveItem(item: EditableItem): AdminKuaishouCloudFulfillmentItem {
provider: item.provider.trim() || 'khhao', provider: item.provider.trim() || 'khhao',
platform: item.platform.trim() || 'kuaishou', platform: item.platform.trim() || 'kuaishou',
shopId: item.shopId.trim(), shopId: item.shopId.trim(),
khhaoShopId: item.khhaoShopId.trim(),
internalSkuCode: item.internalSkuCode.trim(), internalSkuCode: item.internalSkuCode.trim(),
internalSkuName: item.internalSkuName.trim(), internalSkuName: item.internalSkuName.trim(),
externalSkuCode: item.externalSkuCode.trim(), externalSkuCode: item.externalSkuCode.trim(),
@@ -193,6 +196,7 @@ function hasMeaningfulContent(item: EditableItem) {
|| item.cloudSkuName.trim() || item.cloudSkuName.trim()
|| item.vnKey.trim() || item.vnKey.trim()
|| item.shopId.trim() || item.shopId.trim()
|| item.khhaoShopId.trim()
|| item.notes.trim() || item.notes.trim()
|| item.provider.trim() !== 'khhao' || item.provider.trim() !== 'khhao'
|| item.platform.trim() !== 'kuaishou' || item.platform.trim() !== 'kuaishou'
@@ -398,6 +402,7 @@ function createItemFromKhhaoOrder(item: AdminKhhaoOrderPreview): EditableItem {
provider: item.provider || 'khhao', provider: item.provider || 'khhao',
platform: item.platform || 'kuaishou', platform: item.platform || 'kuaishou',
shopId: item.kuaishouShopId || item.shopId || '', shopId: item.kuaishouShopId || item.shopId || '',
khhaoShopId: item.khhaoShopId || item.shopId || '',
internalSkuCode: '', internalSkuCode: '',
internalSkuName: item.itemTitle || '', internalSkuName: item.itemTitle || '',
externalSkuCode: item.skuCode || '', externalSkuCode: item.skuCode || '',
@@ -425,7 +430,10 @@ function findExistingItemFromKhhaoOrder(order: AdminKhhaoOrderPreview) {
const orderTitle = String(order.itemTitle || '').trim() const orderTitle = String(order.itemTitle || '').trim()
return items.value.find((item) => { 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 sameItemId = orderItemId && item.externalItemId.trim() === orderItemId
const sameSkuCode = orderSkuCode && item.externalSkuCode.trim() === orderSkuCode const sameSkuCode = orderSkuCode && item.externalSkuCode.trim() === orderSkuCode
const sameTitle = orderTitle && [item.externalSkuName, item.internalSkuName, item.resolvedSkuName].some((value) => value.trim() === orderTitle) const sameTitle = orderTitle && [item.externalSkuName, item.internalSkuName, item.resolvedSkuName].some((value) => value.trim() === orderTitle)
@@ -932,6 +940,10 @@ onMounted(loadConfigs)
<span>店铺 ID</span> <span>店铺 ID</span>
<input v-model="item.shopId" class="text-input" maxlength="80" placeholder="留空表示跨店铺共用" /> <input v-model="item.shopId" class="text-input" maxlength="80" placeholder="留空表示跨店铺共用" />
</label> </label>
<label class="field-block">
<span>khhao 店铺 ID</span>
<input v-model="item.khhaoShopId" class="text-input" maxlength="80" placeholder="例如 10,可用于 khhao 内部店铺匹配" />
</label>
<label class="field-block"> <label class="field-block">
<span>外部 SKU</span> <span>外部 SKU</span>
<input v-model="item.externalSkuCode" class="text-input" maxlength="120" placeholder="例如 khhao 的 skuCode" /> <input v-model="item.externalSkuCode" class="text-input" maxlength="120" placeholder="例如 khhao 的 skuCode" />