-- ============================================================ -- Phase 8: mv_store_risk_rating 参数化函数 -- 替换普通表 mv_store_risk_rating(4月快照)为参数化函数 -- 依赖: bill_fact, v_anomaly_bills -- ============================================================ CREATE OR REPLACE FUNCTION analytics.fn_store_risk_rating(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, avg_guest_value numeric, discount_rate_pct numeric, theoretical_margin_pct numeric, member_bill_share_pct numeric, anomaly_rate_pct numeric, risk_level text, primary_issue text ) AS $$ WITH base AS ( SELECT b.store_code, b.store_name, count(*) AS bill_count, count(DISTINCT b.closed_at::date) AS active_days, sum(b.received_total) AS received, round(sum(b.received_total) / NULLIF(count(DISTINCT b.closed_at::date), 0), 2) AS avg_daily_received, round(sum(b.received_total) / count(*), 2) AS avg_bill_value, round(sum(b.received_total) / NULLIF(sum(b.guest_count), 0), 2) AS avg_guest_value, round(sum(b.discount_total) / NULLIF(sum(b.consumption), 0) * 100, 2) AS discount_rate_pct, round(sum(b.theoretical_profit) / NULLIF(sum(b.received_total), 0) * 100, 2) AS theoretical_margin_pct, round(count(*) FILTER (WHERE b.member_id IS NOT NULL)::numeric / count(*) * 100, 2) AS member_bill_share_pct 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, b.store_name ), anomaly AS ( SELECT a.store_code, round(count(*)::numeric / NULLIF(bc.bill_count, 0) * 100, 2) AS anomaly_rate_pct FROM analytics.v_anomaly_bills a JOIN (SELECT store_code, count(*) AS bill_count 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) GROUP BY store_code) bc ON a.store_code = bc.store_code WHERE a.closed_at >= analytics.fn_month_start_ts(p_month) AND a.closed_at < analytics.fn_next_month_start_ts(p_month) GROUP BY a.store_code, bc.bill_count ), combined AS ( SELECT b.*, COALESCE(an.anomaly_rate_pct, 0) AS anomaly_rate_pct, CASE WHEN b.received IS NULL OR b.received = 0 THEN '绿色' WHEN b.theoretical_margin_pct < 68 OR b.discount_rate_pct > 25 OR COALESCE(an.anomaly_rate_pct, 0) > 3 THEN '红色' WHEN b.theoretical_margin_pct >= 70 AND b.discount_rate_pct <= 22 AND COALESCE(an.anomaly_rate_pct, 0) <= 1.5 THEN '绿色' ELSE '黄色' END AS risk_level, CASE WHEN b.theoretical_margin_pct < 68 AND b.discount_rate_pct > 25 AND COALESCE(an.anomaly_rate_pct, 0) > 3 THEN '毛利率低;优惠率高;异常率高' WHEN b.theoretical_margin_pct < 68 AND b.discount_rate_pct > 25 THEN '毛利率低;优惠率高' WHEN b.theoretical_margin_pct < 68 AND COALESCE(an.anomaly_rate_pct, 0) > 3 THEN '毛利率低;异常率高' WHEN b.discount_rate_pct > 25 AND COALESCE(an.anomaly_rate_pct, 0) > 3 THEN '优惠率高;异常率高' WHEN b.theoretical_margin_pct < 68 THEN '毛利率低' WHEN b.discount_rate_pct > 25 THEN '优惠率高' WHEN COALESCE(an.anomaly_rate_pct, 0) > 3 THEN '异常率高' ELSE '' END AS primary_issue FROM base b LEFT JOIN anomaly an ON b.store_code = an.store_code ) SELECT store_code, store_name, bill_count, active_days, received, avg_daily_received, avg_bill_value, avg_guest_value, discount_rate_pct, theoretical_margin_pct, member_bill_share_pct, anomaly_rate_pct, risk_level, primary_issue FROM combined $$ LANGUAGE SQL STABLE;