-- ============================================================ -- 03_create_views.sql -- 周度检查视图 + 月度验收视图 + 闭环健康度视图 + 数据质量视图 -- ============================================================ -- 周度检查视图 CREATE OR REPLACE VIEW analytics.v_task_weekly_check AS SELECT w.id, w.task_id, w.store_code, w.store_name, w.problem_indicator, w.iso_week, round(w.this_week_value, 2) AS this_week_value, round(w.last_week_value, 2) AS last_week_value, w.change_direction, w.consecutive_no_improve_weeks, w.check_comment, w.checked_by, w.checked_at, t.plan_month, t.priority, t.status AS task_status, t.target_value, CASE WHEN w.consecutive_no_improve_weeks >= 2 THEN '需重新判断原因' WHEN w.change_direction = 'up' AND t.problem_indicator IN ('优惠率','异常率','实际成本率','平台加权成本率') THEN '改善中' WHEN w.change_direction = 'down' AND t.problem_indicator IN ('优惠率','异常率','实际成本率','平台加权成本率') THEN '需关注' WHEN w.change_direction = 'up' AND t.problem_indicator IN ('理论毛利率','会员复购率','饮品搭售率','经营稳定性') THEN '改善中' WHEN w.change_direction = 'down' AND t.problem_indicator IN ('理论毛利率','会员复购率','饮品搭售率','经营稳定性') THEN '需关注' ELSE '观察中' END AS improvement_status FROM analytics.task_weekly_check w JOIN analytics.store_task t ON w.task_id = t.task_id; -- 月度验收视图 CREATE OR REPLACE VIEW analytics.v_task_monthly_review AS SELECT r.id, r.task_id, r.store_code, r.store_name, r.plan_month, r.problem_indicator, round(r.baseline_value, 2) AS baseline_value, round(r.target_value, 2) AS target_value, round(r.actual_value, 2) AS actual_value, r.review_result, r.revenue_stable, r.margin_improved, r.customer_stable, r.anomaly_decreased, CASE WHEN r.review_result = '达标' THEN true WHEN r.review_result = '改善中' AND r.revenue_stable AND r.customer_stable THEN true ELSE false END AS can_promote, t.priority, t.owner, t.action_required, t.process_evidence, t.incomplete_reason, t.next_step FROM analytics.task_monthly_review r JOIN analytics.store_task t ON r.task_id = t.task_id; -- 闭环健康度视图 CREATE OR REPLACE VIEW analytics.v_loop_health AS WITH task_stats AS ( SELECT count(DISTINCT store_code) AS total_stores, count(DISTINCT CASE WHEN status != '待启动' THEN store_code END) AS executed_stores, count(*) AS total_tasks, count(*) FILTER (WHERE status = '已验收') AS verified_tasks, count(*) FILTER (WHERE status = '已回滚') AS rolled_back_tasks FROM analytics.store_task WHERE plan_month = date_trunc('month', COALESCE( (SELECT max(plan_month) FROM analytics.store_task), CURRENT_DATE)) ), weekly_stats AS ( SELECT count(DISTINCT t.store_code) AS stores_with_weekly_check FROM analytics.task_weekly_check w JOIN analytics.store_task t ON w.task_id = t.task_id WHERE t.plan_month = date_trunc('month', COALESCE( (SELECT max(plan_month) FROM analytics.store_task), CURRENT_DATE)) AND w.checked_at >= date_trunc('month', COALESCE( (SELECT max(plan_month) FROM analytics.store_task), CURRENT_DATE)) ), practice_stats AS ( SELECT count(*) AS total_practices, count(*) FILTER (WHERE status = '已推广') AS promoted_practices FROM analytics.standardized_practice ), store_count AS ( SELECT count(DISTINCT store_code) AS total FROM analytics.v_store_scorecard ) SELECT round(COALESCE(ts.total_stores::numeric / NULLIF(sc.total, 0) * 100, 0), 1) AS task_generation_rate, round(COALESCE(ts.executed_stores::numeric / NULLIF(ts.total_stores, 0) * 100, 0), 1) AS store_execution_rate, round(COALESCE(ws.stores_with_weekly_check::numeric / NULLIF(ts.total_stores, 0) * 100, 0), 1) AS weekly_check_rate, round(COALESCE(ts.verified_tasks::numeric / NULLIF(ts.total_tasks, 0) * 100, 0), 1) AS monthly_review_rate, round(COALESCE(ps.promoted_practices::numeric / NULLIF(ps.total_practices, 0) * 100, 0), 1) AS practice_promotion_rate FROM task_stats ts CROSS JOIN weekly_stats ws CROSS JOIN practice_stats ps CROSS JOIN store_count sc; -- 数据质量检查视图 CREATE OR REPLACE VIEW analytics.v_data_quality_check AS WITH bill_stats AS ( SELECT count(*) AS total_bills, count(*) FILTER (WHERE NULLIF(c005, '') IS NULL) AS missing_bill_no, count(*) FILTER (WHERE NULLIF(c002, '') IS NULL) AS missing_store_code, count(*) FILTER (WHERE c009::numeric <= 0 OR c009 IS NULL) AS zero_consumption, count(*) FILTER (WHERE c114::numeric < 0) AS negative_received, count(DISTINCT NULLIF(c002, '')) AS store_count, min(c175::timestamp) AS min_date, max(c176::timestamp) AS max_date FROM bill_records ), dish_stats AS ( SELECT count(*) AS total_dish_records, count(*) FILTER (WHERE store_code IS NULL OR store_code = '') AS missing_store, count(*) FILTER (WHERE dish_name IS NULL OR dish_name = '') AS missing_dish FROM dish_sales_details ) SELECT b.total_bills, b.missing_bill_no, b.missing_store_code, b.zero_consumption, b.negative_received, b.store_count, b.min_date, b.max_date, d.total_dish_records, d.missing_store AS dish_missing_store, d.missing_dish AS dish_missing_dish, CASE WHEN b.missing_bill_no > 0 THEN '有账单缺失单号' WHEN b.missing_store_code > 0 THEN '有账单缺失门店编码' WHEN b.negative_received > 0 THEN '有负实收账单' ELSE '数据完整性正常' END AS bill_quality_status, CASE WHEN d.missing_store > 0 OR d.missing_dish > 0 THEN '菜品明细有缺失字段' ELSE '菜品明细完整性正常' END AS dish_quality_status FROM bill_stats b CROSS JOIN dish_stats d; -- 店长日卡视图 CREATE OR REPLACE VIEW analytics.v_store_daily_card AS WITH latest_date AS ( SELECT max(closed_at)::date AS business_date FROM analytics.bill_fact WHERE closed_at IS NOT NULL ), -- 收入模块 revenue AS ( SELECT bf.store_code, bf.store_name, '收入' AS module, jsonb_build_array( jsonb_build_object('metric', '实收', 'value', round(sum(bf.received_total), 2), 'baseline', round(avg(sw.received), 2), 'is_anomaly', sum(bf.received_total) < avg(sw.received) * 0.8), jsonb_build_object('metric', '账单数', 'value', count(*), 'baseline', round(avg(sw.bill_count), 0), 'is_anomaly', count(*) < avg(sw.bill_count) * 0.8), jsonb_build_object('metric', '客单价', 'value', round(sum(bf.received_total)/count(*), 2), 'baseline', round(avg(sw.avg_bill), 2), 'is_anomaly', sum(bf.received_total)/count(*) < avg(sw.avg_bill) * 0.9) ) AS anomalies FROM analytics.bill_fact bf CROSS JOIN latest_date ld LEFT JOIN LATERAL ( SELECT sum(r.received_total) AS received, count(*) AS bill_count, sum(r.received_total)/count(*) AS avg_bill FROM analytics.bill_fact r WHERE r.store_code = bf.store_code AND r.closed_at::date >= ld.business_date - 28 AND r.closed_at::date < ld.business_date AND extract(isodow FROM r.closed_at) = extract(isodow FROM ld.business_date) ) sw ON true WHERE bf.closed_at::date = ld.business_date GROUP BY bf.store_code, bf.store_name ), -- 优惠模块 discount AS ( SELECT bf.store_code, bf.store_name, '优惠' AS module, jsonb_build_array( jsonb_build_object('metric', '优惠率', 'value', round(sum(bf.discount_total)/nullif(sum(bf.consumption), 0) * 100, 2), 'baseline', 20.23, 'is_anomaly', sum(bf.discount_total)/nullif(sum(bf.consumption), 0) * 100 > 25), jsonb_build_object('metric', '异常优惠账单', 'value', count(*) FILTER (WHERE bf.discount_total > bf.consumption AND bf.consumption > 0), 'baseline', 0, 'is_anomaly', count(*) FILTER (WHERE bf.discount_total > bf.consumption AND bf.consumption > 0) > 5) ) AS anomalies FROM analytics.bill_fact bf CROSS JOIN latest_date ld WHERE bf.closed_at::date = ld.business_date GROUP BY bf.store_code, bf.store_name ), -- 风险模块 risk AS ( SELECT bf.store_code, bf.store_name, '风险' AS module, jsonb_build_array( jsonb_build_object('metric', '零实收账单', 'value', count(*) FILTER (WHERE bf.received_total = 0), 'baseline', 0, 'is_anomaly', count(*) FILTER (WHERE bf.received_total = 0) > 3), jsonb_build_object('metric', '撤单/退款', 'value', count(*) FILTER (WHERE bf.bill_status IN ('撤单', '退款')), 'baseline', 0, 'is_anomaly', count(*) FILTER (WHERE bf.bill_status IN ('撤单', '退款')) > 2) ) AS anomalies FROM analytics.bill_fact bf CROSS JOIN latest_date ld WHERE bf.closed_at::date = ld.business_date GROUP BY bf.store_code, bf.store_name ) SELECT COALESCE(r.store_code, d.store_code, rk.store_code) AS store_code, COALESCE(r.store_name, d.store_name, rk.store_name) AS store_name, COALESCE(r.module, d.module, rk.module) AS module, COALESCE(r.anomalies, d.anomalies, rk.anomalies) AS anomalies FROM revenue r FULL JOIN discount d USING (store_code, store_name) FULL JOIN risk rk USING (store_code, store_name);