feat(finance): 下单入口补内部 SKU,卡券/人工按商品能力兜底
- 纯数字/空内部 SKU 入口查 internal_sku_mappings,补 spec.skuNick - 快照记录 internalSku/source,便于审计 - 财务 channel_assign 对卡券/人工使用商品渠道能力兜底(source=auto_mapping) - 自动发货/接单仍要求真实任务/工单载体,避免只改口径
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<string, unknown> | 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 }
|
||||
}
|
||||
|
||||
@@ -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<ResolvedFulfillmentItem> {
|
||||
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,
|
||||
|
||||
@@ -117,6 +117,7 @@ export async function upsertOrderFromSource(
|
||||
resolveOrderItemForFulfillment({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
item,
|
||||
}),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user