fix(finance): 收敛内部 SKU 履约路由与载体状态
统一内部 SKU 与渠道主/兜底解析,禁止并列主渠道。 运营指定渠道改为先创建真实载体,失败保持明确 pending。 历史缺载体、多载体冲突和自动发货兜底统一由管道修复。 迁移人工配置与旧 sku_code 规则,补充严格预检和集成测试。
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user