-- ============================================ -- 优惠券效果分析 RPC 集合(Supabase / Postgres) -- 说明: -- - 这些函数只依赖业务域表: -- coupon_templates, user_coupons, coupon_usage_logs, orders -- - 不创建/修改业务表结构,由业务侧 schema 负责。 -- - 仅提供 Analytics Dashboard 所需的聚合统计。 -- - 调用方:/pages/mall/analytics/coupon-analysis.uvue -- ============================================ -- 安全注意: -- - 函数使用 SECURITY DEFINER,并将执行权限收敛到 authenticated 角色。 -- - 具体可见文件末尾的 REVOKE / GRANT 语句。 -- 1) 概览 KPI:发放/使用/GMV 提升/ROI/整体到期情况 CREATE OR REPLACE FUNCTION public.rpc_coupon_effectiveness_overview( p_start timestamptz, p_end timestamptz, p_merchant_id uuid DEFAULT NULL ) RETURNS TABLE ( total_issued numeric, total_used numeric, usage_rate numeric, gmv_increase numeric, issued_growth numeric, gmv_growth numeric, roi numeric, about_to_expire_cnt numeric ) LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_prev_start timestamptz; v_prev_end timestamptz; v_prev_issued numeric := 0; v_prev_gmv numeric := 0; v_discount_sum numeric := 0; BEGIN IF p_start IS NULL OR p_end IS NULL OR p_start >= p_end THEN RAISE EXCEPTION 'invalid period'; END IF; -- 上一周期窗口:长度与当前周期一致 v_prev_start := p_start - (p_end - p_start); v_prev_end := p_start; -- 当前周期:发放数量(user_coupons.received_at) SELECT COALESCE(COUNT(uc.id), 0)::numeric INTO total_issued FROM user_coupons uc JOIN coupon_templates ct ON ct.id = uc.template_id WHERE uc.received_at >= p_start AND uc.received_at < p_end AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id); -- 当前周期:使用记录 + GMV / 优惠额 SELECT COALESCE(COUNT(cul.id), 0)::numeric, COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric, COALESCE(SUM(cul.discount_amount), 0)::numeric INTO total_used, gmv_increase, v_discount_sum FROM coupon_usage_logs cul JOIN coupon_templates ct ON ct.id = cul.template_id LEFT JOIN orders o ON o.id = cul.order_id WHERE cul.used_at >= p_start AND cul.used_at < p_end AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id); -- 使用率(0-100) IF total_issued > 0 THEN usage_rate := ROUND((total_used / total_issued) * 100.0, 2); ELSE usage_rate := 0; END IF; -- 上一周期:发放与 GMV,用于增长率 SELECT COALESCE(COUNT(uc.id), 0)::numeric INTO v_prev_issued FROM user_coupons uc JOIN coupon_templates ct ON ct.id = uc.template_id WHERE uc.received_at >= v_prev_start AND uc.received_at < v_prev_end AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id); SELECT COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric INTO v_prev_gmv FROM coupon_usage_logs cul JOIN coupon_templates ct ON ct.id = cul.template_id LEFT JOIN orders o ON o.id = cul.order_id WHERE cul.used_at >= v_prev_start AND cul.used_at < v_prev_end AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id); -- 发放增长率 IF v_prev_issued > 0 THEN issued_growth := ROUND(((total_issued - v_prev_issued) / v_prev_issued) * 100.0, 2); ELSE issued_growth := CASE WHEN total_issued > 0 THEN 100.0 ELSE 0.0 END; END IF; -- GMV 增长率 IF v_prev_gmv > 0 THEN gmv_growth := ROUND(((gmv_increase - v_prev_gmv) / v_prev_gmv) * 100.0, 2); ELSE gmv_growth := CASE WHEN gmv_increase > 0 THEN 100.0 ELSE 0.0 END; END IF; -- ROI = (GMV_with_coupon - discount_sum) / discount_sum IF v_discount_sum > 0 THEN roi := ROUND(((gmv_increase - v_discount_sum) / v_discount_sum) * 100.0, 2); ELSE roi := 0; END IF; -- 未来 7 天内即将到期且未使用的券数量(整体概览) SELECT COALESCE(COUNT(uc2.id), 0)::numeric INTO about_to_expire_cnt FROM user_coupons uc2 JOIN coupon_templates ct2 ON ct2.id = uc2.template_id WHERE uc2.status = 1 -- 假设 1 = unused(参考 mall.md) AND uc2.expire_at > now() AND uc2.expire_at <= now() + interval '7 days' AND (p_merchant_id IS NULL OR ct2.merchant_id = p_merchant_id); RETURN; END; $$; -- 2) 按券类型统计:8 种券类型效果 CREATE OR REPLACE FUNCTION public.rpc_coupon_type_stats( p_start timestamptz, p_end timestamptz, p_merchant_id uuid DEFAULT NULL ) RETURNS TABLE ( coupon_type integer, total_issued numeric, total_used numeric, usage_rate numeric, gmv_with_coupon numeric, amount_saved numeric ) LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$ SELECT ct.coupon_type::integer AS coupon_type, COALESCE(COUNT(DISTINCT uc.id), 0)::numeric AS total_issued, COALESCE(COUNT(DISTINCT cul.id), 0)::numeric AS total_used, CASE WHEN COUNT(DISTINCT uc.id) > 0 THEN ROUND( (COUNT(DISTINCT cul.id)::numeric / COUNT(DISTINCT uc.id)::numeric) * 100.0, 2 ) ELSE 0 END AS usage_rate, COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric AS gmv_with_coupon, COALESCE(SUM(cul.discount_amount), 0)::numeric AS amount_saved FROM coupon_templates ct LEFT JOIN user_coupons uc ON uc.template_id = ct.id AND uc.received_at >= p_start AND uc.received_at < p_end LEFT JOIN coupon_usage_logs cul ON cul.template_id = ct.id AND cul.used_at >= p_start AND cul.used_at < p_end LEFT JOIN orders o ON o.id = cul.order_id WHERE (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id) GROUP BY ct.coupon_type ORDER BY total_issued DESC; $$; -- 3) 按发放渠道统计:主动领取/自动发放/活动/邀请/客服/积分 CREATE OR REPLACE FUNCTION public.rpc_coupon_channel_stats( p_start timestamptz, p_end timestamptz, p_merchant_id uuid DEFAULT NULL ) RETURNS TABLE ( channel text, total_issued numeric, total_used numeric, usage_rate numeric, gmv_with_coupon numeric ) LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$ SELECT uc.obtain_channel::text AS channel, COALESCE(COUNT(DISTINCT uc.id), 0)::numeric AS total_issued, COALESCE(COUNT(DISTINCT cul.id), 0)::numeric AS total_used, CASE WHEN COUNT(DISTINCT uc.id) > 0 THEN ROUND( (COUNT(DISTINCT cul.id)::numeric / COUNT(DISTINCT uc.id)::numeric) * 100.0, 2 ) ELSE 0 END AS usage_rate, COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric AS gmv_with_coupon FROM user_coupons uc JOIN coupon_templates ct ON ct.id = uc.template_id LEFT JOIN coupon_usage_logs cul ON cul.user_coupon_id = uc.id AND cul.used_at >= p_start AND cul.used_at < p_end LEFT JOIN orders o ON o.id = cul.order_id WHERE uc.received_at >= p_start AND uc.received_at < p_end AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id) AND uc.obtain_channel IS NOT NULL GROUP BY uc.obtain_channel ORDER BY total_issued DESC; $$; -- 4) 使用趋势:按天发放 vs 使用 CREATE OR REPLACE FUNCTION public.rpc_coupon_trend_daily( p_start timestamptz, p_end timestamptz, p_merchant_id uuid DEFAULT NULL ) RETURNS TABLE ( day date, issued numeric, used numeric ) LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$ WITH days AS ( SELECT generate_series( date_trunc('day', p_start)::date, date_trunc('day', p_end)::date, interval '1 day' )::date AS d ), issued AS ( SELECT uc_day::date AS d, COUNT(*)::numeric AS cnt FROM ( SELECT DATE(uc.received_at) AS uc_day FROM user_coupons uc JOIN coupon_templates ct ON ct.id = uc.template_id WHERE uc.received_at >= p_start AND uc.received_at < p_end AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id) ) t GROUP BY uc_day ), used AS ( SELECT cul_day::date AS d, COUNT(*)::numeric AS cnt FROM ( SELECT DATE(cul.used_at) AS cul_day FROM coupon_usage_logs cul JOIN coupon_templates ct ON ct.id = cul.template_id WHERE cul.used_at >= p_start AND cul.used_at < p_end AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id) ) t GROUP BY cul_day ) SELECT d.d AS day, COALESCE(i.cnt, 0) AS issued, COALESCE(u.cnt, 0) AS used FROM days d LEFT JOIN issued i ON i.d = d.d LEFT JOIN used u ON u.d = d.d ORDER BY d.d; $$; -- 5) 转化效果:有券 vs 无券(GMV/订单数/客单价) CREATE OR REPLACE FUNCTION public.rpc_coupon_conversion_effect( p_start timestamptz, p_end timestamptz, p_merchant_id uuid DEFAULT NULL ) RETURNS TABLE ( metric text, with_coupon numeric, without_coupon numeric ) LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_with_orders numeric := 0; v_without_orders numeric := 0; v_with_gmv numeric := 0; v_without_gmv numeric := 0; BEGIN -- 有券订单集合 SELECT COALESCE(COUNT(DISTINCT o.id), 0)::numeric, COALESCE(SUM(o.total_amount), 0)::numeric INTO v_with_orders, v_with_gmv FROM orders o WHERE o.created_at >= p_start AND o.created_at < p_end AND (p_merchant_id IS NULL OR o.merchant_id = p_merchant_id) AND EXISTS ( SELECT 1 FROM coupon_usage_logs cul WHERE cul.order_id = o.id ); -- 无券订单集合 SELECT COALESCE(COUNT(DISTINCT o.id), 0)::numeric, COALESCE(SUM(o.total_amount), 0)::numeric INTO v_without_orders, v_without_gmv FROM orders o WHERE o.created_at >= p_start AND o.created_at < p_end AND (p_merchant_id IS NULL OR o.merchant_id = p_merchant_id) AND NOT EXISTS ( SELECT 1 FROM coupon_usage_logs cul WHERE cul.order_id = o.id ); -- GMV 行 metric := 'GMV'; with_coupon := v_with_gmv; without_coupon := v_without_gmv; RETURN NEXT; -- 订单数 行 metric := 'orders'; with_coupon := v_with_orders; without_coupon := v_without_orders; RETURN NEXT; -- 客单价 行 metric := 'avg_order_amount'; with_coupon := CASE WHEN v_with_orders > 0 THEN ROUND(v_with_gmv / v_with_orders, 2) ELSE 0 END; without_coupon := CASE WHEN v_without_orders > 0 THEN ROUND(v_without_gmv / v_without_orders, 2) ELSE 0 END; RETURN NEXT; RETURN; END; $$; -- ============================================ -- 权限收敛:只允许 authenticated 角色调用 -- ============================================ REVOKE ALL ON FUNCTION public.rpc_coupon_effectiveness_overview(timestamptz,timestamptz,uuid) FROM PUBLIC; REVOKE ALL ON FUNCTION public.rpc_coupon_type_stats(timestamptz,timestamptz,uuid) FROM PUBLIC; REVOKE ALL ON FUNCTION public.rpc_coupon_channel_stats(timestamptz,timestamptz,uuid) FROM PUBLIC; REVOKE ALL ON FUNCTION public.rpc_coupon_trend_daily(timestamptz,timestamptz,uuid) FROM PUBLIC; REVOKE ALL ON FUNCTION public.rpc_coupon_conversion_effect(timestamptz,timestamptz,uuid) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_coupon_effectiveness_overview(timestamptz,timestamptz,uuid) TO authenticated; GRANT EXECUTE ON FUNCTION public.rpc_coupon_type_stats(timestamptz,timestamptz,uuid) TO authenticated; GRANT EXECUTE ON FUNCTION public.rpc_coupon_channel_stats(timestamptz,timestamptz,uuid) TO authenticated; GRANT EXECUTE ON FUNCTION public.rpc_coupon_trend_daily(timestamptz,timestamptz,uuid) TO authenticated; GRANT EXECUTE ON FUNCTION public.rpc_coupon_conversion_effect(timestamptz,timestamptz,uuid) TO authenticated; -- 完成 SELECT 'Coupon analysis RPCs created successfully!' AS message;