支持 khhao 快手双店铺ID匹配
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -172,6 +172,7 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', ()
|
||||
provider: 'khhao',
|
||||
platform: 'kuaishou',
|
||||
shopId: '',
|
||||
khhaoShopId: '',
|
||||
internalSkuCode: '',
|
||||
internalSkuName: '',
|
||||
externalSkuCode: '',
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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',
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
shopIdAliases: event.shopIdAliases,
|
||||
item,
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopIdAliases: parsed.shopIdAliases,
|
||||
items: parsed.items,
|
||||
})
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export {}
|
||||
* provider?: string
|
||||
* platform?: string
|
||||
* shopId?: string
|
||||
* khhaoShopId?: string
|
||||
* internalSkuCode?: string
|
||||
* internalSkuName?: string
|
||||
* externalSkuCode?: string
|
||||
|
||||
@@ -378,6 +378,7 @@ export interface AdminKuaishouCloudFulfillmentItem {
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
khhaoShopId: string
|
||||
internalSkuCode: string
|
||||
internalSkuName: string
|
||||
externalSkuCode: string
|
||||
|
||||
@@ -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)
|
||||
<span>店铺 ID</span>
|
||||
<input v-model="item.shopId" class="text-input" maxlength="80" placeholder="留空表示跨店铺共用" />
|
||||
</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">
|
||||
<span>外部 SKU</span>
|
||||
<input v-model="item.externalSkuCode" class="text-input" maxlength="120" placeholder="例如 khhao 的 skuCode" />
|
||||
|
||||
Reference in New Issue
Block a user