合并本轮全部文档改动(原 5 个 docs 提交压成 1 个)。 口径与索引: - 新增 docs/README.md 文档总索引:三来源口径(91 + 快手 send-code = kuaishou-91)、 「一个订单 = 骨架 + 买家凭证 + 一个执行载体」、成本只改一处、文档状态机 - 过期阶段文档移入 docs/archive/2026-10-08/(保留内容,不删除) 分析报告: - docs/项目全量分析报告-2026-10-17.md:架构、五条业务链路、P0/P1/P2 风险与改进路线 - docs/载体判定与成本中心落地-复核与草案-2026-10-17.md:生产只读克隆量化、上一轮结论复核与 4 处修正 方案文档: - docs/成本中心与三块履约模型.md:三块执行载体、标准商品、成本版本与快照、人工支出登记 - docs/财务系统集成方案.md:复式记账模型、分录表、分期落地 - docs/影子总账说明.md:影子期与转正期的边界、对账与转正标准 SQL 草案(docs/drafts/,未接入迁移): - 载体判定与人工块量化.sql:量化查询、carrier_type 判定与回填、守恒校验 - 成本中心与销售事实-DDL草案.sql:product_catalog / channel_product_mappings / cost_items / cost_versions / cost_snapshots / cost_exceptions / sales_daily_facts - 人工支出登记-DDL草案.sql:manual_fulfillment_cases / manual_expense_records / manual_expense_events 业务口径确认(2026-10-17): - 收入 = 用户实付(orders.total_amount),与快手 paymentFen 数据核验 100% 一致 - 打手报酬 = 支出/成本(最终提现打款),按总额法 - 人工块以「支出可追溯」为先,SLA 监管延后 - 退款/仲裁暂未接入,先预留字段与反向冲正关系 三份草案已在一次性沙箱库按依赖顺序零错误应用(10 张新表、27 个 CHECK 约束)。
401 lines
21 KiB
SQL
401 lines
21 KiB
SQL
-- ============================================================================
|
||
-- 通用成本中心 + 标准商品 + 销售事实(建表草案)
|
||
-- ----------------------------------------------------------------------------
|
||
-- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。
|
||
-- 接入时应拆成 `101_product_catalog.sql`、`102_cost_center.sql`、`103_sales_daily_facts.sql`,
|
||
-- 并先跑 `npm run check:sql`(SQL 防退化护栏)。
|
||
-- 基线:main @ ccd5ed2a;数据依据:本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14)。
|
||
-- 金额单位:分(BIGINT)。本文件所有 *_fen 列均为分。
|
||
-- 设计依据:docs/成本中心与三块履约模型.md(三块载体、成本版本与快照)、docs/财务系统集成方案.md。
|
||
--
|
||
-- 目标(业务原话):以后改成本只在一个地方。
|
||
-- -> 所有成本金额只写 cost_versions;业务表只保存 cost_snapshot_id 与派生金额,永不回写历史。
|
||
-- ============================================================================
|
||
|
||
|
||
-- ============================================================================
|
||
-- §1 标准商品主数据
|
||
-- ----------------------------------------------------------------------------
|
||
-- 为什么必须先做:同一个商品名对应多个 sku_code(实测「苏尔南钻石100颗」47 个码、
|
||
-- 「指挥官密钥1个」42 个码),直接按 sku_code 或 sku_name 统计都会出错。
|
||
-- ============================================================================
|
||
|
||
CREATE TABLE IF NOT EXISTS product_catalog (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
canonical_sku TEXT NOT NULL,
|
||
canonical_name TEXT NOT NULL,
|
||
category_id BIGINT,
|
||
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_category ON product_catalog (category_id);
|
||
|
||
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;
|
||
END $$;
|
||
|
||
COMMENT ON TABLE product_catalog IS '标准商品/皮肤主数据:报表统计口径的唯一入口';
|
||
COMMENT ON COLUMN product_catalog.canonical_sku IS '标准编码,报表维度用它,不用原始 sku_code';
|
||
COMMENT ON COLUMN product_catalog.unit_name IS '计量单位:件/个/颗,配合 normalized_quantity 使用';
|
||
|
||
-- 渠道映射:来源 + 店铺 + 平台SKU -> 标准商品
|
||
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',
|
||
note TEXT NOT NULL DEFAULT '',
|
||
created_at TIMESTAMPTZ NOT NULL,
|
||
updated_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
-- 同一来源+店铺+SKU 只能映射到一个标准商品;shop_id 为空表示该来源的全局映射
|
||
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);
|
||
|
||
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;
|
||
END $$;
|
||
|
||
COMMENT ON TABLE channel_product_mappings IS '来源+店铺+平台SKU -> 标准商品映射(报表口径归一化)';
|
||
COMMENT ON COLUMN channel_product_mappings.quantity_multiplier IS 'SKU 名称内含数量时的换算系数,例如「5个」=5';
|
||
|
||
|
||
-- ============================================================================
|
||
-- §2 成本中心
|
||
-- ============================================================================
|
||
|
||
-- 成本项:定义成本类型与计价方式,不存金额
|
||
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,
|
||
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);
|
||
|
||
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', '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', 'legacy', '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;
|
||
END $$;
|
||
|
||
COMMENT ON TABLE cost_items IS '成本项定义:类型 + 载体 + 计价方式;金额一律放 cost_versions';
|
||
COMMENT ON COLUMN cost_items.carrier_type IS '适用执行载体;shared 表示与载体无关的通用成本(如支付通道费)';
|
||
|
||
-- 成本版本:**唯一改价入口**,按生效时间新增版本,永不覆盖历史
|
||
CREATE TABLE IF NOT EXISTS cost_versions (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
cost_item_id BIGINT NOT NULL REFERENCES cost_items(id) ON DELETE RESTRICT,
|
||
provider TEXT NOT NULL DEFAULT '',
|
||
shop_id TEXT NOT NULL DEFAULT '',
|
||
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||
category_id BIGINT,
|
||
amount_fen BIGINT NOT NULL,
|
||
effective_from TIMESTAMPTZ NOT NULL,
|
||
effective_to TIMESTAMPTZ,
|
||
approval_status TEXT NOT NULL DEFAULT 'approved',
|
||
approved_by TEXT NOT NULL DEFAULT '',
|
||
reason TEXT NOT NULL DEFAULT '',
|
||
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, provider, shop_id, coalesce(canonical_product_id, 0), effective_from);
|
||
CREATE INDEX IF NOT EXISTS idx_cost_versions_lookup
|
||
ON cost_versions (cost_item_id, effective_from DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_cost_versions_product
|
||
ON cost_versions (canonical_product_id, 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_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_approval') THEN
|
||
ALTER TABLE cost_versions
|
||
ADD CONSTRAINT chk_cost_versions_approval
|
||
CHECK (approval_status IN ('draft', 'pending', 'approved', 'rejected'));
|
||
END IF;
|
||
END $$;
|
||
|
||
COMMENT ON TABLE cost_versions IS '成本版本:改价只在这里新增版本;历史成本靠 cost_snapshots 固化,不回写';
|
||
COMMENT ON COLUMN cost_versions.amount_fen IS '金额(分)。pricing_mode=percent_of_revenue 时表示万分比';
|
||
COMMENT ON COLUMN cost_versions.shop_id IS '空串=不限店铺;解析优先级 店铺+SKU > SKU > 分类 > 来源 > 全局';
|
||
|
||
-- 成本快照:每笔订单项/工单/人工单实际用了哪个版本、多少钱
|
||
CREATE TABLE IF NOT EXISTS cost_snapshots (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
subject_type TEXT NOT NULL,
|
||
subject_id BIGINT NOT NULL,
|
||
order_id BIGINT,
|
||
order_item_id BIGINT,
|
||
work_order_id BIGINT,
|
||
carrier_type TEXT NOT NULL,
|
||
cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
|
||
cost_version_id BIGINT 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,
|
||
source TEXT NOT NULL DEFAULT 'resolved',
|
||
resolved_at TIMESTAMPTZ NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_subject ON cost_snapshots (subject_type, subject_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 UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_subject_item
|
||
ON cost_snapshots (subject_type, subject_id, cost_item_id);
|
||
|
||
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_source') THEN
|
||
ALTER TABLE cost_snapshots
|
||
ADD CONSTRAINT chk_cost_snapshots_source
|
||
CHECK (source IN ('resolved', 'legacy_rule', 'manual_input', 'missing'));
|
||
END IF;
|
||
END $$;
|
||
|
||
COMMENT ON TABLE cost_snapshots IS '成本快照:下单/建工单/人工完成时固化,后续改价不影响历史';
|
||
COMMENT ON COLUMN cost_snapshots.source IS 'resolved=命中成本中心;legacy_rule=回退旧 work_product_rules 金额;missing=未匹配到(禁止默认 0)';
|
||
|
||
-- 成本异常队列:匹配不到成本不能默认 0,必须进队列
|
||
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,
|
||
work_order_id BIGINT,
|
||
carrier_type TEXT NOT NULL,
|
||
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 TEXT NOT NULL DEFAULT ''
|
||
);
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_exceptions_subject
|
||
ON cost_exceptions (subject_type, subject_id, reason);
|
||
CREATE INDEX IF NOT EXISTS idx_cost_exceptions_open
|
||
ON cost_exceptions (status, created_at DESC);
|
||
|
||
DO $$
|
||
BEGIN
|
||
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 数量暴露出来,禁止静默按 0 计';
|
||
|
||
|
||
-- ============================================================================
|
||
-- §3 订单项口径补齐(金额与数量)
|
||
-- ----------------------------------------------------------------------------
|
||
-- 现状实测:order_items 只有 sku_code/sku_name/quantity/spec_json/snapshot,**没有任何金额列**;
|
||
-- 且 quantity 不可信(316,585 项里 316,578 项 = 1,名称带「N个」的有 104,710 项)。
|
||
-- 现状利好:目前每笔已支付订单恰好 1 个订单项(316,585 单 = 316,585 项),
|
||
-- 所以 orders.total_amount 暂时可当订单项收入;但 schema 允许多项,属**潜在**缺口。
|
||
--
|
||
-- 【口径已确认 2026-10-17】收入 = 用户付款金额,取 orders.total_amount:
|
||
-- · 91:total_amount 即用户付款(与快手发码配合)。数据核验:91 已支付 312,569 笔中
|
||
-- 235,271 笔含快手 kuaishouSendCode.paymentFen,其中 235,268 笔与 total_amount **完全一致**
|
||
-- (100.00%),仅 3 笔差 ¥1.80~2.00(2026-09-05 / 09-18 / 10-04,可单独抽查)。
|
||
-- · PDD:order_amount;抖音:pay_amount。三者同为“用户实付”,故统一 revenue_basis = 'user_payment'。
|
||
-- · 打手报酬 = **支出/成本**(最终由打手提现、我方付款),因此按**总额法**:
|
||
-- 收入记用户实付全额,打手报酬作为履约成本,不做代收代付净额。
|
||
-- · 退款/仲裁**暂未接入**:refunded_amount_fen / refunded_at 为**预留字段**,本轮不写逻辑。
|
||
-- ============================================================================
|
||
|
||
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 revenue_amount_fen BIGINT,
|
||
ADD COLUMN IF NOT EXISTS revenue_basis TEXT NOT NULL DEFAULT '',
|
||
ADD COLUMN IF NOT EXISTS refunded_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS refunded_at TIMESTAMPTZ;
|
||
|
||
DO $$
|
||
BEGIN
|
||
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 ('', 'user_payment', 'manual_adjustment'));
|
||
END IF;
|
||
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 IS NULL OR revenue_amount_fen >= 0);
|
||
END IF;
|
||
END $$;
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_order_items_canonical_product ON order_items (canonical_product_id);
|
||
|
||
COMMENT ON COLUMN order_items.normalized_quantity IS '归一化件数:从 SKU 名称解析并固化,禁止直接用 quantity 做销量';
|
||
COMMENT ON COLUMN order_items.revenue_amount_fen IS '收入(分)= 用户实付,取 orders.total_amount;revenue_basis 通常为 user_payment';
|
||
COMMENT ON COLUMN order_items.refunded_at IS '【预留】退款时间:退款/仲裁暂未接入,本轮不写逻辑;接入后按日净销售额 = 销售额 - 退款额';
|
||
|
||
-- 过渡:让工单规则逐步改用成本中心(有 cost_item_id 走成本中心,否则回退旧金额字段)
|
||
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=旧规则金额,便于统计未迁移规则数';
|
||
-- 口径已确认(2026-10-17):打手报酬是**支出**(工单结算 -> 可提现 -> 提现打款),
|
||
-- 按总额法计入履约成本,与用户实付收入分开记,不做代收代付净额。
|
||
|
||
|
||
-- ============================================================================
|
||
-- §4 销售事实表(店铺 × 日期 × 标准商品)
|
||
-- ============================================================================
|
||
|
||
CREATE TABLE IF NOT EXISTS sales_daily_facts (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
stat_date DATE NOT NULL,
|
||
provider TEXT NOT NULL,
|
||
shop_id TEXT NOT NULL DEFAULT '',
|
||
canonical_product_id BIGINT NOT NULL REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||
carrier_type TEXT NOT NULL,
|
||
order_count INTEGER NOT NULL DEFAULT 0,
|
||
item_count INTEGER NOT NULL DEFAULT 0,
|
||
gross_revenue_fen BIGINT NOT NULL DEFAULT 0,
|
||
refund_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||
net_revenue_fen BIGINT NOT NULL DEFAULT 0,
|
||
cost_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||
cost_missing_count INTEGER NOT NULL DEFAULT 0,
|
||
gross_profit_fen BIGINT NOT NULL DEFAULT 0,
|
||
computed_at TIMESTAMPTZ NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL,
|
||
updated_at TIMESTAMPTZ NOT NULL
|
||
);
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uq_sales_daily_facts_key
|
||
ON sales_daily_facts (stat_date, provider, shop_id, canonical_product_id, carrier_type);
|
||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_date ON sales_daily_facts (stat_date DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_shop ON sales_daily_facts (provider, shop_id, stat_date DESC);
|
||
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_carrier') THEN
|
||
ALTER TABLE sales_daily_facts
|
||
ADD CONSTRAINT chk_sales_daily_facts_carrier
|
||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy', 'manual'));
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_profit') THEN
|
||
ALTER TABLE sales_daily_facts
|
||
ADD CONSTRAINT chk_sales_daily_facts_profit
|
||
CHECK (gross_profit_fen = net_revenue_fen - cost_amount_fen);
|
||
END IF;
|
||
END $$;
|
||
|
||
COMMENT ON TABLE sales_daily_facts IS '店铺×日期(Asia/Shanghai)×标准商品×载体 的销售与成本事实;报表直接用这张表';
|
||
COMMENT ON COLUMN sales_daily_facts.stat_date IS '销售日按 Asia/Shanghai 归属,取 paid_at;退款按 refunded_at 归期';
|
||
COMMENT ON COLUMN sales_daily_facts.cost_missing_count IS '成本缺失件数:>0 时该行毛利不可信,报表必须标注';
|
||
|
||
|
||
-- ============================================================================
|
||
-- §5 校验与重算(跑完后必须对平,差异不为 0 说明实现有问题)
|
||
-- ============================================================================
|
||
|
||
-- Q1 成本解析覆盖率:open 异常占比应低于约定阈值,且不得为 0 成本静默通过
|
||
-- SELECT carrier_type, count(*) FILTER (WHERE source = 'missing') AS missing,
|
||
-- count(*) AS total,
|
||
-- round(100.0 * count(*) FILTER (WHERE source = 'missing') / count(*), 2) AS missing_pct
|
||
-- FROM cost_snapshots GROUP BY 1;
|
||
|
||
-- Q2 毛利守恒:销售事实的净收入必须等于订单项收入汇总(按同一维度)
|
||
-- SELECT f.stat_date, f.provider, f.shop_id,
|
||
-- f.net_revenue_fen AS fact_revenue,
|
||
-- coalesce(sum(oi.revenue_amount_fen - oi.refunded_amount_fen), 0) AS item_revenue
|
||
-- FROM sales_daily_facts f
|
||
-- LEFT JOIN order_items oi ON oi.canonical_product_id = f.canonical_product_id
|
||
-- LEFT JOIN orders o ON o.id = oi.order_id AND o.provider = f.provider AND o.shop_id = f.shop_id
|
||
-- GROUP BY 1, 2, 3, f.net_revenue_fen
|
||
-- HAVING f.net_revenue_fen <> coalesce(sum(oi.revenue_amount_fen - oi.refunded_amount_fen), 0)
|
||
-- LIMIT 100;
|
||
|
||
-- Q3 历史成本不回写校验:改 cost_versions 后,已有 cost_snapshots 金额不得变化
|
||
-- SELECT count(*) FROM cost_snapshots WHERE resolved_at < now() - interval '1 day' AND created_at <> resolved_at;
|
||
|
||
|
||
-- ============================================================================
|
||
-- §6 落地顺序(与 docs/成本中心与三块履约模型.md §九 一致)
|
||
-- 1) §1 商品主数据 + 渠道映射 -> 解决「同一商品 47 个 SKU 码」与销量口径
|
||
-- 2) §3 订单项口径(数量/收入)-> 解决 7.3 倍数量低估与收入口径不统一
|
||
-- 3) §2 成本中心影子期 -> 自动发货接上游固定成本;工单接现有规则金额;人工接按单成本
|
||
-- 4) §4 销售事实表 -> 店铺/日期/皮肤 的销量、收入、成本、毛利
|
||
-- 5) 之后才是影子总账(docs/财务系统集成方案.md)
|
||
--
|
||
-- 【口径已确认 2026-10-17】
|
||
-- a) 收入 = 用户付款金额(orders.total_amount),计收入;已用数据核验与快手 paymentFen 100% 对齐。
|
||
-- b) 打手报酬 = 支出/成本(最终提现打款),按总额法。
|
||
-- c) 人工块:第一优先级是**人工支出可追溯**(付款项目/金额/订单号/付款人/凭证),
|
||
-- 见 `2026-10-17-人工支出登记-DDL草案.sql`;按单还是按小时的取值口径仍待定,先按单登记。
|
||
-- d) 退款/仲裁:**暂未接入,先预留**(字段 + reversal_of_id + 事件类型,不实现逻辑)。
|
||
--
|
||
-- 【仍待确认】
|
||
-- 1) 收入确认时点(付款 / 发货 / 验收 / 售后期结束)——影响利润表与预收账款;
|
||
-- 2) 自动发货 cost_amount 是否稳定、是否含平台积分/汇率;
|
||
-- 3) 平台服务费/支付通道费由谁承担、结算周期与账单口径;
|
||
-- 4) 寄卖货款的采购成本确认时点。
|
||
-- ============================================================================
|