feat(finance): 统一内部 SKU 驱动的履约与财务分流

- 新增 internal_sku_mappings / internal_sku_channels,平台字段统一映射到内部中文 SKU
- 下单入口按内部 SKU 渠道决策修正默认人工路由,自动发货补全 dash sku
- 财务 channel_assign 读取统一渠道表;卡券/人工能力兜底,自动发货/接单要求真实载体
- carrier reconciliation 自动补自动发货任务和接单工单
- pending 落明确原因:无内部 SKU / 无渠道能力 / 无履约载体
- 合并、重判、旧 sku_code 规则清理、部署后 preflight 核对脚本
- 待分类队列按内部 SKU 聚合并展示原因
This commit is contained in:
yml2213
2026-11-02 14:17:08 +08:00
parent 531a23d78b
commit 5fe33b0b46
18 changed files with 1020 additions and 15 deletions
@@ -0,0 +1,67 @@
-- 118_internal_sku_channels.sql
--
-- 内部中文 SKU 的渠道决策单一真源。
-- 从商品渠道能力 + 现有 internal_sku 全局规则回填;后续 intake/finance 都读这张表。
CREATE TABLE IF NOT EXISTS internal_sku_channels (
internal_sku TEXT NOT NULL,
channel TEXT NOT NULL CHECK (channel IN ('work_order', 'affiliate_dash', 'third_party_voucher', 'manual')),
priority INTEGER NOT NULL DEFAULT 100,
is_fallback BOOLEAN NOT NULL DEFAULT FALSE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE SET NULL,
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
source TEXT NOT NULL DEFAULT 'backfill',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (internal_sku, channel)
);
CREATE INDEX IF NOT EXISTS idx_internal_sku_channels_priority
ON internal_sku_channels (internal_sku, priority, channel)
WHERE enabled = TRUE;
COMMENT ON TABLE internal_sku_channels IS
'内部中文 SKU 渠道能力:priority 越小越优先,is_fallback 表示兜底渠道';
WITH capability_source AS (
SELECT
btrim(p.canonical_name) AS internal_sku,
c.channel,
MIN(c.priority) AS priority,
BOOL_AND(c.is_fallback) AS is_fallback,
MIN(p.id) AS canonical_product_id
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) <> ''
GROUP BY 1, 2
)
INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, canonical_product_id, source
)
SELECT internal_sku, channel, priority, is_fallback, canonical_product_id, 'capability'
FROM capability_source
ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = LEAST(internal_sku_channels.priority, EXCLUDED.priority),
is_fallback = internal_sku_channels.is_fallback AND EXCLUDED.is_fallback,
canonical_product_id = COALESCE(internal_sku_channels.canonical_product_id, EXCLUDED.canonical_product_id),
updated_at = NOW();
WITH rule_source AS (
SELECT btrim(match_key) AS internal_sku, channel, MIN(priority) AS priority
FROM sku_channel_rules
WHERE match_key_type = 'internal_sku'
AND provider = '' AND shop_id = ''
AND enabled = TRUE
AND btrim(match_key) <> ''
GROUP BY 1, 2
)
INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, canonical_product_id, source
)
SELECT internal_sku, channel, priority, FALSE, NULL, 'rule'
FROM rule_source
ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = LEAST(internal_sku_channels.priority, EXCLUDED.priority),
is_fallback = FALSE,
updated_at = NOW();
@@ -0,0 +1,14 @@
-- 119_fulfillment_pending_reason.sql
--
-- 待分类不再是一个模糊状态:落明确原因,供队列/审计区分。
-- no_internal_sku 没有内部中文 SKU
-- no_channel_capability 有内部 SKU,但没有渠道能力/规则
-- no_carrier 有渠道能力,但缺任务/工单载体
ALTER TABLE order_items
ADD COLUMN IF NOT EXISTS fulfillment_pending_reason TEXT,
ADD COLUMN IF NOT EXISTS fulfillment_pending_detail JSONB;
CREATE INDEX IF NOT EXISTS idx_order_items_pending_reason
ON order_items (fulfillment_pending_reason)
WHERE fulfillment_status = 'pending';