389 lines
12 KiB
PL/PgSQL
389 lines
12 KiB
PL/PgSQL
-- ============================================
|
||
-- 优惠券效果分析 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;
|
||
|