From ace3c7b47712ca83dfc7449dcb312b23fa4094d0 Mon Sep 17 00:00:00 2001 From: yml Date: Mon, 4 May 2026 15:12:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=BD=E7=A6=BB=E5=90=8E=E5=8F=B0=E5=B1=A5?= =?UTF-8?q?=E7=BA=A6=E8=A7=84=E5=88=99=E6=A0=A1=E9=AA=8C=E8=BE=85=E5=8A=A9?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/admin-platform-config-service.js | 129 ++--------------- .../admin/admin-platform-config-validation.js | 137 ++++++++++++++++++ .../admin-platform-config-validation.test.js | 119 +++++++++++++++ 3 files changed, 271 insertions(+), 114 deletions(-) create mode 100644 apps/backend/src/services/admin/admin-platform-config-validation.js create mode 100644 apps/backend/src/services/admin/admin-platform-config-validation.test.js diff --git a/apps/backend/src/services/admin/admin-platform-config-service.js b/apps/backend/src/services/admin/admin-platform-config-service.js index 5db70109..5102f4e1 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -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 diff --git a/apps/backend/src/services/admin/admin-platform-config-validation.js b/apps/backend/src/services/admin/admin-platform-config-validation.js new file mode 100644 index 00000000..d3924d99 --- /dev/null +++ b/apps/backend/src/services/admin/admin-platform-config-validation.js @@ -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} */ (options.kuaishouEticketSource || {}) + const resolveKuaishouEticketShopConfig = /** @type {(shop: Record, source: Record) => Record | 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('::') +} diff --git a/apps/backend/src/services/admin/admin-platform-config-validation.test.js b/apps/backend/src/services/admin/admin-platform-config-validation.test.js new file mode 100644 index 00000000..f3177f58 --- /dev/null +++ b/apps/backend/src/services/admin/admin-platform-config-validation.test.js @@ -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', + ) +})