feat: 新增门店选址分析功能

- 创建选址分析7个SQL视图+5张物化表(38s→33ms)
- 后端新增5个选址API端点
- 前端新增SiteSelectionPage页面(散点图+4Tab)
- 侧边栏新增门店选址导航入口
- 修复SQL列引用错误(d.→a.,去掉重复列)
- 创建v_store_action_priority_deep_april和v_store_area_efficiency_april基础视图
This commit is contained in:
freedakgmail
2026-07-27 08:33:42 +08:00
parent 521ba88937
commit 6e0c1bd7a4
18 changed files with 1892 additions and 30 deletions
+492
View File
@@ -0,0 +1,492 @@
-- ============================================================
-- 10_materialize_slow_views.sql
-- 将门店详情页慢视图转为物化视图,加 store_code 索引
-- 预期:/stores/:code 从 ~15s 降至 <100ms
-- ============================================================
-- ============================================================
-- Step 1: 依赖视图按逆序 DROP
-- ============================================================
DROP VIEW IF EXISTS analytics.v_store_execution_priority;
DROP VIEW IF EXISTS analytics.v_store_deep_diagnosis_april;
DROP VIEW IF EXISTS analytics.v_store_benchmark_composite;
DROP VIEW IF EXISTS analytics.v_store_action_list;
-- ============================================================
-- Step 2: 基础视图 → 物化视图
-- ============================================================
-- 2a. v_store_platform_economics
DROP VIEW IF EXISTS analytics.v_store_platform_economics;
CREATE MATERIALIZED VIEW analytics.v_store_platform_economics AS
SELECT NULLIF(bill_records.c002, ''::text) AS store_code,
NULLIF(bill_records.c003, ''::text) AS store_name,
sum(COALESCE(NULLIF(bill_records.c151, ''::text)::numeric, 0::numeric)) AS meituan_received,
sum(COALESCE(NULLIF(bill_records.c101, ''::text)::numeric, 0::numeric)) AS meituan_discount,
sum(COALESCE(NULLIF(bill_records.c097, ''::text)::numeric, 0::numeric)) AS meituan_commission,
sum(COALESCE(NULLIF(bill_records.c152, ''::text)::numeric, 0::numeric)) AS taobao_received,
sum(COALESCE(NULLIF(bill_records.c102, ''::text)::numeric, 0::numeric)) AS taobao_discount,
sum(COALESCE(NULLIF(bill_records.c098, ''::text)::numeric, 0::numeric)) AS taobao_commission,
sum(COALESCE(NULLIF(bill_records.c150, ''::text)::numeric, 0::numeric)) AS jd_received,
sum(COALESCE(NULLIF(bill_records.c099, ''::text)::numeric, 0::numeric)) AS jd_discount,
sum(COALESCE(NULLIF(bill_records.c100, ''::text)::numeric, 0::numeric)) AS jd_commission,
round((sum(COALESCE(NULLIF(bill_records.c101, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c097, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c151, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c101, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c097, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS meituan_cost_rate_pct,
round((sum(COALESCE(NULLIF(bill_records.c102, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c098, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c152, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c102, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c098, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS taobao_cost_rate_pct,
round((sum(COALESCE(NULLIF(bill_records.c099, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c100, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c150, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c099, ''::text)::numeric, 0::numeric)) + sum(COALESCE(NULLIF(bill_records.c100, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS jd_cost_rate_pct
FROM bill_records
WHERE NULLIF(bill_records.c005, ''::text) IS NOT NULL
GROUP BY (NULLIF(bill_records.c002, ''::text)), (NULLIF(bill_records.c003, ''::text))
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_platform_economics_store ON analytics.v_store_platform_economics (store_code);
-- 2b. v_store_category_mix
DROP VIEW IF EXISTS analytics.v_store_category_mix;
CREATE MATERIALIZED VIEW analytics.v_store_category_mix AS
SELECT NULLIF(bill_records.c002, ''::text) AS store_code,
NULLIF(bill_records.c003, ''::text) AS store_name,
sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)) AS consumption,
sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)) AS lanzhou_noodle,
sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)) AS western_staple,
sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)) AS delivery_package,
sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)) AS night_bbq,
sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)) AS cold_dishes,
sum(COALESCE(NULLIF(bill_records.c019, ''::text)::numeric, 0::numeric)) AS silk_road_food,
round(sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)) / NULLIF(sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS noodle_share_pct,
round(sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)) / NULLIF(sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS delivery_package_share_pct,
round(GREATEST(sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c019, ''::text)::numeric, 0::numeric))) / NULLIF(sum(COALESCE(NULLIF(bill_records.c009, ''::text)::numeric, 0::numeric)), 0::numeric) * 100::numeric, 2) AS top_category_share_pct,
CASE GREATEST(sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)), sum(COALESCE(NULLIF(bill_records.c019, ''::text)::numeric, 0::numeric)))
WHEN sum(COALESCE(NULLIF(bill_records.c010, ''::text)::numeric, 0::numeric)) THEN '兰州牛肉面'::text
WHEN sum(COALESCE(NULLIF(bill_records.c016, ''::text)::numeric, 0::numeric)) THEN '西部主食'::text
WHEN sum(COALESCE(NULLIF(bill_records.c027, ''::text)::numeric, 0::numeric)) THEN '外卖套餐'::text
WHEN sum(COALESCE(NULLIF(bill_records.c013, ''::text)::numeric, 0::numeric)) THEN '夜市烧烤'::text
WHEN sum(COALESCE(NULLIF(bill_records.c015, ''::text)::numeric, 0::numeric)) THEN '爽口凉菜'::text
ELSE '丝路美食'::text
END AS top_category
FROM bill_records
WHERE NULLIF(bill_records.c005, ''::text) IS NOT NULL
GROUP BY (NULLIF(bill_records.c002, ''::text)), (NULLIF(bill_records.c003, ''::text))
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_category_mix_store ON analytics.v_store_category_mix (store_code);
-- 2c. v_store_benchmark
DROP VIEW IF EXISTS analytics.v_store_benchmark;
CREATE MATERIALIZED VIEW analytics.v_store_benchmark AS
WITH eligible AS (
SELECT v_store_risk_rating.store_code,
v_store_risk_rating.store_name,
v_store_risk_rating.bill_count,
v_store_risk_rating.active_days,
v_store_risk_rating.received,
v_store_risk_rating.avg_daily_received,
v_store_risk_rating.avg_bill_value,
v_store_risk_rating.avg_guest_value,
v_store_risk_rating.discount_rate_pct,
v_store_risk_rating.theoretical_margin_pct,
v_store_risk_rating.member_bill_share_pct,
v_store_risk_rating.anomaly_rate_pct,
v_store_risk_rating.risk_level,
v_store_risk_rating.primary_issue
FROM analytics.v_store_risk_rating
WHERE v_store_risk_rating.active_days >= 25 AND v_store_risk_rating.bill_count >= 5000
), stats AS (
SELECT percentile_cont(0.25::double precision) WITHIN GROUP (ORDER BY (eligible.avg_daily_received::double precision)) AS revenue_p25,
percentile_cont(0.50::double precision) WITHIN GROUP (ORDER BY (eligible.avg_daily_received::double precision)) AS revenue_p50,
percentile_cont(0.75::double precision) WITHIN GROUP (ORDER BY (eligible.avg_daily_received::double precision)) AS revenue_p75,
percentile_cont(0.50::double precision) WITHIN GROUP (ORDER BY (eligible.theoretical_margin_pct::double precision)) AS margin_p50,
percentile_cont(0.75::double precision) WITHIN GROUP (ORDER BY (eligible.theoretical_margin_pct::double precision)) AS margin_p75,
percentile_cont(0.75::double precision) WITHIN GROUP (ORDER BY (eligible.discount_rate_pct::double precision)) AS discount_p75
FROM eligible
), base AS (
SELECT e.store_code,
e.store_name,
e.bill_count,
e.active_days,
e.received,
e.avg_daily_received,
e.avg_bill_value,
e.avg_guest_value,
e.discount_rate_pct,
e.theoretical_margin_pct,
e.member_bill_share_pct,
e.anomaly_rate_pct,
e.risk_level,
e.primary_issue,
s.revenue_p25,
s.revenue_p50,
s.revenue_p75,
s.margin_p50,
s.margin_p75,
s.discount_p75
FROM eligible e
CROSS JOIN stats s
)
SELECT base.store_code,
base.store_name,
base.bill_count,
base.active_days,
base.received,
base.avg_daily_received,
base.avg_bill_value,
base.avg_guest_value,
base.discount_rate_pct,
base.theoretical_margin_pct,
base.member_bill_share_pct,
base.anomaly_rate_pct,
base.risk_level,
base.primary_issue,
base.revenue_p25,
base.revenue_p50,
base.revenue_p75,
base.margin_p50,
base.margin_p75,
base.discount_p75,
CASE
WHEN base.avg_daily_received::double precision >= base.revenue_p75 AND base.theoretical_margin_pct::double precision >= base.margin_p50 THEN '明星门店'::text
WHEN base.avg_daily_received::double precision >= base.revenue_p75 AND base.theoretical_margin_pct::double precision < base.margin_p50 THEN '规模承压'::text
WHEN base.avg_daily_received::double precision < base.revenue_p75 AND base.theoretical_margin_pct::double precision >= base.margin_p75 THEN '高效潜力'::text
WHEN base.avg_daily_received::double precision <= base.revenue_p25 OR base.theoretical_margin_pct < 68::numeric OR base.discount_rate_pct::double precision > base.discount_p75 THEN '重点改善'::text
ELSE '稳健经营'::text
END AS management_quadrant,
round(GREATEST(base.received / NULLIF(1::numeric - base.discount_rate_pct / 100::numeric, 0::numeric) * (base.discount_rate_pct - 20.23) / 100::numeric, 0::numeric), 2) AS discount_saving_to_company_avg,
round(GREATEST(base.received * 0.7092 - base.received * base.theoretical_margin_pct / 100::numeric, 0::numeric), 2) AS profit_uplift_to_company_margin
FROM base
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_benchmark_store ON analytics.v_store_benchmark (store_code);
-- ============================================================
-- Step 3: 组合视图 → 物化视图
-- ============================================================
-- 3a. v_store_action_list (joins benchmark + category_mix + platform_economics)
CREATE MATERIALIZED VIEW analytics.v_store_action_list AS
SELECT b.store_code,
b.store_name,
b.management_quadrant,
b.risk_level,
b.primary_issue,
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,
b.discount_saving_to_company_avg,
b.profit_uplift_to_company_margin,
c.top_category,
c.top_category_share_pct,
c.delivery_package_share_pct,
p.meituan_cost_rate_pct,
p.taobao_cost_rate_pct,
p.jd_cost_rate_pct
FROM analytics.v_store_benchmark b
LEFT JOIN analytics.v_store_category_mix c USING (store_code, store_name)
LEFT JOIN analytics.v_store_platform_economics p USING (store_code, store_name)
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_action_list_store ON analytics.v_store_action_list (store_code);
-- 3b. v_store_execution_priority (joins action_list + meal + member + zero_bill)
CREATE MATERIALIZED VIEW analytics.v_store_execution_priority AS
WITH meal AS (
SELECT v_store_meal_opportunity.store_code,
sum(v_store_meal_opportunity.avg_bill_uplift_scenario) AS meal_uplift_scenario
FROM analytics.v_store_meal_opportunity
WHERE v_store_meal_opportunity.bill_count >= 500
GROUP BY v_store_meal_opportunity.store_code
), zero_bill AS (
SELECT v_zero_received_detail.store_code,
count(*) AS zero_received_bills,
count(*) FILTER (WHERE v_zero_received_detail.zero_received_type = '优惠不足但无实收'::text) AS unexplained_zero_bills
FROM analytics.v_zero_received_detail
GROUP BY v_zero_received_detail.store_code
)
SELECT a.store_code,
a.store_name,
a.management_quadrant,
a.risk_level,
a.primary_issue,
a.received,
a.avg_daily_received,
a.avg_bill_value,
a.discount_rate_pct,
a.theoretical_margin_pct,
a.anomaly_rate_pct,
a.member_bill_share_pct,
a.discount_saving_to_company_avg,
a.profit_uplift_to_company_margin,
a.top_category,
a.top_category_share_pct,
a.delivery_package_share_pct,
a.meituan_cost_rate_pct,
a.taobao_cost_rate_pct,
a.jd_cost_rate_pct,
round(COALESCE(m.meal_uplift_scenario, 0::numeric), 2) AS meal_uplift_scenario,
mo.member_share_pct,
mo.conversion_bill_scenario,
mo.revenue_uplift_scenario AS member_revenue_uplift_scenario,
COALESCE(z.zero_received_bills, 0::bigint) AS zero_received_bills,
COALESCE(z.unexplained_zero_bills, 0::bigint) AS unexplained_zero_bills
FROM analytics.v_store_action_list a
LEFT JOIN meal m USING (store_code)
LEFT JOIN analytics.v_store_member_opportunity mo USING (store_code, store_name)
LEFT JOIN zero_bill z USING (store_code)
WITH DATA;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_execution_priority_store ON analytics.v_store_execution_priority (store_code);
-- ============================================================
-- Step 4: 重建依赖普通视图(基于物化视图,查询会走索引)
-- ============================================================
-- 4a. v_store_benchmark_composite
CREATE OR REPLACE VIEW analytics.v_store_benchmark_composite 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 = '2026-04-01'::date
LEFT JOIN analytics.v_store_platform_economics p ON p.store_code = b.store_code
WHERE b.theoretical_margin_pct >= 60::numeric AND b.theoretical_margin_pct <= 80::numeric AND r.identified_members >= 500
), scored AS (
SELECT eligible.store_code,
eligible.store_name,
eligible.received,
eligible.avg_daily_received,
eligible.avg_bill_value,
eligible.discount_rate_pct,
eligible.theoretical_margin_pct,
eligible.anomaly_rate_pct,
eligible.member_bill_share_pct,
eligible.identified_members,
eligible.repeat_rate_pct,
eligible.avg_orders,
eligible.repeat_revenue_share_pct,
eligible.meituan_cost_rate_pct,
eligible.taobao_cost_rate_pct,
eligible.jd_cost_rate_pct,
percent_rank() OVER (ORDER BY eligible.avg_daily_received) AS revenue_score,
percent_rank() OVER (ORDER BY eligible.theoretical_margin_pct) AS margin_score,
1::double precision - percent_rank() OVER (ORDER BY eligible.discount_rate_pct) AS discount_score,
1::double precision - percent_rank() OVER (ORDER BY eligible.anomaly_rate_pct) AS anomaly_score,
percent_rank() OVER (ORDER BY eligible.repeat_rate_pct) AS repeat_score
FROM eligible
)
SELECT scored.store_code,
scored.store_name,
scored.received,
scored.avg_daily_received,
scored.avg_bill_value,
scored.discount_rate_pct,
scored.theoretical_margin_pct,
scored.anomaly_rate_pct,
scored.member_bill_share_pct,
scored.identified_members,
scored.repeat_rate_pct,
scored.avg_orders,
scored.repeat_revenue_share_pct,
scored.meituan_cost_rate_pct,
scored.taobao_cost_rate_pct,
scored.jd_cost_rate_pct,
scored.revenue_score,
scored.margin_score,
scored.discount_score,
scored.anomaly_score,
scored.repeat_score,
round(((scored.revenue_score * 0.25::double precision + scored.margin_score * 0.25::double precision + scored.discount_score * 0.20::double precision + scored.anomaly_score * 0.15::double precision + scored.repeat_score * 0.15::double precision) * 100::double precision)::numeric, 2) AS benchmark_score
FROM scored;
-- 4b. v_store_deep_diagnosis_april
CREATE OR REPLACE VIEW analytics.v_store_deep_diagnosis_april 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::numeric) + COALESCE(p.taobao_discount, 0::numeric) + COALESCE(p.jd_discount, 0::numeric) + COALESCE(p.meituan_commission, 0::numeric) + COALESCE(p.taobao_commission, 0::numeric) + COALESCE(p.jd_commission, 0::numeric)) / NULLIF(COALESCE(p.meituan_received, 0::numeric) + COALESCE(p.taobao_received, 0::numeric) + COALESCE(p.jd_received, 0::numeric) + COALESCE(p.meituan_discount, 0::numeric) + COALESCE(p.taobao_discount, 0::numeric) + COALESCE(p.jd_discount, 0::numeric) + COALESCE(p.meituan_commission, 0::numeric) + COALESCE(p.taobao_commission, 0::numeric) + COALESCE(p.jd_commission, 0::numeric), 0::numeric) * 100::numeric, 2) AS combined_platform_cost_rate_pct,
CASE
WHEN s.store_name ~ '机场|火锅|商城|快手|哈马尔罕'::text THEN '特殊业态'::text
ELSE '标准门店'::text
END AS business_type
FROM analytics.v_store_scorecard s
LEFT JOIN analytics.dish_store_summary_april d USING (store_code)
LEFT JOIN analytics.v_store_theoretical_actual_cost_april c USING (store_code)
LEFT JOIN analytics.v_store_benchmark_composite b USING (store_code)
LEFT JOIN analytics.v_store_platform_economics p USING (store_code)
), tiered AS (
SELECT store_base.store_code,
store_base.store_name,
store_base.bill_count,
store_base.active_days,
store_base.received,
store_base.avg_daily_received,
store_base.avg_bill_value,
store_base.discount_rate_pct,
store_base.theoretical_margin_pct,
store_base.member_bill_share_pct,
store_base.items_per_bill,
store_base.skus_per_bill,
store_base.delivery_bill_share_pct,
store_base.noodle_snack_attach_pct,
store_base.noodle_drink_attach_pct,
store_base.noodle_cold_attach_pct,
store_base.combo_bill_share_pct,
store_base.theoretical_cost,
store_base.actual_food_cost,
store_base.food_cost_variance,
store_base.theoretical_cost_rate_pct,
store_base.actual_food_cost_rate_pct,
store_base.variance_to_theoretical_pct,
store_base.comparison_status,
store_base.variance_level,
store_base.identified_members,
store_base.repeat_rate_pct,
store_base.repeat_revenue_share_pct,
store_base.benchmark_score,
store_base.meituan_received,
store_base.taobao_received,
store_base.jd_received,
store_base.combined_platform_cost_rate_pct,
store_base.business_type,
CASE
WHEN store_base.business_type = '特殊业态'::text THEN '特殊业态'::text
WHEN percent_rank() OVER (PARTITION BY store_base.business_type ORDER BY store_base.received) >= 0.67::double precision THEN '高规模'::text
WHEN percent_rank() OVER (PARTITION BY store_base.business_type ORDER BY store_base.received) >= 0.33::double precision THEN '中规模'::text
ELSE '低规模'::text
END AS scale_tier
FROM store_base
)
SELECT tiered.store_code,
tiered.store_name,
tiered.bill_count,
tiered.active_days,
tiered.received,
tiered.avg_daily_received,
tiered.avg_bill_value,
tiered.discount_rate_pct,
tiered.theoretical_margin_pct,
tiered.member_bill_share_pct,
tiered.items_per_bill,
tiered.skus_per_bill,
tiered.delivery_bill_share_pct,
tiered.noodle_snack_attach_pct,
tiered.noodle_drink_attach_pct,
tiered.noodle_cold_attach_pct,
tiered.combo_bill_share_pct,
tiered.theoretical_cost,
tiered.actual_food_cost,
tiered.food_cost_variance,
tiered.theoretical_cost_rate_pct,
tiered.actual_food_cost_rate_pct,
tiered.variance_to_theoretical_pct,
tiered.comparison_status,
tiered.variance_level,
tiered.identified_members,
tiered.repeat_rate_pct,
tiered.repeat_revenue_share_pct,
tiered.benchmark_score,
tiered.meituan_received,
tiered.taobao_received,
tiered.jd_received,
tiered.combined_platform_cost_rate_pct,
tiered.business_type,
tiered.scale_tier,
CASE
WHEN tiered.comparison_status = '可比'::text AND tiered.variance_to_theoretical_pct >= 20::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.discount_rate_pct >= 23.02 THEN 1
ELSE 0
END +
CASE
WHEN tiered.theoretical_margin_pct < 70::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.identified_members >= 500 AND tiered.repeat_rate_pct < 30::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.combined_platform_cost_rate_pct >= 40::numeric THEN 1
ELSE 0
END +
CASE
WHEN tiered.noodle_drink_attach_pct < 12::numeric THEN 1
ELSE 0
END AS problem_count,
concat_ws(''::text,
CASE
WHEN tiered.comparison_status = '理论成本口径异常'::text THEN '成本口径异常'::text
ELSE NULL::text
END,
CASE
WHEN tiered.comparison_status = '可比'::text AND tiered.variance_to_theoretical_pct >= 20::numeric THEN '实际成本严重超耗'::text
ELSE NULL::text
END,
CASE
WHEN tiered.discount_rate_pct >= 23.02 THEN '优惠偏高'::text
ELSE NULL::text
END,
CASE
WHEN tiered.theoretical_margin_pct < 70::numeric THEN '理论毛利偏低'::text
ELSE NULL::text
END,
CASE
WHEN tiered.identified_members >= 500 AND tiered.repeat_rate_pct < 30::numeric THEN '会员复购偏低'::text
ELSE NULL::text
END,
CASE
WHEN tiered.combined_platform_cost_rate_pct >= 40::numeric THEN '平台成本偏高'::text
ELSE NULL::text
END,
CASE
WHEN tiered.noodle_drink_attach_pct < 12::numeric THEN '饮品搭售偏低'::text
ELSE NULL::text
END) AS problem_combination
FROM tiered;
-- ============================================================
-- Step 5: 验证
-- ============================================================
SELECT 'Materialized views created successfully' AS status;