Files
SBrainCO/db/migration_phase2_parameterized_functions.sql
T

1056 lines
39 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================
-- Phase 2 Migration: 参数化函数替换 _april 后缀视图
-- 核心利润链第1批:9个对象
--
-- 依赖链:
-- fn_inventory_cost_classified (base)
-- ├── fn_inventory_abnormal_items
-- ├── fn_inventory_finance_category
-- └── fn_store_theoretical_actual_cost (also uses bill_fact)
-- └── fn_store_deep_diagnosis (also uses dish_store_summary, benchmark_composite)
-- └── fn_store_action_priority_deep
-- └── fn_store_area_efficiency
--
-- fn_dish_sales (base, uses bill_fact + dish_sales_details)
-- └── fn_dish_basket
-- └── fn_dish_store_summary
--
-- fn_store_benchmark_composite (uses v_store_repeat_summary_monthly)
--
-- 向后兼容:保留原 _april 视图,新增函数可并行使用
-- ============================================================
-- 辅助函数:获取月份的起始时间戳(带时区,+08:00)
CREATE OR REPLACE FUNCTION analytics.fn_month_start_ts(p_month date)
RETURNS timestamptz AS $$
BEGIN
RETURN (p_month::text || ' 00:00:00+08')::timestamptz;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- 辅助函数:获取下月起始时间戳
CREATE OR REPLACE FUNCTION analytics.fn_next_month_start_ts(p_month date)
RETURNS timestamptz AS $$
BEGIN
RETURN ((p_month + INTERVAL '1 month')::date::text || ' 00:00:00+08')::timestamptz;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- ============================================================
-- 1. fn_inventory_cost_classified(p_month)
-- 替换 v_inventory_cost_classified_april
-- 依赖: v_inventory_cost_operating
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_inventory_cost_classified(p_month date)
RETURNS TABLE (
record_id bigint,
report_month date,
source_file text,
source_row integer,
brand text,
region text,
cost_unit_name text,
cost_unit_code text,
item_name text,
specification text,
major_category text,
minor_category text,
finance_category text,
item_code text,
unit text,
opening_quantity numeric,
opening_unit_cost numeric,
opening_amount numeric,
opening_tax numeric,
opening_tax_inclusive numeric,
purchase_quantity numeric,
purchase_unit_cost numeric,
purchase_amount numeric,
purchase_tax numeric,
purchase_tax_inclusive numeric,
ending_quantity numeric,
ending_unit_cost numeric,
ending_amount numeric,
ending_tax numeric,
ending_tax_inclusive numeric,
consumption_quantity numeric,
consumption_unit_cost numeric,
consumption_amount numeric,
consumption_tax numeric,
consumption_tax_inclusive numeric,
conversion_precision_amount numeric,
conversion_precision_tax_inclusive numeric,
return_loss_amount numeric,
return_loss_tax_inclusive numeric,
imported_at timestamptz,
calculated_consumption_amount numeric,
is_negative_consumption boolean,
sales_store_code text,
sales_store_name text,
unit_type text,
mapping_method text,
management_cost_scope text
) AS $$
SELECT
i.record_id,
i.report_month,
i.source_file,
i.source_row,
i.brand,
i.region,
i.cost_unit_name,
i.cost_unit_code,
i.item_name,
i.specification,
i.major_category,
i.minor_category,
i.finance_category,
i.item_code,
i.unit,
i.opening_quantity,
i.opening_unit_cost,
i.opening_amount,
i.opening_tax,
i.opening_tax_inclusive,
i.purchase_quantity,
i.purchase_unit_cost,
i.purchase_amount,
i.purchase_tax,
i.purchase_tax_inclusive,
i.ending_quantity,
i.ending_unit_cost,
i.ending_amount,
i.ending_tax,
i.ending_tax_inclusive,
i.consumption_quantity,
i.consumption_unit_cost,
i.consumption_amount,
i.consumption_tax,
i.consumption_tax_inclusive,
i.conversion_precision_amount,
i.conversion_precision_tax_inclusive,
i.return_loss_amount,
i.return_loss_tax_inclusive,
i.imported_at,
i.calculated_consumption_amount,
i.is_negative_consumption,
i.sales_store_code,
i.sales_store_name,
i.unit_type,
i.mapping_method,
CASE
WHEN i.major_category = ANY (ARRAY['原料类', '半成品类', '烟酒饮料类', '(新)原料类']) THEN '可比食材成本'
WHEN i.major_category = '包材类' OR i.finance_category = '食品包装' THEN '食品包装'
WHEN i.major_category = '易耗品类' THEN '易耗及营运物料'
WHEN i.major_category = ANY (ARRAY['维修配件类', '电子信息类']) THEN '维修及设备'
WHEN i.major_category = '新零售' THEN '新零售货品'
ELSE '其他成本'
END AS management_cost_scope
FROM analytics.v_inventory_cost_operating i
WHERE i.report_month = p_month;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 2. fn_store_theoretical_actual_cost(p_month)
-- 替换 v_store_theoretical_actual_cost_april
-- 依赖: bill_fact, fn_inventory_cost_classified
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_store_theoretical_actual_cost(p_month date)
RETURNS TABLE (
store_code text,
store_name text,
bill_count bigint,
sales_consumption numeric,
sales_received numeric,
theoretical_cost numeric,
theoretical_profit numeric,
actual_food_cost numeric,
actual_total_cost numeric,
packaging_cost numeric,
operating_supply_cost numeric,
repair_equipment_cost numeric,
new_retail_cost numeric,
ending_inventory_amount numeric,
negative_item_lines bigint,
negative_consumption_amount numeric,
food_cost_variance numeric,
theoretical_cost_rate_pct numeric,
actual_food_cost_rate_pct numeric,
variance_to_theoretical_pct numeric,
cost_rate_gap_pct numeric,
comparison_status text,
variance_level text
) AS $$
WITH theoretical AS (
SELECT
b.store_code,
min(b.store_name) AS store_name,
count(*) AS bill_count,
sum(b.consumption) AS sales_consumption,
sum(b.received_total) AS sales_received,
sum(b.theoretical_cost) AS theoretical_cost,
sum(b.theoretical_profit) AS theoretical_profit
FROM analytics.bill_fact b
WHERE b.closed_at >= analytics.fn_month_start_ts(p_month)
AND b.closed_at < analytics.fn_next_month_start_ts(p_month)
GROUP BY b.store_code
), actual AS (
SELECT
ic.sales_store_code AS store_code,
sum(ic.consumption_amount) AS actual_total_cost,
sum(ic.consumption_amount) FILTER (WHERE ic.management_cost_scope = '可比食材成本') AS actual_food_cost,
sum(ic.consumption_amount) FILTER (WHERE ic.management_cost_scope = '食品包装') AS packaging_cost,
sum(ic.consumption_amount) FILTER (WHERE ic.management_cost_scope = '易耗及营运物料') AS operating_supply_cost,
sum(ic.consumption_amount) FILTER (WHERE ic.management_cost_scope = '维修及设备') AS repair_equipment_cost,
sum(ic.consumption_amount) FILTER (WHERE ic.management_cost_scope = '新零售货品') AS new_retail_cost,
count(*) FILTER (WHERE ic.is_negative_consumption) AS negative_item_lines,
sum(ic.consumption_amount) FILTER (WHERE ic.is_negative_consumption) AS negative_consumption_amount,
sum(ic.ending_amount) AS ending_inventory_amount
FROM analytics.fn_inventory_cost_classified(p_month) ic
GROUP BY ic.sales_store_code
)
SELECT
t.store_code,
t.store_name,
t.bill_count,
t.sales_consumption,
t.sales_received,
t.theoretical_cost,
t.theoretical_profit,
COALESCE(a.actual_food_cost, 0) AS actual_food_cost,
COALESCE(a.actual_total_cost, 0) AS actual_total_cost,
COALESCE(a.packaging_cost, 0) AS packaging_cost,
COALESCE(a.operating_supply_cost, 0) AS operating_supply_cost,
COALESCE(a.repair_equipment_cost, 0) AS repair_equipment_cost,
COALESCE(a.new_retail_cost, 0) AS new_retail_cost,
COALESCE(a.ending_inventory_amount, 0) AS ending_inventory_amount,
COALESCE(a.negative_item_lines, 0) AS negative_item_lines,
COALESCE(a.negative_consumption_amount, 0) AS negative_consumption_amount,
COALESCE(a.actual_food_cost, 0) - t.theoretical_cost AS food_cost_variance,
round(t.theoretical_cost / NULLIF(t.sales_consumption, 0) * 100, 2) AS theoretical_cost_rate_pct,
round(COALESCE(a.actual_food_cost, 0) / NULLIF(t.sales_consumption, 0) * 100, 2) AS actual_food_cost_rate_pct,
round((COALESCE(a.actual_food_cost, 0) - t.theoretical_cost) / NULLIF(t.theoretical_cost, 0) * 100, 2) AS variance_to_theoretical_pct,
round((COALESCE(a.actual_food_cost, 0) - t.theoretical_cost) / NULLIF(t.sales_consumption, 0) * 100, 2) AS cost_rate_gap_pct,
CASE
WHEN t.sales_consumption > 0 AND (t.theoretical_cost / t.sales_consumption) >= 0.10 THEN '可比'
ELSE '理论成本口径异常'
END AS comparison_status,
CASE
WHEN t.sales_consumption = 0 OR (t.theoretical_cost / t.sales_consumption) < 0.10 THEN '灰色-口径异常'
WHEN ((COALESCE(a.actual_food_cost, 0) - t.theoretical_cost) / t.theoretical_cost) >= 0.20 THEN '红色-严重超耗'
WHEN ((COALESCE(a.actual_food_cost, 0) - t.theoretical_cost) / t.theoretical_cost) >= 0.10 THEN '橙色-明显超耗'
WHEN ((COALESCE(a.actual_food_cost, 0) - t.theoretical_cost) / t.theoretical_cost) <= -0.20 THEN '蓝色-倒挤偏低'
WHEN ((COALESCE(a.actual_food_cost, 0) - t.theoretical_cost) / t.theoretical_cost) <= -0.10 THEN '黄色-低于理论'
ELSE '绿色-基本正常'
END AS variance_level
FROM theoretical t
JOIN actual a USING (store_code);
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 3. fn_inventory_abnormal_items(p_month)
-- 替换 v_inventory_abnormal_items_april
-- 依赖: fn_inventory_cost_classified
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_inventory_abnormal_items(p_month date)
RETURNS TABLE (
record_id bigint,
report_month date,
source_file text,
source_row integer,
brand text,
region text,
cost_unit_name text,
cost_unit_code text,
item_name text,
specification text,
major_category text,
minor_category text,
finance_category text,
item_code text,
unit text,
opening_quantity numeric,
opening_unit_cost numeric,
opening_amount numeric,
opening_tax numeric,
opening_tax_inclusive numeric,
purchase_quantity numeric,
purchase_unit_cost numeric,
purchase_amount numeric,
purchase_tax numeric,
purchase_tax_inclusive numeric,
ending_quantity numeric,
ending_unit_cost numeric,
ending_amount numeric,
ending_tax numeric,
ending_tax_inclusive numeric,
consumption_quantity numeric,
consumption_unit_cost numeric,
consumption_amount numeric,
consumption_tax numeric,
consumption_tax_inclusive numeric,
conversion_precision_amount numeric,
conversion_precision_tax_inclusive numeric,
return_loss_amount numeric,
return_loss_tax_inclusive numeric,
imported_at timestamptz,
calculated_consumption_amount numeric,
is_negative_consumption boolean,
sales_store_code text,
sales_store_name text,
unit_type text,
mapping_method text,
management_cost_scope text,
observed_stores bigint,
median_unit_cost double precision,
median_abs_quantity double precision,
abnormal_reason text,
unit_cost_vs_median numeric,
quantity_vs_median numeric
) AS $$
WITH stats AS (
SELECT
ic.item_code,
count(DISTINCT ic.sales_store_code) AS observed_stores,
percentile_cont(0.5) WITHIN GROUP (ORDER BY (ic.consumption_unit_cost::double precision))
FILTER (WHERE ic.consumption_unit_cost > 0) AS median_unit_cost,
percentile_cont(0.5) WITHIN GROUP (ORDER BY (abs(ic.consumption_quantity)::double precision))
FILTER (WHERE ic.consumption_quantity <> 0) AS median_abs_quantity
FROM analytics.fn_inventory_cost_classified(p_month) ic
GROUP BY ic.item_code
), flagged AS (
SELECT
ic.*,
s.observed_stores,
s.median_unit_cost,
s.median_abs_quantity,
CASE
WHEN ic.consumption_amount < 0 THEN '负倒挤成本'
WHEN ic.consumption_quantity < 0 THEN '负倒挤数量'
WHEN s.observed_stores >= 5 AND s.median_unit_cost > 0 AND ic.consumption_unit_cost::double precision > (s.median_unit_cost * 1.5) THEN '成本单价显著偏高'
WHEN s.observed_stores >= 5 AND s.median_unit_cost > 0 AND ic.consumption_unit_cost > 0 AND ic.consumption_unit_cost::double precision < (s.median_unit_cost * 0.5) THEN '成本单价显著偏低'
WHEN s.observed_stores >= 5 AND s.median_abs_quantity > 0 AND ic.consumption_amount > 1000 AND abs(ic.consumption_quantity)::double precision > (s.median_abs_quantity * 3) THEN '耗用数量显著偏高'
ELSE NULL
END AS abnormal_reason
FROM analytics.fn_inventory_cost_classified(p_month) ic
JOIN stats s USING (item_code)
)
SELECT
f.record_id, f.report_month, f.source_file, f.source_row,
f.brand, f.region, f.cost_unit_name, f.cost_unit_code,
f.item_name, f.specification, f.major_category, f.minor_category,
f.finance_category, f.item_code, f.unit,
f.opening_quantity, f.opening_unit_cost, f.opening_amount,
f.opening_tax, f.opening_tax_inclusive,
f.purchase_quantity, f.purchase_unit_cost, f.purchase_amount,
f.purchase_tax, f.purchase_tax_inclusive,
f.ending_quantity, f.ending_unit_cost, f.ending_amount,
f.ending_tax, f.ending_tax_inclusive,
f.consumption_quantity, f.consumption_unit_cost, f.consumption_amount,
f.consumption_tax, f.consumption_tax_inclusive,
f.conversion_precision_amount, f.conversion_precision_tax_inclusive,
f.return_loss_amount, f.return_loss_tax_inclusive,
f.imported_at, f.calculated_consumption_amount, f.is_negative_consumption,
f.sales_store_code, f.sales_store_name, f.unit_type, f.mapping_method,
f.management_cost_scope,
f.observed_stores, f.median_unit_cost, f.median_abs_quantity,
f.abnormal_reason,
round((f.consumption_unit_cost::double precision / NULLIF(f.median_unit_cost, 0))::numeric, 2) AS unit_cost_vs_median,
round((abs(f.consumption_quantity)::double precision / NULLIF(f.median_abs_quantity, 0))::numeric, 2) AS quantity_vs_median
FROM flagged f
WHERE f.abnormal_reason IS NOT NULL;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 4. fn_inventory_finance_category(p_month)
-- 替换 v_inventory_finance_category_april
-- 依赖: fn_inventory_cost_classified
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_inventory_finance_category(p_month date)
RETURNS TABLE (
management_cost_scope text,
finance_category text,
detail_lines bigint,
store_count bigint,
item_count bigint,
consumption_quantity numeric,
consumption_amount numeric,
all_cost_share_pct numeric,
negative_lines bigint,
negative_amount numeric,
ending_inventory_amount numeric
) AS $$
SELECT
ic.management_cost_scope,
ic.finance_category,
count(*) AS detail_lines,
count(DISTINCT ic.sales_store_code) AS store_count,
count(DISTINCT ic.item_code) AS item_count,
sum(ic.consumption_quantity) AS consumption_quantity,
sum(ic.consumption_amount) AS consumption_amount,
round(sum(ic.consumption_amount) / NULLIF(sum(sum(ic.consumption_amount)) OVER (), 0) * 100, 2) AS all_cost_share_pct,
count(*) FILTER (WHERE ic.is_negative_consumption) AS negative_lines,
sum(ic.consumption_amount) FILTER (WHERE ic.is_negative_consumption) AS negative_amount,
sum(ic.ending_amount) AS ending_inventory_amount
FROM analytics.fn_inventory_cost_classified(p_month) ic
GROUP BY ic.management_cost_scope, ic.finance_category;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 5. fn_dish_sales(p_month)
-- 替换 dish_sales_april
-- 依赖: dish_sales_details (public), bill_fact
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_dish_sales(p_month date)
RETURNS TABLE (
detail_id bigint,
source_file text,
source_row integer,
store_code text,
store_name text,
bill_no text,
business_date date,
closed_at timestamptz,
ordered_at timestamp without time zone,
meal_period text,
business_type text,
dish_name text,
category_level1 text,
category_level2 text,
item_type text,
production_department text,
unit text,
preparation_method text,
sales_quantity numeric,
unit_price numeric,
gross_amount numeric,
received_amount numeric,
dish_discount_amount numeric,
bill_guest_count integer,
bill_consumption numeric,
bill_received_total numeric,
bill_discount_total numeric,
member_id text,
member_level text,
marketing_plan text,
order_tag text,
bill_theoretical_cost numeric,
bill_theoretical_profit numeric,
bill_theoretical_margin numeric,
meituan_delivery_received numeric,
taobao_delivery_received numeric,
jd_delivery_received numeric
) AS $$
SELECT
d.detail_id,
d.source_file,
d.source_row,
d.store_code,
d.store_name,
d.bill_no,
b.closed_at::date AS business_date,
b.closed_at,
d.ordered_at,
COALESCE(NULLIF(b.meal_period, ''), d.business_type) AS meal_period,
d.business_type,
d.dish_name,
d.category_level1,
d.category_level2,
d.item_type,
d.production_department,
d.unit,
d.preparation_method,
d.sales_quantity,
d.unit_price,
d.gross_amount,
d.received_amount,
d.gross_amount - d.received_amount AS dish_discount_amount,
b.guest_count AS bill_guest_count,
b.consumption AS bill_consumption,
b.received_total AS bill_received_total,
b.discount_total AS bill_discount_total,
b.member_id,
b.member_level,
b.marketing_plan,
b.order_tag,
b.theoretical_cost AS bill_theoretical_cost,
b.theoretical_profit AS bill_theoretical_profit,
b.theoretical_margin AS bill_theoretical_margin,
b.meituan_delivery_received,
b.taobao_delivery_received,
b.jd_delivery_received
FROM public.dish_sales_details d
JOIN analytics.bill_fact b ON b.store_code = d.store_code AND b.bill_no = d.bill_no
WHERE b.closed_at >= analytics.fn_month_start_ts(p_month)
AND b.closed_at < analytics.fn_next_month_start_ts(p_month);
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 6. fn_dish_basket(p_month)
-- 替换 dish_basket_april
-- 依赖: fn_dish_sales
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_dish_basket(p_month date)
RETURNS TABLE (
store_code text,
store_name text,
bill_no text,
business_date date,
closed_at timestamptz,
meal_period text,
member_id text,
member_level text,
guest_count integer,
bill_consumption numeric,
bill_received_total numeric,
bill_discount_total numeric,
sku_count bigint,
item_quantity numeric,
dish_gross_amount numeric,
dish_received_amount numeric,
has_noodle boolean,
has_snack boolean,
has_drink boolean,
has_cold_dish boolean,
has_combo boolean,
is_delivery boolean
) AS $$
SELECT
ds.store_code,
min(ds.store_name) AS store_name,
ds.bill_no,
min(ds.business_date) AS business_date,
min(ds.closed_at) AS closed_at,
min(ds.meal_period) AS meal_period,
min(ds.member_id) AS member_id,
min(ds.member_level) AS member_level,
max(ds.bill_guest_count) AS guest_count,
max(ds.bill_consumption) AS bill_consumption,
max(ds.bill_received_total) AS bill_received_total,
max(ds.bill_discount_total) AS bill_discount_total,
count(DISTINCT ds.dish_name) AS sku_count,
sum(ds.sales_quantity) AS item_quantity,
sum(ds.gross_amount) AS dish_gross_amount,
sum(ds.received_amount) AS dish_received_amount,
bool_or(ds.category_level1 = '兰州牛肉面') AS has_noodle,
bool_or(ds.category_level1 = '特色小吃') AS has_snack,
bool_or(ds.category_level1 = ANY (ARRAY['酒水饮料', '丝路茶饮'])) AS has_drink,
bool_or(ds.category_level1 = '爽口凉菜') AS has_cold_dish,
bool_or(ds.item_type ~~ '%套餐%' OR (ds.category_level1 = ANY (ARRAY['堂食套餐', '外卖套餐']))) AS has_combo,
bool_or(ds.business_type ~~ '%外卖%' OR COALESCE(ds.order_tag, '') ~~ '%外卖%' OR ds.meituan_delivery_received <> 0 OR ds.taobao_delivery_received <> 0 OR ds.jd_delivery_received <> 0) AS is_delivery
FROM analytics.fn_dish_sales(p_month) ds
GROUP BY ds.store_code, ds.bill_no;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 7. fn_dish_store_summary(p_month)
-- 替换 dish_store_summary_april
-- 依赖: fn_dish_basket
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_dish_store_summary(p_month date)
RETURNS TABLE (
store_code text,
store_name text,
bill_count bigint,
active_days bigint,
received_amount numeric,
avg_bill_value numeric,
items_per_bill numeric,
skus_per_bill numeric,
avg_guest_value numeric,
bill_discount_rate_pct numeric,
member_bill_share_pct numeric,
delivery_bill_share_pct numeric,
noodle_snack_attach_pct numeric,
noodle_drink_attach_pct numeric,
noodle_cold_attach_pct numeric,
combo_bill_share_pct numeric
) AS $$
SELECT
db.store_code,
min(db.store_name) AS store_name,
count(*) AS bill_count,
count(DISTINCT db.business_date) AS active_days,
sum(db.bill_received_total) AS received_amount,
round(avg(db.bill_received_total), 2) AS avg_bill_value,
round(sum(db.item_quantity) / NULLIF(count(*), 0), 2) AS items_per_bill,
round(avg(db.sku_count), 2) AS skus_per_bill,
round(sum(db.bill_received_total) / NULLIF(sum(db.guest_count), 0), 2) AS avg_guest_value,
round(sum(db.bill_discount_total) / NULLIF(sum(db.bill_consumption), 0) * 100, 2) AS bill_discount_rate_pct,
round(count(*) FILTER (WHERE db.member_id IS NOT NULL)::numeric / NULLIF(count(*), 0) * 100, 2) AS member_bill_share_pct,
round(count(*) FILTER (WHERE db.is_delivery)::numeric / NULLIF(count(*), 0) * 100, 2) AS delivery_bill_share_pct,
round(count(*) FILTER (WHERE db.has_noodle AND db.has_snack)::numeric / NULLIF(count(*) FILTER (WHERE db.has_noodle), 0) * 100, 2) AS noodle_snack_attach_pct,
round(count(*) FILTER (WHERE db.has_noodle AND db.has_drink)::numeric / NULLIF(count(*) FILTER (WHERE db.has_noodle), 0) * 100, 2) AS noodle_drink_attach_pct,
round(count(*) FILTER (WHERE db.has_noodle AND db.has_cold_dish)::numeric / NULLIF(count(*) FILTER (WHERE db.has_noodle), 0) * 100, 2) AS noodle_cold_attach_pct,
round(count(*) FILTER (WHERE db.has_combo)::numeric / NULLIF(count(*), 0) * 100, 2) AS combo_bill_share_pct
FROM analytics.fn_dish_basket(p_month) db
GROUP BY db.store_code;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 8. fn_store_benchmark_composite(p_month)
-- 替换 v_store_benchmark_composite(硬编码 '2026-04-01'
-- 依赖: v_store_benchmark, v_store_repeat_summary_monthly, fn_store_platform_economics
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_store_benchmark_composite(p_month date)
RETURNS TABLE (
store_code text,
store_name text,
received numeric,
avg_daily_received numeric,
avg_bill_value numeric,
discount_rate_pct numeric,
theoretical_margin_pct numeric,
anomaly_rate_pct numeric,
member_bill_share_pct numeric,
identified_members bigint,
repeat_rate_pct numeric,
avg_orders numeric,
repeat_revenue_share_pct numeric,
meituan_cost_rate_pct numeric,
taobao_cost_rate_pct numeric,
jd_cost_rate_pct numeric,
revenue_score double precision,
margin_score double precision,
discount_score double precision,
anomaly_score double precision,
repeat_score double precision,
benchmark_score numeric
) AS $$
WITH eligible AS (
SELECT
b.store_code,
b.store_name,
b.received,
b.avg_daily_received,
b.avg_bill_value,
b.discount_rate_pct,
b.theoretical_margin_pct,
b.anomaly_rate_pct,
b.member_bill_share_pct,
r.identified_members,
r.repeat_rate_pct,
r.avg_orders,
r.repeat_revenue_share_pct,
p.meituan_cost_rate_pct,
p.taobao_cost_rate_pct,
p.jd_cost_rate_pct
FROM analytics.v_store_benchmark b
JOIN analytics.v_store_repeat_summary_monthly r ON r.store_code = b.store_code AND r.month_start = p_month
LEFT JOIN analytics.fn_store_platform_economics(p_month) p ON p.store_code = b.store_code
WHERE b.theoretical_margin_pct >= 60 AND b.theoretical_margin_pct <= 80 AND r.identified_members >= 500
), scored AS (
SELECT
e.*,
percent_rank() OVER (ORDER BY e.avg_daily_received) AS revenue_score,
percent_rank() OVER (ORDER BY e.theoretical_margin_pct) AS margin_score,
1 - percent_rank() OVER (ORDER BY e.discount_rate_pct) AS discount_score,
1 - percent_rank() OVER (ORDER BY e.anomaly_rate_pct) AS anomaly_score,
percent_rank() OVER (ORDER BY e.repeat_rate_pct) AS repeat_score
FROM eligible e
)
SELECT
s.store_code,
s.store_name,
s.received,
s.avg_daily_received,
s.avg_bill_value,
s.discount_rate_pct,
s.theoretical_margin_pct,
s.anomaly_rate_pct,
s.member_bill_share_pct,
s.identified_members,
s.repeat_rate_pct,
s.avg_orders,
s.repeat_revenue_share_pct,
s.meituan_cost_rate_pct,
s.taobao_cost_rate_pct,
s.jd_cost_rate_pct,
s.revenue_score,
s.margin_score,
s.discount_score,
s.anomaly_score,
s.repeat_score,
round(((s.revenue_score * 0.25 + s.margin_score * 0.25 + s.discount_score * 0.20 + s.anomaly_score * 0.15 + s.repeat_score * 0.15) * 100)::numeric, 2) AS benchmark_score
FROM scored s;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 9. fn_store_deep_diagnosis(p_month)
-- 替换 v_store_deep_diagnosis_april
-- 依赖: v_store_scorecard, fn_dish_store_summary, fn_store_theoretical_actual_cost,
-- fn_store_benchmark_composite, fn_store_platform_economics
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_store_deep_diagnosis(p_month date)
RETURNS TABLE (
store_code text,
store_name text,
bill_count bigint,
active_days bigint,
received numeric,
avg_daily_received numeric,
avg_bill_value numeric,
discount_rate_pct numeric,
theoretical_margin_pct numeric,
member_bill_share_pct numeric,
items_per_bill numeric,
skus_per_bill numeric,
delivery_bill_share_pct numeric,
noodle_snack_attach_pct numeric,
noodle_drink_attach_pct numeric,
noodle_cold_attach_pct numeric,
combo_bill_share_pct numeric,
theoretical_cost numeric,
actual_food_cost numeric,
food_cost_variance numeric,
theoretical_cost_rate_pct numeric,
actual_food_cost_rate_pct numeric,
variance_to_theoretical_pct numeric,
comparison_status text,
variance_level text,
identified_members bigint,
repeat_rate_pct numeric,
repeat_revenue_share_pct numeric,
benchmark_score numeric,
meituan_received numeric,
taobao_received numeric,
jd_received numeric,
combined_platform_cost_rate_pct numeric,
business_type text,
scale_tier text,
problem_count integer,
problem_combination text
) AS $$
WITH store_base AS (
SELECT
s.store_code,
s.store_name,
s.bill_count,
s.active_days,
s.received,
s.avg_daily_received,
s.avg_bill_value,
s.discount_rate_pct,
s.theoretical_margin_pct,
s.member_bill_share_pct,
d.items_per_bill,
d.skus_per_bill,
d.delivery_bill_share_pct,
d.noodle_snack_attach_pct,
d.noodle_drink_attach_pct,
d.noodle_cold_attach_pct,
d.combo_bill_share_pct,
c.theoretical_cost,
c.actual_food_cost,
c.food_cost_variance,
c.theoretical_cost_rate_pct,
c.actual_food_cost_rate_pct,
c.variance_to_theoretical_pct,
c.comparison_status,
c.variance_level,
b.identified_members,
b.repeat_rate_pct,
b.repeat_revenue_share_pct,
b.benchmark_score,
p.meituan_received,
p.taobao_received,
p.jd_received,
round((COALESCE(p.meituan_discount, 0) + COALESCE(p.taobao_discount, 0) + COALESCE(p.jd_discount, 0) +
COALESCE(p.meituan_commission, 0) + COALESCE(p.taobao_commission, 0) + COALESCE(p.jd_commission, 0)) /
NULLIF(COALESCE(p.meituan_received, 0) + COALESCE(p.taobao_received, 0) + COALESCE(p.jd_received, 0) +
COALESCE(p.meituan_discount, 0) + COALESCE(p.taobao_discount, 0) + COALESCE(p.jd_discount, 0) +
COALESCE(p.meituan_commission, 0) + COALESCE(p.taobao_commission, 0) + COALESCE(p.jd_commission, 0), 0) * 100, 2) AS combined_platform_cost_rate_pct,
CASE
WHEN s.store_name ~ '机场|火锅|商城|快手|哈马尔罕' THEN '特殊业态'
ELSE '标准门店'
END AS business_type
FROM analytics.v_store_scorecard s
LEFT JOIN analytics.fn_dish_store_summary(p_month) d USING (store_code)
LEFT JOIN analytics.fn_store_theoretical_actual_cost(p_month) c USING (store_code)
LEFT JOIN analytics.fn_store_benchmark_composite(p_month) b USING (store_code)
LEFT JOIN analytics.fn_store_platform_economics(p_month) p USING (store_code)
), tiered AS (
SELECT
sb.*,
CASE
WHEN sb.business_type = '特殊业态' THEN '特殊业态'
WHEN percent_rank() OVER (PARTITION BY sb.business_type ORDER BY sb.received) >= 0.67 THEN '高规模'
WHEN percent_rank() OVER (PARTITION BY sb.business_type ORDER BY sb.received) >= 0.33 THEN '中规模'
ELSE '低规模'
END AS scale_tier
FROM store_base sb
)
SELECT
t.store_code,
t.store_name,
t.bill_count,
t.active_days,
t.received,
t.avg_daily_received,
t.avg_bill_value,
t.discount_rate_pct,
t.theoretical_margin_pct,
t.member_bill_share_pct,
t.items_per_bill,
t.skus_per_bill,
t.delivery_bill_share_pct,
t.noodle_snack_attach_pct,
t.noodle_drink_attach_pct,
t.noodle_cold_attach_pct,
t.combo_bill_share_pct,
t.theoretical_cost,
t.actual_food_cost,
t.food_cost_variance,
t.theoretical_cost_rate_pct,
t.actual_food_cost_rate_pct,
t.variance_to_theoretical_pct,
t.comparison_status,
t.variance_level,
t.identified_members,
t.repeat_rate_pct,
t.repeat_revenue_share_pct,
t.benchmark_score,
t.meituan_received,
t.taobao_received,
t.jd_received,
t.combined_platform_cost_rate_pct,
t.business_type,
t.scale_tier,
(CASE WHEN t.comparison_status = '可比' AND t.variance_to_theoretical_pct >= 20 THEN 1 ELSE 0 END +
CASE WHEN t.discount_rate_pct >= 23.02 THEN 1 ELSE 0 END +
CASE WHEN t.theoretical_margin_pct < 70 THEN 1 ELSE 0 END +
CASE WHEN t.identified_members >= 500 AND t.repeat_rate_pct < 30 THEN 1 ELSE 0 END +
CASE WHEN t.combined_platform_cost_rate_pct >= 40 THEN 1 ELSE 0 END +
CASE WHEN t.noodle_drink_attach_pct < 12 THEN 1 ELSE 0 END) AS problem_count,
concat_ws('',
CASE WHEN t.comparison_status = '理论成本口径异常' THEN '成本口径异常' ELSE NULL END,
CASE WHEN t.comparison_status = '可比' AND t.variance_to_theoretical_pct >= 20 THEN '实际成本严重超耗' ELSE NULL END,
CASE WHEN t.discount_rate_pct >= 23.02 THEN '优惠偏高' ELSE NULL END,
CASE WHEN t.theoretical_margin_pct < 70 THEN '理论毛利偏低' ELSE NULL END,
CASE WHEN t.identified_members >= 500 AND t.repeat_rate_pct < 30 THEN '会员复购偏低' ELSE NULL END,
CASE WHEN t.combined_platform_cost_rate_pct >= 40 THEN '平台成本偏高' ELSE NULL END,
CASE WHEN t.noodle_drink_attach_pct < 12 THEN '饮品搭售偏低' ELSE NULL END
) AS problem_combination
FROM tiered t;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 10. fn_store_action_priority_deep(p_month)
-- 替换 v_store_action_priority_deep_april
-- 依赖: fn_store_deep_diagnosis
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_store_action_priority_deep(p_month date)
RETURNS TABLE (
store_code text,
store_name text,
bill_count bigint,
active_days bigint,
received numeric,
avg_daily_received numeric,
avg_bill_value numeric,
discount_rate_pct numeric,
theoretical_margin_pct numeric,
member_bill_share_pct numeric,
items_per_bill numeric,
skus_per_bill numeric,
delivery_bill_share_pct numeric,
noodle_snack_attach_pct numeric,
noodle_drink_attach_pct numeric,
noodle_cold_attach_pct numeric,
combo_bill_share_pct numeric,
theoretical_cost numeric,
actual_food_cost numeric,
food_cost_variance numeric,
theoretical_cost_rate_pct numeric,
actual_food_cost_rate_pct numeric,
variance_to_theoretical_pct numeric,
comparison_status text,
variance_level text,
identified_members bigint,
repeat_rate_pct numeric,
repeat_revenue_share_pct numeric,
benchmark_score numeric,
meituan_received numeric,
taobao_received numeric,
jd_received numeric,
combined_platform_cost_rate_pct numeric,
business_type text,
scale_tier text,
problem_count integer,
problem_combination text,
action_priority text
) AS $$
SELECT
d.store_code,
d.store_name,
d.bill_count,
d.active_days,
d.received,
d.avg_daily_received,
d.avg_bill_value,
d.discount_rate_pct,
d.theoretical_margin_pct,
d.member_bill_share_pct,
d.items_per_bill,
d.skus_per_bill,
d.delivery_bill_share_pct,
d.noodle_snack_attach_pct,
d.noodle_drink_attach_pct,
d.noodle_cold_attach_pct,
d.combo_bill_share_pct,
d.theoretical_cost,
d.actual_food_cost,
d.food_cost_variance,
d.theoretical_cost_rate_pct,
d.actual_food_cost_rate_pct,
d.variance_to_theoretical_pct,
d.comparison_status,
d.variance_level,
d.identified_members,
d.repeat_rate_pct,
d.repeat_revenue_share_pct,
d.benchmark_score,
d.meituan_received,
d.taobao_received,
d.jd_received,
d.combined_platform_cost_rate_pct,
d.business_type,
d.scale_tier,
d.problem_count,
d.problem_combination,
CASE
WHEN d.business_type = '特殊业态' THEN '特殊业态'
WHEN d.problem_count >= 4 OR (d.comparison_status = '理论成本口径异常' AND d.problem_count >= 2) THEN 'P0-综合专项整改'
WHEN d.comparison_status = '理论成本口径异常' THEN 'P0-修复数据口径'
WHEN d.problem_count >= 2 THEN 'P1-重点整改'
WHEN d.problem_count = 1 THEN 'P2-单项改善'
WHEN d.benchmark_score >= 70 THEN '标杆候选'
ELSE '持续跟踪'
END AS action_priority
FROM analytics.fn_store_deep_diagnosis(p_month) d;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 11. fn_store_area_efficiency(p_month)
-- 替换 v_store_area_efficiency_april
-- 依赖: fn_store_action_priority_deep, v_store_location_operating
-- ============================================================
CREATE OR REPLACE FUNCTION analytics.fn_store_area_efficiency(p_month date)
RETURNS TABLE (
store_code text,
store_name text,
bill_count bigint,
active_days bigint,
received numeric,
avg_daily_received numeric,
avg_bill_value numeric,
discount_rate_pct numeric,
theoretical_margin_pct numeric,
member_bill_share_pct numeric,
items_per_bill numeric,
skus_per_bill numeric,
delivery_bill_share_pct numeric,
noodle_snack_attach_pct numeric,
noodle_drink_attach_pct numeric,
noodle_cold_attach_pct numeric,
combo_bill_share_pct numeric,
theoretical_cost numeric,
actual_food_cost numeric,
food_cost_variance numeric,
theoretical_cost_rate_pct numeric,
actual_food_cost_rate_pct numeric,
variance_to_theoretical_pct numeric,
comparison_status text,
variance_level text,
identified_members bigint,
repeat_rate_pct numeric,
repeat_revenue_share_pct numeric,
benchmark_score numeric,
meituan_received numeric,
taobao_received numeric,
jd_received numeric,
combined_platform_cost_rate_pct numeric,
business_type text,
scale_tier text,
problem_count integer,
problem_combination text,
action_priority text,
area_sqm numeric,
business_address text,
city text,
district text,
latitude_gcj02 double precision,
longitude_gcj02 double precision,
open_date date,
lease_expiry_date date,
store_age_years numeric,
monthly_received_per_sqm numeric,
daily_received_per_sqm numeric,
estimated_inventory_days numeric
) AS $$
SELECT
d.store_code,
d.store_name,
d.bill_count,
d.active_days,
d.received,
d.avg_daily_received,
d.avg_bill_value,
d.discount_rate_pct,
d.theoretical_margin_pct,
d.member_bill_share_pct,
d.items_per_bill,
d.skus_per_bill,
d.delivery_bill_share_pct,
d.noodle_snack_attach_pct,
d.noodle_drink_attach_pct,
d.noodle_cold_attach_pct,
d.combo_bill_share_pct,
d.theoretical_cost,
d.actual_food_cost,
d.food_cost_variance,
d.theoretical_cost_rate_pct,
d.actual_food_cost_rate_pct,
d.variance_to_theoretical_pct,
d.comparison_status,
d.variance_level,
d.identified_members,
d.repeat_rate_pct,
d.repeat_revenue_share_pct,
d.benchmark_score,
d.meituan_received,
d.taobao_received,
d.jd_received,
d.combined_platform_cost_rate_pct,
d.business_type,
d.scale_tier,
d.problem_count,
d.problem_combination,
d.action_priority,
loc.area_sqm,
loc.business_address,
loc.city,
loc.district,
loc.latitude_gcj02,
loc.longitude_gcj02,
loc.opened_date AS open_date,
loc.lease_expiry_date,
round(EXTRACT(year FROM age(p_month::timestamp with time zone, loc.opened_date::timestamp with time zone)) +
EXTRACT(day FROM age(p_month::timestamp with time zone, loc.opened_date::timestamp with time zone)) / 365.25, 2) AS store_age_years,
round(d.received / NULLIF(loc.area_sqm, 0), 2) AS monthly_received_per_sqm,
round(d.avg_daily_received / NULLIF(loc.area_sqm, 0), 2) AS daily_received_per_sqm,
NULL::numeric AS estimated_inventory_days
FROM analytics.fn_store_action_priority_deep(p_month) d
LEFT JOIN analytics.v_store_location_operating loc ON loc.sales_store_code = d.store_code;
$$ LANGUAGE sql STABLE;
-- ============================================================
-- 验证:对比函数结果与原视图(以2026-04为例)
-- ============================================================
-- SELECT count(*) FROM analytics.fn_inventory_cost_classified('2026-04-01');
-- SELECT count(*) FROM analytics.v_inventory_cost_classified_april;
-- SELECT count(*) FROM analytics.fn_store_theoretical_actual_cost('2026-04-01');
-- SELECT count(*) FROM analytics.v_store_theoretical_actual_cost_april;
-- SELECT count(*) FROM analytics.fn_store_action_priority_deep('2026-04-01');
-- SELECT count(*) FROM analytics.v_store_action_priority_deep_april;