diff --git a/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts b/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts index 93c43546..24d8665f 100644 --- a/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts +++ b/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts @@ -6,6 +6,7 @@ import { listAdminKuaishouFeifeiProducts, matchAdminKuaishouFeifeiProduct, queryAdminKuaishouFeifeiTestOrder, + syncAdminKuaishouFeifeiProductRules, updateAdminKuaishouFeifeiConfig, } from "../../../services/admin/platform-config/kuaishou-feifei-service.js"; import { createJsonHandler } from "../session.js"; @@ -74,6 +75,33 @@ router.post( ) ); +router.post( + "/kuaishou-feifei/sync-products", + createJsonHandler( + (req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord), + { + successMessage: "kuaishou-feifei 商品映射已同步", + errorMessage: "同步 kuaishou-feifei 商品映射失败", + scope: "[admin/platform-config/kuaishou-feifei/sync-products]", + audit: (_req, data) => { + const result = data as JsonRecord; + const sync = result.sync as JsonRecord | undefined; + + return { + action: "platform_kuaishou_feifei_products_synced", + targetType: "platform_config", + targetId: "kuaishou_feifei", + data: { + productCount: Number(sync?.productCount || 0) || 0, + ruleCount: Number(sync?.ruleCount || 0) || 0, + status: String(sync?.status || "").trim(), + }, + }; + }, + } + ) +); + router.post( "/kuaishou-feifei/test-order", createJsonHandler( diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts index 04bea363..9c81d7c9 100644 --- a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts +++ b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts @@ -3,6 +3,7 @@ import { createKuaishouFeifeiOrder, listKuaishouFeifeiProducts, queryKuaishouFeifeiOrder, + type KuaishouFeifeiProductListResult, } from '../../platforms/kuaishou-feifei/order-service.js' import { resolveKuaishouFeifeiProductByName } from '../../platforms/kuaishou-feifei/product-rule-service.js' import { @@ -64,6 +65,52 @@ export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {}) }) } +export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) { + const source = getAdminEditableKuaishouFeifeiConfig() + const status = String(payload.status || 'on_sale').trim() || 'on_sale' + const supplyProductName = String(payload.supplyProductName || payload.supply_product_name || '').trim() + const products = await listAllKuaishouFeifeiProducts({ + status, + supplyProductName, + }) + const productRules = products + .map((product) => { + const productName = String(product.name || '').trim() + const productCode = String(product.productCode || '').trim() + if (!productName || !productCode) { + return null + } + + return { + id: normalizeCloudtentaclesMatchName(productName) || productCode, + enabled: product.status !== 'off_sale' && product.supplyStatus !== 'off_sale', + productName, + normalizedProductName: normalizeCloudtentaclesMatchName(productName), + productCode, + skuName: productName, + notes: `feifei 商品目录同步:${product.channel || 'unknown'}`, + } + }) + .filter((rule): rule is NonNullable => Boolean(rule)) + const saved = saveKuaishouFeifeiSourceConfig({ + ...source, + productRules, + }) + const effective = getKuaishouFeifeiConfig() + + return { + filePath: getKuaishouFeifeiConfigFilePath(), + source: saved, + effective: mapEffectiveKuaishouFeifeiConfig(effective), + sync: { + productCount: products.length, + ruleCount: saved.productRules.length, + status, + supplyProductName, + }, + } +} + export async function createAdminKuaishouFeifeiTestOrder(payload: JsonObject = {}) { const platformOrderNo = String(payload.platformOrderNo || payload.platform_order_no || '').trim() const productCode = String(payload.productCode || payload.product_code || '').trim() @@ -139,6 +186,34 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType +export type KuaishouFeifeiProductListResult = ReturnType + export async function listKuaishouFeifeiProducts(input: { page?: number perPage?: number diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.test.ts b/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.test.ts new file mode 100644 index 00000000..eb5050d1 --- /dev/null +++ b/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.test.ts @@ -0,0 +1,22 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { resolveKuaishouFeifeiProductByName } from './product-rule-service.js' + +test('resolveKuaishouFeifeiProductByName matches generated name-code mapping by normalized name', () => { + const match = resolveKuaishouFeifeiProductByName('【自动发货】套装-Alan Walker', { + rules: [ + { + productName: '套装-Alan Walker', + normalizedProductName: '套装 alan walker', + productCode: '10000013', + skuName: '套装-Alan Walker', + }, + ], + }) + + assert.equal(match?.productCode, '10000013') + assert.equal(match?.matchMode, 'kuaishou_feifei_name') + assert.equal(match?.skuName, '套装-Alan Walker') + assert.equal(match?.normalizedProductName, '套装 alan walker') +}) diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts index 5f4db9f5..26737627 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts @@ -3,7 +3,7 @@ import { getKuaishouFeifeiConfig } from './config.js' import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js' export type KuaishouFeifeiProductMatch = { - matchMode: 'kuaishou_feifei_rule' + matchMode: 'kuaishou_feifei_name' productName: string normalizedProductName: string productCode: string @@ -37,7 +37,7 @@ export function resolveKuaishouFeifeiProductByName( } return { - matchMode: 'kuaishou_feifei_rule', + matchMode: 'kuaishou_feifei_name', productName: String(matched.productName || productName || '').trim(), normalizedProductName, productCode, diff --git a/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts b/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts index 76282c0b..b5324f95 100644 --- a/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts +++ b/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts @@ -5,6 +5,7 @@ import type { AdminKuaishouFeifeiMatchResult, AdminKuaishouFeifeiOrderResult, AdminKuaishouFeifeiProductListResult, + AdminKuaishouFeifeiProductSyncResult, } from '@/types/admin' export function fetchAdminKuaishouFeifeiConfig() { @@ -39,6 +40,16 @@ export function fetchAdminKuaishouFeifeiProducts(payload: { ) } +export function syncAdminKuaishouFeifeiProducts(payload: { + status?: string + supplyProductName?: string +} = {}) { + return apiPost( + '/api/v1/admin/platform-config/kuaishou-feifei/sync-products', + payload, + ) +} + export function createAdminKuaishouFeifeiTestOrder(payload: { platformOrderNo: string productCode: string diff --git a/apps/frontend/src/types/admin/index.ts b/apps/frontend/src/types/admin/index.ts index 2a3b0d89..db08037a 100644 --- a/apps/frontend/src/types/admin/index.ts +++ b/apps/frontend/src/types/admin/index.ts @@ -54,6 +54,7 @@ export type { AdminKuaishouFeifeiConfig, AdminKuaishouFeifeiEffectiveConfig, AdminKuaishouFeifeiConfigResponse, + AdminKuaishouFeifeiProductSyncResult, AdminKuaishouFeifeiProductMatch, AdminKuaishouFeifeiMatchResult, AdminKuaishouFeifeiProductItem, diff --git a/apps/frontend/src/types/admin/platform-config/index.ts b/apps/frontend/src/types/admin/platform-config/index.ts index bd2ff06e..560c4c6d 100644 --- a/apps/frontend/src/types/admin/platform-config/index.ts +++ b/apps/frontend/src/types/admin/platform-config/index.ts @@ -35,6 +35,7 @@ export type { AdminKuaishouFeifeiConfig, AdminKuaishouFeifeiEffectiveConfig, AdminKuaishouFeifeiConfigResponse, + AdminKuaishouFeifeiProductSyncResult, AdminKuaishouFeifeiProductMatch, AdminKuaishouFeifeiMatchResult, AdminKuaishouFeifeiProductItem, diff --git a/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts b/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts index a36980ca..ecea64ef 100644 --- a/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts +++ b/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts @@ -34,6 +34,15 @@ export interface AdminKuaishouFeifeiConfigResponse { effective: AdminKuaishouFeifeiEffectiveConfig } +export interface AdminKuaishouFeifeiProductSyncResult extends AdminKuaishouFeifeiConfigResponse { + sync: { + productCount: number + ruleCount: number + status: string + supplyProductName: string + } +} + export interface AdminKuaishouFeifeiProductMatch { matchMode: string productName: string diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue b/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue index d7ee9a4f..7df3ae92 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue @@ -1,56 +1,44 @@ @@ -278,14 +226,13 @@ function createRuleId() { .rule-overview { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(150px, auto); + grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(190px, auto); gap: var(--space-3); align-items: stretch; } .metric-card, -.rule-panel, -.rule-item { +.rule-panel { min-width: 0; border: 1px solid var(--border-default); border-radius: var(--radius-md); @@ -300,7 +247,8 @@ function createRuleId() { .metric-card span, .metric-card small, -.section-head p { +.section-head p, +.product-cell small { color: var(--text-secondary); } @@ -312,8 +260,7 @@ function createRuleId() { .overview-actions, .rule-head-title, -.match-checker, -.rule-head { +.match-checker { display: flex; align-items: center; gap: var(--space-2); @@ -323,8 +270,7 @@ function createRuleId() { justify-content: flex-end; } -.rule-panel, -.rule-item { +.rule-panel { display: grid; gap: var(--space-4); padding: var(--space-4); @@ -371,29 +317,25 @@ function createRuleId() { background: var(--el-color-success-light-9); } -.rule-list { - display: grid; - gap: var(--space-3); -} - -.rule-head { - justify-content: space-between; -} - -.rule-form { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--space-3); -} - -.rule-form :deep(.el-input), -.rule-form :deep(.el-select) { +.rule-table { width: 100%; } +.product-cell { + display: grid; + gap: 4px; + min-width: 0; +} + +.product-cell strong, +.product-cell small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + @media (max-width: 900px) { - .rule-overview, - .rule-form { + .rule-overview { grid-template-columns: 1fr; }