diff --git a/apps/backend/src/services/fulfillment/channel-assign-service.ts b/apps/backend/src/services/fulfillment/channel-assign-service.ts index dc2ec145..82163509 100644 --- a/apps/backend/src/services/fulfillment/channel-assign-service.ts +++ b/apps/backend/src/services/fulfillment/channel-assign-service.ts @@ -63,6 +63,7 @@ export type ChannelCandidateItem = { normalized_sku_name: string | null has_work_order: boolean has_affiliate_task: boolean + capability_channel: FulfillmentChannel | null basis_date: string } @@ -144,6 +145,18 @@ export function resolveChannelDecision( } } + // 卡券/人工没有必须创建的履约任务,可直接用商品渠道能力兜底; + // 自动发货/接单平台仍要求真实载体,避免只改口径不派单。 + if (item.capability_channel === 'third_party_voucher' || item.capability_channel === 'manual') { + return { + channel: item.capability_channel, + source: 'auto_mapping', + conflict: false, + conflictChannels: [], + matchedRuleIds: [], + } + } + return { channel: null, source: '', conflict: false, conflictChannels: [], matchedRuleIds: [] } } @@ -208,9 +221,28 @@ export async function assignPendingOrderItemChannels( EXISTS ( SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash' - ) AS has_affiliate_task + ) AS has_affiliate_task, + cap.channel AS capability_channel FROM order_items oi JOIN orders o ON o.id = oi.order_id + LEFT JOIN LATERAL ( + SELECT c.channel + FROM product_catalog p + JOIN product_channel_capabilities c + ON c.product_id = p.id AND c.enabled = TRUE + WHERE p.status = 'active' + AND btrim(p.canonical_name) = ${INTERNAL_SKU_SQL} + ORDER BY + CASE c.channel + WHEN 'affiliate_dash' THEN 1 + WHEN 'work_order' THEN 2 + WHEN 'third_party_voucher' THEN 3 + WHEN 'manual' THEN 4 + ELSE 9 + END, + c.priority ASC + LIMIT 1 + ) cap ON TRUE WHERE oi.fulfillment_status = 'pending' AND oi.fulfillment_locked = FALSE AND oi.id > $1 diff --git a/apps/backend/src/services/fulfillment/internal-sku-mapping-service.ts b/apps/backend/src/services/fulfillment/internal-sku-mapping-service.ts index abc9a07e..fb53621f 100644 --- a/apps/backend/src/services/fulfillment/internal-sku-mapping-service.ts +++ b/apps/backend/src/services/fulfillment/internal-sku-mapping-service.ts @@ -113,3 +113,41 @@ export async function upsertInternalSkuMapping(input: { ) return Number(result.rows[0]?.id || 0) } + +function containsCjk(value: string): boolean { + return /[\u4e00-\u9fa5]/.test(value) +} + +/** + * 订单项级内部 SKU 补全: + * - 现有 skuNick/outSkuSn/skuCode/skuName 已有中文 → 不查库; + * - 只有纯数字/空值 → 查 internal_sku_mappings,命中则返回中文内部 SKU。 + */ +export async function resolveInternalSkuForOrderItem(input: { + provider?: string | null + shopId?: string | null + spec?: Record | null + skuCode?: unknown + skuName?: unknown +}): Promise<{ internalSku: string; source: string; mappingId: number } | null> { + const spec = input.spec && typeof input.spec === 'object' ? input.spec : {} + const current = normalizeInternalSkuMappingValue( + spec.skuNick || spec.outSkuSn || input.skuCode || input.skuName || '', + ) + if (current && containsCjk(current)) return null + // 只有纯数字/空值才需要查映射;英文 SKU 是合法的内部键,不能误当平台码。 + if (current && !/^[0-9]+$/.test(current)) return null + + const mapping = await resolveInternalSkuMapping({ + provider: input.provider ?? null, + shopId: input.shopId ?? null, + candidates: [ + { matchType: 'sku_nick', value: spec.skuNick }, + { matchType: 'out_sku_sn', value: spec.outSkuSn }, + { matchType: 'sku_code', value: input.skuCode }, + { matchType: 'sku_name', value: input.skuName }, + ], + }) + if (!mapping) return null + return { internalSku: mapping.internalSku, source: mapping.source, mappingId: mapping.id } +} diff --git a/apps/backend/src/services/fulfillment/product-resolution-service.ts b/apps/backend/src/services/fulfillment/product-resolution-service.ts index 1d067eda..87923372 100644 --- a/apps/backend/src/services/fulfillment/product-resolution-service.ts +++ b/apps/backend/src/services/fulfillment/product-resolution-service.ts @@ -10,6 +10,7 @@ import { mergeAffiliateDashSkuMapping } from '../platforms/affiliate-dash/sku-ma import type { AffiliateDashSkuMapping } from '../../types/runtime-config.js' import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js' import { normalizeProductMatchName } from '../order/product-match-utils.js' +import { resolveInternalSkuForOrderItem } from './internal-sku-mapping-service.js' import { logIntegration } from '../../utils/logger.js' export type FulfillmentItem = { @@ -28,6 +29,7 @@ export type FulfillmentItem = { type ResolveOrderItemForFulfillmentInput = { provider?: string platform?: string + shopId?: string item?: FulfillmentItem } @@ -82,6 +84,7 @@ export type ResolvedFulfillmentItem = FulfillmentItem & { export async function resolveOrderItemForFulfillment({ provider = '', platform = '', + shopId = '', item = {}, }: ResolveOrderItemForFulfillmentInput): Promise { const candidate = await resolveConfiguredItemCandidate({ @@ -119,6 +122,17 @@ export async function resolveOrderItemForFulfillment({ externalSkuName, resolvedSkuCode, ]) + const internalSkuMapping = await resolveInternalSkuForOrderItem({ + provider, + shopId, + spec: item.spec ?? null, + skuCode: externalSkuCode, + skuName: externalSkuName, + }) + const nextSpec = internalSkuMapping + ? { ...(isPlainObject(item.spec) ? item.spec : {}), skuNick: internalSkuMapping.internalSku } + : item.spec + const snapshot = { ...(isPlainObject(item.snapshot) ? item.snapshot : {}), externalItemId, @@ -140,6 +154,13 @@ export async function resolveOrderItemForFulfillment({ } : null, isConfigured: candidate.isConfigured, + ...(internalSkuMapping + ? { + internalSku: internalSkuMapping.internalSku, + internalSkuSource: internalSkuMapping.source, + internalSkuMappingId: internalSkuMapping.mappingId, + } + : {}), // 抖音:引流长标题单独存档(skuName 已改存内部 SKU,标题不能丢) ...(isDouyinMashida ? { productTitle: pickFirstNonEmpty([item.skuName, externalSkuName]) } @@ -150,6 +171,7 @@ export async function resolveOrderItemForFulfillment({ return { ...item, + ...(nextSpec ? { spec: nextSpec } : {}), itemId: externalItemId, externalItemId, externalSkuCode, diff --git a/apps/backend/src/services/order/order-service.ts b/apps/backend/src/services/order/order-service.ts index 82ca5166..29b5653a 100644 --- a/apps/backend/src/services/order/order-service.ts +++ b/apps/backend/src/services/order/order-service.ts @@ -117,6 +117,7 @@ export async function upsertOrderFromSource( resolveOrderItemForFulfillment({ provider: event.provider, platform: event.platform, + shopId: event.shopId, item, }), ),