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

854 lines
34 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.
-- ============================================
-- 数据分析模块数据库 SchemaSupabase/Postgres
-- ============================================
-- 用途:创建 analytics_* 表、索引、RLS策略、RPC函数
-- 参考:`docs/ANALYTICS_DB_DESIGN.md`
--
-- 执行顺序:
-- 1. 先执行基础业务表(`01_create_tables.sql`
-- 2. 再执行本文档(`ANALYTICS_DB_SCHEMA.sql`
-- 3. 最后执行 `ANALYTICS_TEST_SEED.sql` 插入测试数据(受 RLS 影响,需较高权限执行)
-- ============================================
-- ============================================
-- 1. Analytics 表结构
-- ============================================
-- 说明:
-- - 本目录pages/mall/analytics/test中的基础表结构由 01_create_tables.sql 提供。
-- - 本文件只负责 analytics_* 表 + RLS + RPC。
-- 1.1 分析师偏好设置
CREATE TABLE IF NOT EXISTS analytics_user_preferences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
default_period TEXT NOT NULL DEFAULT '7d',
timezone TEXT DEFAULT 'Asia/Shanghai',
currency TEXT DEFAULT 'CNY',
kpi_cards JSONB DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id)
);
-- 中文注释
COMMENT ON TABLE public.analytics_user_preferences IS '分析师偏好设置表';
COMMENT ON COLUMN public.analytics_user_preferences.id IS '主键';
COMMENT ON COLUMN public.analytics_user_preferences.user_id IS '用户ID关联 users.id / 建议与 auth.uid 对齐)';
COMMENT ON COLUMN public.analytics_user_preferences.default_period IS '默认统计周期(如 7d/30d/90d/1y';
COMMENT ON COLUMN public.analytics_user_preferences.timezone IS '时区(默认 Asia/Shanghai';
COMMENT ON COLUMN public.analytics_user_preferences.currency IS '币种(默认 CNY';
COMMENT ON COLUMN public.analytics_user_preferences.kpi_cards IS 'KPI 卡片配置JSON 数组)';
COMMENT ON COLUMN public.analytics_user_preferences.created_at IS '创建时间';
COMMENT ON COLUMN public.analytics_user_preferences.updated_at IS '更新时间';
-- 1.2 报表定义
CREATE TABLE IF NOT EXISTS analytics_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
merchant_id UUID REFERENCES merchants(id) ON DELETE SET NULL,
title TEXT NOT NULL,
description TEXT DEFAULT '',
type TEXT NOT NULL, -- sales/users/orders/conversion/coupon/delivery/market/custom
period TEXT NOT NULL, -- 7d/30d/90d/1y
date_start DATE,
date_end DATE,
status TEXT NOT NULL DEFAULT 'ready', -- pending/ready/failed/scheduled/shared
generated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 中文注释
COMMENT ON TABLE public.analytics_reports IS '分析报表表(报表定义/实例)';
COMMENT ON COLUMN public.analytics_reports.id IS '报表ID';
COMMENT ON COLUMN public.analytics_reports.owner_user_id IS '报表所属用户ID创建者';
COMMENT ON COLUMN public.analytics_reports.merchant_id IS '商家ID可空表示全站/不限定商家)';
COMMENT ON COLUMN public.analytics_reports.title IS '报表标题';
COMMENT ON COLUMN public.analytics_reports.description IS '报表描述';
COMMENT ON COLUMN public.analytics_reports.type IS '报表类型sales/users/orders/conversion 等)';
COMMENT ON COLUMN public.analytics_reports.period IS '统计周期7d/30d/90d/1y 或自定义)';
COMMENT ON COLUMN public.analytics_reports.date_start IS '自定义开始日期(可空)';
COMMENT ON COLUMN public.analytics_reports.date_end IS '自定义结束日期(可空)';
COMMENT ON COLUMN public.analytics_reports.status IS '状态pending/ready/failed/scheduled/shared';
COMMENT ON COLUMN public.analytics_reports.generated_at IS '生成时间';
COMMENT ON COLUMN public.analytics_reports.created_at IS '创建时间';
COMMENT ON COLUMN public.analytics_reports.updated_at IS '更新时间';
-- 1.3 报表核心指标
CREATE TABLE IF NOT EXISTS analytics_report_metrics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
report_id UUID NOT NULL REFERENCES analytics_reports(id) ON DELETE CASCADE,
metric_key TEXT NOT NULL,
metric_label TEXT NOT NULL,
metric_value_num NUMERIC,
metric_value_text TEXT,
format TEXT NOT NULL DEFAULT 'number', -- number/currency/percent
change_pct NUMERIC DEFAULT 0,
icon TEXT DEFAULT '',
color TEXT DEFAULT '#3b82f6',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 中文注释
COMMENT ON TABLE public.analytics_report_metrics IS '报表核心指标表';
COMMENT ON COLUMN public.analytics_report_metrics.id IS '主键';
COMMENT ON COLUMN public.analytics_report_metrics.report_id IS '所属报表ID';
COMMENT ON COLUMN public.analytics_report_metrics.metric_key IS '指标Key如 gmv/orders/conversion_rate';
COMMENT ON COLUMN public.analytics_report_metrics.metric_label IS '指标名称(展示用)';
COMMENT ON COLUMN public.analytics_report_metrics.metric_value_num IS '指标数值(数值型)';
COMMENT ON COLUMN public.analytics_report_metrics.metric_value_text IS '指标文本(已格式化,如百分比字符串)';
COMMENT ON COLUMN public.analytics_report_metrics.format IS '展示格式number/currency/percent';
COMMENT ON COLUMN public.analytics_report_metrics.change_pct IS '变化百分比(环比/同比,单位:%';
COMMENT ON COLUMN public.analytics_report_metrics.icon IS '图标(可选,建议使用纯文本 key';
COMMENT ON COLUMN public.analytics_report_metrics.color IS '颜色UI 展示用)';
COMMENT ON COLUMN public.analytics_report_metrics.created_at IS '创建时间';
-- 1.4 报表明细行(趋势数据)
CREATE TABLE IF NOT EXISTS analytics_report_rows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
report_id UUID NOT NULL REFERENCES analytics_reports(id) ON DELETE CASCADE,
row_date DATE NOT NULL,
gmv NUMERIC DEFAULT 0,
orders INTEGER DEFAULT 0,
users INTEGER DEFAULT 0,
conversion NUMERIC DEFAULT 0,
avg_order_amount NUMERIC DEFAULT 0,
extra JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 中文注释
COMMENT ON TABLE public.analytics_report_rows IS '报表明细行表(趋势数据)';
COMMENT ON COLUMN public.analytics_report_rows.id IS '主键';
COMMENT ON COLUMN public.analytics_report_rows.report_id IS '所属报表ID';
COMMENT ON COLUMN public.analytics_report_rows.row_date IS '统计日期';
COMMENT ON COLUMN public.analytics_report_rows.gmv IS 'GMV';
COMMENT ON COLUMN public.analytics_report_rows.orders IS '订单数';
COMMENT ON COLUMN public.analytics_report_rows.users IS '用户数(可选)';
COMMENT ON COLUMN public.analytics_report_rows.conversion IS '转化率0-100';
COMMENT ON COLUMN public.analytics_report_rows.avg_order_amount IS '客单价';
COMMENT ON COLUMN public.analytics_report_rows.extra IS '扩展字段(用于自定义报表列)';
COMMENT ON COLUMN public.analytics_report_rows.created_at IS '创建时间';
-- 1.5 数据洞察
CREATE TABLE IF NOT EXISTS analytics_insights (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
report_id UUID REFERENCES analytics_reports(id) ON DELETE CASCADE,
owner_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
type TEXT NOT NULL, -- positive/warning/negative/info
impact TEXT NOT NULL DEFAULT 'medium', -- high/medium/low
title TEXT NOT NULL,
content TEXT NOT NULL,
tags TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 中文注释
COMMENT ON TABLE public.analytics_insights IS '数据洞察表(建议/预警/提示)';
COMMENT ON COLUMN public.analytics_insights.id IS '洞察ID';
COMMENT ON COLUMN public.analytics_insights.report_id IS '关联报表ID可空';
COMMENT ON COLUMN public.analytics_insights.owner_user_id IS '所属用户ID可空系统生成';
COMMENT ON COLUMN public.analytics_insights.type IS '洞察类型positive/warning/negative/info';
COMMENT ON COLUMN public.analytics_insights.impact IS '影响等级high/medium/low';
COMMENT ON COLUMN public.analytics_insights.title IS '洞察标题';
COMMENT ON COLUMN public.analytics_insights.content IS '洞察内容';
COMMENT ON COLUMN public.analytics_insights.tags IS '标签数组';
COMMENT ON COLUMN public.analytics_insights.created_at IS '创建时间';
-- 1.6 报表收藏
CREATE TABLE IF NOT EXISTS analytics_report_favorites (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
report_id UUID NOT NULL REFERENCES analytics_reports(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, report_id)
);
-- 中文注释
COMMENT ON TABLE public.analytics_report_favorites IS '报表收藏表';
COMMENT ON COLUMN public.analytics_report_favorites.id IS '主键';
COMMENT ON COLUMN public.analytics_report_favorites.user_id IS '用户ID';
COMMENT ON COLUMN public.analytics_report_favorites.report_id IS '报表ID';
COMMENT ON COLUMN public.analytics_report_favorites.created_at IS '创建时间';
-- 1.7 导出任务
CREATE TABLE IF NOT EXISTS analytics_export_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
report_id UUID NOT NULL REFERENCES analytics_reports(id) ON DELETE CASCADE,
format TEXT NOT NULL, -- csv/xlsx/pdf/json
status TEXT NOT NULL DEFAULT 'queued', -- queued/running/done/failed
file_path TEXT,
error_message TEXT DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW(),
finished_at TIMESTAMPTZ
);
-- 中文注释
COMMENT ON TABLE public.analytics_export_jobs IS '导出任务表(导出历史/队列)';
COMMENT ON COLUMN public.analytics_export_jobs.id IS '导出任务ID';
COMMENT ON COLUMN public.analytics_export_jobs.user_id IS '发起用户ID';
COMMENT ON COLUMN public.analytics_export_jobs.report_id IS '关联报表ID';
COMMENT ON COLUMN public.analytics_export_jobs.format IS '导出格式csv/xlsx/pdf/json';
COMMENT ON COLUMN public.analytics_export_jobs.status IS '任务状态queued/running/done/failed';
COMMENT ON COLUMN public.analytics_export_jobs.file_path IS '文件路径Storage 路径,可空)';
COMMENT ON COLUMN public.analytics_export_jobs.error_message IS '失败原因(可空)';
COMMENT ON COLUMN public.analytics_export_jobs.created_at IS '创建时间';
COMMENT ON COLUMN public.analytics_export_jobs.finished_at IS '完成时间(可空)';
-- ============================================
-- 2. 索引
-- ============================================
-- analytics_reports
CREATE INDEX IF NOT EXISTS idx_analytics_reports_owner_created ON analytics_reports(owner_user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_analytics_reports_type_generated ON analytics_reports(type, generated_at DESC);
CREATE INDEX IF NOT EXISTS idx_analytics_reports_status ON analytics_reports(status);
-- analytics_report_metrics
CREATE INDEX IF NOT EXISTS idx_analytics_report_metrics_report ON analytics_report_metrics(report_id, metric_key);
-- analytics_report_rows
CREATE INDEX IF NOT EXISTS idx_analytics_report_rows_report_date ON analytics_report_rows(report_id, row_date);
-- analytics_insights
CREATE INDEX IF NOT EXISTS idx_analytics_insights_created ON analytics_insights(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_analytics_insights_report ON analytics_insights(report_id, created_at DESC);
-- analytics_export_jobs
CREATE INDEX IF NOT EXISTS idx_analytics_export_jobs_user ON analytics_export_jobs(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_analytics_export_jobs_status ON analytics_export_jobs(status);
-- ============================================
-- 3. RLSRow Level Security策略
-- ============================================
-- 启用 RLS
ALTER TABLE analytics_user_preferences ENABLE ROW LEVEL SECURITY;
ALTER TABLE analytics_reports ENABLE ROW LEVEL SECURITY;
ALTER TABLE analytics_report_metrics ENABLE ROW LEVEL SECURITY;
ALTER TABLE analytics_report_rows ENABLE ROW LEVEL SECURITY;
ALTER TABLE analytics_insights ENABLE ROW LEVEL SECURITY;
ALTER TABLE analytics_report_favorites ENABLE ROW LEVEL SECURITY;
ALTER TABLE analytics_export_jobs ENABLE ROW LEVEL SECURITY;
-- analytics_user_preferences: 用户只能访问自己的偏好
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_user_preferences' AND policyname='Users can view own preferences') THEN
EXECUTE 'CREATE POLICY "Users can view own preferences" ON public.analytics_user_preferences FOR SELECT USING (auth.uid() = user_id)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_user_preferences' AND policyname='Users can insert own preferences') THEN
EXECUTE 'CREATE POLICY "Users can insert own preferences" ON public.analytics_user_preferences FOR INSERT WITH CHECK (auth.uid() = user_id)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_user_preferences' AND policyname='Users can update own preferences') THEN
EXECUTE 'CREATE POLICY "Users can update own preferences" ON public.analytics_user_preferences FOR UPDATE USING (auth.uid() = user_id)';
END IF;
END $$;
-- analytics_reports: 用户可访问自己创建的报表和共享报表
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_reports' AND policyname='Users can view own reports') THEN
EXECUTE 'CREATE POLICY "Users can view own reports" ON public.analytics_reports FOR SELECT USING (auth.uid() = owner_user_id OR status = ''shared'')';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_reports' AND policyname='Users can insert own reports') THEN
EXECUTE 'CREATE POLICY "Users can insert own reports" ON public.analytics_reports FOR INSERT WITH CHECK (auth.uid() = owner_user_id)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_reports' AND policyname='Users can update own reports') THEN
EXECUTE 'CREATE POLICY "Users can update own reports" ON public.analytics_reports FOR UPDATE USING (auth.uid() = owner_user_id)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_reports' AND policyname='Users can delete own reports') THEN
EXECUTE 'CREATE POLICY "Users can delete own reports" ON public.analytics_reports FOR DELETE USING (auth.uid() = owner_user_id)';
END IF;
END $$;
-- analytics_report_metrics: 通过 report_id 关联权限
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_report_metrics' AND policyname='Users can view metrics of accessible reports') THEN
EXECUTE 'CREATE POLICY "Users can view metrics of accessible reports" ON public.analytics_report_metrics FOR SELECT USING (EXISTS (SELECT 1 FROM public.analytics_reports WHERE public.analytics_reports.id = public.analytics_report_metrics.report_id AND (public.analytics_reports.owner_user_id = auth.uid() OR public.analytics_reports.status = ''shared'')))';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_report_metrics' AND policyname='Users can manage metrics of own reports') THEN
EXECUTE 'CREATE POLICY "Users can manage metrics of own reports" ON public.analytics_report_metrics FOR ALL USING (EXISTS (SELECT 1 FROM public.analytics_reports WHERE public.analytics_reports.id = public.analytics_report_metrics.report_id AND public.analytics_reports.owner_user_id = auth.uid()))';
END IF;
END $$;
-- analytics_report_rows: 通过 report_id 关联权限
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_report_rows' AND policyname='Users can view rows of accessible reports') THEN
EXECUTE 'CREATE POLICY "Users can view rows of accessible reports" ON public.analytics_report_rows FOR SELECT USING (EXISTS (SELECT 1 FROM public.analytics_reports WHERE public.analytics_reports.id = public.analytics_report_rows.report_id AND (public.analytics_reports.owner_user_id = auth.uid() OR public.analytics_reports.status = ''shared'')))';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_report_rows' AND policyname='Users can manage rows of own reports') THEN
EXECUTE 'CREATE POLICY "Users can manage rows of own reports" ON public.analytics_report_rows FOR ALL USING (EXISTS (SELECT 1 FROM public.analytics_reports WHERE public.analytics_reports.id = public.analytics_report_rows.report_id AND public.analytics_reports.owner_user_id = auth.uid()))';
END IF;
END $$;
-- analytics_insights: 通过 report_id 或 owner_user_id 关联权限
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_insights' AND policyname='Users can view accessible insights') THEN
EXECUTE 'CREATE POLICY "Users can view accessible insights" ON public.analytics_insights FOR SELECT USING (owner_user_id = auth.uid() OR (report_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.analytics_reports WHERE public.analytics_reports.id = public.analytics_insights.report_id AND (public.analytics_reports.owner_user_id = auth.uid() OR public.analytics_reports.status = ''shared''))))';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_insights' AND policyname='Users can manage own insights') THEN
EXECUTE 'CREATE POLICY "Users can manage own insights" ON public.analytics_insights FOR ALL USING (owner_user_id = auth.uid())';
END IF;
END $$;
-- analytics_report_favorites: 用户只能访问自己的收藏
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_report_favorites' AND policyname='Users can manage own favorites') THEN
EXECUTE 'CREATE POLICY "Users can manage own favorites" ON public.analytics_report_favorites FOR ALL USING (auth.uid() = user_id)';
END IF;
END $$;
-- analytics_export_jobs: 用户只能访问自己的导出任务
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='analytics_export_jobs' AND policyname='Users can manage own export jobs') THEN
EXECUTE 'CREATE POLICY "Users can manage own export jobs" ON public.analytics_export_jobs FOR ALL USING (auth.uid() = user_id)';
END IF;
END $$;
-- ============================================
-- 4. RPC 函数Postgres Functions
-- ============================================
-- 4.1 实时 KPI 计算函数
CREATE OR REPLACE FUNCTION rpc_analytics_realtime_kpis(
p_start TIMESTAMPTZ,
p_end TIMESTAMPTZ,
p_compare_start TIMESTAMPTZ,
p_compare_end TIMESTAMPTZ,
p_merchant_id UUID DEFAULT NULL
)
RETURNS TABLE (
gmv NUMERIC,
gmv_growth NUMERIC,
orders INTEGER,
order_growth NUMERIC,
online_users INTEGER,
conversion_rate NUMERIC,
conversion_growth NUMERIC
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_gmv NUMERIC := 0;
v_gmv_compare NUMERIC := 0;
v_orders INTEGER := 0;
v_orders_compare INTEGER := 0;
v_online_users INTEGER := 0;
v_order_users INTEGER := 0;
v_visitors INTEGER := 0;
v_visitors_compare INTEGER := 0;
v_order_users_compare INTEGER := 0;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
-- 计算当前时间段 GMV 和订单数
SELECT
COALESCE(SUM(total_amount), 0),
COUNT(*)
INTO v_gmv, v_orders
FROM orders
WHERE created_at >= p_start
AND created_at < p_end
AND status = 2 -- 已支付
AND (p_merchant_id IS NULL OR merchant_id = p_merchant_id);
-- 计算对比时间段 GMV 和订单数
SELECT
COALESCE(SUM(total_amount), 0),
COUNT(*)
INTO v_gmv_compare, v_orders_compare
FROM orders
WHERE created_at >= p_compare_start
AND created_at < p_compare_end
AND status = 2 -- 已支付
AND (p_merchant_id IS NULL OR merchant_id = p_merchant_id);
-- 计算在线用户最近5分钟活跃
SELECT COUNT(DISTINCT user_id)
INTO v_online_users
FROM user_sessions
WHERE last_active_at >= (NOW() - INTERVAL '5 minutes')
AND is_active = true;
-- 计算当前时间段下单用户数
SELECT COUNT(DISTINCT user_id)
INTO v_order_users
FROM orders
WHERE created_at >= p_start
AND created_at < p_end
AND status = 2
AND (p_merchant_id IS NULL OR merchant_id = p_merchant_id);
-- 计算当前时间段访问用户数
SELECT COUNT(DISTINCT user_id)
INTO v_visitors
FROM user_sessions
WHERE created_at >= p_start
AND created_at < p_end;
-- 计算对比时间段访问用户数和下单用户数
SELECT
COUNT(DISTINCT user_id),
(SELECT COUNT(DISTINCT user_id) FROM orders
WHERE created_at >= p_compare_start AND created_at < p_compare_end
AND status = 2 AND (p_merchant_id IS NULL OR merchant_id = p_merchant_id))
INTO v_visitors_compare, v_order_users_compare
FROM user_sessions
WHERE created_at >= p_compare_start
AND created_at < p_compare_end;
-- 返回结果
RETURN QUERY SELECT
v_gmv,
CASE WHEN v_gmv_compare > 0 THEN ((v_gmv - v_gmv_compare) / v_gmv_compare * 100) ELSE (CASE WHEN v_gmv > 0 THEN 100 ELSE 0 END) END,
v_orders,
CASE WHEN v_orders_compare > 0 THEN ((v_orders - v_orders_compare)::NUMERIC / v_orders_compare * 100) ELSE (CASE WHEN v_orders > 0 THEN 100 ELSE 0 END) END,
COALESCE(v_online_users, 0),
CASE WHEN v_visitors > 0 THEN (v_order_users::NUMERIC / v_visitors * 100) ELSE 0 END,
CASE WHEN v_visitors_compare > 0 AND v_order_users_compare > 0 THEN
(((v_order_users::NUMERIC / NULLIF(v_visitors, 0)) - (v_order_users_compare::NUMERIC / NULLIF(v_visitors_compare, 0))) / (v_order_users_compare::NUMERIC / NULLIF(v_visitors_compare, 0)) * 100)
ELSE 0 END;
END;
$$;
-- 4.2 趋势数据查询函数(按日期聚合)
CREATE OR REPLACE FUNCTION rpc_analytics_trend_data(
p_start_date DATE,
p_end_date DATE,
p_merchant_id UUID DEFAULT NULL
)
RETURNS TABLE (
date DATE,
gmv NUMERIC,
orders INTEGER,
users INTEGER
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
RETURN QUERY
SELECT
DATE(o.created_at) AS date,
COALESCE(SUM(o.total_amount), 0) AS gmv,
COUNT(*)::INTEGER AS orders,
COUNT(DISTINCT o.user_id)::INTEGER AS users
FROM orders o
WHERE DATE(o.created_at) >= p_start_date
AND DATE(o.created_at) <= p_end_date
AND o.status = 2 -- 已支付
AND (p_merchant_id IS NULL OR o.merchant_id = p_merchant_id)
GROUP BY DATE(o.created_at)
ORDER BY date;
END;
$$;
-- 4.3 热销商品 TOP按 GMV
CREATE OR REPLACE FUNCTION rpc_analytics_top_products(
p_start_date DATE,
p_end_date DATE,
p_limit INTEGER DEFAULT 10,
p_merchant_id UUID DEFAULT NULL
)
RETURNS TABLE (
id UUID,
name TEXT,
sales NUMERIC
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
RETURN QUERY
SELECT
p.id,
CAST(p.name AS TEXT) AS name,
CAST(COALESCE(SUM(oi.total_amount), 0) AS NUMERIC) AS sales
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE DATE(o.created_at) >= p_start_date
AND DATE(o.created_at) <= p_end_date
AND o.status = 2
AND (p_merchant_id IS NULL OR o.merchant_id = p_merchant_id)
GROUP BY p.id, p.name
ORDER BY sales DESC
LIMIT p_limit;
END;
$$;
-- 4.4 商家 TOP按 GMV
CREATE OR REPLACE FUNCTION rpc_analytics_top_merchants(
p_start_date DATE,
p_end_date DATE,
p_limit INTEGER DEFAULT 10
)
RETURNS TABLE (
id UUID,
name TEXT,
sales NUMERIC,
growth NUMERIC
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_days INTEGER := GREATEST(1, (p_end_date - p_start_date + 1));
v_prev_start DATE := p_start_date - v_days;
v_prev_end DATE := p_start_date - 1;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
RETURN QUERY
WITH cur AS (
SELECT
m.id AS merchant_id,
CAST(COALESCE(m.shop_name, '未知商家') AS TEXT) AS name,
CAST(COALESCE(SUM(o.total_amount), 0) AS NUMERIC) AS sales
FROM merchants m
LEFT JOIN orders o ON o.merchant_id = m.id
AND DATE(o.created_at) >= p_start_date
AND DATE(o.created_at) <= p_end_date
AND o.status = 2
GROUP BY m.id, m.shop_name
),
prev AS (
SELECT
m.id AS merchant_id,
CAST(COALESCE(SUM(o.total_amount), 0) AS NUMERIC) AS sales
FROM merchants m
LEFT JOIN orders o ON o.merchant_id = m.id
AND DATE(o.created_at) >= v_prev_start
AND DATE(o.created_at) <= v_prev_end
AND o.status = 2
GROUP BY m.id
)
SELECT
CAST(cur.merchant_id AS UUID) AS id,
CAST(cur.name AS TEXT) AS name,
CAST(cur.sales AS NUMERIC) AS sales,
CAST(
CASE
WHEN COALESCE(prev.sales, 0) > 0 THEN ((cur.sales - prev.sales) / prev.sales * 100)
WHEN cur.sales > 0 THEN 100
ELSE 0
END AS NUMERIC
) AS growth
FROM cur
LEFT JOIN prev ON prev.merchant_id = cur.merchant_id
ORDER BY cur.sales DESC
LIMIT p_limit;
END;
$$;
-- 4.5 用户结构(分群:未消费/消费一次/留存客户/回流客户)
CREATE OR REPLACE FUNCTION rpc_analytics_user_segments(
p_start_date DATE,
p_end_date DATE
)
RETURNS TABLE (
name TEXT,
value INTEGER
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
RETURN QUERY
WITH u AS (
SELECT id AS user_id FROM users
),
cur_orders AS (
SELECT user_id, COUNT(*) AS cnt
FROM orders
WHERE DATE(created_at) >= p_start_date
AND DATE(created_at) <= p_end_date
AND status = 2
GROUP BY user_id
),
has_before AS (
SELECT user_id, 1 AS has_before
FROM orders
WHERE DATE(created_at) < p_start_date
AND status = 2
GROUP BY user_id
),
cls AS (
SELECT
u.user_id,
COALESCE(c.cnt, 0) AS cur_cnt,
COALESCE(b.has_before, 0) AS before_flag
FROM u
LEFT JOIN cur_orders c ON c.user_id = u.user_id
LEFT JOIN has_before b ON b.user_id = u.user_id
)
SELECT CAST('未消费用户' AS TEXT) AS name, CAST(COUNT(*) AS INTEGER) AS value FROM cls WHERE cur_cnt = 0
UNION ALL
SELECT CAST('消费一次用户' AS TEXT) AS name, CAST(COUNT(*) AS INTEGER) AS value FROM cls WHERE cur_cnt = 1
UNION ALL
SELECT CAST('留存客户' AS TEXT) AS name, CAST(COUNT(*) AS INTEGER) AS value FROM cls WHERE cur_cnt >= 2
UNION ALL
SELECT CAST('回流客户' AS TEXT) AS name, CAST(COUNT(*) AS INTEGER) AS value FROM cls WHERE cur_cnt >= 1 AND before_flag = 1;
END;
$$;
-- 4.6 流量来源(基于 page_views.source
CREATE OR REPLACE FUNCTION rpc_analytics_traffic_sources(
p_start_date DATE,
p_end_date DATE
)
RETURNS TABLE (
name TEXT,
value INTEGER
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
RETURN QUERY
SELECT
CAST(
CASE
WHEN source = 'direct' THEN '直接访问'
WHEN source = 'search' THEN '搜索引擎'
WHEN source = 'social' THEN '社交媒体'
WHEN source = 'ad' THEN '广告推广'
ELSE COALESCE(source, '未知')
END AS TEXT
) AS name,
CAST(COUNT(*) AS INTEGER) AS value
FROM page_views
WHERE DATE(created_at) >= p_start_date
AND DATE(created_at) <= p_end_date
GROUP BY 1
ORDER BY value DESC;
END;
$$;
-- 4.7 用户分析 KPI总用户/新用户/活跃/复购率 等)
-- 说明:指标口径为“占总用户数的百分比”,增长为与上一周期对比的百分比变化。
CREATE OR REPLACE FUNCTION rpc_analytics_user_kpis(
p_start_date DATE,
p_end_date DATE
)
RETURNS TABLE (
total_users INTEGER,
user_growth NUMERIC,
new_users INTEGER,
new_user_growth NUMERIC,
active_rate NUMERIC,
active_growth NUMERIC,
repurchase_rate NUMERIC,
repurchase_growth NUMERIC
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_days INTEGER := GREATEST(1, (p_end_date - p_start_date + 1));
v_prev_start DATE := p_start_date - v_days;
v_prev_end DATE := p_start_date - 1;
v_total INTEGER := 0;
v_new INTEGER := 0;
v_prev_new INTEGER := 0;
v_active INTEGER := 0;
v_prev_active INTEGER := 0;
v_repurchase INTEGER := 0;
v_prev_repurchase INTEGER := 0;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
-- 总用户数(统计兼容表)
SELECT COUNT(*)::INTEGER INTO v_total FROM public.users;
-- 新用户:按 users.created_at 的日期口径
SELECT COUNT(*)::INTEGER INTO v_new
FROM public.users
WHERE DATE(created_at) >= p_start_date AND DATE(created_at) <= p_end_date;
SELECT COUNT(*)::INTEGER INTO v_prev_new
FROM public.users
WHERE DATE(created_at) >= v_prev_start AND DATE(created_at) <= v_prev_end;
-- 活跃用户:最近活跃时间落在周期内的去重 user_id会话表
SELECT COUNT(DISTINCT user_id)::INTEGER INTO v_active
FROM public.user_sessions
WHERE DATE(last_active_at) >= p_start_date AND DATE(last_active_at) <= p_end_date
AND is_active = true;
SELECT COUNT(DISTINCT user_id)::INTEGER INTO v_prev_active
FROM public.user_sessions
WHERE DATE(last_active_at) >= v_prev_start AND DATE(last_active_at) <= v_prev_end
AND is_active = true;
-- 复购用户:周期内已支付订单数 >= 2 的用户
SELECT COUNT(*)::INTEGER INTO v_repurchase
FROM (
SELECT user_id
FROM public.orders
WHERE DATE(created_at) >= p_start_date AND DATE(created_at) <= p_end_date
AND status = 2
GROUP BY user_id
HAVING COUNT(*) >= 2
) t;
SELECT COUNT(*)::INTEGER INTO v_prev_repurchase
FROM (
SELECT user_id
FROM public.orders
WHERE DATE(created_at) >= v_prev_start AND DATE(created_at) <= v_prev_end
AND status = 2
GROUP BY user_id
HAVING COUNT(*) >= 2
) t;
RETURN QUERY
SELECT
v_total,
CASE WHEN v_total > 0 AND v_prev_new > 0 THEN ((v_new - v_prev_new)::NUMERIC / v_prev_new * 100) ELSE 0 END,
v_new,
CASE WHEN v_prev_new > 0 THEN ((v_new - v_prev_new)::NUMERIC / v_prev_new * 100) ELSE (CASE WHEN v_new > 0 THEN 100 ELSE 0 END) END,
CASE WHEN v_total > 0 THEN (v_active::NUMERIC / v_total * 100) ELSE 0 END,
CASE WHEN v_prev_active > 0 THEN ((v_active - v_prev_active)::NUMERIC / v_prev_active * 100) ELSE (CASE WHEN v_active > 0 THEN 100 ELSE 0 END) END,
CASE WHEN v_total > 0 THEN (v_repurchase::NUMERIC / v_total * 100) ELSE 0 END,
CASE WHEN v_prev_repurchase > 0 THEN ((v_repurchase - v_prev_repurchase)::NUMERIC / v_prev_repurchase * 100) ELSE (CASE WHEN v_repurchase > 0 THEN 100 ELSE 0 END) END;
END;
$$;
-- 4.8 用户增长趋势(按天)
CREATE OR REPLACE FUNCTION rpc_analytics_user_growth_trend(
p_start_date DATE,
p_end_date DATE
)
RETURNS TABLE (
date DATE,
new_users INTEGER,
total_users INTEGER
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_total_before INTEGER := 0;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'unauthorized';
END IF;
SELECT COUNT(*)::INTEGER INTO v_total_before
FROM public.users
WHERE DATE(created_at) < p_start_date;
RETURN QUERY
WITH days AS (
SELECT generate_series(p_start_date, p_end_date, interval '1 day')::date AS d
),
nu AS (
SELECT DATE(created_at) AS d, COUNT(*)::INTEGER AS c
FROM public.users
WHERE DATE(created_at) >= p_start_date AND DATE(created_at) <= p_end_date
GROUP BY DATE(created_at)
),
joined AS (
SELECT days.d, COALESCE(nu.c, 0) AS new_users
FROM days
LEFT JOIN nu ON nu.d = days.d
ORDER BY days.d
)
SELECT
joined.d AS date,
joined.new_users,
(v_total_before + SUM(joined.new_users) OVER (ORDER BY joined.d))::INTEGER AS total_users
FROM joined;
END;
$$;
-- ============================================
-- 5. 触发器(自动更新 updated_at
-- ============================================
-- 为需要的表添加触发器
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_analytics_user_preferences_updated_at') THEN
EXECUTE 'CREATE TRIGGER update_analytics_user_preferences_updated_at BEFORE UPDATE ON public.analytics_user_preferences FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column()';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_analytics_reports_updated_at') THEN
EXECUTE 'CREATE TRIGGER update_analytics_reports_updated_at BEFORE UPDATE ON public.analytics_reports FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column()';
END IF;
END $$;
-- ============================================
-- 完成
-- ============================================
-- ============================================
-- 6. RPC 授权收敛(只允许 authenticated 调用)
-- ============================================
REVOKE ALL ON FUNCTION rpc_analytics_realtime_kpis(TIMESTAMPTZ, TIMESTAMPTZ, TIMESTAMPTZ, TIMESTAMPTZ, UUID) FROM PUBLIC;
REVOKE ALL ON FUNCTION rpc_analytics_trend_data(DATE, DATE, UUID) FROM PUBLIC;
REVOKE ALL ON FUNCTION rpc_analytics_top_products(DATE, DATE, INTEGER, UUID) FROM PUBLIC;
REVOKE ALL ON FUNCTION rpc_analytics_top_merchants(DATE, DATE, INTEGER) FROM PUBLIC;
REVOKE ALL ON FUNCTION rpc_analytics_user_segments(DATE, DATE) FROM PUBLIC;
REVOKE ALL ON FUNCTION rpc_analytics_traffic_sources(DATE, DATE) FROM PUBLIC;
REVOKE ALL ON FUNCTION rpc_analytics_user_kpis(DATE, DATE) FROM PUBLIC;
REVOKE ALL ON FUNCTION rpc_analytics_user_growth_trend(DATE, DATE) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION rpc_analytics_realtime_kpis(TIMESTAMPTZ, TIMESTAMPTZ, TIMESTAMPTZ, TIMESTAMPTZ, UUID) TO authenticated;
GRANT EXECUTE ON FUNCTION rpc_analytics_trend_data(DATE, DATE, UUID) TO authenticated;
GRANT EXECUTE ON FUNCTION rpc_analytics_top_products(DATE, DATE, INTEGER, UUID) TO authenticated;
GRANT EXECUTE ON FUNCTION rpc_analytics_top_merchants(DATE, DATE, INTEGER) TO authenticated;
GRANT EXECUTE ON FUNCTION rpc_analytics_user_segments(DATE, DATE) TO authenticated;
GRANT EXECUTE ON FUNCTION rpc_analytics_traffic_sources(DATE, DATE) TO authenticated;
GRANT EXECUTE ON FUNCTION rpc_analytics_user_kpis(DATE, DATE) TO authenticated;
GRANT EXECUTE ON FUNCTION rpc_analytics_user_growth_trend(DATE, DATE) TO authenticated;
-- ============================================
-- 完成
-- ============================================
SELECT 'Analytics database schema created successfully!' AS message;