854 lines
34 KiB
PL/PgSQL
854 lines
34 KiB
PL/PgSQL
-- ============================================
|
||
-- 数据分析模块数据库 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;
|