抽离后台履约规则校验辅助逻辑

This commit is contained in:
yml
2026-05-04 15:12:41 +08:00
parent da6668c6ef
commit ace3c7b477
3 changed files with 271 additions and 114 deletions
@@ -76,7 +76,6 @@ import {
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import { createHttpError } from '../../utils/http.js'
import { syncConfiguredFulfillmentBindings } from '../bootstrap/fulfillment-bootstrap-service.js'
import { normalizeProductName } from '../order/product-match-service.js'
import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
import {
hasCloudtentaclesCredentialContextChanged,
@@ -90,13 +89,17 @@ import {
normalizeFulfillmentLookupPayload,
resolveFulfillmentLookupDetail,
} from './admin-platform-config-fulfillment.js'
import {
assertAdminFulfillmentBindingsInput,
buildAdminFulfillmentBindingUniqueKey,
normalizeAdminFulfillmentBindingItem,
} from './admin-platform-config-validation.js'
import {
applyOptionalStringField,
mapAdminAgisoMessagingDefaults,
mapAdminAgisoShopConfigItem,
mapAdminFulfillmentBindingConfigItem,
mapAdminObservedProductItem,
resolveFulfillmentBindingMatchShopId,
} from './admin-platform-config-domain.js'
import {
mapAdminCloudtentaclesSession,
@@ -845,111 +848,28 @@ export async function updateAdminFulfillmentBindingConfigs(
}
async function validateAdminFulfillmentBindingConfigs(bindings = []) {
if (!Array.isArray(bindings)) {
throw createHttpError('履约配置格式不正确', {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_payload',
})
}
assertAdminFulfillmentBindingsInput(bindings)
const seenKeys = new Set()
const kuaishouEticketSource = getKuaishouEticketSourceConfig()
const normalizedBindings = []
for (const [index, rawBinding] of bindings.entries()) {
if (!isPlainObject(rawBinding)) {
throw createHttpError(`${index + 1} 条规则格式不正确`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_item',
})
}
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, {
index,
kuaishouEticketSource,
resolveKuaishouEticketShopConfig,
})
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
const platform = String(rawBinding.platform || '').trim()
let shopId = String(rawBinding.shopId || '').trim()
let shopName = String(rawBinding.shopName || '').trim()
const khhaoShopId = String(rawBinding.khhaoShopId || '').trim()
const skuCode = String(rawBinding.skuCode || '').trim()
const skuName = String(rawBinding.skuName || '').trim()
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
const match = isPlainObject(rawBinding.match) ? rawBinding.match : {}
const externalItemId = String(match.externalItemId || '').trim()
const externalSkuCode = String(match.externalSkuCode || '').trim()
const externalSkuName = String(match.externalSkuName || '').trim()
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
if (provider === 'khhao' && platform === 'kuaishou') {
if (!khhaoShopId) {
throw createHttpError(`${index + 1} 条快手规则缺少 khhao 店铺 ID`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_khhao_shop_id',
})
}
const matchedShop = resolveKuaishouEticketShopConfig(
{
shopId,
shopName,
},
kuaishouEticketSource,
)
if (!matchedShop?.shopId || !matchedShop?.kshopName) {
throw createHttpError(
`${index + 1} 条快手规则的店铺未匹配到已配置的快手官方店铺,请先到“平台配置”补齐 Cookie 后再选择`,
{
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_kuaishou_shop_not_configured',
},
)
}
shopId = String(matchedShop.shopId || '').trim()
shopName = String(matchedShop.kshopName || '').trim()
config.kuaishouShop = {
...(isPlainObject(config.kuaishouShop) ? config.kuaishouShop : {}),
shopId,
shopName,
khhaoShopId,
}
}
if (!skuCode) {
throw createHttpError(`${index + 1} 条规则缺少内部履约 SKU`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_sku_code',
})
}
if (!externalItemId && !externalSkuCode && !externalSkuName) {
throw createHttpError(`${index + 1} 条规则至少需要一种外部匹配条件`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_match_condition',
})
}
const profile = await getFulfillmentProfileByKey(profileKey)
const profile = await getFulfillmentProfileByKey(normalized.profileKey)
if (!profile) {
throw createHttpError(`${index + 1} 条规则使用了不存在的履约方式: ${profileKey}`, {
throw createHttpError(`${index + 1} 条规则使用了不存在的履约方式: ${normalized.profileKey}`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_profile_key',
})
}
const uniqueKey = [
provider,
platform,
resolveFulfillmentBindingMatchShopId({
provider,
platform,
shopId,
khhaoShopId,
}),
externalItemId,
externalSkuCode,
normalizeProductName(externalSkuName),
skuCode,
].join('::')
const uniqueKey = buildAdminFulfillmentBindingUniqueKey(normalized)
if (seenKeys.has(uniqueKey)) {
throw createHttpError(`${index + 1} 条规则与其它规则重复,请调整匹配条件或内部履约 SKU`, {
@@ -959,26 +879,7 @@ async function validateAdminFulfillmentBindingConfigs(bindings = []) {
}
seenKeys.add(uniqueKey)
normalizedBindings.push({
provider,
platform,
shopId,
shopName,
khhaoShopId,
skuCode,
skuName,
profileKey,
enabled: rawBinding.enabled !== false,
priority: rawBinding.priority,
config,
match: {
externalSkuCode,
externalItemId,
externalSkuName,
config: isPlainObject(match.config) ? match.config : {},
},
})
normalizedBindings.push(normalized)
}
return normalizedBindings
@@ -0,0 +1,137 @@
// @ts-check
import { createHttpError } from '../../utils/http.js'
import { normalizeProductName } from '../order/product-match-service.js'
import { isPlainObject } from './admin-platform-config-context.js'
import { resolveFulfillmentBindingMatchShopId } from './admin-platform-config-domain.js'
export function assertAdminFulfillmentBindingsInput(bindings) {
if (Array.isArray(bindings)) {
return
}
throw createHttpError('履约配置格式不正确', {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_payload',
})
}
export function normalizeAdminFulfillmentBindingItem(
rawBinding,
options = {},
) {
const index = Number(options.index || 0)
const kuaishouEticketSource = /** @type {Record<string, unknown>} */ (options.kuaishouEticketSource || {})
const resolveKuaishouEticketShopConfig = /** @type {(shop: Record<string, unknown>, source: Record<string, unknown>) => Record<string, unknown> | null} */ (
options.resolveKuaishouEticketShopConfig || (() => null)
)
if (!isPlainObject(rawBinding)) {
throw createHttpError(`${index + 1} 条规则格式不正确`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_invalid_item',
})
}
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
const platform = String(rawBinding.platform || '').trim()
let shopId = String(rawBinding.shopId || '').trim()
let shopName = String(rawBinding.shopName || '').trim()
const khhaoShopId = String(rawBinding.khhaoShopId || '').trim()
const skuCode = String(rawBinding.skuCode || '').trim()
const skuName = String(rawBinding.skuName || '').trim()
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
const match = isPlainObject(rawBinding.match) ? rawBinding.match : {}
const externalItemId = String(match.externalItemId || '').trim()
const externalSkuCode = String(match.externalSkuCode || '').trim()
const externalSkuName = String(match.externalSkuName || '').trim()
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
if (provider === 'khhao' && platform === 'kuaishou') {
if (!khhaoShopId) {
throw createHttpError(`${index + 1} 条快手规则缺少 khhao 店铺 ID`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_khhao_shop_id',
})
}
const matchedShop = resolveKuaishouEticketShopConfig(
{
shopId,
shopName,
},
kuaishouEticketSource,
)
const matchedShopId = String(matchedShop?.shopId || '').trim()
const matchedShopName = String(matchedShop?.kshopName || '').trim()
if (!matchedShopId || !matchedShopName) {
throw createHttpError(
`${index + 1} 条快手规则的店铺未匹配到已配置的快手官方店铺,请先到“平台配置”补齐 Cookie 后再选择`,
{
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_kuaishou_shop_not_configured',
},
)
}
shopId = matchedShopId
shopName = matchedShopName
config.kuaishouShop = {
...(isPlainObject(config.kuaishouShop) ? config.kuaishouShop : {}),
shopId,
shopName,
khhaoShopId,
}
}
if (!skuCode) {
throw createHttpError(`${index + 1} 条规则缺少内部履约 SKU`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_sku_code',
})
}
if (!externalItemId && !externalSkuCode && !externalSkuName) {
throw createHttpError(`${index + 1} 条规则至少需要一种外部匹配条件`, {
statusCode: 400,
errorCode: 'admin_fulfillment_bindings_missing_match_condition',
})
}
return {
provider,
platform,
shopId,
shopName,
khhaoShopId,
skuCode,
skuName,
profileKey,
enabled: rawBinding.enabled !== false,
priority: rawBinding.priority,
config,
match: {
externalSkuCode,
externalItemId,
externalSkuName,
config: isPlainObject(match.config) ? match.config : {},
},
}
}
export function buildAdminFulfillmentBindingUniqueKey(binding) {
return [
String(binding?.provider || '').trim(),
String(binding?.platform || '').trim(),
resolveFulfillmentBindingMatchShopId({
provider: binding?.provider,
platform: binding?.platform,
shopId: binding?.shopId,
khhaoShopId: binding?.khhaoShopId,
}),
String(binding?.match?.externalItemId || '').trim(),
String(binding?.match?.externalSkuCode || '').trim(),
normalizeProductName(String(binding?.match?.externalSkuName || '').trim()),
String(binding?.skuCode || '').trim(),
].join('::')
}
@@ -0,0 +1,119 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
assertAdminFulfillmentBindingsInput,
buildAdminFulfillmentBindingUniqueKey,
normalizeAdminFulfillmentBindingItem,
} from './admin-platform-config-validation.js'
test('assertAdminFulfillmentBindingsInput rejects non-array payloads', () => {
assert.doesNotThrow(() => assertAdminFulfillmentBindingsInput([]))
assert.throws(() => assertAdminFulfillmentBindingsInput({}), /履约配置格式不正确/)
})
test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapping', () => {
const result = normalizeAdminFulfillmentBindingItem(
{
provider: 'khhao',
platform: 'kuaishou',
shopId: 'legacy-shop',
shopName: '旧店铺',
khhaoShopId: 'khhao-1',
skuCode: 'sku-1',
match: {
externalSkuCode: 'ext-1',
},
config: {
kuaishouShop: {
note: 'keep',
},
},
},
{
index: 0,
kuaishouEticketSource: { source: true },
resolveKuaishouEticketShopConfig() {
return {
shopId: 'ks-100',
kshopName: '快手店铺A',
}
},
},
)
assert.deepEqual(result, {
provider: 'khhao',
platform: 'kuaishou',
shopId: 'ks-100',
shopName: '快手店铺A',
khhaoShopId: 'khhao-1',
skuCode: 'sku-1',
skuName: '',
profileKey: 'manual_review',
enabled: true,
priority: undefined,
config: {
kuaishouShop: {
note: 'keep',
shopId: 'ks-100',
shopName: '快手店铺A',
khhaoShopId: 'khhao-1',
},
},
match: {
externalSkuCode: 'ext-1',
externalItemId: '',
externalSkuName: '',
config: {},
},
})
})
test('normalizeAdminFulfillmentBindingItem rejects missing sku and match conditions', () => {
assert.throws(
() =>
normalizeAdminFulfillmentBindingItem(
{
provider: 'agiso',
platform: 'xianyu',
match: {
externalSkuCode: 'ext-1',
},
},
{ index: 1 },
),
/第 2 条规则缺少内部履约 SKU/,
)
assert.throws(
() =>
normalizeAdminFulfillmentBindingItem(
{
provider: 'agiso',
platform: 'xianyu',
skuCode: 'sku-1',
},
{ index: 2 },
),
/第 3 条规则至少需要一种外部匹配条件/,
)
})
test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name and shop id rules', () => {
assert.equal(
buildAdminFulfillmentBindingUniqueKey({
provider: 'khhao',
platform: 'kuaishou',
shopId: 'shop-a',
khhaoShopId: 'khhao-a',
skuCode: 'sku-1',
match: {
externalItemId: '',
externalSkuCode: '',
externalSkuName: ' 礼包 A ',
},
}),
'khhao::kuaishou::khhao-a::::::礼包 a::sku-1',
)
})