-- ============================================ -- 数据分析模块数据库 Schema(Supabase/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. RLS(Row 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;