mall数据库文件

This commit is contained in:
comlibmb
2026-01-30 16:17:13 +08:00
parent cfec4a16c0
commit 8f181b2b6a
42 changed files with 12758 additions and 2 deletions

View File

@@ -0,0 +1,853 @@
-- ============================================
-- 数据分析模块数据库 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;