fix(finance): 收敛内部 SKU 履约路由与载体状态

统一内部 SKU 与渠道主/兜底解析,禁止并列主渠道。

运营指定渠道改为先创建真实载体,失败保持明确 pending。

历史缺载体、多载体冲突和自动发货兜底统一由管道修复。

迁移人工配置与旧 sku_code 规则,补充严格预检和集成测试。
This commit is contained in:
yml2213
2026-11-02 15:02:49 +08:00
parent 5fe33b0b46
commit 2f87445ed9
33 changed files with 2199 additions and 446 deletions
@@ -0,0 +1,225 @@
-- 120_fulfillment_routing_invariants.sql
--
-- 履约路由唯一口径与数据清理:
-- 1. 订单项内部 SKU 由一个不可变 SQL 函数统一解析;
-- 2. 人工配置自动成为 internal_sku_channels 的 manual 能力;
-- 3. manual 与其他渠道并存时统一降为兜底,避免同优先级被静默抢占;
-- 4. 可确定的旧 sku_code 规则迁移为内部 SKU 渠道并停用旧规则。
CREATE OR REPLACE FUNCTION resolve_internal_sku(
p_manual_sku_key TEXT,
p_sku_name TEXT,
p_sku_code TEXT
) RETURNS TEXT
LANGUAGE SQL
IMMUTABLE
PARALLEL SAFE
AS $$
SELECT COALESCE(
CASE
WHEN BTRIM(COALESCE(p_manual_sku_key, '')) ~ '[一-龥]'
THEN BTRIM(p_manual_sku_key)
END,
CASE
WHEN BTRIM(COALESCE(p_sku_name, '')) ~ '[一-龥]'
THEN BTRIM(p_sku_name)
END,
NULLIF(BTRIM(COALESCE(p_manual_sku_key, '')), ''),
NULLIF(BTRIM(COALESCE(p_sku_name, '')), ''),
NULLIF(BTRIM(COALESCE(p_sku_code, '')), '')
)
$$;
COMMENT ON FUNCTION resolve_internal_sku(TEXT, TEXT, TEXT) IS
'订单项内部 SKU 唯一 SQL 口径:中文 manual_sku_key/sku_name 优先,再按历史生成列顺序回退';
-- 人工配置本身就是一个 manual 渠道声明。存在其他渠道时默认作为 90 优先级兜底。
INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, enabled,
canonical_product_id, config_json, source, created_at, updated_at
)
SELECT
BTRIM(cfg.sku_key),
'manual',
CASE WHEN EXISTS (
SELECT 1
FROM internal_sku_channels c
WHERE BTRIM(c.internal_sku) = BTRIM(cfg.sku_key)
AND c.enabled = TRUE
AND c.channel <> 'manual'
) THEN 90 ELSE 10 END,
CASE WHEN EXISTS (
SELECT 1
FROM internal_sku_channels c
WHERE BTRIM(c.internal_sku) = BTRIM(cfg.sku_key)
AND c.enabled = TRUE
AND c.channel <> 'manual'
) THEN TRUE ELSE FALSE END,
cfg.enabled,
cfg.canonical_product_id,
jsonb_build_object(
'manualConfigId', cfg.id,
'platformKey', cfg.platform_key,
'creditQuantity', cfg.credit_quantity,
'costMode', cfg.cost_mode
),
'manual_config',
NOW(),
NOW()
FROM manual_fulfillment_configs cfg
WHERE BTRIM(COALESCE(cfg.sku_key, '')) <> ''
ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = CASE
WHEN internal_sku_channels.source = 'operator' THEN internal_sku_channels.priority
ELSE EXCLUDED.priority
END,
is_fallback = CASE
WHEN internal_sku_channels.source = 'operator' THEN internal_sku_channels.is_fallback
ELSE EXCLUDED.is_fallback
END,
enabled = EXCLUDED.enabled,
canonical_product_id = COALESCE(EXCLUDED.canonical_product_id, internal_sku_channels.canonical_product_id),
config_json = internal_sku_channels.config_json || EXCLUDED.config_json,
updated_at = NOW();
-- 只要同一内部 SKU 还有自动发货/接单/卡券能力,人工渠道就不能作为并列主渠道。
UPDATE internal_sku_channels manual_channel
SET priority = GREATEST(manual_channel.priority, 90),
is_fallback = TRUE,
updated_at = NOW()
WHERE manual_channel.channel = 'manual'
AND manual_channel.enabled = TRUE
AND NOT (manual_channel.source = 'operator' AND manual_channel.priority = 10)
AND EXISTS (
SELECT 1
FROM internal_sku_channels other_channel
WHERE BTRIM(other_channel.internal_sku) = BTRIM(manual_channel.internal_sku)
AND other_channel.enabled = TRUE
AND other_channel.channel <> 'manual'
);
-- 自动发货旧 sku_code 规则能通过 aff:<sku> 商品稳定定位内部 SKU,直接迁移。
INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, enabled,
canonical_product_id, config_json, source, created_at, updated_at
)
SELECT
BTRIM(product.canonical_name),
'affiliate_dash',
MIN(rule.priority),
FALSE,
TRUE,
MIN(product.id),
jsonb_build_object('legacySkuCode', MIN(rule.match_key)),
'legacy-rule',
NOW(),
NOW()
FROM sku_channel_rules rule
JOIN product_catalog product
ON product.canonical_sku = CONCAT('aff:', rule.match_key)
AND product.status = 'active'
WHERE rule.match_key_type = 'sku_code'
AND rule.channel = 'affiliate_dash'
AND rule.enabled = TRUE
AND BTRIM(COALESCE(product.canonical_name, '')) <> ''
GROUP BY BTRIM(product.canonical_name)
ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = LEAST(internal_sku_channels.priority, EXCLUDED.priority),
enabled = TRUE,
canonical_product_id = COALESCE(EXCLUDED.canonical_product_id, internal_sku_channels.canonical_product_id),
config_json = internal_sku_channels.config_json || EXCLUDED.config_json,
updated_at = NOW();
UPDATE sku_channel_rules rule
SET enabled = FALSE,
note = CONCAT(COALESCE(rule.note, ''), ' [已迁移 internal_sku]'),
updated_at = NOW()
WHERE rule.match_key_type = 'sku_code'
AND rule.channel = 'affiliate_dash'
AND rule.enabled = TRUE
AND EXISTS (
SELECT 1
FROM product_catalog product
WHERE product.canonical_sku = CONCAT('aff:', rule.match_key)
AND product.status = 'active'
AND BTRIM(COALESCE(product.canonical_name, '')) <> ''
);
-- 卡券旧规则按历史订单中唯一可解析的内部 SKU 迁移;歧义数据保持原规则不动。
WITH resolved_legacy AS (
SELECT
rule.id,
matched.internal_sku,
MIN(rule.priority) AS priority
FROM sku_channel_rules rule
CROSS JOIN LATERAL (
SELECT
MIN(resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code)) AS internal_sku,
COUNT(DISTINCT resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code)) AS internal_count
FROM order_items oi
JOIN orders source_order ON source_order.id = oi.order_id
WHERE oi.sku_code = rule.match_key
AND (rule.provider = '' OR source_order.provider = rule.provider)
AND (rule.shop_id = '' OR source_order.shop_id = rule.shop_id)
) matched
WHERE rule.match_key_type = 'sku_code'
AND rule.channel = 'third_party_voucher'
AND rule.enabled = TRUE
AND matched.internal_count = 1
AND BTRIM(COALESCE(matched.internal_sku, '')) <> ''
GROUP BY rule.id, matched.internal_sku
)
INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, enabled,
config_json, source, created_at, updated_at
)
SELECT
internal_sku,
'third_party_voucher',
priority,
FALSE,
TRUE,
'{}'::jsonb,
'legacy-rule',
NOW(),
NOW()
FROM resolved_legacy
ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = LEAST(internal_sku_channels.priority, EXCLUDED.priority),
enabled = TRUE,
updated_at = NOW();
WITH resolved_legacy AS (
SELECT rule.id
FROM sku_channel_rules rule
CROSS JOIN LATERAL (
SELECT
MIN(resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code)) AS internal_sku,
COUNT(DISTINCT resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code)) AS internal_count
FROM order_items oi
JOIN orders source_order ON source_order.id = oi.order_id
WHERE oi.sku_code = rule.match_key
AND (rule.provider = '' OR source_order.provider = rule.provider)
AND (rule.shop_id = '' OR source_order.shop_id = rule.shop_id)
) matched
WHERE rule.match_key_type = 'sku_code'
AND rule.channel = 'third_party_voucher'
AND rule.enabled = TRUE
AND matched.internal_count = 1
AND BTRIM(COALESCE(matched.internal_sku, '')) <> ''
)
UPDATE sku_channel_rules rule
SET enabled = FALSE,
note = CONCAT(COALESCE(rule.note, ''), ' [已迁移 internal_sku]'),
updated_at = NOW()
FROM resolved_legacy
WHERE rule.id = resolved_legacy.id;
ALTER TABLE internal_sku_channels
DROP CONSTRAINT IF EXISTS chk_internal_sku_channels_priority_nonnegative;
ALTER TABLE internal_sku_channels
ADD CONSTRAINT chk_internal_sku_channels_priority_nonnegative CHECK (priority >= 0);
CREATE INDEX IF NOT EXISTS idx_internal_sku_channels_fallback
ON internal_sku_channels (internal_sku, priority, channel)
WHERE enabled = TRUE AND is_fallback = TRUE;
@@ -0,0 +1,25 @@
-- 121_internal_sku_single_primary.sql
--
-- 一个内部 SKU 只能有一个主渠道;其他渠道必须显式标记 is_fallback。
-- 迁移 120 已把人工并列渠道收敛为兜底;这里在任何剩余歧义上显式阻止部署。
DO $$
DECLARE
conflict_sku TEXT;
BEGIN
SELECT internal_sku INTO conflict_sku
FROM internal_sku_channels
WHERE enabled = TRUE AND is_fallback = FALSE
GROUP BY internal_sku
HAVING count(*) > 1
ORDER BY internal_sku
LIMIT 1;
IF conflict_sku IS NOT NULL THEN
RAISE EXCEPTION '内部 SKU % 存在多个主渠道,请先将其余渠道标记为 is_fallback', conflict_sku;
END IF;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS uq_internal_sku_channels_single_primary
ON internal_sku_channels (internal_sku)
WHERE enabled = TRUE AND is_fallback = FALSE;