-- ============================================================ -- 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 ( SELECT store_code, bill_no, closed_at, meal_period, guest_count, consumption, received_total, discount_total, member_id, member_level, marketing_plan, order_tag, theoretical_cost, theoretical_profit, theoretical_margin, meituan_delivery_received, taobao_delivery_received, jd_delivery_received FROM analytics.bill_fact WHERE closed_at >= analytics.fn_month_start_ts(p_month) AND closed_at < analytics.fn_next_month_start_ts(p_month) ) b ON b.store_code = d.store_code AND b.bill_no = d.bill_no WHERE d.dish_name IS NOT NULL; $$ 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;