refactor(db): 12 个财务迁移合并重写为 5 个
原 103–114 十二个迁移(1,087 行)分批落地,字段反复摇摆,且同一语义 (履约载体)在四张表各存一份,口径互相打架。本次整体重写为五个: - 103_product_catalog:product_catalog 不再存载体,来源改由 canonical_sku 前缀表达(aff: / tpl: / wo: / man:);channel_product_mappings 直接 包含 match_type 与 platform_sku_name_key - 104_cost_center:cost_items 用 cost_model 取代 cost_type + carrier_type; cost_snapshots / cost_exceptions 删除载体列;pricing_mode 精简为 per_item / per_order / percent_of_revenue(删除按小时与按天固定) - 105_sales_daily_facts:carrier_type 改名 fulfillment_mode - 106_manual_expense_records:合并原 111 与 113 - 107_order_item_fulfillment_mode:履约事实三列 核心口径:order_items.fulfillment_mode 是履约方式唯一真源;成本模型 与履约方式正交;核销码不参与履约判定。 本地库已按新 schema 原地改造(scripts/rewrite-local-db-2026-10-23.sql), 316,585 项四项计数与总纲一致,NULL = 0。
This commit is contained in:
@@ -3,11 +3,18 @@
|
||||
-- 目标:把「平台原始 SKU / 商品名」归一到内部标准商品,后续成本、销量、
|
||||
-- 店铺日报和订单毛利都只认 canonical_product_id。
|
||||
--
|
||||
-- 设计要点(2026-10-23 重写):
|
||||
-- 1. 商品表不存「履约载体」。履约方式只由 order_items.fulfillment_mode 决定,
|
||||
-- 商品的来源由 canonical_sku 前缀表达(aff: 自动发货 / tpl: 接单模板 /
|
||||
-- wo: 历史工单证据 / man: 人工),避免两处口径互相打架。
|
||||
-- 2. 映射唯一键包含匹配方式与名称键,91 同店铺复用 sku_code 才能消歧。
|
||||
-- 3. normalized_quantity 不在这里预填:预填会把「5 个」类 SKU 永久锁死成 1,
|
||||
-- 必须由回填脚本按 SKU 名称解析后写入。
|
||||
--
|
||||
-- 约束:
|
||||
-- 1. canonical_sku 是内部稳定编码,创建后不随平台改名;
|
||||
-- 2. 同一 provider + shop_id + platform_sku_code 只能有一条映射;
|
||||
-- 3. shop_id 为空串表示该来源的全局映射,具体店铺映射优先;
|
||||
-- 4. order_items.normalized_quantity 从 SKU 名称解析,解析失败先回填 quantity。
|
||||
-- 2. shop_id 为空串表示该来源的全局映射,具体店铺映射优先;
|
||||
-- 3. 任何一层匹配出现多个候选都不自动选择,交由冲突清单人工处理。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_catalog (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -25,8 +32,6 @@ 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
|
||||
@@ -48,16 +53,19 @@ BEGIN
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE product_catalog IS '标准商品/皮肤主数据:报表、成本和销量统计的唯一商品维度';
|
||||
COMMENT ON COLUMN product_catalog.canonical_sku IS '内部稳定编码;创建后不随平台改名';
|
||||
COMMENT ON COLUMN product_catalog.canonical_sku IS
|
||||
'内部稳定编码;前缀表达来源:aff: 自动发货、tpl: 接单模板、wo: 工单证据、man: 人工';
|
||||
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_code TEXT NOT NULL DEFAULT '',
|
||||
platform_sku_name TEXT NOT NULL DEFAULT '',
|
||||
platform_sku_name_key TEXT NOT NULL DEFAULT '',
|
||||
canonical_product_id BIGINT NOT NULL REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||||
match_type TEXT NOT NULL DEFAULT 'sku_code',
|
||||
quantity_multiplier INTEGER NOT NULL DEFAULT 1,
|
||||
confidence TEXT NOT NULL DEFAULT 'manual',
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
@@ -67,12 +75,25 @@ CREATE TABLE IF NOT EXISTS channel_product_mappings (
|
||||
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 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
|
||||
);
|
||||
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);
|
||||
ON channel_product_mappings (
|
||||
provider,
|
||||
shop_id,
|
||||
platform_sku_code,
|
||||
match_type,
|
||||
platform_sku_name_key,
|
||||
enabled
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
@@ -91,23 +112,41 @@ BEGIN
|
||||
ADD CONSTRAINT chk_channel_product_mappings_multiplier
|
||||
CHECK (quantity_multiplier > 0);
|
||||
END IF;
|
||||
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 $$;
|
||||
|
||||
COMMENT ON TABLE channel_product_mappings IS '来源 + 店铺 + 平台 SKU -> 标准商品映射';
|
||||
COMMENT ON COLUMN channel_product_mappings.shop_id IS '空串=该来源的全局映射;具体店铺映射优先';
|
||||
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 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;
|
||||
ADD COLUMN IF NOT EXISTS normalized_quantity INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS normalized_sku_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS normalized_sku_series TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_canonical_product
|
||||
ON order_items (canonical_product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_normalized_sku_lookup
|
||||
ON order_items (normalized_sku_name, normalized_sku_series);
|
||||
|
||||
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;
|
||||
COMMENT ON COLUMN order_items.normalized_quantity IS
|
||||
'归一化件数:从 SKU 名称解析并固化,禁止直接用 quantity 做销量;未解析时保持 NULL';
|
||||
COMMENT ON COLUMN order_items.normalized_sku_name IS
|
||||
'NFKC、小写、去空白后的 SKU 名称,用于编码 + 名称消歧';
|
||||
COMMENT ON COLUMN order_items.normalized_sku_series IS
|
||||
'剥离数量并归一同义字后的 SKU 名称系列,用于数量系列匹配';
|
||||
|
||||
@@ -1,50 +1,61 @@
|
||||
-- 104_cost_center.sql —— 成本项目、成本版本、成本快照与成本异常。
|
||||
-- 104_cost_center.sql —— 成本项、成本版本、成本快照、成本异常与订单项收入固化。
|
||||
--
|
||||
-- 目标:所有成本金额只在 cost_versions 维护;订单项/工单/人工单只保存
|
||||
-- cost_snapshot_id 或快照行,历史成本永不回写。
|
||||
-- 目标:所有成本金额只在 cost_versions 维护;订单项/工单/人工单只保存快照行,
|
||||
-- 历史成本永不回写。
|
||||
--
|
||||
-- 口径(2026-10-17 已确认):
|
||||
-- 设计要点(2026-10-23 重写):
|
||||
-- 1. 成本项用 cost_model 表达「成本怎么算」,不再复用 carrier_type。
|
||||
-- cost_model ∈ { affiliate_purchase, work_order_reward, manual_actual,
|
||||
-- platform_fee, payment_fee, after_sales_loss, other }
|
||||
-- 履约方式在 order_items.fulfillment_mode,两者互不推导。
|
||||
-- 2. 快照表不存 carrier_type:快照属于哪个订单项,join order_items 就有履约方式。
|
||||
-- 3. 异常表同理,异常属于哪个订单项由 order_item_id 决定。
|
||||
-- 4. 成本版本按业务发生时间解析(支付时间/验收时间),不是回填时的 now()。
|
||||
-- 5. 匹配不到成本不得默认 0,必须进入 cost_exceptions。
|
||||
--
|
||||
-- 口径(2026-10-22 确认):
|
||||
-- * 自动发货 = 上游采购成本;
|
||||
-- * 接单平台 = 打手报酬,按总额法计履约成本;
|
||||
-- * 人工客服 = 人工支出登记,按 5102 履约成本-人工客服;
|
||||
-- * 匹配不到成本不得默认 0,必须进入 cost_exceptions。
|
||||
-- * 人工客服 = 人工支出登记的实际金额;
|
||||
-- * 收入按 paid_at 归属销售日;一单一品归整单,多品按等额拆分并标 basis。
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. 成本项
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
cost_model 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);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_items_status_model
|
||||
ON cost_items (status, cost_model);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_cost_type') THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_cost_model') 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'));
|
||||
ADD CONSTRAINT chk_cost_items_cost_model
|
||||
CHECK (cost_model IN (
|
||||
'affiliate_purchase', 'work_order_reward', 'manual_actual',
|
||||
'platform_fee', 'payment_fee', 'after_sales_loss', 'other'
|
||||
));
|
||||
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'));
|
||||
CHECK (pricing_mode IN ('per_item', 'per_order', 'percent_of_revenue'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_status') THEN
|
||||
ALTER TABLE cost_items
|
||||
@@ -53,9 +64,17 @@ BEGIN
|
||||
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';
|
||||
COMMENT ON TABLE cost_items IS '成本项定义:成本模型 + 计价方式;金额一律放 cost_versions';
|
||||
COMMENT ON COLUMN cost_items.cost_model IS
|
||||
'成本模型:affiliate_purchase=上游采购 / work_order_reward=打手报酬 / manual_actual=人工实际支出 / platform_fee / payment_fee / after_sales_loss / other';
|
||||
COMMENT ON COLUMN cost_items.pricing_mode IS
|
||||
'计价方式:per_item 按件 / per_order 按单 / percent_of_revenue 按收入比例';
|
||||
COMMENT ON COLUMN cost_items.code IS
|
||||
'成本项编码;sys_ 前缀为系统内置项,业务侧不要删除或改编码';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. 成本版本:改价只在这里新增版本
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cost_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -99,13 +118,11 @@ 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);
|
||||
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);
|
||||
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
|
||||
@@ -134,6 +151,10 @@ COMMENT ON COLUMN cost_versions.amount_fen IS '固定金额(分);percent_o
|
||||
COMMENT ON COLUMN cost_versions.rate_bps IS '比例成本,单位万分比;10000=100%';
|
||||
COMMENT ON COLUMN cost_versions.scope_type IS '匹配优先级:rule > product > shop > provider > global';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. 成本快照
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cost_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
subject_type TEXT NOT NULL,
|
||||
@@ -141,8 +162,6 @@ CREATE TABLE IF NOT EXISTS cost_snapshots (
|
||||
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,
|
||||
@@ -152,18 +171,24 @@ CREATE TABLE IF NOT EXISTS cost_snapshots (
|
||||
snapshot_stage TEXT NOT NULL DEFAULT 'estimated',
|
||||
source TEXT NOT NULL DEFAULT 'resolved',
|
||||
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
cost_basis_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ NOT NULL,
|
||||
superseded_at TIMESTAMPTZ,
|
||||
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 UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_active_subject_item
|
||||
ON cost_snapshots (subject_type, subject_id, cost_item_id)
|
||||
WHERE superseded_at IS NULL;
|
||||
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);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_active_subject
|
||||
ON cost_snapshots (subject_type, subject_id)
|
||||
WHERE superseded_at IS NULL;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
@@ -172,11 +197,6 @@ BEGIN
|
||||
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
|
||||
@@ -194,8 +214,17 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE cost_snapshots IS '成本快照:订单项/工单/人工单实际使用的成本版本与金额';
|
||||
COMMENT ON TABLE cost_snapshots IS
|
||||
'成本快照:订单项/工单/人工单实际使用的成本版本与金额;履约方式看 order_items.fulfillment_mode';
|
||||
COMMENT ON COLUMN cost_snapshots.source IS 'resolved=成本中心命中;legacy_rule=旧规则回退;manual_input=人工;missing=未匹配';
|
||||
COMMENT ON COLUMN cost_snapshots.cost_basis_at IS
|
||||
'解析成本版本时使用的业务时间:订单支付时间/工单验收时间/人工完成时间';
|
||||
COMMENT ON COLUMN cost_snapshots.superseded_at IS
|
||||
'被更新的实际成本替代的时间;非空行只保留历史,不再参与成本汇总';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. 成本异常队列
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cost_exceptions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -203,7 +232,6 @@ CREATE TABLE IF NOT EXISTS cost_exceptions (
|
||||
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,
|
||||
@@ -216,6 +244,11 @@ 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);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_exceptions_item
|
||||
ON cost_exceptions (order_item_id, status);
|
||||
-- 按履约方式筛异常时走半连接,这条索引让 order_items 侧只扫目标履约方式的行。
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_id
|
||||
ON order_items (fulfillment_mode, id);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
@@ -231,14 +264,212 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE cost_exceptions IS '成本缺失/冲突异常队列;open 数量必须在后台暴露';
|
||||
COMMENT ON TABLE cost_exceptions IS
|
||||
'成本缺失/冲突异常队列;open 数量必须在后台暴露;履约方式看 order_items.fulfillment_mode';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. 接单模板过渡列
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
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';
|
||||
ADD COLUMN IF NOT EXISTS cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT;
|
||||
|
||||
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=旧规则金额';
|
||||
COMMENT ON COLUMN work_product_rules.cost_item_id IS
|
||||
'过渡列:非空则金额优先走成本中心;空则回退 reward_amount/unit_price_fen';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. 订单项收入固化
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS revenue_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS revenue_basis TEXT NOT NULL DEFAULT 'unknown';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_amount'
|
||||
) THEN
|
||||
ALTER TABLE order_items
|
||||
ADD CONSTRAINT chk_order_items_revenue_amount
|
||||
CHECK (revenue_amount_fen >= 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_basis'
|
||||
) THEN
|
||||
ALTER TABLE order_items
|
||||
ADD CONSTRAINT chk_order_items_revenue_basis
|
||||
CHECK (revenue_basis IN ('order_total', 'equal_split', 'platform_item', 'manual', 'unknown'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 历史订单回填:当前生产快照全部是一单一品,仍按通用规则处理多订单项。
|
||||
WITH item_counts AS (
|
||||
SELECT order_id, COUNT(*)::int AS item_count
|
||||
FROM order_items
|
||||
GROUP BY order_id
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
oi.id,
|
||||
GREATEST(COALESCE(o.total_amount, 0), 0)::bigint AS order_revenue_fen,
|
||||
ic.item_count,
|
||||
ROW_NUMBER() OVER (PARTITION BY oi.order_id ORDER BY oi.id ASC) AS item_rank
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN item_counts ic ON ic.order_id = oi.order_id
|
||||
)
|
||||
UPDATE order_items oi
|
||||
SET
|
||||
revenue_amount_fen = CASE
|
||||
WHEN r.item_count = 1 THEN r.order_revenue_fen
|
||||
ELSE (r.order_revenue_fen / r.item_count)
|
||||
+ CASE WHEN r.item_rank = r.item_count THEN (r.order_revenue_fen % r.item_count) ELSE 0 END
|
||||
END,
|
||||
revenue_basis = CASE WHEN r.item_count = 1 THEN 'order_total' ELSE 'equal_split' END,
|
||||
updated_at = NOW()
|
||||
FROM ranked r
|
||||
WHERE oi.id = r.id
|
||||
AND oi.revenue_basis = 'unknown';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_revenue_basis
|
||||
ON order_items (revenue_basis);
|
||||
|
||||
COMMENT ON COLUMN order_items.revenue_amount_fen IS
|
||||
'订单项收入(分):一单一品=订单实付;多品=当前按等额拆分,basis 标明口径';
|
||||
COMMENT ON COLUMN order_items.revenue_basis IS
|
||||
'order_total=整单归属于唯一订单项;equal_split=多订单项等额拆分;platform_item=平台明细;manual/unknown=未定';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. 成本版本页与成本中心页的聚合覆盖索引
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_work_orders_rule_cover
|
||||
ON work_orders (product_rule_id, order_id, reward_amount);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_revenue_cover
|
||||
ON order_items (order_id, revenue_amount_fen);
|
||||
|
||||
-- 商品目录按分类名 lateral join work_categories,没有索引时每行都扫全表。
|
||||
CREATE INDEX IF NOT EXISTS idx_work_categories_name
|
||||
ON work_categories (name);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. 系统内置成本项
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- 接单平台打手报酬:实际金额取工单 reward_amount,预估阶段不参与;
|
||||
-- 版本金额 0 只是占位,让 cost_snapshots.cost_version_id 有稳定引用。
|
||||
INSERT INTO cost_items (
|
||||
code, name, cost_model, pricing_mode, unit_name, currency,
|
||||
status, note, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'sys_work_order_reward',
|
||||
'接单平台打手报酬(实际结算)',
|
||||
'work_order_reward',
|
||||
'per_order',
|
||||
'单',
|
||||
'CNY',
|
||||
'active',
|
||||
'系统内置:实际金额取工单 reward_amount;版本金额仅占位,不参与预估成本',
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
cost_model = EXCLUDED.cost_model,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 人工客服履约成本:只能由公司人工支出登记给出实际金额。
|
||||
INSERT INTO cost_items (
|
||||
code, name, cost_model, pricing_mode, unit_name, currency,
|
||||
status, note, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'sys_manual_service',
|
||||
'人工客服履约成本',
|
||||
'manual_actual',
|
||||
'per_order',
|
||||
'单',
|
||||
'CNY',
|
||||
'active',
|
||||
'系统内置:由公司人工支出登记写入 actual 快照;逐单实际金额,不按工时或固定单价',
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
cost_model = EXCLUDED.cost_model,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 自动发货上游采购成本
|
||||
INSERT INTO cost_items (
|
||||
code, name, cost_model, pricing_mode, unit_name, currency,
|
||||
status, note, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'sys_affiliate_purchase',
|
||||
'自动发货上游采购成本',
|
||||
'affiliate_purchase',
|
||||
'per_item',
|
||||
'件',
|
||||
'CNY',
|
||||
'active',
|
||||
'系统内置:按上游 SKU 固定成本版本结算',
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
cost_model = EXCLUDED.cost_model,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 接单平台:active 占位版本,供 cost_snapshots.cost_version_id 引用。
|
||||
INSERT INTO cost_versions (
|
||||
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
|
||||
work_product_rule_id, amount_fen, rate_bps, effective_from, source,
|
||||
status, change_reason, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
ci.id, 'global', '', '', NULL, NULL, 0, 0,
|
||||
'1970-01-01T00:00:00+08:00', 'manual', 'active',
|
||||
'系统占位:接单平台实际金额取工单 reward_amount',
|
||||
NOW(), NOW()
|
||||
FROM cost_items ci
|
||||
WHERE ci.code = 'sys_work_order_reward'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 人工客服:draft 版本,避免在财务未填写金额时静默按 0 计成本。
|
||||
INSERT INTO cost_versions (
|
||||
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
|
||||
work_product_rule_id, amount_fen, rate_bps, effective_from, source,
|
||||
status, change_reason, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
ci.id, 'global', '', '', NULL, NULL, 0, 0,
|
||||
'1970-01-01T00:00:00+08:00', 'manual', 'draft',
|
||||
'草稿占位:待财务填写人工客服实际按单成本后启用',
|
||||
NOW(), NOW()
|
||||
FROM cost_items ci
|
||||
WHERE ci.code = 'sys_manual_service'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 9. 财务角色补齐成本中心维护权限
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
VALUES ('finance', 'finance.config')
|
||||
ON CONFLICT (role_key, permission_key) DO NOTHING;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
-- 105_cost_center_bootstrap_fix.sql
|
||||
--
|
||||
-- 背景:
|
||||
-- 103_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;
|
||||
+20
-12
@@ -1,9 +1,11 @@
|
||||
-- 109_sales_daily_facts.sql —— 店铺 × 日期 × 标准商品 × 履约载体的销售事实。
|
||||
-- 105_sales_daily_facts.sql —— 店铺 × 日期 × 标准商品 × 履约方式的销售事实。
|
||||
--
|
||||
-- 口径(2026-10-22 确认):
|
||||
-- 口径(2026-10-23 重写):
|
||||
-- * 销售日 = orders.paid_at 的 Asia/Shanghai 日期;没有 paid_at 时退回 created_at;
|
||||
-- * 收入 = order_items.revenue_amount_fen(迁移 108 固化),不再直接拿整单金额;
|
||||
-- * 未归一商品允许 canonical_product_id 为空,单独成“未归一”行,保证总收入能对平;
|
||||
-- * 收入 = order_items.revenue_amount_fen(迁移 104 固化),不再直接拿整单金额;
|
||||
-- * 履约方式 = order_items.fulfillment_mode,四种:work_order / affiliate_dash /
|
||||
-- legacy_platform / manual;未判定的订单项先归 manual 并由定时任务补齐;
|
||||
-- * 未归一商品允许 canonical_product_id 为空,单独成「未归一」行,保证总收入对平;
|
||||
-- * 成本来自 cost_snapshots 当前有效行;没有快照的订单项计入 cost_missing_count;
|
||||
-- * 退款暂未接入,refund_amount_fen 先按 pay_status=refunded 预留。
|
||||
|
||||
@@ -13,7 +15,7 @@ CREATE TABLE IF NOT EXISTS sales_daily_facts (
|
||||
provider TEXT NOT NULL,
|
||||
shop_id TEXT NOT NULL DEFAULT '',
|
||||
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||||
carrier_type TEXT NOT NULL,
|
||||
fulfillment_mode TEXT NOT NULL,
|
||||
order_count INTEGER NOT NULL DEFAULT 0,
|
||||
item_count INTEGER NOT NULL DEFAULT 0,
|
||||
quantity NUMERIC(14, 3) NOT NULL DEFAULT 0,
|
||||
@@ -29,14 +31,14 @@ CREATE TABLE IF NOT EXISTS sales_daily_facts (
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
-- canonical_product_id 为空时唯一索引仍要生效,因此用 COALESCE(..., 0) 作为“未归一”桶。
|
||||
-- canonical_product_id 为空时唯一索引仍要生效,因此用 COALESCE(..., 0) 作为「未归一」桶。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_sales_daily_facts_key
|
||||
ON sales_daily_facts (
|
||||
stat_date,
|
||||
provider,
|
||||
shop_id,
|
||||
COALESCE(canonical_product_id, 0),
|
||||
carrier_type
|
||||
fulfillment_mode
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_date
|
||||
ON sales_daily_facts (stat_date DESC);
|
||||
@@ -44,15 +46,19 @@ CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_shop
|
||||
ON sales_daily_facts (provider, shop_id, stat_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_product
|
||||
ON sales_daily_facts (canonical_product_id, stat_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_mode
|
||||
ON sales_daily_facts (fulfillment_mode, stat_date DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_carrier'
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_fulfillment_mode'
|
||||
) THEN
|
||||
ALTER TABLE sales_daily_facts
|
||||
ADD CONSTRAINT chk_sales_daily_facts_carrier
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'manual', 'shared', 'unknown'));
|
||||
ADD CONSTRAINT chk_sales_daily_facts_fulfillment_mode
|
||||
CHECK (fulfillment_mode IN (
|
||||
'work_order', 'affiliate_dash', 'legacy_platform', 'manual'
|
||||
));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_cost_stage'
|
||||
@@ -87,9 +93,11 @@ BEGIN
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE sales_daily_facts IS
|
||||
'店铺×日期(Asia/Shanghai)×标准商品×履约载体的销售与成本事实;报表直接读这张表';
|
||||
'店铺×日期(Asia/Shanghai)×标准商品×履约方式的销售与成本事实;报表直接读这张表';
|
||||
COMMENT ON COLUMN sales_daily_facts.canonical_product_id IS
|
||||
'标准商品;为空表示渠道映射未命中,报表单独展示“未归一”占比';
|
||||
'标准商品;为空表示渠道映射未命中,报表单独展示「未归一」占比';
|
||||
COMMENT ON COLUMN sales_daily_facts.fulfillment_mode IS
|
||||
'履约方式:work_order 接单平台 / affiliate_dash 自动发货 / legacy_platform 历史平台 / manual 人工';
|
||||
COMMENT ON COLUMN sales_daily_facts.cost_stage IS
|
||||
'missing=全部无成本快照;partial=部分无快照;estimated=全部预估;actual=全部实际';
|
||||
COMMENT ON COLUMN sales_daily_facts.cost_missing_count IS
|
||||
+47
-7
@@ -1,11 +1,15 @@
|
||||
-- 111_manual_expense_records.sql —— 公司人工支出/垫付登记。
|
||||
-- 106_manual_expense_records.sql —— 公司人工支出登记 + 备用金账户编号可读化。
|
||||
--
|
||||
-- 与迁移 100 的 `manual_expenses` 严格区分:
|
||||
-- 与迁移 100 的 manual_expenses 严格区分:
|
||||
-- manual_expenses = 客服备用金支出明细(减少客服余额)
|
||||
-- manual_expense_records = 公司口径人工支出/垫付登记(谁为哪个订单付了多少钱)
|
||||
-- manual_expense_records = 公司口径人工支出登记(谁为哪个订单付了多少钱)
|
||||
-- 两者通过 fund_expense_id 关联,不合并、不改名、不共用索引名。
|
||||
--
|
||||
-- 本轮只做登记、审批、报销与冲正预留;影子总账分录在 finance_* 阶段接入。
|
||||
-- 口径(2026-10-23 确认):
|
||||
-- * 人工成本逐单、按实际金额录入,不按工时、不按固定单量/固定单价;
|
||||
-- * 人工支出从客服备用金账户扣款,客服角色只能用本人账户;
|
||||
-- * 历史人工支出不补录,只取少量样本做校准;
|
||||
-- * 客服必须在上线后 24 小时内为人工单录入成本。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manual_expense_records (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -14,7 +18,7 @@ CREATE TABLE IF NOT EXISTS manual_expense_records (
|
||||
order_item_id BIGINT REFERENCES order_items(id) ON DELETE SET NULL,
|
||||
work_order_id BIGINT REFERENCES work_orders(id) ON DELETE SET NULL,
|
||||
fund_expense_id BIGINT REFERENCES manual_expenses(id) ON DELETE SET NULL,
|
||||
cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
|
||||
cost_item_id BIGINT NOT NULL REFERENCES cost_items(id) ON DELETE RESTRICT,
|
||||
expense_category TEXT NOT NULL DEFAULT 'other',
|
||||
amount_fen BIGINT NOT NULL,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
@@ -129,9 +133,9 @@ BEGIN
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE manual_expense_records IS
|
||||
'公司人工支出登记:付款项目/金额/订单号/付款人/收款人/凭证;与客服备用金 manual_expenses 通过 fund_expense_id 关联';
|
||||
'公司人工支出登记:逐单实际金额;付款项目/收款方/凭证/审批;与客服备用金 manual_expenses 通过 fund_expense_id 关联';
|
||||
COMMENT ON COLUMN manual_expense_records.fund_expense_id IS
|
||||
'来源客服备用金支出(manual_expenses.id);公司直付为空';
|
||||
'来源客服备用金支出(manual_expenses.id);人工支出必须从备用金扣款,因此正常必有值';
|
||||
COMMENT ON COLUMN manual_expense_records.cost_item_id IS
|
||||
'付款项目 -> cost_items;为空表示未归类,报表必须暴露未归类占比';
|
||||
COMMENT ON COLUMN manual_expense_records.evidence_json IS
|
||||
@@ -159,3 +163,39 @@ CREATE INDEX IF NOT EXISTS idx_manual_expense_events_type
|
||||
|
||||
COMMENT ON TABLE manual_expense_events IS
|
||||
'人工支出状态变更审计:只追加,不更新、不删除';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 备用金账户编号改为可读的 FA-<用户名>
|
||||
--
|
||||
-- 历史编号是 FA + 10 位随机十六进制(如 FA7683D6B58B),在「备用金」列表里
|
||||
-- 无法一眼对应到具体客服。新规则:FA-<大写用户名>,重名追加 -2、-3。
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
WITH base AS (
|
||||
SELECT
|
||||
fa.id,
|
||||
'FA-' || COALESCE(
|
||||
NULLIF(
|
||||
trim(
|
||||
both '_' FROM upper(
|
||||
regexp_replace(COALESCE(NULLIF(au.username, ''), 'account-' || fa.id), '[^A-Za-z0-9_]+', '_', 'g')
|
||||
)
|
||||
),
|
||||
''
|
||||
),
|
||||
'ACCOUNT-' || fa.id
|
||||
) AS base_no
|
||||
FROM fund_accounts fa
|
||||
LEFT JOIN admin_users au ON au.id = fa.owner_admin_user_id
|
||||
),
|
||||
numbered AS (
|
||||
SELECT
|
||||
id,
|
||||
base_no,
|
||||
row_number() OVER (PARTITION BY base_no ORDER BY id) AS rn
|
||||
FROM base
|
||||
)
|
||||
UPDATE fund_accounts fa
|
||||
SET account_no = CASE WHEN n.rn = 1 THEN n.base_no ELSE n.base_no || '-' || n.rn END
|
||||
FROM numbered n
|
||||
WHERE fa.id = n.id;
|
||||
@@ -1,72 +0,0 @@
|
||||
-- 106_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 名称系列,用于数量系列匹配';
|
||||
@@ -0,0 +1,48 @@
|
||||
-- 107_order_item_fulfillment_mode.sql —— 统一履约事实。
|
||||
--
|
||||
-- 目标:订单页、成本快照、销售事实只读 order_items.fulfillment_mode,
|
||||
-- 不再各自用 provider / voucher / task 临时推断。
|
||||
--
|
||||
-- 设计要点(2026-10-23 重写):
|
||||
-- 1. 履约方式只有一个真源,就是 order_items.fulfillment_mode;
|
||||
-- 2. 商品表、成本项表、快照表都不再存载体字段,避免四处口径分叉;
|
||||
-- 3. 核销码只是状态,不参与履约判定;
|
||||
-- 4. 字段允许短暂为 NULL(订单项创建时通常还没有工单/任务),
|
||||
-- 由统一决策服务 + 定时兜底在 24 小时内补齐。
|
||||
--
|
||||
-- 本迁移只做 DDL;31 万行回填由 scripts/backfill-fulfillment-mode.ts 分批执行。
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS fulfillment_mode TEXT,
|
||||
ADD COLUMN IF NOT EXISTS fulfillment_decided_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS fulfillment_reason TEXT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_fulfillment_mode'
|
||||
) THEN
|
||||
ALTER TABLE order_items
|
||||
ADD CONSTRAINT chk_order_items_fulfillment_mode
|
||||
CHECK (
|
||||
fulfillment_mode IS NULL
|
||||
OR fulfillment_mode IN (
|
||||
'work_order', 'affiliate_dash', 'legacy_platform', 'manual'
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_mode
|
||||
ON order_items (fulfillment_mode, order_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_pending
|
||||
ON order_items (order_id)
|
||||
WHERE fulfillment_mode IS NULL;
|
||||
|
||||
COMMENT ON COLUMN order_items.fulfillment_mode IS
|
||||
'履约方式:work_order 接单平台 / affiliate_dash 自动发货 / legacy_platform 历史平台(已下线)/ manual 人工;唯一真源,报表与后台只读此字段';
|
||||
COMMENT ON COLUMN order_items.fulfillment_decided_at IS
|
||||
'履约方式最后一次判定时间';
|
||||
COMMENT ON COLUMN order_items.fulfillment_reason IS
|
||||
'履约方式判定来源:订单同步 / 定时归集 / 历史回填等';
|
||||
@@ -1,95 +0,0 @@
|
||||
-- 107_product_catalog_carrier.sql —— 标准商品增加「默认履约载体」。
|
||||
--
|
||||
-- 背景:商品主数据此前只有游戏/品类分类,成本项目与成本版本却按
|
||||
-- 自动发货 / 接单平台 / 人工 三类执行载体配置,后台配置时找不到对应商品。
|
||||
--
|
||||
-- 口径(2026-10-22 修正):
|
||||
-- 1. 只有真正走 affiliate-dash(自动发货执行器)的商品才算 affiliate_dash;
|
||||
-- 以 fulfillment_tasks.executor_key = 'affiliate_dash' 的履约证据为准,
|
||||
-- 按 context_json.skuCode / skuName 建标准商品,SKU 固定为 `aff:<skuCode>`。
|
||||
-- 2. 其余标准商品(接单模板初始化出来的 tpl:*)全部是 work_order;
|
||||
-- 渠道映射只代表「平台 SKU 能归一到标准商品」,不代表自动发货。
|
||||
-- 3. manual / shared 不回填,由后台按需手工调整。
|
||||
--
|
||||
-- 2026-10-22 生产快照口径:自动发货任务 41,676 条,涉及 28 个 skuCode。
|
||||
|
||||
ALTER TABLE product_catalog
|
||||
ADD COLUMN IF NOT EXISTS carrier_type TEXT NOT NULL DEFAULT 'work_order';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_product_catalog_carrier_type'
|
||||
) THEN
|
||||
ALTER TABLE product_catalog
|
||||
ADD CONSTRAINT chk_product_catalog_carrier_type
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'manual', 'shared'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_product_catalog_status_carrier
|
||||
ON product_catalog (status, carrier_type, canonical_name);
|
||||
|
||||
-- 1) 先把非自动发货商品统一归到接单平台(模板商品、历史误标都回到 work_order)。
|
||||
UPDATE product_catalog
|
||||
SET carrier_type = 'work_order',
|
||||
updated_at = NOW()
|
||||
WHERE carrier_type IS DISTINCT FROM 'work_order'
|
||||
AND canonical_sku NOT LIKE 'aff:%';
|
||||
|
||||
-- 2) 按自动发货履约证据建商品:每个 skuCode 取一个最像人名的名称。
|
||||
WITH auto_task AS (
|
||||
SELECT
|
||||
btrim(context_json->>'skuCode') AS sku_code,
|
||||
btrim(context_json->>'skuName') AS sku_name,
|
||||
created_at
|
||||
FROM fulfillment_tasks
|
||||
WHERE executor_key = 'affiliate_dash'
|
||||
AND btrim(COALESCE(context_json->>'skuCode', '')) <> ''
|
||||
AND btrim(COALESCE(context_json->>'skuName', '')) <> ''
|
||||
),
|
||||
agg AS (
|
||||
SELECT sku_code, count(*)::int AS task_count, max(created_at) AS last_fulfilled_at
|
||||
FROM auto_task
|
||||
GROUP BY sku_code
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
sku_code,
|
||||
sku_name,
|
||||
row_number() OVER (
|
||||
PARTITION BY sku_code
|
||||
ORDER BY (sku_name <> sku_code) DESC, count(*) DESC, sku_name
|
||||
) AS rn
|
||||
FROM auto_task
|
||||
GROUP BY sku_code, sku_name
|
||||
)
|
||||
INSERT INTO product_catalog (
|
||||
canonical_sku, canonical_name, category, unit_name, carrier_type, status,
|
||||
attributes_json, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'aff:' || ranked.sku_code,
|
||||
ranked.sku_name,
|
||||
'',
|
||||
CASE WHEN ranked.sku_name LIKE '%个' THEN '个' ELSE '件' END,
|
||||
'affiliate_dash',
|
||||
'active',
|
||||
jsonb_build_object(
|
||||
'source', 'affiliate_dash_task',
|
||||
'skuCode', ranked.sku_code,
|
||||
'taskCount', agg.task_count,
|
||||
'lastFulfilledAt', agg.last_fulfilled_at
|
||||
),
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM ranked
|
||||
JOIN agg ON agg.sku_code = ranked.sku_code
|
||||
WHERE ranked.rn = 1
|
||||
ON CONFLICT (canonical_sku) DO UPDATE
|
||||
SET carrier_type = 'affiliate_dash',
|
||||
attributes_json = EXCLUDED.attributes_json,
|
||||
updated_at = NOW();
|
||||
|
||||
COMMENT ON COLUMN product_catalog.carrier_type IS
|
||||
'默认履约载体:affiliate_dash=自动发货,work_order=接单平台,manual=人工,shared=多载体共用';
|
||||
@@ -1,98 +0,0 @@
|
||||
-- 108_cost_snapshot_runtime.sql —— 成本快照进入运行层。
|
||||
--
|
||||
-- 目标:
|
||||
-- 1. 成本版本按业务发生时间(订单支付/工单验收)解析,而不是迁移或回填时的 now();
|
||||
-- 2. 预估值可以在结算时被实际值替代,替代后旧快照保留但不再参与汇总;
|
||||
-- 3. 订单项固化收入金额,销售事实可以按订单项归集,不再直接拿整单金额。
|
||||
--
|
||||
-- 口径(2026-10-22 确认):
|
||||
-- * 收入按 paid_at 归属销售日;成本版本按 paid_at / accepted_at 解析;
|
||||
-- * 一单一品时订单收入全部归该订单项;多订单项按等额拆分,余数给最后一项,
|
||||
-- 并把 revenue_basis 标成 equal_split,后续有平台明细时再替换;
|
||||
-- * 历史快照不删除,只把被替代行标记 superseded_at。
|
||||
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD COLUMN IF NOT EXISTS cost_basis_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS superseded_at TIMESTAMPTZ;
|
||||
|
||||
-- 已存在的快照发生在旧逻辑的 now(),没有更好的业务时间,先用 resolved_at 兜底。
|
||||
UPDATE cost_snapshots
|
||||
SET cost_basis_at = resolved_at
|
||||
WHERE cost_basis_at IS NULL;
|
||||
|
||||
ALTER TABLE cost_snapshots
|
||||
ALTER COLUMN cost_basis_at SET DEFAULT NOW();
|
||||
|
||||
-- 同一订单项/工单/人工单的同一成本项,只允许一条“当前有效”快照。
|
||||
DROP INDEX IF EXISTS uq_cost_snapshots_subject_item;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_active_subject_item
|
||||
ON cost_snapshots (subject_type, subject_id, cost_item_id)
|
||||
WHERE superseded_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_active_subject
|
||||
ON cost_snapshots (subject_type, subject_id)
|
||||
WHERE superseded_at IS NULL;
|
||||
|
||||
COMMENT ON COLUMN cost_snapshots.cost_basis_at IS
|
||||
'解析成本版本时使用的业务时间:订单支付时间/工单验收时间/人工完成时间';
|
||||
COMMENT ON COLUMN cost_snapshots.superseded_at IS
|
||||
'被更新的实际成本替代的时间;非空行只保留历史,不再参与成本汇总';
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS revenue_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS revenue_basis TEXT NOT NULL DEFAULT 'unknown';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_amount'
|
||||
) THEN
|
||||
ALTER TABLE order_items
|
||||
ADD CONSTRAINT chk_order_items_revenue_amount
|
||||
CHECK (revenue_amount_fen >= 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_basis'
|
||||
) THEN
|
||||
ALTER TABLE order_items
|
||||
ADD CONSTRAINT chk_order_items_revenue_basis
|
||||
CHECK (revenue_basis IN ('order_total', 'equal_split', 'platform_item', 'manual', 'unknown'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 历史订单回填:当前生产快照全部是一单一品,仍按通用规则处理多订单项。
|
||||
WITH item_counts AS (
|
||||
SELECT order_id, COUNT(*)::int AS item_count
|
||||
FROM order_items
|
||||
GROUP BY order_id
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
oi.id,
|
||||
GREATEST(COALESCE(o.total_amount, 0), 0)::bigint AS order_revenue_fen,
|
||||
ic.item_count,
|
||||
ROW_NUMBER() OVER (PARTITION BY oi.order_id ORDER BY oi.id ASC) AS item_rank
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN item_counts ic ON ic.order_id = oi.order_id
|
||||
)
|
||||
UPDATE order_items oi
|
||||
SET
|
||||
revenue_amount_fen = CASE
|
||||
WHEN r.item_count = 1 THEN r.order_revenue_fen
|
||||
ELSE (r.order_revenue_fen / r.item_count)
|
||||
+ CASE WHEN r.item_rank = r.item_count THEN (r.order_revenue_fen % r.item_count) ELSE 0 END
|
||||
END,
|
||||
revenue_basis = CASE WHEN r.item_count = 1 THEN 'order_total' ELSE 'equal_split' END,
|
||||
updated_at = NOW()
|
||||
FROM ranked r
|
||||
WHERE oi.id = r.id
|
||||
AND oi.revenue_basis = 'unknown';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_revenue_basis
|
||||
ON order_items (revenue_basis);
|
||||
|
||||
COMMENT ON COLUMN order_items.revenue_amount_fen IS
|
||||
'订单项收入(分):一单一品=订单实付;多品=当前按等额拆分,basis 标明口径';
|
||||
COMMENT ON COLUMN order_items.revenue_basis IS
|
||||
'order_total=整单归属于唯一订单项;equal_split=多订单项等额拆分;platform_item=平台明细;manual/unknown=未定';
|
||||
@@ -1,95 +0,0 @@
|
||||
-- 110_system_cost_items.sql —— 内置接单平台实际报酬与人工客服成本项。
|
||||
--
|
||||
-- 背景:接单平台的成本金额在验收时已经由 work_orders.reward_amount 确定,
|
||||
-- 不需要财务再按模板猜一个预估价;人工客服成本则必须由人工支出登记给出。
|
||||
--
|
||||
-- 口径:
|
||||
-- 1. sys_work_order_reward 由系统维护,预估阶段不参与;工单验收后按实际
|
||||
-- reward_amount 直接写 actual 快照。版本金额 0 只是占位,不参与汇总。
|
||||
-- 2. sys_manual_service 由人工支出登记使用,先建草稿版本占位;
|
||||
-- 财务填写真实按单金额并启用后,才参与成本解析。
|
||||
|
||||
INSERT INTO cost_items (
|
||||
code, name, cost_type, carrier_type, pricing_mode, unit_name, currency,
|
||||
status, note, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'sys_work_order_reward',
|
||||
'接单平台打手报酬(实际结算)',
|
||||
'work_order_reward',
|
||||
'work_order',
|
||||
'per_order',
|
||||
'单',
|
||||
'CNY',
|
||||
'active',
|
||||
'系统内置:实际金额取工单 reward_amount;版本金额仅占位,不参与预估成本',
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
cost_type = EXCLUDED.cost_type,
|
||||
carrier_type = EXCLUDED.carrier_type,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW();
|
||||
|
||||
INSERT INTO cost_items (
|
||||
code, name, cost_type, carrier_type, pricing_mode, unit_name, currency,
|
||||
status, note, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'sys_manual_service',
|
||||
'人工客服履约成本',
|
||||
'manual_service',
|
||||
'manual',
|
||||
'per_order',
|
||||
'单',
|
||||
'CNY',
|
||||
'active',
|
||||
'系统内置:由公司人工支出登记写入 actual 快照;草稿版本待财务填写实际单价',
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
cost_type = EXCLUDED.cost_type,
|
||||
carrier_type = EXCLUDED.carrier_type,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 接单平台:active 占位版本,供 cost_snapshots.cost_version_id 引用。
|
||||
INSERT INTO cost_versions (
|
||||
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
|
||||
work_product_rule_id, amount_fen, rate_bps, effective_from, source,
|
||||
status, change_reason, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
ci.id, 'global', '', '', NULL, NULL, 0, 0,
|
||||
'1970-01-01T00:00:00+08:00', 'manual', 'active',
|
||||
'系统占位:接单平台实际金额取工单 reward_amount',
|
||||
NOW(), NOW()
|
||||
FROM cost_items ci
|
||||
WHERE ci.code = 'sys_work_order_reward'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 人工客服:draft 版本,避免在财务未填写金额时静默按 0 计成本。
|
||||
INSERT INTO cost_versions (
|
||||
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
|
||||
work_product_rule_id, amount_fen, rate_bps, effective_from, source,
|
||||
status, change_reason, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
ci.id, 'global', '', '', NULL, NULL, 0, 0,
|
||||
'1970-01-01T00:00:00+08:00', 'manual', 'draft',
|
||||
'草稿占位:待财务填写人工客服实际按单成本后启用',
|
||||
NOW(), NOW()
|
||||
FROM cost_items ci
|
||||
WHERE ci.code = 'sys_manual_service'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
COMMENT ON COLUMN cost_items.code IS
|
||||
'成本项编码;sys_ 前缀为系统内置项,业务侧不要删除或改编码';
|
||||
@@ -1,14 +0,0 @@
|
||||
-- 112_cost_reporting_indexes.sql —— 成本版本页按接单规则汇总用户实付。
|
||||
--
|
||||
-- 背景:成本版本列表需要同时展示「用户实付均价」和「模板发单金额」。
|
||||
-- 原查询按 work_orders.product_rule_id 聚合约 11 万工单 + 31 万订单项,
|
||||
-- 没有覆盖索引时需要 1 秒以上;以下两个覆盖索引把聚合降到约 150 毫秒。
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_work_orders_rule_cover
|
||||
ON work_orders (product_rule_id)
|
||||
INCLUDE (order_item_id)
|
||||
WHERE order_item_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_revenue_cover
|
||||
ON order_items (id)
|
||||
INCLUDE (order_id, revenue_amount_fen);
|
||||
@@ -1,35 +0,0 @@
|
||||
-- 113_fund_account_no_readable.sql —— 备用金账户编号改为可读的 FA-<用户名>。
|
||||
--
|
||||
-- 背景:历史编号是 FA + 10 位随机十六进制(如 FA7683D6B58B),
|
||||
-- 在「备用金」列表里无法一眼对应到具体客服。
|
||||
-- 新规则:FA-<大写用户名>,例如 FA-KF_2、FA-JIAJIALI;
|
||||
-- 同一用户名重名时追加 -2、-3。
|
||||
|
||||
WITH base AS (
|
||||
SELECT
|
||||
fa.id,
|
||||
'FA-' || COALESCE(
|
||||
NULLIF(
|
||||
trim(
|
||||
both '_' FROM upper(
|
||||
regexp_replace(COALESCE(NULLIF(au.username, ''), 'account-' || fa.id), '[^A-Za-z0-9_]+', '_', 'g')
|
||||
)
|
||||
),
|
||||
''
|
||||
),
|
||||
'ACCOUNT-' || fa.id
|
||||
) AS base_no
|
||||
FROM fund_accounts fa
|
||||
LEFT JOIN admin_users au ON au.id = fa.owner_admin_user_id
|
||||
),
|
||||
numbered AS (
|
||||
SELECT
|
||||
id,
|
||||
base_no,
|
||||
row_number() OVER (PARTITION BY base_no ORDER BY id) AS rn
|
||||
FROM base
|
||||
)
|
||||
UPDATE fund_accounts fa
|
||||
SET account_no = CASE WHEN n.rn = 1 THEN n.base_no ELSE n.base_no || '-' || n.rn END
|
||||
FROM numbered n
|
||||
WHERE fa.id = n.id;
|
||||
@@ -1,45 +0,0 @@
|
||||
-- 114_order_item_fulfillment_mode.sql
|
||||
--
|
||||
-- 统一履约事实:订单页、成本快照、销售事实只读 order_items.fulfillment_mode,
|
||||
-- 不再各自用 provider / voucher / task 临时推断。
|
||||
--
|
||||
-- 说明:
|
||||
-- 1. 本迁移只做 DDL;31 万行回填由 scripts/backfill-fulfillment-mode.ts 分批执行。
|
||||
-- 2. product_catalog / cost_items / cost_snapshots / sales_daily_facts 的
|
||||
-- carrier_type CHECK 目前都不接受 legacy_platform,必须同时放宽,
|
||||
-- 否则历史平台数据写入会在运行时报错。
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS fulfillment_mode text,
|
||||
ADD COLUMN IF NOT EXISTS fulfillment_decided_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS fulfillment_reason text;
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD CONSTRAINT chk_order_items_fulfillment_mode
|
||||
CHECK (
|
||||
fulfillment_mode IS NULL
|
||||
OR fulfillment_mode IN ('work_order', 'affiliate_dash', 'legacy_platform', 'manual')
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_mode
|
||||
ON order_items (fulfillment_mode, order_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_pending
|
||||
ON order_items (order_id)
|
||||
WHERE fulfillment_mode IS NULL;
|
||||
|
||||
ALTER TABLE product_catalog DROP CONSTRAINT IF EXISTS chk_product_catalog_carrier_type;
|
||||
ALTER TABLE product_catalog ADD CONSTRAINT chk_product_catalog_carrier_type
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared'));
|
||||
|
||||
ALTER TABLE cost_items DROP CONSTRAINT IF EXISTS chk_cost_items_carrier_type;
|
||||
ALTER TABLE cost_items ADD CONSTRAINT chk_cost_items_carrier_type
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared'));
|
||||
|
||||
ALTER TABLE cost_snapshots DROP CONSTRAINT IF EXISTS chk_cost_snapshots_carrier_type;
|
||||
ALTER TABLE cost_snapshots ADD CONSTRAINT chk_cost_snapshots_carrier_type
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared'));
|
||||
|
||||
ALTER TABLE sales_daily_facts DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_carrier;
|
||||
ALTER TABLE sales_daily_facts ADD CONSTRAINT chk_sales_daily_facts_carrier
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared', 'unknown'));
|
||||
Reference in New Issue
Block a user