feat(cost-center): 成本中心与商品映射开发草案 (含文档与订单页调整 WIP)
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
-- 102_product_catalog.sql —— 标准商品主数据 + 渠道 SKU 映射。
|
||||
--
|
||||
-- 目标:把「平台原始 SKU / 商品名」归一到内部标准商品,后续成本、销量、
|
||||
-- 店铺日报和订单毛利都只认 canonical_product_id。
|
||||
--
|
||||
-- 约束:
|
||||
-- 1. canonical_sku 是内部稳定编码,创建后不随平台改名;
|
||||
-- 2. 同一 provider + shop_id + platform_sku_code 只能有一条映射;
|
||||
-- 3. shop_id 为空串表示该来源的全局映射,具体店铺映射优先;
|
||||
-- 4. order_items.normalized_quantity 从 SKU 名称解析,解析失败先回填 quantity。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_catalog (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
canonical_sku TEXT NOT NULL,
|
||||
canonical_name TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT '',
|
||||
unit_name TEXT NOT NULL DEFAULT '件',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
attributes_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_product_catalog_canonical_sku
|
||||
ON product_catalog (canonical_sku);
|
||||
CREATE INDEX IF NOT EXISTS idx_product_catalog_status_category
|
||||
ON product_catalog (status, category, canonical_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_product_catalog_name
|
||||
ON product_catalog (canonical_name);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_product_catalog_status') THEN
|
||||
ALTER TABLE product_catalog
|
||||
ADD CONSTRAINT chk_product_catalog_status
|
||||
CHECK (status IN ('active', 'archived'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_product_catalog_sku') THEN
|
||||
ALTER TABLE product_catalog
|
||||
ADD CONSTRAINT chk_product_catalog_sku
|
||||
CHECK (length(trim(canonical_sku)) > 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_product_catalog_name') THEN
|
||||
ALTER TABLE product_catalog
|
||||
ADD CONSTRAINT chk_product_catalog_name
|
||||
CHECK (length(trim(canonical_name)) > 0);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE product_catalog IS '标准商品/皮肤主数据:报表、成本和销量统计的唯一商品维度';
|
||||
COMMENT ON COLUMN product_catalog.canonical_sku IS '内部稳定编码;创建后不随平台改名';
|
||||
COMMENT ON COLUMN product_catalog.unit_name IS '计量单位:件/个/颗等,配合 normalized_quantity 使用';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_product_mappings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
provider TEXT NOT NULL,
|
||||
shop_id TEXT NOT NULL DEFAULT '',
|
||||
platform_sku_code TEXT NOT NULL,
|
||||
platform_sku_name TEXT NOT NULL DEFAULT '',
|
||||
canonical_product_id BIGINT NOT NULL REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||||
quantity_multiplier INTEGER NOT NULL DEFAULT 1,
|
||||
confidence TEXT NOT NULL DEFAULT 'manual',
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_product_mappings_key
|
||||
ON channel_product_mappings (provider, shop_id, platform_sku_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_product_mappings_product
|
||||
ON channel_product_mappings (canonical_product_id, enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_product_mappings_lookup
|
||||
ON channel_product_mappings (provider, shop_id, platform_sku_code, enabled);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_channel_product_mappings_confidence') THEN
|
||||
ALTER TABLE channel_product_mappings
|
||||
ADD CONSTRAINT chk_channel_product_mappings_confidence
|
||||
CHECK (confidence IN ('exact', 'rule', 'manual'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_channel_product_mappings_source') THEN
|
||||
ALTER TABLE channel_product_mappings
|
||||
ADD CONSTRAINT chk_channel_product_mappings_source
|
||||
CHECK (source IN ('manual', 'import', 'rule'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_channel_product_mappings_multiplier') THEN
|
||||
ALTER TABLE channel_product_mappings
|
||||
ADD CONSTRAINT chk_channel_product_mappings_multiplier
|
||||
CHECK (quantity_multiplier > 0);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE channel_product_mappings IS '来源 + 店铺 + 平台 SKU -> 标准商品映射';
|
||||
COMMENT ON COLUMN channel_product_mappings.shop_id IS '空串=该来源的全局映射;具体店铺映射优先';
|
||||
COMMENT ON COLUMN channel_product_mappings.quantity_multiplier IS 'SKU 名称内含数量时的换算系数,例如「5个」=5';
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||||
ADD COLUMN IF NOT EXISTS normalized_quantity INTEGER;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_canonical_product
|
||||
ON order_items (canonical_product_id);
|
||||
|
||||
COMMENT ON COLUMN order_items.canonical_product_id IS '归一到标准商品;由渠道映射或人工指定';
|
||||
COMMENT ON COLUMN order_items.normalized_quantity IS '归一化件数:从 SKU 名称解析并固化,禁止直接用 quantity 做销量';
|
||||
|
||||
-- 初始回填只保证不为空;后续由成本中心页面/脚本按 SKU 名称解析覆盖。
|
||||
UPDATE order_items
|
||||
SET normalized_quantity = GREATEST(COALESCE(quantity, 1), 1)
|
||||
WHERE normalized_quantity IS NULL;
|
||||
@@ -0,0 +1,244 @@
|
||||
-- 103_cost_center.sql —— 成本项目、成本版本、成本快照与成本异常。
|
||||
--
|
||||
-- 目标:所有成本金额只在 cost_versions 维护;订单项/工单/人工单只保存
|
||||
-- cost_snapshot_id 或快照行,历史成本永不回写。
|
||||
--
|
||||
-- 口径(2026-10-17 已确认):
|
||||
-- * 自动发货 = 上游采购成本;
|
||||
-- * 接单平台 = 打手报酬,按总额法计履约成本;
|
||||
-- * 人工客服 = 人工支出登记,按 5102 履约成本-人工客服;
|
||||
-- * 匹配不到成本不得默认 0,必须进入 cost_exceptions。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cost_items (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
cost_type TEXT NOT NULL,
|
||||
carrier_type TEXT NOT NULL,
|
||||
pricing_mode TEXT NOT NULL,
|
||||
unit_name TEXT NOT NULL DEFAULT '件',
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_items_code
|
||||
ON cost_items (code);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_items_status_carrier
|
||||
ON cost_items (status, carrier_type, cost_type);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_cost_type') THEN
|
||||
ALTER TABLE cost_items
|
||||
ADD CONSTRAINT chk_cost_items_cost_type
|
||||
CHECK (cost_type IN ('auto_purchase', 'work_order_reward', 'manual_service', 'platform_fee', 'payment_fee', 'after_sales_loss', 'shipping_other', 'other'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_carrier_type') THEN
|
||||
ALTER TABLE cost_items
|
||||
ADD CONSTRAINT chk_cost_items_carrier_type
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'manual', 'shared'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_pricing_mode') THEN
|
||||
ALTER TABLE cost_items
|
||||
ADD CONSTRAINT chk_cost_items_pricing_mode
|
||||
CHECK (pricing_mode IN ('per_item', 'per_order', 'per_hour', 'percent_of_revenue', 'fixed_daily'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_status') THEN
|
||||
ALTER TABLE cost_items
|
||||
ADD CONSTRAINT chk_cost_items_status
|
||||
CHECK (status IN ('active', 'archived'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE cost_items IS '成本项定义:类型 + 载体 + 计价方式;金额一律放 cost_versions';
|
||||
COMMENT ON COLUMN cost_items.carrier_type IS '适用执行载体;shared 表示与载体无关的通用成本';
|
||||
COMMENT ON COLUMN cost_items.pricing_mode IS 'per_item/per_order/per_hour/percent_of_revenue/fixed_daily';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cost_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
cost_item_id BIGINT NOT NULL REFERENCES cost_items(id) ON DELETE RESTRICT,
|
||||
scope_type TEXT NOT NULL DEFAULT 'global',
|
||||
provider TEXT NOT NULL DEFAULT '',
|
||||
shop_id TEXT NOT NULL DEFAULT '',
|
||||
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||||
work_product_rule_id BIGINT REFERENCES work_product_rules(id) ON DELETE RESTRICT,
|
||||
amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
rate_bps INTEGER NOT NULL DEFAULT 0,
|
||||
effective_from TIMESTAMPTZ NOT NULL,
|
||||
effective_to TIMESTAMPTZ,
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
change_reason TEXT NOT NULL DEFAULT '',
|
||||
created_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
approved_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_versions_scope
|
||||
ON cost_versions (
|
||||
cost_item_id,
|
||||
scope_type,
|
||||
provider,
|
||||
shop_id,
|
||||
COALESCE(canonical_product_id, 0),
|
||||
COALESCE(work_product_rule_id, 0),
|
||||
effective_from
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_versions_lookup
|
||||
ON cost_versions (cost_item_id, status, effective_from DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_versions_product
|
||||
ON cost_versions (canonical_product_id, status, effective_from DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_versions_rule
|
||||
ON cost_versions (work_product_rule_id, status, effective_from DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_amount') THEN
|
||||
ALTER TABLE cost_versions
|
||||
ADD CONSTRAINT chk_cost_versions_amount
|
||||
CHECK (amount_fen >= 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_rate') THEN
|
||||
ALTER TABLE cost_versions
|
||||
ADD CONSTRAINT chk_cost_versions_rate
|
||||
CHECK (rate_bps >= 0 AND rate_bps <= 10000);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_period') THEN
|
||||
ALTER TABLE cost_versions
|
||||
ADD CONSTRAINT chk_cost_versions_period
|
||||
CHECK (effective_to IS NULL OR effective_to > effective_from);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_scope') THEN
|
||||
ALTER TABLE cost_versions
|
||||
ADD CONSTRAINT chk_cost_versions_scope
|
||||
CHECK (scope_type IN ('global', 'provider', 'shop', 'product', 'rule'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_source') THEN
|
||||
ALTER TABLE cost_versions
|
||||
ADD CONSTRAINT chk_cost_versions_source
|
||||
CHECK (source IN ('manual', 'upstream_sync', 'import'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_status') THEN
|
||||
ALTER TABLE cost_versions
|
||||
ADD CONSTRAINT chk_cost_versions_status
|
||||
CHECK (status IN ('draft', 'active', 'retired'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE cost_versions IS '成本版本:改价只在这里新增版本;历史成本靠 cost_snapshots 固化';
|
||||
COMMENT ON COLUMN cost_versions.amount_fen IS '固定金额(分);percent_of_revenue 时使用 rate_bps';
|
||||
COMMENT ON COLUMN cost_versions.rate_bps IS '比例成本,单位万分比;10000=100%';
|
||||
COMMENT ON COLUMN cost_versions.scope_type IS '匹配优先级:rule > product > shop > provider > global';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cost_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
subject_type TEXT NOT NULL,
|
||||
subject_id BIGINT NOT NULL,
|
||||
order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL,
|
||||
order_item_id BIGINT REFERENCES order_items(id) ON DELETE SET NULL,
|
||||
work_order_id BIGINT REFERENCES work_orders(id) ON DELETE SET NULL,
|
||||
manual_case_id BIGINT,
|
||||
carrier_type TEXT NOT NULL,
|
||||
cost_item_id BIGINT NOT NULL REFERENCES cost_items(id) ON DELETE RESTRICT,
|
||||
cost_version_id BIGINT NOT NULL REFERENCES cost_versions(id) ON DELETE RESTRICT,
|
||||
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||||
quantity NUMERIC(14, 3) NOT NULL DEFAULT 1,
|
||||
unit_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
total_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
snapshot_stage TEXT NOT NULL DEFAULT 'estimated',
|
||||
source TEXT NOT NULL DEFAULT 'resolved',
|
||||
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
resolved_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_subject_item
|
||||
ON cost_snapshots (subject_type, subject_id, cost_item_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_item
|
||||
ON cost_snapshots (order_item_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_work_order
|
||||
ON cost_snapshots (work_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_product
|
||||
ON cost_snapshots (canonical_product_id, resolved_at DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_subject_type') THEN
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD CONSTRAINT chk_cost_snapshots_subject_type
|
||||
CHECK (subject_type IN ('order_item', 'work_order', 'manual_case'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_carrier_type') THEN
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD CONSTRAINT chk_cost_snapshots_carrier_type
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'manual', 'shared'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_stage') THEN
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD CONSTRAINT chk_cost_snapshots_stage
|
||||
CHECK (snapshot_stage IN ('estimated', 'actual', 'adjustment'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_source') THEN
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD CONSTRAINT chk_cost_snapshots_source
|
||||
CHECK (source IN ('resolved', 'legacy_rule', 'manual_input', 'missing'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_amount') THEN
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD CONSTRAINT chk_cost_snapshots_amount
|
||||
CHECK (quantity >= 0 AND unit_amount_fen >= 0 AND total_amount_fen >= 0);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE cost_snapshots IS '成本快照:订单项/工单/人工单实际使用的成本版本与金额';
|
||||
COMMENT ON COLUMN cost_snapshots.source IS 'resolved=成本中心命中;legacy_rule=旧规则回退;manual_input=人工;missing=未匹配';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cost_exceptions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
subject_type TEXT NOT NULL,
|
||||
subject_id BIGINT NOT NULL,
|
||||
order_item_id BIGINT REFERENCES order_items(id) ON DELETE SET NULL,
|
||||
work_order_id BIGINT REFERENCES work_orders(id) ON DELETE SET NULL,
|
||||
carrier_type TEXT NOT NULL DEFAULT '',
|
||||
reason TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
detail_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
resolved_at TIMESTAMPTZ,
|
||||
resolved_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_exceptions_subject_reason
|
||||
ON cost_exceptions (subject_type, subject_id, reason);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_exceptions_status_created
|
||||
ON cost_exceptions (status, created_at DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_exceptions_subject_type') THEN
|
||||
ALTER TABLE cost_exceptions
|
||||
ADD CONSTRAINT chk_cost_exceptions_subject_type
|
||||
CHECK (subject_type IN ('order_item', 'work_order', 'manual_case'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_exceptions_status') THEN
|
||||
ALTER TABLE cost_exceptions
|
||||
ADD CONSTRAINT chk_cost_exceptions_status
|
||||
CHECK (status IN ('open', 'resolved', 'ignored'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE cost_exceptions IS '成本缺失/冲突异常队列;open 数量必须在后台暴露';
|
||||
|
||||
ALTER TABLE work_product_rules
|
||||
ADD COLUMN IF NOT EXISTS cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
|
||||
ADD COLUMN IF NOT EXISTS legacy_cost_source TEXT NOT NULL DEFAULT 'rule_amount';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_work_product_rules_cost_item
|
||||
ON work_product_rules (cost_item_id);
|
||||
|
||||
COMMENT ON COLUMN work_product_rules.cost_item_id IS '过渡列:非空则金额优先走成本中心;空则回退 reward_amount/unit_price_fen';
|
||||
COMMENT ON COLUMN work_product_rules.legacy_cost_source IS '旧成本回退标记:rule_amount=旧规则金额';
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 104_cost_center_bootstrap_fix.sql
|
||||
--
|
||||
-- 背景:
|
||||
-- 102_product_catalog 已执行,里面把 normalized_quantity 直接用 quantity 填充。
|
||||
-- quantity 长期几乎恒为 1,会让「5个」类 SKU 的归一数量永远无法再通过解析器修正。
|
||||
--
|
||||
-- 处理:
|
||||
-- 1. 清空初始化时写入的归一数量,由成本中心「回填数量」用 resolveSkuNameQuantity 重新解析;
|
||||
-- 2. 给财务角色补 finance.config,允许维护商品映射与成本版本(管理员仍绕过权限检查)。
|
||||
--
|
||||
-- 该列是本期新列,清空不会影响既有订单字段;回填接口只处理 normalized_quantity IS NULL。
|
||||
|
||||
UPDATE order_items
|
||||
SET normalized_quantity = NULL
|
||||
WHERE normalized_quantity IS NOT NULL;
|
||||
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
VALUES ('finance', 'finance.config')
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,72 @@
|
||||
-- 105_product_mapping_match_keys.sql —— 渠道商品映射支持 SKU 名称/系列消歧。
|
||||
--
|
||||
-- 背景:91 的 sku_code 会在同一店铺内被多个商品复用,单靠
|
||||
-- provider + shop_id + platform_sku_code 无法唯一确定标准商品。
|
||||
--
|
||||
-- 规则:
|
||||
-- 1. sku_code:编码唯一,直接命中;
|
||||
-- 2. sku_code_name:编码冲突,但编码 + 精确 SKU 名称可唯一命中;
|
||||
-- 3. sku_code_series:编码冲突,但编码 + SKU 名称系列可唯一命中;
|
||||
-- 4. sku_name_series:编码无法提供时,用来源 + 店铺 + SKU 名称系列兜底;
|
||||
-- 5. 任何一层出现多个候选都不自动选择,交由冲突清单人工处理。
|
||||
|
||||
ALTER TABLE channel_product_mappings
|
||||
ADD COLUMN IF NOT EXISTS match_type TEXT NOT NULL DEFAULT 'sku_code',
|
||||
ADD COLUMN IF NOT EXISTS platform_sku_name_key TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS normalized_sku_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS normalized_sku_series TEXT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_channel_product_mappings_match_type'
|
||||
) THEN
|
||||
ALTER TABLE channel_product_mappings
|
||||
ADD CONSTRAINT chk_channel_product_mappings_match_type
|
||||
CHECK (match_type IN ('sku_code', 'sku_code_name', 'sku_code_series', 'sku_name_series'));
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_channel_product_mappings_match_key'
|
||||
) THEN
|
||||
ALTER TABLE channel_product_mappings
|
||||
ADD CONSTRAINT chk_channel_product_mappings_match_key
|
||||
CHECK (platform_sku_code <> '' OR platform_sku_name_key <> '');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DROP INDEX IF EXISTS uq_channel_product_mappings_key;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_product_mappings_match_key
|
||||
ON channel_product_mappings (
|
||||
provider,
|
||||
shop_id,
|
||||
platform_sku_code,
|
||||
match_type,
|
||||
platform_sku_name_key
|
||||
);
|
||||
|
||||
DROP INDEX IF EXISTS idx_channel_product_mappings_lookup;
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_product_mappings_match_lookup
|
||||
ON channel_product_mappings (
|
||||
provider,
|
||||
shop_id,
|
||||
platform_sku_code,
|
||||
match_type,
|
||||
platform_sku_name_key,
|
||||
enabled
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_normalized_sku_lookup
|
||||
ON order_items (normalized_sku_name, normalized_sku_series);
|
||||
|
||||
COMMENT ON COLUMN channel_product_mappings.match_type IS
|
||||
'映射匹配方式:sku_code / sku_code_name / sku_code_series / sku_name_series';
|
||||
COMMENT ON COLUMN channel_product_mappings.platform_sku_name_key IS
|
||||
'归一后的 SKU 名称或数量系列键;sku_code 模式为空串';
|
||||
COMMENT ON COLUMN order_items.normalized_sku_name IS
|
||||
'NFKC、小写、去空白后的 SKU 名称,用于编码 + 名称消歧';
|
||||
COMMENT ON COLUMN order_items.normalized_sku_series IS
|
||||
'剥离数量并归一同义字后的 SKU 名称系列,用于数量系列匹配';
|
||||
Reference in New Issue
Block a user