|
|
|
@@ -0,0 +1,345 @@
|
|
|
|
|
-- 120_fulfillment_routing_invariants.sql
|
|
|
|
|
--
|
|
|
|
|
-- 生产迁移 119 之后的履约路由统一迁移。
|
|
|
|
|
--
|
|
|
|
|
-- 目标:
|
|
|
|
|
-- 1. 订单项内部 SKU 只有一个 SQL 解析口径;
|
|
|
|
|
-- 2. internal_sku_channels 成为唯一渠道配置源;
|
|
|
|
|
-- 3. 人工配置、旧 sku_code 规则和渠道适配参数全部回填到统一表;
|
|
|
|
|
-- 4. 原有 sku_code 规则只做一次性迁移,迁移后可确定的规则全部停用。
|
|
|
|
|
|
|
|
|
|
-- 稳定 id 让后台渠道配置可以通过统一 API 增删改,而不必继续暴露旧规则表。
|
|
|
|
|
ALTER TABLE internal_sku_channels
|
|
|
|
|
ADD COLUMN IF NOT EXISTS id BIGSERIAL,
|
|
|
|
|
ADD COLUMN IF NOT EXISTS note TEXT NOT NULL DEFAULT '',
|
|
|
|
|
ADD COLUMN IF NOT EXISTS created_by_admin_user_id BIGINT
|
|
|
|
|
REFERENCES admin_users(id) ON DELETE SET NULL;
|
|
|
|
|
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_internal_sku_channels_id
|
|
|
|
|
ON internal_sku_channels (id);
|
|
|
|
|
|
|
|
|
|
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_code 规则能稳定解析出的内部 SKU,统一补到入口映射表。
|
|
|
|
|
WITH legacy_candidates AS (
|
|
|
|
|
SELECT
|
|
|
|
|
rule.provider,
|
|
|
|
|
rule.shop_id,
|
|
|
|
|
rule.match_key AS sku_code,
|
|
|
|
|
BTRIM(product.canonical_name) AS internal_sku,
|
|
|
|
|
10 AS priority
|
|
|
|
|
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 BTRIM(COALESCE(product.canonical_name, '')) <> ''
|
|
|
|
|
|
|
|
|
|
UNION ALL
|
|
|
|
|
|
|
|
|
|
SELECT
|
|
|
|
|
rule.provider,
|
|
|
|
|
rule.shop_id,
|
|
|
|
|
rule.match_key AS sku_code,
|
|
|
|
|
matched.internal_sku,
|
|
|
|
|
20 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 matched.internal_count = 1
|
|
|
|
|
AND BTRIM(COALESCE(matched.internal_sku, '')) <> ''
|
|
|
|
|
),
|
|
|
|
|
ranked_legacy_candidates AS (
|
|
|
|
|
SELECT *,
|
|
|
|
|
ROW_NUMBER() OVER (
|
|
|
|
|
PARTITION BY provider, shop_id, sku_code
|
|
|
|
|
ORDER BY priority, internal_sku
|
|
|
|
|
) AS candidate_rank
|
|
|
|
|
FROM legacy_candidates
|
|
|
|
|
)
|
|
|
|
|
INSERT INTO internal_sku_mappings (
|
|
|
|
|
provider, shop_id, match_type, match_value, internal_sku,
|
|
|
|
|
priority, enabled, source, note, created_at, updated_at
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
provider, shop_id, 'sku_code', sku_code, internal_sku,
|
|
|
|
|
priority, TRUE, 'legacy-rule', '由旧 sku_code 渠道规则迁移', NOW(), NOW()
|
|
|
|
|
FROM ranked_legacy_candidates
|
|
|
|
|
WHERE candidate_rank = 1
|
|
|
|
|
ON CONFLICT (provider, shop_id, match_type, match_value) DO UPDATE SET
|
|
|
|
|
internal_sku = EXCLUDED.internal_sku,
|
|
|
|
|
priority = LEAST(internal_sku_mappings.priority, EXCLUDED.priority),
|
|
|
|
|
enabled = TRUE,
|
|
|
|
|
source = EXCLUDED.source,
|
|
|
|
|
note = EXCLUDED.note,
|
|
|
|
|
updated_at = NOW();
|
|
|
|
|
|
|
|
|
|
-- 可确定的旧 sku_code 渠道转换为 internal_sku_channels。
|
|
|
|
|
WITH resolved_rules AS (
|
|
|
|
|
SELECT
|
|
|
|
|
rule.id,
|
|
|
|
|
rule.provider,
|
|
|
|
|
rule.shop_id,
|
|
|
|
|
rule.match_key,
|
|
|
|
|
rule.channel,
|
|
|
|
|
rule.priority,
|
|
|
|
|
COALESCE(
|
|
|
|
|
(
|
|
|
|
|
SELECT BTRIM(product.canonical_name)
|
|
|
|
|
FROM product_catalog product
|
|
|
|
|
WHERE product.canonical_sku = CONCAT('aff:', rule.match_key)
|
|
|
|
|
AND product.status = 'active'
|
|
|
|
|
ORDER BY product.id
|
|
|
|
|
LIMIT 1
|
|
|
|
|
),
|
|
|
|
|
matched.internal_sku
|
|
|
|
|
) AS internal_sku,
|
|
|
|
|
matched.canonical_product_id
|
|
|
|
|
FROM sku_channel_rules rule
|
|
|
|
|
LEFT 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,
|
|
|
|
|
MIN(oi.canonical_product_id) AS canonical_product_id
|
|
|
|
|
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 ON matched.internal_count = 1
|
|
|
|
|
WHERE rule.match_key_type = 'sku_code'
|
|
|
|
|
AND rule.enabled = TRUE
|
|
|
|
|
)
|
|
|
|
|
INSERT INTO internal_sku_channels (
|
|
|
|
|
internal_sku, channel, priority, is_fallback, enabled,
|
|
|
|
|
canonical_product_id, config_json, source, created_at, updated_at
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
BTRIM(internal_sku),
|
|
|
|
|
channel,
|
|
|
|
|
MIN(priority),
|
|
|
|
|
FALSE,
|
|
|
|
|
TRUE,
|
|
|
|
|
MIN(canonical_product_id),
|
|
|
|
|
jsonb_build_object(
|
|
|
|
|
'legacySkuCode', MIN(match_key),
|
|
|
|
|
'legacyProvider', MIN(provider),
|
|
|
|
|
'legacyShopId', MIN(shop_id)
|
|
|
|
|
),
|
|
|
|
|
'legacy-rule',
|
|
|
|
|
NOW(),
|
|
|
|
|
NOW()
|
|
|
|
|
FROM resolved_rules
|
|
|
|
|
WHERE BTRIM(COALESCE(internal_sku, '')) <> ''
|
|
|
|
|
GROUP BY BTRIM(internal_sku), channel
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
-- 已转成内部 SKU 的旧规则停用;真正无法解析的规则保留 enabled,交给严格预检阻断。
|
|
|
|
|
WITH resolved_rule_ids AS (
|
|
|
|
|
SELECT rule.id
|
|
|
|
|
FROM sku_channel_rules rule
|
|
|
|
|
LEFT 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 ON matched.internal_count = 1
|
|
|
|
|
WHERE rule.match_key_type = 'sku_code'
|
|
|
|
|
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'
|
|
|
|
|
)
|
|
|
|
|
OR 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_rule_ids
|
|
|
|
|
WHERE rule.id = resolved_rule_ids.id;
|
|
|
|
|
|
|
|
|
|
-- 旧 internal_sku 规则已完整复制到统一表,不再参与运行时判定。
|
|
|
|
|
UPDATE sku_channel_rules
|
|
|
|
|
SET enabled = FALSE,
|
|
|
|
|
note = CONCAT(COALESCE(note, ''), ' [internal_sku_channels 已成为真源]'),
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE match_key_type = 'internal_sku' AND enabled = TRUE;
|
|
|
|
|
|
|
|
|
|
-- 回填适配参数,避免统一表只有“渠道名”而实际还要猜 affiliate SKU / 工单模板。
|
|
|
|
|
WITH affiliate_config AS (
|
|
|
|
|
SELECT
|
|
|
|
|
BTRIM(product.canonical_name) AS internal_sku,
|
|
|
|
|
MIN(SUBSTRING(product.canonical_sku FROM 5)) AS affiliate_sku,
|
|
|
|
|
MIN(product.id) AS canonical_product_id
|
|
|
|
|
FROM product_catalog product
|
|
|
|
|
WHERE product.status = 'active' AND product.canonical_sku LIKE 'aff:%'
|
|
|
|
|
GROUP BY BTRIM(product.canonical_name)
|
|
|
|
|
)
|
|
|
|
|
UPDATE internal_sku_channels channel
|
|
|
|
|
SET config_json = channel.config_json || jsonb_build_object(
|
|
|
|
|
'affiliateSku', affiliate_config.affiliate_sku
|
|
|
|
|
),
|
|
|
|
|
canonical_product_id = COALESCE(channel.canonical_product_id, affiliate_config.canonical_product_id),
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
FROM affiliate_config
|
|
|
|
|
WHERE channel.channel = 'affiliate_dash'
|
|
|
|
|
AND BTRIM(channel.internal_sku) = affiliate_config.internal_sku;
|
|
|
|
|
|
|
|
|
|
WITH work_order_config AS (
|
|
|
|
|
SELECT
|
|
|
|
|
BTRIM(product.canonical_name) AS internal_sku,
|
|
|
|
|
MIN(SUBSTRING(product.canonical_sku FROM 5)) AS work_rule_key,
|
|
|
|
|
MIN(product.id) AS canonical_product_id
|
|
|
|
|
FROM product_catalog product
|
|
|
|
|
WHERE product.status = 'active' AND product.canonical_sku LIKE 'tpl:%'
|
|
|
|
|
GROUP BY BTRIM(product.canonical_name)
|
|
|
|
|
)
|
|
|
|
|
UPDATE internal_sku_channels channel
|
|
|
|
|
SET config_json = channel.config_json || jsonb_build_object(
|
|
|
|
|
'workRuleKey', work_order_config.work_rule_key
|
|
|
|
|
),
|
|
|
|
|
canonical_product_id = COALESCE(channel.canonical_product_id, work_order_config.canonical_product_id),
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
FROM work_order_config
|
|
|
|
|
WHERE channel.channel = 'work_order'
|
|
|
|
|
AND BTRIM(channel.internal_sku) = work_order_config.internal_sku;
|
|
|
|
|
|
|
|
|
|
WITH voucher_config AS (
|
|
|
|
|
SELECT
|
|
|
|
|
BTRIM(product.canonical_name) AS internal_sku,
|
|
|
|
|
MIN(SUBSTRING(product.canonical_sku FROM 5)) AS voucher_sku,
|
|
|
|
|
MIN(product.id) AS canonical_product_id
|
|
|
|
|
FROM product_catalog product
|
|
|
|
|
WHERE product.status = 'active' AND product.canonical_sku LIKE 'tpv:%'
|
|
|
|
|
GROUP BY BTRIM(product.canonical_name)
|
|
|
|
|
)
|
|
|
|
|
UPDATE internal_sku_channels channel
|
|
|
|
|
SET config_json = channel.config_json || jsonb_build_object(
|
|
|
|
|
'voucherSku', voucher_config.voucher_sku
|
|
|
|
|
),
|
|
|
|
|
canonical_product_id = COALESCE(channel.canonical_product_id, voucher_config.canonical_product_id),
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
FROM voucher_config
|
|
|
|
|
WHERE channel.channel = 'third_party_voucher'
|
|
|
|
|
AND BTRIM(channel.internal_sku) = voucher_config.internal_sku;
|
|
|
|
|
|
|
|
|
|
-- 只要同一内部 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'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
ALTER TABLE internal_sku_channels
|
|
|
|
|
DROP CONSTRAINT IF EXISTS chk_internal_sku_channels_config_object;
|
|
|
|
|
ALTER TABLE internal_sku_channels
|
|
|
|
|
ADD CONSTRAINT chk_internal_sku_channels_config_object
|
|
|
|
|
CHECK (jsonb_typeof(config_json) = 'object');
|
|
|
|
|
|
|
|
|
|
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;
|