Files
SBrainCO/db/ontology_standard.sql
2026-07-26 22:48:08 +08:00

827 lines
35 KiB
SQL

-- ============================================================
-- 连锁餐饮企业数字化运营平台 — 本体标准数据库
-- 创建时间: 2026-07-26
-- 说明: 维度主数据表 + 事实表 + 指标标准 + 枚举标准
-- ============================================================
-- ============================================================
-- 一、维度主数据表 (Dimension Tables)
-- ============================================================
-- 1. 门店主数据 (扩展现有 dim_store)
ALTER TABLE analytics.dim_store
ADD COLUMN IF NOT EXISTS brand TEXT DEFAULT '马兰拉面',
ADD COLUMN IF NOT EXISTS comparable BOOLEAN DEFAULT true,
ADD COLUMN IF NOT EXISTS management_quadrant TEXT,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT now();
-- 2. SKU主数据
CREATE TABLE IF NOT EXISTS analytics.dim_sku (
sku_code TEXT PRIMARY KEY,
standard_name TEXT NOT NULL,
pos_code TEXT,
meituan_code TEXT,
taobao_code TEXT,
jd_code TEXT,
historical_names TEXT[],
sku_type TEXT NOT NULL DEFAULT '单品', -- 单品/套餐主品/套餐组件/赠品/包装/服务项目
category_l1 TEXT,
category_l2 TEXT,
status TEXT NOT NULL DEFAULT '在售', -- 在售/停用/季节停/待定
effective_date DATE,
discontinued_date DATE,
brand TEXT DEFAULT '马兰拉面',
business_type TEXT, -- 兰州牛肉面/夜市烧烤/火锅等
applicable_stores TEXT[], -- NULL表示全部门店
tags TEXT[], -- 新品/季节品/区域品/战略品
abc_class TEXT, -- A-核心/B-成长/C-长尾
unit TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 3. 原料主数据
CREATE TABLE IF NOT EXISTS analytics.dim_material (
material_code TEXT PRIMARY KEY,
material_name TEXT NOT NULL,
specification TEXT,
major_category TEXT, -- 牛肉类/面粉类/蔬菜类/调味料类等
minor_category TEXT,
finance_category TEXT, -- 原材料/辅料/包装物/低值易耗品
base_unit TEXT NOT NULL, -- kg/个/包/瓶
conversion_ratio JSONB, -- {"kg": 1, "斤": 2}
supplier_code TEXT,
status TEXT DEFAULT '启用',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 4. 会员主数据
CREATE TABLE IF NOT EXISTS analytics.dim_member (
member_id TEXT PRIMARY KEY,
phone_hash TEXT, -- 脱敏手机号
register_channel TEXT, -- 堂食/美团/淘宝/京东/小程序
register_store TEXT,
register_date DATE,
member_level TEXT DEFAULT '普通', -- 普通/银卡/金卡/钻石
total_orders INTEGER DEFAULT 0,
total_revenue NUMERIC DEFAULT 0,
last_order_date DATE,
status TEXT DEFAULT '活跃', -- 活跃/沉睡/流失
tags TEXT[],
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 5. 员工主数据
CREATE TABLE IF NOT EXISTS analytics.dim_employee (
employee_id TEXT PRIMARY KEY,
employee_name TEXT NOT NULL,
position TEXT, -- 店长/副店长/厨师长/厨师/服务员/收银员/配送员
store_code TEXT,
hire_date DATE,
leave_date DATE,
status TEXT DEFAULT '在职', -- 在职/离职/休假
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 6. 渠道主数据
CREATE TABLE IF NOT EXISTS analytics.dim_channel (
channel_code TEXT PRIMARY KEY,
channel_name TEXT NOT NULL,
channel_group TEXT NOT NULL, -- 堂食/外卖/支付
channel_type TEXT, -- 堂食/美团到店/美团外卖/支付宝/微信/现金/银联/抖音/挂账/京东到家/淘宝外卖
platform TEXT, -- 美团/淘宝/京东/抖音/自有
commission_rate NUMERIC, -- 平台佣金率%
is_delivery BOOLEAN DEFAULT false,
sort_order INTEGER DEFAULT 0,
status TEXT DEFAULT '启用',
created_at TIMESTAMPTZ DEFAULT now()
);
-- 7. 活动主数据
CREATE TABLE IF NOT EXISTS analytics.dim_promotion (
promotion_id TEXT PRIMARY KEY,
promotion_name TEXT NOT NULL,
channel TEXT, -- 美团/淘宝/京东/堂食/全渠道
start_date DATE,
end_date DATE,
applicable_stores TEXT[], -- NULL表示全部
applicable_skus TEXT[], -- NULL表示全部
platform_bear NUMERIC DEFAULT 0, -- 平台承担金额
company_bear NUMERIC DEFAULT 0, -- 公司承担金额
store_bear NUMERIC DEFAULT 0, -- 门店承担金额
target_audience TEXT, -- 新客/老客/全客
budget NUMERIC,
status TEXT DEFAULT '进行中', -- 计划中/进行中/已结束/已暂停
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 8. 日历主数据
CREATE TABLE IF NOT EXISTS analytics.dim_calendar (
date_value DATE PRIMARY KEY,
year INTEGER NOT NULL,
month INTEGER NOT NULL,
day INTEGER NOT NULL,
weekday_no INTEGER NOT NULL, -- 1=周一, 7=周日
weekday_name TEXT NOT NULL,
is_weekend BOOLEAN NOT NULL,
is_holiday BOOLEAN DEFAULT false,
holiday_name TEXT,
is_operating_day BOOLEAN DEFAULT true,
weather TEXT, -- 晴/阴/雨/雪/雾
temperature NUMERIC,
special_event TEXT, -- 商圈活动/门店周边事件
created_at TIMESTAMPTZ DEFAULT now()
);
-- 9. 供应商主数据
CREATE TABLE IF NOT EXISTS analytics.dim_supplier (
supplier_code TEXT PRIMARY KEY,
supplier_name TEXT NOT NULL,
supplier_type TEXT, -- 原料/包装/设备/服务
contact_person TEXT,
contact_phone TEXT,
region TEXT,
status TEXT DEFAULT '合作中',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 二、事实表 (Fact Tables)
-- ============================================================
-- 1. 账单事实表 (物化视图已存在 analytics.bill_fact,此处建规范事实表)
CREATE TABLE IF NOT EXISTS analytics.fact_bill (
record_id BIGINT PRIMARY KEY,
source_file TEXT,
source_row INTEGER,
store_code TEXT NOT NULL,
store_name TEXT,
meal_period TEXT, -- 午市/晚市/夜宵/外卖
bill_no TEXT,
business_area TEXT,
table_type TEXT,
table_no TEXT,
business_date DATE GENERATED ALWAYS AS ((closed_at AT TIME ZONE 'Asia/Shanghai')::date) STORED,
consumption NUMERIC DEFAULT 0,
discount_total NUMERIC DEFAULT 0,
received_total NUMERIC DEFAULT 0,
guest_count NUMERIC DEFAULT 0,
table_count NUMERIC DEFAULT 0,
theoretical_cost NUMERIC DEFAULT 0,
theoretical_profit NUMERIC DEFAULT 0,
theoretical_margin NUMERIC,
third_party_discount NUMERIC DEFAULT 0,
marketing_plan TEXT,
member_id TEXT,
member_level TEXT,
waiter TEXT,
cashier TEXT,
shift_name TEXT,
bill_status TEXT,
order_tag TEXT,
opened_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
duration_minutes NUMERIC,
-- 支付渠道金额
cash_received NUMERIC DEFAULT 0,
alipay_received NUMERIC DEFAULT 0,
wechat_received NUMERIC DEFAULT 0,
meituan_received NUMERIC DEFAULT 0,
unionpay_received NUMERIC DEFAULT 0,
douyin_received NUMERIC DEFAULT 0,
credit_received NUMERIC DEFAULT 0,
jd_delivery_received NUMERIC DEFAULT 0,
meituan_delivery_received NUMERIC DEFAULT 0,
taobao_delivery_received NUMERIC DEFAULT 0,
-- 优惠明细
points_discount NUMERIC DEFAULT 0,
member_coupon_discount NUMERIC DEFAULT 0,
member_prestore_discount NUMERIC DEFAULT 0,
douyin_discount NUMERIC DEFAULT 0,
-- 平台佣金
meituan_delivery_commission NUMERIC DEFAULT 0,
taobao_delivery_commission NUMERIC DEFAULT 0,
jd_delivery_commission NUMERIC DEFAULT 0,
-- 异常标记
anomaly_reason TEXT,
is_anomaly BOOLEAN DEFAULT false,
is_zero_received BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 2. 账单明细事实表
CREATE TABLE IF NOT EXISTS analytics.fact_bill_item (
detail_id BIGINT PRIMARY KEY,
source_file TEXT,
source_row INTEGER,
store_code TEXT NOT NULL,
store_name TEXT,
bill_no TEXT NOT NULL,
business_type TEXT,
table_or_pickup_no TEXT,
guest_count NUMERIC,
opened_at TIMESTAMP,
closed_at TIMESTAMP,
ordered_at TIMESTAMP,
dish_name TEXT NOT NULL,
sku_code TEXT, -- 关联 dim_sku
category_l1 TEXT,
category_l2 TEXT,
item_type TEXT, -- 单品/套餐/赠品/加料/包装
production_department TEXT,
unit TEXT,
preparation_method TEXT,
sales_quantity NUMERIC DEFAULT 0,
unit_price NUMERIC DEFAULT 0,
gross_amount NUMERIC DEFAULT 0,
received_amount NUMERIC DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 3. 支付事实表 (一张账单一种支付方式一行)
CREATE TABLE IF NOT EXISTS analytics.fact_payment (
id BIGSERIAL PRIMARY KEY,
bill_record_id BIGINT NOT NULL, -- 关联 fact_bill.record_id
store_code TEXT NOT NULL,
bill_no TEXT,
business_date DATE,
channel_code TEXT NOT NULL, -- 关联 dim_channel
amount NUMERIC DEFAULT 0,
commission NUMERIC DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 4. 优惠使用事实表
CREATE TABLE IF NOT EXISTS analytics.fact_promotion_usage (
id BIGSERIAL PRIMARY KEY,
bill_record_id BIGINT,
store_code TEXT NOT NULL,
bill_no TEXT,
business_date DATE,
promotion_id TEXT, -- 关联 dim_promotion
discount_amount NUMERIC DEFAULT 0,
platform_bear NUMERIC DEFAULT 0,
company_bear NUMERIC DEFAULT 0,
store_bear NUMERIC DEFAULT 0,
is_new_customer BOOLEAN,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 5. 平台订单损益表
CREATE TABLE IF NOT EXISTS analytics.fact_platform_order (
id BIGSERIAL PRIMARY KEY,
platform_order_no TEXT NOT NULL,
platform TEXT NOT NULL, -- 美团/淘宝/京东
store_code TEXT NOT NULL,
business_date DATE NOT NULL,
gross_amount NUMERIC DEFAULT 0,
discount_amount NUMERIC DEFAULT 0,
received_amount NUMERIC DEFAULT 0,
commission NUMERIC DEFAULT 0,
delivery_fee NUMERIC DEFAULT 0,
refund_amount NUMERIC DEFAULT 0,
net_revenue NUMERIC DEFAULT 0,
theoretical_cost NUMERIC DEFAULT 0,
gross_profit NUMERIC DEFAULT 0,
is_cancelled BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 6. 库存快照事实表
CREATE TABLE IF NOT EXISTS analytics.fact_inventory_snapshot (
id BIGSERIAL PRIMARY KEY,
store_code TEXT NOT NULL,
material_code TEXT NOT NULL,
snapshot_date DATE NOT NULL,
opening_quantity NUMERIC DEFAULT 0,
opening_amount NUMERIC DEFAULT 0,
purchase_quantity NUMERIC DEFAULT 0,
purchase_amount NUMERIC DEFAULT 0,
consumption_quantity NUMERIC DEFAULT 0,
consumption_amount NUMERIC DEFAULT 0,
ending_quantity NUMERIC DEFAULT 0,
ending_amount NUMERIC DEFAULT 0,
waste_quantity NUMERIC DEFAULT 0,
waste_amount NUMERIC DEFAULT 0,
transfer_in_quantity NUMERIC DEFAULT 0,
transfer_out_quantity NUMERIC DEFAULT 0,
inventory_days NUMERIC, -- 库存天数
is_negative BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(store_code, material_code, snapshot_date)
);
-- 7. 采购收货事实表
CREATE TABLE IF NOT EXISTS analytics.fact_purchase_receipt (
id BIGSERIAL PRIMARY KEY,
receipt_no TEXT,
store_code TEXT NOT NULL,
supplier_code TEXT,
material_code TEXT NOT NULL,
receipt_date DATE NOT NULL,
quantity NUMERIC DEFAULT 0,
unit_price_excl_tax NUMERIC DEFAULT 0,
unit_price_incl_tax NUMERIC DEFAULT 0,
tax_rate NUMERIC DEFAULT 0,
amount_excl_tax NUMERIC DEFAULT 0,
tax_amount NUMERIC DEFAULT 0,
amount_incl_tax NUMERIC DEFAULT 0,
return_quantity NUMERIC DEFAULT 0,
batch_no TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 8. 调拨事实表
CREATE TABLE IF NOT EXISTS analytics.fact_transfer (
id BIGSERIAL PRIMARY KEY,
transfer_no TEXT,
from_store_code TEXT NOT NULL,
to_store_code TEXT NOT NULL,
material_code TEXT NOT NULL,
transfer_date DATE NOT NULL,
quantity NUMERIC DEFAULT 0,
amount NUMERIC DEFAULT 0,
operator TEXT,
approver TEXT,
reason TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 9. 报损事实表
CREATE TABLE IF NOT EXISTS analytics.fact_waste (
id BIGSERIAL PRIMARY KEY,
waste_no TEXT,
store_code TEXT NOT NULL,
material_code TEXT NOT NULL,
waste_date DATE NOT NULL,
quantity NUMERIC DEFAULT 0,
amount NUMERIC DEFAULT 0,
reason TEXT, -- 过期/损坏/变质/操作失误/盘点差异
operator TEXT,
approver TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 10. BOM配方事实表
CREATE TABLE IF NOT EXISTS analytics.fact_recipe_bom (
id BIGSERIAL PRIMARY KEY,
sku_code TEXT NOT NULL,
material_code TEXT NOT NULL,
bom_version TEXT NOT NULL DEFAULT 'v1',
standard_gross_quantity NUMERIC NOT NULL, -- 标准毛料量
standard_net_quantity NUMERIC NOT NULL, -- 标准净料量
unit TEXT NOT NULL,
yield_rate NUMERIC, -- 出成率%
waste_rate NUMERIC, -- 损耗率%
substitute_material TEXT, -- 替代原料
effective_date DATE NOT NULL DEFAULT '2026-01-01',
expiry_date DATE,
applicable_stores TEXT[],
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(sku_code, material_code, bom_version)
);
-- 11. 员工排班事实表
CREATE TABLE IF NOT EXISTS analytics.fact_employee_shift (
id BIGSERIAL PRIMARY KEY,
employee_id TEXT NOT NULL,
employee_name TEXT,
store_code TEXT NOT NULL,
shift_date DATE NOT NULL,
shift_name TEXT, -- 早班/中班/晚班/全天
planned_hours NUMERIC DEFAULT 0,
actual_hours NUMERIC DEFAULT 0,
overtime_hours NUMERIC DEFAULT 0,
wage NUMERIC DEFAULT 0,
overtime_pay NUMERIC DEFAULT 0,
meal_period TEXT, -- 午市/晚市/夜宵
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(employee_id, store_code, shift_date, shift_name)
);
-- 12. 会员触达事实表
CREATE TABLE IF NOT EXISTS analytics.fact_customer_contact (
id BIGSERIAL PRIMARY KEY,
member_id TEXT NOT NULL,
contact_type TEXT NOT NULL, -- 短信/推送/券/电话/企微
contact_date DATE NOT NULL,
store_code TEXT,
campaign_id TEXT,
channel TEXT,
is_responded BOOLEAN DEFAULT false,
response_date DATE,
is_converted BOOLEAN DEFAULT false, -- 是否转化为消费
conversion_date DATE,
conversion_amount NUMERIC DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 13. 门店任务事实表 (已存在 analytics.store_task,此处建规范版)
CREATE TABLE IF NOT EXISTS analytics.fact_store_task (
task_id INTEGER PRIMARY KEY,
plan_month DATE,
store_code TEXT NOT NULL,
priority TEXT NOT NULL, -- P0/P1/P2/P3
problem_indicator TEXT NOT NULL,
problem_module TEXT, -- 收入/成本/优惠/会员/库存/风险
current_value NUMERIC,
benchmark_value NUMERIC,
target_value NUMERIC,
problem_description TEXT,
action_required TEXT,
owner TEXT,
collaborators TEXT,
deadline DATE,
status TEXT DEFAULT '待启动',
process_evidence TEXT,
verification_indicator TEXT,
verification_result TEXT,
incomplete_reason TEXT,
next_step TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 三、指标标准表 (Metric Dictionary)
-- ============================================================
-- 扩展现有 indicator_dictionary 表
ALTER TABLE analytics.indicator_dictionary
ADD COLUMN IF NOT EXISTS metric_code TEXT,
ADD COLUMN IF NOT EXISTS metric_category TEXT, -- 收入/成本/优惠/会员/库存/风险/人效
ADD COLUMN IF NOT EXISTS calculation_sql TEXT,
ADD COLUMN IF NOT EXISTS direction TEXT DEFAULT 'higher_better', -- higher_better/lower_better
ADD COLUMN IF NOT EXISTS unit TEXT, -- 元/%/个/天
ADD COLUMN IF NOT EXISTS granularity TEXT, -- 门店/区域/公司
ADD COLUMN IF NOT EXISTS version TEXT DEFAULT 'v1.0',
ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true;
-- ============================================================
-- 四、枚举标准表 (Enum Tables)
-- ============================================================
-- 1. 风险等级枚举
CREATE TABLE IF NOT EXISTS analytics.enum_risk_level (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
color TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 2. 任务优先级枚举
CREATE TABLE IF NOT EXISTS analytics.enum_priority (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
response_time TEXT,
sort_order INTEGER DEFAULT 0
);
-- 3. 任务状态枚举
CREATE TABLE IF NOT EXISTS analytics.enum_task_status (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 4. 验收结果枚举
CREATE TABLE IF NOT EXISTS analytics.enum_verification_result (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 5. SKU类型枚举
CREATE TABLE IF NOT EXISTS analytics.enum_sku_type (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 6. SKU状态枚举
CREATE TABLE IF NOT EXISTS analytics.enum_sku_status (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 7. 业态枚举
CREATE TABLE IF NOT EXISTS analytics.enum_business_type (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 8. 渠道组枚举
CREATE TABLE IF NOT EXISTS analytics.enum_channel_group (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 9. 指标方向枚举
CREATE TABLE IF NOT EXISTS analytics.enum_metric_direction (
code TEXT PRIMARY KEY,
label TEXT NOT NULL
);
-- 10. 会员状态枚举
CREATE TABLE IF NOT EXISTS analytics.enum_member_status (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 11. 报损原因枚举
CREATE TABLE IF NOT EXISTS analytics.enum_waste_reason (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- 12. 管理象限枚举
CREATE TABLE IF NOT EXISTS analytics.enum_management_quadrant (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INTEGER DEFAULT 0
);
-- ============================================================
-- 五、创建索引
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_dim_sku_status ON analytics.dim_sku(status);
CREATE INDEX IF NOT EXISTS idx_dim_sku_type ON analytics.dim_sku(sku_type);
CREATE INDEX IF NOT EXISTS idx_dim_sku_category ON analytics.dim_sku(category_l1, category_l2);
CREATE INDEX IF NOT EXISTS idx_dim_material_category ON analytics.dim_material(major_category);
CREATE INDEX IF NOT EXISTS idx_dim_member_status ON analytics.dim_member(status);
CREATE INDEX IF NOT EXISTS idx_dim_employee_store ON analytics.dim_employee(store_code);
CREATE INDEX IF NOT EXISTS idx_dim_channel_group ON analytics.dim_channel(channel_group);
CREATE INDEX IF NOT EXISTS idx_dim_promotion_status ON analytics.dim_promotion(status);
CREATE INDEX IF NOT EXISTS idx_dim_calendar_date ON analytics.dim_calendar(date_value);
CREATE INDEX IF NOT EXISTS idx_fact_bill_store_date ON analytics.fact_bill(store_code, business_date);
CREATE INDEX IF NOT EXISTS idx_fact_bill_no ON analytics.fact_bill(bill_no);
CREATE INDEX IF NOT EXISTS idx_fact_bill_anomaly ON analytics.fact_bill(is_anomaly) WHERE is_anomaly = true;
CREATE INDEX IF NOT EXISTS idx_fact_bill_item_store ON analytics.fact_bill_item(store_code);
CREATE INDEX IF NOT EXISTS idx_fact_bill_item_bill_no ON analytics.fact_bill_item(bill_no);
CREATE INDEX IF NOT EXISTS idx_fact_bill_item_dish ON analytics.fact_bill_item(dish_name);
CREATE INDEX IF NOT EXISTS idx_fact_payment_bill ON analytics.fact_payment(bill_record_id);
CREATE INDEX IF NOT EXISTS idx_fact_payment_channel ON analytics.fact_payment(channel_code);
CREATE INDEX IF NOT EXISTS idx_fact_inventory_store_date ON analytics.fact_inventory_snapshot(store_code, snapshot_date);
CREATE INDEX IF NOT EXISTS idx_fact_inventory_negative ON analytics.fact_inventory_snapshot(is_negative) WHERE is_negative = true;
CREATE INDEX IF NOT EXISTS idx_fact_purchase_store_date ON analytics.fact_purchase_receipt(store_code, receipt_date);
CREATE INDEX IF NOT EXISTS idx_fact_waste_store_date ON analytics.fact_waste(store_code, waste_date);
CREATE INDEX IF NOT EXISTS idx_fact_bom_sku ON analytics.fact_recipe_bom(sku_code);
CREATE INDEX IF NOT EXISTS idx_fact_shift_store_date ON analytics.fact_employee_shift(store_code, shift_date);
CREATE INDEX IF NOT EXISTS idx_fact_contact_member ON analytics.fact_customer_contact(member_id);
CREATE INDEX IF NOT EXISTS idx_fact_task_store ON analytics.fact_store_task(store_code);
CREATE INDEX IF NOT EXISTS idx_fact_task_priority ON analytics.fact_store_task(priority);
-- ============================================================
-- 六、初始化枚举数据
-- ============================================================
INSERT INTO analytics.enum_risk_level (code, label, color, sort_order) VALUES
('红色', '红色', '#ef4444', 1),
('黄色', '黄色', '#eab308', 2),
('绿色', '绿色', '#22c55e', 3)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_priority (code, label, response_time, sort_order) VALUES
('P0', 'P0-紧急', '当日确认、7天内形成方案', 1),
('P1', 'P1-重点', '48小时确认、当月整改', 2),
('P2', 'P2-改善', '一周内纳入计划', 3),
('P3', 'P3-观察', '持续监控', 4)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_task_status (code, label, sort_order) VALUES
('待启动', '待启动', 1),
('进行中', '进行中', 2),
('待验收', '待验收', 3),
('已验收', '已验收', 4),
('已回滚', '已回滚', 5)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_verification_result (code, label, sort_order) VALUES
('达标', '达标', 1),
('改善中', '改善中', 2),
('未达标', '未达标', 3),
('待验收', '待验收', 4)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_sku_type (code, label, sort_order) VALUES
('单品', '单品', 1),
('套餐主品', '套餐主品', 2),
('套餐组件', '套餐组件', 3),
('赠品', '赠品', 4),
('包装', '包装', 5),
('服务项目', '服务项目', 6)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_sku_status (code, label, sort_order) VALUES
('在售', '在售', 1),
('停用', '停用', 2),
('季节停', '季节停', 3),
('待定', '待定', 4)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_business_type (code, label, sort_order) VALUES
('标准门店', '标准门店', 1),
('特殊业态', '特殊业态', 2),
('旗舰店', '旗舰店', 3),
('社区店', '社区店', 4),
('交通枢纽店', '交通枢纽店', 5)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_channel_group (code, label, sort_order) VALUES
('堂食', '堂食', 1),
('外卖', '外卖', 2),
('支付', '支付', 3)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_metric_direction (code, label) VALUES
('higher_better', '越高越好'),
('lower_better', '越低越好')
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_member_status (code, label, sort_order) VALUES
('活跃', '活跃', 1),
('沉睡', '沉睡(30天未消费)', 2),
('流失', '流失(90天未消费)', 3)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_waste_reason (code, label, sort_order) VALUES
('过期', '过期', 1),
('损坏', '损坏', 2),
('变质', '变质', 3),
('操作失误', '操作失误', 4),
('盘点差异', '盘点差异', 5)
ON CONFLICT (code) DO NOTHING;
INSERT INTO analytics.enum_management_quadrant (code, label, sort_order) VALUES
('明星', '明星门店', 1),
('金牛', '金牛门店', 2),
('问题', '问题门店', 3),
('瘦狗', '瘦狗门店', 4)
ON CONFLICT (code) DO NOTHING;
-- ============================================================
-- 七、初始化渠道主数据
-- ============================================================
INSERT INTO analytics.dim_channel (channel_code, channel_name, channel_group, channel_type, platform, is_delivery, sort_order, status) VALUES
('cash', '现金', '支付', '现金', NULL, false, 1, '启用'),
('alipay', '支付宝', '支付', '支付宝', '支付宝', false, 2, '启用'),
('wechat', '微信', '支付', '微信', '微信', false, 3, '启用'),
('unionpay', '银联', '支付', '银联', '银联', false, 4, '启用'),
('credit', '挂账', '支付', '挂账', NULL, false, 5, '启用'),
('meituan', '美团到店', '堂食', '美团到店', '美团', false, 6, '启用'),
('meituan_delivery', '美团外卖', '外卖', '美团外卖', '美团', true, 7, '启用'),
('taobao_delivery', '淘宝外卖', '外卖', '淘宝外卖', '淘宝', true, 8, '启用'),
('jd_delivery', '京东到家', '外卖', '京东到家', '京东', true, 9, '启用'),
('douyin', '抖音', '堂食', '抖音', '抖音', false, 10, '启用')
ON CONFLICT (channel_code) DO NOTHING;
-- ============================================================
-- 八、初始化日历主数据 (2026年全年)
-- ============================================================
INSERT INTO analytics.dim_calendar (date_value, year, month, day, weekday_no, weekday_name, is_weekend, is_holiday, holiday_name, is_operating_day)
SELECT
d::date,
EXTRACT(year FROM d)::int,
EXTRACT(month FROM d)::int,
EXTRACT(day FROM d)::int,
EXTRACT(dow FROM d)::int,
CASE EXTRACT(dow FROM d)::int
WHEN 0 THEN '周日' WHEN 1 THEN '周一' WHEN 2 THEN '周二'
WHEN 3 THEN '周三' WHEN 4 THEN '周四' WHEN 5 THEN '周五'
WHEN 6 THEN '周六'
END,
EXTRACT(dow FROM d)::int IN (0, 6),
false, NULL, true
FROM generate_series('2026-01-01'::date, '2026-12-31'::date, '1 day'::interval) AS d
ON CONFLICT (date_value) DO NOTHING;
-- 标记2026年主要节假日
UPDATE analytics.dim_calendar SET is_holiday = true, holiday_name = '元旦' WHERE date_value = '2026-01-01';
UPDATE analytics.dim_calendar SET is_holiday = true, holiday_name = '春节' WHERE date_value BETWEEN '2026-02-17' AND '2026-02-23';
UPDATE analytics.dim_calendar SET is_holiday = true, holiday_name = '清明节' WHERE date_value BETWEEN '2026-04-04' AND '2026-04-06';
UPDATE analytics.dim_calendar SET is_holiday = true, holiday_name = '劳动节' WHERE date_value BETWEEN '2026-05-01' AND '2026-05-05';
UPDATE analytics.dim_calendar SET is_holiday = true, holiday_name = '端午节' WHERE date_value BETWEEN '2026-06-19' AND '2026-06-21';
UPDATE analytics.dim_calendar SET is_holiday = true, holiday_name = '中秋节' WHERE date_value BETWEEN '2026-09-25' AND '2026-09-27';
UPDATE analytics.dim_calendar SET is_holiday = true, holiday_name = '国庆节' WHERE date_value BETWEEN '2026-10-01' AND '2026-10-07';
-- ============================================================
-- 九、初始化指标标准 (扩展现有 indicator_dictionary)
-- ============================================================
UPDATE analytics.indicator_dictionary SET
metric_code = 'received_total',
metric_category = '收入',
direction = 'higher_better',
unit = '',
granularity = '门店/区域/公司',
version = 'v1.0',
is_active = true
WHERE indicator_name = '实收' AND metric_code IS NULL;
UPDATE analytics.indicator_dictionary SET
metric_code = 'avg_bill_value',
metric_category = '收入',
direction = 'higher_better',
unit = '',
granularity = '门店/区域/公司',
version = 'v1.0',
is_active = true
WHERE indicator_name = '客单价' AND metric_code IS NULL;
UPDATE analytics.indicator_dictionary SET
metric_code = 'discount_rate',
metric_category = '优惠',
direction = 'lower_better',
unit = '%',
granularity = '门店/区域/公司',
version = 'v1.0',
is_active = true
WHERE indicator_name = '优惠率' AND metric_code IS NULL;
-- 补充缺失的核心指标 (upsert: 已有的更新metric_code等字段,新的插入)
INSERT INTO analytics.indicator_dictionary (indicator_name, business_definition, formula, data_source, update_frequency, owner, scope, yellow_threshold, red_threshold, version_date, metric_code, metric_category, direction, unit, granularity, version, is_active)
SELECT * FROM (VALUES
('账单数', '有效账单总数', 'count(*)', 'bill_fact', '', '运营部', '门店/区域/公司', NULL, NULL, '2026-04-01'::date, 'bill_count', '收入', 'higher_better', '', '门店/区域/公司', 'v1.0', true),
('理论成本率', '理论成本占实收比例', 'sum(theoretical_cost)/sum(received_total)*100', 'bill_fact', '', '财务部', '门店/区域/公司', 40, 45, '2026-04-01'::date, 'theoretical_cost_rate', '成本', 'lower_better', '%', '门店/区域/公司', 'v1.0', true),
('实际成本率', '盘点倒挤成本占实收比例', 'sum(consumption_amount)/sum(received_total)*100', 'inventory_cost_records', '', '财务部', '门店/区域/公司', 38, 42, '2026-04-01'::date, 'actual_cost_rate', '成本', 'lower_better', '%', '门店/区域/公司', 'v1.0', true),
('成本差异率', '实际成本率与理论成本率差异', 'actual_cost_rate - theoretical_cost_rate', '计算指标', '', '财务部', '门店/区域/公司', 3, 5, '2026-04-01'::date, 'cost_variance_rate', '成本', 'lower_better', '%', '门店/区域/公司', 'v1.0', true),
('平台合并加权成本率', '三平台(美团+淘宝+京东)加权成本率', '(sum(commission)+sum(discount))/sum(received)*100', 'v_store_platform_economics', '', '运营部', '门店/区域/公司', 25, 30, '2026-04-01'::date, 'platform_weighted_cost_rate', '优惠', 'lower_better', '%', '门店/区域/公司', 'v1.0', true),
('会员识别率', '会员账单占总账单比例', 'count(member_id IS NOT NULL)/count(*)*100', 'bill_fact', '', '会员部', '门店/区域/公司', 15, 10, '2026-04-01'::date, 'member_identify_rate', '会员', 'higher_better', '%', '门店/区域/公司', 'v1.0', true),
('30日复购率', '30天内二次消费会员占比', 'count(repeat_members)/count(identified_members)*100', 'v_store_repeat_summary_monthly', '', '会员部', '门店/区域/公司', 30, 20, '2026-04-01'::date, 'repeat_rate_30d', '会员', 'higher_better', '%', '门店/区域/公司', 'v1.0', true),
('异常账单率', '异常账单数占总账单比例', 'count(is_anomaly)/count(*)*100', 'bill_fact', '', '运营部', '门店/区域/公司', 0.1, 0.5, '2026-04-01'::date, 'anomaly_bill_rate', '风险', 'lower_better', '%', '门店/区域/公司', 'v1.0', true),
('零实收账单数', '实收为0的账单数量', 'count(received_total=0)', 'bill_fact', '', '运营部', '门店', 5, 10, '2026-04-01'::date, 'zero_received_count', '风险', 'lower_better', '', '门店', 'v1.0', true),
('库存天数', '期末库存可消耗天数', 'ending_amount/(consumption_amount/30)', 'inventory_cost_records', '', '供应链', '门店', 7, 14, '2026-04-01'::date, 'inventory_days', '库存', 'lower_better', '', '门店', 'v1.0', true),
('负耗用货品数', '消耗量为负的货品数量', 'count(consumption_quantity<0)', 'inventory_cost_records', '', '财务部', '门店', 0, 3, '2026-04-01'::date, 'negative_consumption_count', '库存', 'lower_better', '', '门店', 'v1.0', true),
('理论毛利率', '理论毛利占消费额比例', 'sum(theoretical_profit)/sum(consumption)*100', 'bill_fact', '', '财务部', '门店/区域/公司', 65, 60, '2026-04-01'::date, 'theoretical_margin_rate', '收入', 'higher_better', '%', '门店/区域/公司', 'v1.0', true),
('活动新客率', '活动期间新客占活动用户比例', 'count(is_new_customer)/count(*)*100', 'fact_promotion_usage', '', '会员部', '门店/活动', 30, 15, '2026-04-01'::date, 'campaign_new_customer_rate', '优惠', 'higher_better', '%', '门店/活动', 'v1.0', true),
('SKU有效数', '在售且月实收>1000的SKU数量', 'count(sku_code WHERE status=在售 AND received_amount>1000)', 'fact_bill_item JOIN dim_sku', '', '商品部', '公司/区域', 900, 700, '2026-04-01'::date, 'effective_sku_count', '商品', 'lower_better', '', '公司/区域', 'v1.0', true),
('长尾SKU占比', 'C类SKU数占总SKU数比例', 'count(abc_class=C)/count(*)*100', 'dim_sku', '', '商品部', '公司', 50, 60, '2026-04-01'::date, 'longtail_sku_ratio', '商品', 'lower_better', '%', '公司', 'v1.0', true)
) AS t(indicator_name, business_definition, formula, data_source, update_frequency, owner, scope, yellow_threshold, red_threshold, version_date, metric_code, metric_category, direction, unit, granularity, version, is_active)
ON CONFLICT (indicator_name) DO UPDATE SET
metric_code = EXCLUDED.metric_code,
metric_category = EXCLUDED.metric_category,
direction = EXCLUDED.direction,
unit = EXCLUDED.unit,
granularity = EXCLUDED.granularity,
version = EXCLUDED.version,
is_active = EXCLUDED.is_active,
business_definition = EXCLUDED.business_definition,
formula = EXCLUDED.formula,
yellow_threshold = EXCLUDED.yellow_threshold,
red_threshold = EXCLUDED.red_threshold;
-- ============================================================
-- 十、创建数据质量检查日志表
-- ============================================================
CREATE TABLE IF NOT EXISTS analytics.data_quality_log (
id BIGSERIAL PRIMARY KEY,
check_date DATE NOT NULL DEFAULT CURRENT_DATE,
check_type TEXT NOT NULL, -- bill/dish/inventory/mapping
total_records BIGINT,
issue_count INTEGER DEFAULT 0,
issue_details JSONB,
status TEXT DEFAULT '通过', -- 通过/警告/失败
checked_by TEXT DEFAULT 'system',
created_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 十一、创建口径变更日志表
-- ============================================================
CREATE TABLE IF NOT EXISTS analytics.metric_version_log (
id BIGSERIAL PRIMARY KEY,
metric_code TEXT NOT NULL,
version TEXT NOT NULL,
change_date DATE NOT NULL DEFAULT CURRENT_DATE,
change_reason TEXT NOT NULL,
old_formula TEXT,
new_formula TEXT,
impact_amount NUMERIC,
changed_by TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- ============================================================
-- 完成提示
-- ============================================================
DO $$
BEGIN
RAISE NOTICE '本体标准数据库表创建完成';
END $$;