Files
medical-mall/pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql
2026-01-30 16:17:13 +08:00

389 lines
12 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================
-- 优惠券效果分析 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;