mall数据库文件
This commit is contained in:
223
pages/mall/analytics/test/01_create_coupon_tables.sql
Normal file
223
pages/mall/analytics/test/01_create_coupon_tables.sql
Normal file
@@ -0,0 +1,223 @@
|
||||
-- =============================================
|
||||
-- 优惠券业务基础表(最小集,用于优惠券效果分析)
|
||||
-- 说明:
|
||||
-- - 基于项目规格文档 pages/mall/mall.md 第4节“优惠券系统”抽象而来。
|
||||
-- - 仅创建 Analytics 所需的最小字段集合,兼容后续完整业务表替换。
|
||||
-- - 可安全重复执行(IF NOT EXISTS + 列存在性检查)。
|
||||
-- - 依赖:merchants、orders 表已存在(由 01_create_tables.sql 提供)。
|
||||
-- =============================================
|
||||
|
||||
-- 1) 优惠券模板表(coupon_templates)
|
||||
CREATE TABLE IF NOT EXISTS public.coupon_templates (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
coupon_type integer NOT NULL, -- 1..8:满减/折扣/免运费/新人/会员/品类/商家/限时(见 mall.md)
|
||||
discount_type integer NOT NULL, -- 满减 / 折扣 / 免运费 等类型枚举
|
||||
discount_value numeric(10,2) NOT NULL, -- 金额或折扣值
|
||||
min_order_amount numeric(10,2) DEFAULT 0,
|
||||
max_discount_amount numeric(10,2),
|
||||
total_quantity integer,
|
||||
per_user_limit integer DEFAULT 1,
|
||||
usage_limit integer DEFAULT 1,
|
||||
merchant_id uuid,
|
||||
category_ids jsonb,
|
||||
product_ids jsonb,
|
||||
user_type_limit integer,
|
||||
start_time timestamptz NOT NULL,
|
||||
end_time timestamptz NOT NULL,
|
||||
status integer DEFAULT 1,
|
||||
created_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 若缺少 merchant_id 外键且 merchants 表存在,则补充外键
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'merchants'
|
||||
) THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND t.relname = 'coupon_templates'
|
||||
AND c.conname = 'coupon_templates_merchant_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.coupon_templates
|
||||
ADD CONSTRAINT coupon_templates_merchant_id_fkey
|
||||
FOREIGN KEY (merchant_id) REFERENCES public.merchants(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON TABLE public.coupon_templates IS '优惠券模板表(业务权威表,来自 mall.md)';
|
||||
COMMENT ON COLUMN public.coupon_templates.coupon_type IS '券类型:1..8(满减/折扣/免运费/新人/会员/品类/商家/限时)';
|
||||
|
||||
|
||||
-- 2) 用户优惠券表(user_coupons)
|
||||
CREATE TABLE IF NOT EXISTS public.user_coupons (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid,
|
||||
template_id uuid REFERENCES public.coupon_templates(id) ON DELETE CASCADE,
|
||||
coupon_code varchar(50) UNIQUE NOT NULL,
|
||||
status integer DEFAULT 1, -- 1 未使用,2 已使用,3 已过期 等(与 mall.md 对齐)
|
||||
used_at timestamptz,
|
||||
order_id uuid,
|
||||
received_at timestamptz DEFAULT now(),
|
||||
expire_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 若 users 表存在则补 user_id 外键(保证幂等)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'users'
|
||||
) THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND t.relname = 'user_coupons'
|
||||
AND c.conname = 'user_coupons_user_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.user_coupons
|
||||
ADD CONSTRAINT user_coupons_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- 若 orders 表存在则补 order_id 外键
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'orders'
|
||||
) THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND t.relname = 'user_coupons'
|
||||
AND c.conname = 'user_coupons_order_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.user_coupons
|
||||
ADD CONSTRAINT user_coupons_order_id_fkey
|
||||
FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- 分析增强字段:发放渠道 obtain_channel
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'user_coupons' AND column_name = 'obtain_channel'
|
||||
) THEN
|
||||
ALTER TABLE public.user_coupons ADD COLUMN obtain_channel text;
|
||||
END IF;
|
||||
|
||||
-- 分析增强字段:冗余 merchant_id,便于按商家过滤
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'user_coupons' AND column_name = 'merchant_id'
|
||||
) THEN
|
||||
ALTER TABLE public.user_coupons ADD COLUMN merchant_id uuid;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON TABLE public.user_coupons IS '用户优惠券表(领取/使用/到期信息)';
|
||||
COMMENT ON COLUMN public.user_coupons.obtain_channel IS '发放渠道:manual/auto/campaign/invite/cs/points 等';
|
||||
|
||||
|
||||
-- 3) 优惠券使用记录表(coupon_usage_logs)
|
||||
CREATE TABLE IF NOT EXISTS public.coupon_usage_logs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_coupon_id uuid REFERENCES public.user_coupons(id) ON DELETE CASCADE,
|
||||
order_id uuid NOT NULL,
|
||||
discount_amount numeric(10,2) NOT NULL,
|
||||
used_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 若 orders 表存在则补 order_id 外键
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'orders'
|
||||
) THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND t.relname = 'coupon_usage_logs'
|
||||
AND c.conname = 'coupon_usage_logs_order_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.coupon_usage_logs
|
||||
ADD CONSTRAINT coupon_usage_logs_order_id_fkey
|
||||
FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- 分析增强字段:冗余 user_id / template_id / merchant_id / order_amount / created_at
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'coupon_usage_logs' AND column_name = 'user_id'
|
||||
) THEN
|
||||
ALTER TABLE public.coupon_usage_logs ADD COLUMN user_id uuid;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'coupon_usage_logs' AND column_name = 'template_id'
|
||||
) THEN
|
||||
ALTER TABLE public.coupon_usage_logs ADD COLUMN template_id uuid;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'coupon_usage_logs' AND column_name = 'merchant_id'
|
||||
) THEN
|
||||
ALTER TABLE public.coupon_usage_logs ADD COLUMN merchant_id uuid;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'coupon_usage_logs' AND column_name = 'order_amount'
|
||||
) THEN
|
||||
ALTER TABLE public.coupon_usage_logs ADD COLUMN order_amount numeric(10,2);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'coupon_usage_logs' AND column_name = 'created_at'
|
||||
) THEN
|
||||
ALTER TABLE public.coupon_usage_logs ADD COLUMN created_at timestamptz DEFAULT now();
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON TABLE public.coupon_usage_logs IS '优惠券使用记录表(单次核销流水,用于 GMV / ROI 分析)';
|
||||
|
||||
|
||||
-- 4) 索引(幂等)
|
||||
CREATE INDEX IF NOT EXISTS idx_coupon_templates_merchant_id ON public.coupon_templates(merchant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_coupons_user_id ON public.user_coupons(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_coupons_template_id ON public.user_coupons(template_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_coupons_status ON public.user_coupons(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_coupons_received_at ON public.user_coupons(received_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_coupons_expire_at ON public.user_coupons(expire_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_coupon_usage_logs_order_id ON public.coupon_usage_logs(order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_coupon_usage_logs_used_at ON public.coupon_usage_logs(used_at);
|
||||
|
||||
|
||||
-- 完成
|
||||
SELECT 'coupon tables ensured' AS message;
|
||||
|
||||
283
pages/mall/analytics/test/01_create_delivery_tables.sql
Normal file
283
pages/mall/analytics/test/01_create_delivery_tables.sql
Normal file
@@ -0,0 +1,283 @@
|
||||
-- =============================================
|
||||
-- 配送效率分析 - 配送相关基础表(建表 + 升级,支持重复执行)
|
||||
-- 说明:
|
||||
-- - 本脚本用于测试阶段,为配送效率分析页面提供必要的表结构。
|
||||
-- - 使用 CREATE TABLE IF NOT EXISTS + DO 块判断列/约束是否存在,实现可重复执行。
|
||||
-- - 外键:
|
||||
-- - driver_id -> ml_delivery_drivers(id) 必须存在。
|
||||
-- - user_id -> ak_users(id) 仅在 ak_users 表存在时才会创建外键(避免执行报错)。
|
||||
-- =============================================
|
||||
|
||||
-- 1) 创建基础表(若不存在)
|
||||
CREATE TABLE IF NOT EXISTS public.ml_delivery_drivers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
real_name TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.ml_delivery_tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 2) 补齐字段(若不存在)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- =========================
|
||||
-- ml_delivery_drivers
|
||||
-- =========================
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_drivers' AND column_name='user_id'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers ADD COLUMN user_id UUID;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_drivers' AND column_name='work_status'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers ADD COLUMN work_status INTEGER DEFAULT 1;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_drivers' AND column_name='rating_avg'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers ADD COLUMN rating_avg NUMERIC(3,2) DEFAULT 0;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_drivers' AND column_name='rating_count'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers ADD COLUMN rating_count INTEGER DEFAULT 0;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_drivers' AND column_name='updated_at'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers ADD COLUMN updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||
END IF;
|
||||
|
||||
-- =========================
|
||||
-- ml_delivery_tasks
|
||||
-- =========================
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_tasks' AND column_name='order_id'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks ADD COLUMN order_id UUID;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_tasks' AND column_name='driver_id'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks ADD COLUMN driver_id UUID;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_tasks' AND column_name='status'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks ADD COLUMN status INTEGER DEFAULT 1;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_tasks' AND column_name='assigned_at'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks ADD COLUMN assigned_at TIMESTAMPTZ;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_tasks' AND column_name='delivered_at'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks ADD COLUMN delivered_at TIMESTAMPTZ;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_tasks' AND column_name='delivery_fee'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks ADD COLUMN delivery_fee NUMERIC(10,2) DEFAULT 0;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='ml_delivery_tasks' AND column_name='updated_at'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks ADD COLUMN updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 3) 添加表/字段中文注释
|
||||
COMMENT ON TABLE public.ml_delivery_drivers IS '配送员表';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.id IS '配送员ID';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.user_id IS '关联用户ID(可选,若存在 ak_users 表则可建立外键)';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.real_name IS '真实姓名';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.work_status IS '工作状态:1在线 2忙碌 3离线';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.rating_avg IS '平均评分(0-5)';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.rating_count IS '评分次数';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.ml_delivery_drivers.updated_at IS '更新时间';
|
||||
|
||||
COMMENT ON TABLE public.ml_delivery_tasks IS '配送任务表(接单->送达)';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.id IS '任务ID';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.order_id IS '订单ID(测试阶段可空;正式可做 UNIQUE + 外键)';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.driver_id IS '配送员ID';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.status IS '状态:1待接 2已接 3取货 4配送中 5已送达 6配送失败';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.assigned_at IS '接单时间';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.delivered_at IS '送达时间';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.delivery_fee IS '配送费';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.ml_delivery_tasks.updated_at IS '更新时间';
|
||||
|
||||
-- 4) 添加约束(幂等)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- drivers: real_name 唯一
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='u' AND n.nspname='public' AND t.relname='ml_delivery_drivers' AND c.conname='ml_delivery_drivers_real_name_key'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers
|
||||
ADD CONSTRAINT ml_delivery_drivers_real_name_key UNIQUE (real_name);
|
||||
END IF;
|
||||
|
||||
-- drivers: work_status 枚举
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='c' AND n.nspname='public' AND t.relname='ml_delivery_drivers' AND c.conname='chk_ml_delivery_drivers_work_status'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers
|
||||
ADD CONSTRAINT chk_ml_delivery_drivers_work_status CHECK (work_status IN (1,2,3));
|
||||
END IF;
|
||||
|
||||
-- drivers: rating_avg 范围
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='c' AND n.nspname='public' AND t.relname='ml_delivery_drivers' AND c.conname='chk_ml_delivery_drivers_rating_avg'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers
|
||||
ADD CONSTRAINT chk_ml_delivery_drivers_rating_avg CHECK (rating_avg >= 0 AND rating_avg <= 5);
|
||||
END IF;
|
||||
|
||||
-- drivers: rating_count 非负
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='c' AND n.nspname='public' AND t.relname='ml_delivery_drivers' AND c.conname='chk_ml_delivery_drivers_rating_count'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers
|
||||
ADD CONSTRAINT chk_ml_delivery_drivers_rating_count CHECK (rating_count >= 0);
|
||||
END IF;
|
||||
|
||||
-- tasks: status 枚举
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='c' AND n.nspname='public' AND t.relname='ml_delivery_tasks' AND c.conname='chk_ml_delivery_tasks_status'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks
|
||||
ADD CONSTRAINT chk_ml_delivery_tasks_status CHECK (status IN (1,2,3,4,5,6));
|
||||
END IF;
|
||||
|
||||
-- tasks: delivery_fee 非负
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='c' AND n.nspname='public' AND t.relname='ml_delivery_tasks' AND c.conname='chk_ml_delivery_tasks_delivery_fee'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks
|
||||
ADD CONSTRAINT chk_ml_delivery_tasks_delivery_fee CHECK (delivery_fee >= 0);
|
||||
END IF;
|
||||
|
||||
-- tasks: 时间逻辑 delivered_at >= assigned_at
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='c' AND n.nspname='public' AND t.relname='ml_delivery_tasks' AND c.conname='chk_ml_delivery_tasks_time_logic'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks
|
||||
ADD CONSTRAINT chk_ml_delivery_tasks_time_logic CHECK (
|
||||
delivered_at IS NULL OR assigned_at IS NULL OR delivered_at >= assigned_at
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- tasks: driver_id 外键
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='f' AND n.nspname='public' AND t.relname='ml_delivery_tasks' AND c.conname='ml_delivery_tasks_driver_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks
|
||||
ADD CONSTRAINT ml_delivery_tasks_driver_id_fkey FOREIGN KEY (driver_id)
|
||||
REFERENCES public.ml_delivery_drivers(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
-- drivers: user_id 外键(仅当 ak_users 表存在时才创建)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema='public' AND table_name='ak_users'
|
||||
) THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='f' AND n.nspname='public' AND t.relname='ml_delivery_drivers' AND c.conname='ml_delivery_drivers_user_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers
|
||||
ADD CONSTRAINT ml_delivery_drivers_user_id_fkey FOREIGN KEY (user_id)
|
||||
REFERENCES public.ak_users(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- drivers: user_id 唯一(如果你不用 user_id,可以后续删除该约束)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='u' AND n.nspname='public' AND t.relname='ml_delivery_drivers' AND c.conname='ml_delivery_drivers_user_id_key'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_drivers
|
||||
ADD CONSTRAINT ml_delivery_drivers_user_id_key UNIQUE (user_id);
|
||||
END IF;
|
||||
|
||||
-- tasks: order_id 唯一(测试阶段允许 NULL,NULL 不冲突)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.contype='u' AND n.nspname='public' AND t.relname='ml_delivery_tasks' AND c.conname='ml_delivery_tasks_order_id_key'
|
||||
) THEN
|
||||
ALTER TABLE public.ml_delivery_tasks
|
||||
ADD CONSTRAINT ml_delivery_tasks_order_id_key UNIQUE (order_id);
|
||||
END IF;
|
||||
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 5) 索引(幂等)
|
||||
CREATE INDEX IF NOT EXISTS idx_ml_delivery_drivers_work_status ON public.ml_delivery_drivers(work_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_ml_delivery_tasks_status ON public.ml_delivery_tasks(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_ml_delivery_tasks_assigned_at ON public.ml_delivery_tasks(assigned_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ml_delivery_tasks_driver_id ON public.ml_delivery_tasks(driver_id);
|
||||
|
||||
-- 完成
|
||||
SELECT 'delivery tables ensured' AS message;
|
||||
303
pages/mall/analytics/test/01_create_tables.sql
Normal file
303
pages/mall/analytics/test/01_create_tables.sql
Normal file
@@ -0,0 +1,303 @@
|
||||
-- ============================================
|
||||
-- 数据分析实时大屏 - 基础业务表结构(创建版 / Create-only)
|
||||
-- ============================================
|
||||
-- 用途:创建业务核心表(orders, users, user_sessions, products, merchants 等)
|
||||
-- 特点:
|
||||
-- 1. 不做 DROP/DELETE/TRUNCATE(不清空数据)
|
||||
-- 2. 通过 IF NOT EXISTS + 系统表判断,实现可重复执行
|
||||
-- 3. 与 analytics_* 表(ANALYTICS_DB_SCHEMA.sql)配套使用
|
||||
-- ============================================
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 1. 表结构创建
|
||||
-- ============================================
|
||||
|
||||
-- 1.1 商家表
|
||||
CREATE TABLE IF NOT EXISTS merchants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
shop_name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE public.merchants IS '商家表';
|
||||
COMMENT ON COLUMN public.merchants.id IS '商家ID';
|
||||
COMMENT ON COLUMN public.merchants.shop_name IS '店铺名称';
|
||||
COMMENT ON COLUMN public.merchants.created_at IS '创建时间';
|
||||
|
||||
-- 1.2 商品表
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
merchant_id UUID REFERENCES merchants(id) ON DELETE SET NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
sales INTEGER DEFAULT 0,
|
||||
status INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE public.products IS '商品表';
|
||||
COMMENT ON COLUMN public.products.id IS '商品ID';
|
||||
COMMENT ON COLUMN public.products.merchant_id IS '所属商家ID';
|
||||
COMMENT ON COLUMN public.products.name IS '商品名称';
|
||||
COMMENT ON COLUMN public.products.price IS '价格';
|
||||
COMMENT ON COLUMN public.products.sales IS '销量';
|
||||
COMMENT ON COLUMN public.products.status IS '状态(1:上架, 0:下架)';
|
||||
COMMENT ON COLUMN public.products.created_at IS '创建时间';
|
||||
|
||||
-- 1.3 用户(统计兼容)表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY,
|
||||
phone VARCHAR(20) UNIQUE,
|
||||
email VARCHAR(255),
|
||||
nickname VARCHAR(100),
|
||||
last_login_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE public.users IS '用户(统计兼容表,不作为权威用户表)';
|
||||
COMMENT ON COLUMN public.users.id IS '用户ID(建议与 auth.users.id/ak_users.id 对齐)';
|
||||
COMMENT ON COLUMN public.users.phone IS '手机号(可选)';
|
||||
COMMENT ON COLUMN public.users.email IS '邮箱(可选)';
|
||||
COMMENT ON COLUMN public.users.nickname IS '昵称';
|
||||
COMMENT ON COLUMN public.users.last_login_at IS '最后登录时间';
|
||||
COMMENT ON COLUMN public.users.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.users.updated_at IS '更新时间';
|
||||
|
||||
-- 1.4 订单表
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
merchant_id UUID REFERENCES merchants(id) ON DELETE SET NULL,
|
||||
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||
status INTEGER NOT NULL DEFAULT 0,
|
||||
payment_method VARCHAR(50),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE public.orders IS '订单表';
|
||||
COMMENT ON COLUMN public.orders.id IS '订单ID';
|
||||
COMMENT ON COLUMN public.orders.user_id IS '用户ID';
|
||||
COMMENT ON COLUMN public.orders.merchant_id IS '商家ID';
|
||||
COMMENT ON COLUMN public.orders.total_amount IS '订单总金额';
|
||||
COMMENT ON COLUMN public.orders.status IS '订单状态(0:待支付, 1:已支付, 2:已完成, 3:已取消)';
|
||||
COMMENT ON COLUMN public.orders.payment_method IS '支付方式';
|
||||
COMMENT ON COLUMN public.orders.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.orders.updated_at IS '更新时间';
|
||||
|
||||
-- 1.5 订单商品关联表
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
|
||||
quantity INTEGER NOT NULL DEFAULT 1,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE public.order_items IS '订单商品关联表';
|
||||
COMMENT ON COLUMN public.order_items.id IS '主键';
|
||||
COMMENT ON COLUMN public.order_items.order_id IS '订单ID';
|
||||
COMMENT ON COLUMN public.order_items.product_id IS '商品ID';
|
||||
COMMENT ON COLUMN public.order_items.quantity IS '数量';
|
||||
COMMENT ON COLUMN public.order_items.price IS '单价';
|
||||
COMMENT ON COLUMN public.order_items.total_amount IS '总价';
|
||||
COMMENT ON COLUMN public.order_items.created_at IS '创建时间';
|
||||
|
||||
-- 1.6 用户会话表
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
session_token VARCHAR(255) UNIQUE,
|
||||
last_active_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE public.user_sessions IS '用户会话表(用于在线用户统计)';
|
||||
COMMENT ON COLUMN public.user_sessions.id IS '会话ID';
|
||||
COMMENT ON COLUMN public.user_sessions.user_id IS '用户ID';
|
||||
COMMENT ON COLUMN public.user_sessions.session_token IS '会话Token';
|
||||
COMMENT ON COLUMN public.user_sessions.last_active_at IS '最后活跃时间';
|
||||
COMMENT ON COLUMN public.user_sessions.is_active IS '是否活跃';
|
||||
COMMENT ON COLUMN public.user_sessions.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN public.user_sessions.user_agent IS '用户代理';
|
||||
COMMENT ON COLUMN public.user_sessions.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.user_sessions.updated_at IS '更新时间';
|
||||
|
||||
-- 1.7 访问日志表
|
||||
CREATE TABLE IF NOT EXISTS page_views (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
path VARCHAR(255),
|
||||
source VARCHAR(50) DEFAULT 'direct',
|
||||
referrer VARCHAR(255),
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE public.page_views IS '访问日志表(用于转化率分析)';
|
||||
COMMENT ON COLUMN public.page_views.id IS '主键';
|
||||
COMMENT ON COLUMN public.page_views.user_id IS '用户ID(可空,表示匿名访问)';
|
||||
COMMENT ON COLUMN public.page_views.path IS '访问路径';
|
||||
COMMENT ON COLUMN public.page_views.source IS '流量来源(direct/search/social/ad)';
|
||||
COMMENT ON COLUMN public.page_views.referrer IS '来源页面';
|
||||
COMMENT ON COLUMN public.page_views.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN public.page_views.user_agent IS '用户代理';
|
||||
COMMENT ON COLUMN public.page_views.created_at IS '创建时间';
|
||||
|
||||
-- ============================================
|
||||
-- 2. 索引创建
|
||||
-- ============================================
|
||||
|
||||
-- orders
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_created_at_status ON orders(created_at, status);
|
||||
|
||||
-- user_sessions
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_last_active_at ON user_sessions(last_active_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_is_active ON user_sessions(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_created_at ON user_sessions(created_at);
|
||||
|
||||
-- users
|
||||
CREATE INDEX IF NOT EXISTS idx_users_last_login_at ON users(last_login_at);
|
||||
|
||||
-- order_items
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_product_id ON order_items(product_id);
|
||||
|
||||
-- page_views
|
||||
CREATE INDEX IF NOT EXISTS idx_page_views_user_id ON page_views(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_page_views_created_at ON page_views(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_page_views_source ON page_views(source);
|
||||
|
||||
-- ============================================
|
||||
-- 3. 触发器函数和触发器
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 为需要 updated_at 的表添加触发器
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_orders_updated_at') THEN
|
||||
EXECUTE 'CREATE TRIGGER update_orders_updated_at BEFORE UPDATE ON public.orders FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column()';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_user_sessions_updated_at') THEN
|
||||
EXECUTE 'CREATE TRIGGER update_user_sessions_updated_at BEFORE UPDATE ON public.user_sessions FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column()';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_users_updated_at') THEN
|
||||
EXECUTE 'CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column()';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- 4. 行级安全策略(RLS)
|
||||
-- ============================================
|
||||
|
||||
-- 启用 RLS
|
||||
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE user_sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE merchants ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE order_items ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE page_views ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- orders: 用户只能查看和管理自己的订单
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='orders' AND policyname='Users can view own orders') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can view own orders" ON public.orders FOR SELECT USING (auth.uid() = user_id)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='orders' AND policyname='Users can insert own orders') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can insert own orders" ON public.orders FOR INSERT WITH CHECK (auth.uid() = user_id)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='orders' AND policyname='Users can update own orders') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can update own orders" ON public.orders FOR UPDATE USING (auth.uid() = user_id)';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- order_items: 用户只能查看自己订单的商品
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='order_items' AND policyname='Users can view own order items') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can view own order items" ON public.order_items FOR SELECT USING (EXISTS (SELECT 1 FROM public.orders WHERE public.orders.id = public.order_items.order_id AND public.orders.user_id = auth.uid()))';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='order_items' AND policyname='Users can insert own order items') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can insert own order items" ON public.order_items FOR INSERT WITH CHECK (EXISTS (SELECT 1 FROM public.orders WHERE public.orders.id = public.order_items.order_id AND public.orders.user_id = auth.uid()))';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- user_sessions: 用户只能查看和管理自己的会话
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='user_sessions' AND policyname='Users can view own sessions') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can view own sessions" ON public.user_sessions FOR SELECT USING (auth.uid() = user_id)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='user_sessions' AND policyname='Users can insert own sessions') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can insert own sessions" ON public.user_sessions FOR INSERT WITH CHECK (auth.uid() = user_id)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='user_sessions' AND policyname='Users can update own sessions') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can update own sessions" ON public.user_sessions FOR UPDATE USING (auth.uid() = user_id)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='user_sessions' AND policyname='Users can delete own sessions') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can delete own sessions" ON public.user_sessions FOR DELETE USING (auth.uid() = user_id)';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- users: 用户只能查看和管理自己的记录
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='users' AND policyname='Users can view own profile') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can view own profile" ON public.users FOR SELECT USING (auth.uid() = id)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='users' AND policyname='Users can insert own profile') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can insert own profile" ON public.users FOR INSERT WITH CHECK (auth.uid() = id)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='users' AND policyname='Users can update own profile') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can update own profile" ON public.users FOR UPDATE USING (auth.uid() = id)';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- products: 任何人可读,认证用户可管理(简化策略)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='products' AND policyname='Anyone can view products') THEN
|
||||
EXECUTE 'CREATE POLICY "Anyone can view products" ON public.products FOR SELECT USING (true)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='products' AND policyname='Authenticated can manage products') THEN
|
||||
EXECUTE 'CREATE POLICY "Authenticated can manage products" ON public.products FOR ALL USING (auth.role() = ''authenticated'')';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- merchants: 任何人可读,认证用户可管理(简化策略)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='merchants' AND policyname='Anyone can view merchants') THEN
|
||||
EXECUTE 'CREATE POLICY "Anyone can view merchants" ON public.merchants FOR SELECT USING (true)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='merchants' AND policyname='Authenticated can manage merchants') THEN
|
||||
EXECUTE 'CREATE POLICY "Authenticated can manage merchants" ON public.merchants FOR ALL USING (auth.role() = ''authenticated'')';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- page_views: 任何人可插入,用户只能读自己的记录
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='page_views' AND policyname='Anyone can insert page views') THEN
|
||||
EXECUTE 'CREATE POLICY "Anyone can insert page views" ON public.page_views FOR INSERT WITH CHECK (true)';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='page_views' AND policyname='Users can view own page views') THEN
|
||||
EXECUTE 'CREATE POLICY "Users can view own page views" ON public.page_views FOR SELECT USING (auth.uid() = user_id)';
|
||||
END IF;
|
||||
END $$;
|
||||
182
pages/mall/analytics/test/02_insert_coupon_test_data.sql
Normal file
182
pages/mall/analytics/test/02_insert_coupon_test_data.sql
Normal file
@@ -0,0 +1,182 @@
|
||||
-- =============================================
|
||||
-- 优惠券分析测试数据 Seed(至少 20 条样本)
|
||||
-- 说明:
|
||||
-- - 依赖表:coupon_templates, user_coupons, coupon_usage_logs, users, merchants, orders
|
||||
-- - 用于驱动 /pages/mall/analytics/coupon-analysis.uvue 页面联调
|
||||
-- - 设计目标:
|
||||
-- - 8 种券类型覆盖(满减/折扣/免运费/新人/会员/品类/商家/限时)
|
||||
-- - 6 种发放渠道覆盖(manual/auto/campaign/invite/cs/points)
|
||||
-- - 最近 30 天有发放 + 使用 + 未使用 + 即将到期券
|
||||
-- - 可重复执行:通过 WHERE NOT EXISTS / 固定模板名称 避免重复插入
|
||||
-- =============================================
|
||||
|
||||
-- 1) 准备一个测试商家(如不存在则创建)
|
||||
INSERT INTO public.merchants (id, shop_name, created_at)
|
||||
SELECT
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
'优惠券测试商家',
|
||||
now() - interval '60 days'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.merchants WHERE id = '30000000-0000-0000-0000-000000000001'
|
||||
);
|
||||
|
||||
-- 2) 准备 3 个测试用户(如不存在则创建)
|
||||
INSERT INTO public.users (id, phone, email, nickname, created_at)
|
||||
VALUES
|
||||
('40000000-0000-0000-0000-000000000001'::uuid, '13900001001', 'coupon_user1@test.com', '券用户1', now() - interval '30 days'),
|
||||
('40000000-0000-0000-0000-000000000002'::uuid, '13900001002', 'coupon_user2@test.com', '券用户2', now() - interval '20 days'),
|
||||
('40000000-0000-0000-0000-000000000003'::uuid, '13900001003', 'coupon_user3@test.com', '券用户3', now() - interval '10 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3) 插入 8 个优惠券模板(覆盖 mall.md 的 8 种类型)
|
||||
INSERT INTO public.coupon_templates (
|
||||
id, name, description, coupon_type, discount_type, discount_value,
|
||||
min_order_amount, max_discount_amount, total_quantity, per_user_limit,
|
||||
usage_limit, merchant_id, start_time, end_time, status, created_at
|
||||
)
|
||||
VALUES
|
||||
('50000000-0000-0000-0000-000000000001'::uuid, '满100减20', '满100减20元', 1, 1, 20.00, 100.00, NULL, 1000, 3, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '30 days', now() + interval '30 days', 1, now() - interval '30 days'),
|
||||
('50000000-0000-0000-0000-000000000002'::uuid, '9折优惠券', '全场9折', 2, 2, 9.00, 0.00, 50.00, 1000, 3, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '30 days', now() + interval '30 days', 1, now() - interval '30 days'),
|
||||
('50000000-0000-0000-0000-000000000003'::uuid, '免运费券', '免配送费', 3, 3, 0.00, 0.00, 10.00, 1000, 5, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '30 days', now() + interval '30 days', 1, now() - interval '30 days'),
|
||||
('50000000-0000-0000-0000-000000000004'::uuid, '新人专享满50减25', '新用户首单优惠', 4, 1, 25.00, 50.00, NULL, 500, 1, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '15 days', now() + interval '45 days', 1, now() - interval '15 days'),
|
||||
('50000000-0000-0000-0000-000000000005'::uuid, '会员折扣券', '会员专享8.5折', 5, 2, 8.50, 0.00, 80.00, 500, 5, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '20 days', now() + interval '40 days', 1, now() - interval '20 days'),
|
||||
('50000000-0000-0000-0000-000000000006'::uuid, '生鲜品类券', '生鲜类满60减15', 6, 1, 15.00, 60.00, NULL, 500, 3, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '25 days', now() + interval '35 days', 1, now() - interval '25 days'),
|
||||
('50000000-0000-0000-0000-000000000007'::uuid, '商家专属券', '指定商家满80减30', 7, 1, 30.00, 80.00, NULL, 300, 3, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '10 days', now() + interval '50 days', 1, now() - interval '10 days'),
|
||||
('50000000-0000-0000-0000-000000000008'::uuid, '限时秒杀券', '限时满40减10', 8, 1, 10.00, 40.00, NULL, 300, 2, 1,
|
||||
'30000000-0000-0000-0000-000000000001', now() - interval '5 days', now() + interval '10 days', 1, now() - interval '5 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
-- 4) 生成用户优惠券 + 使用数据
|
||||
DO $$
|
||||
DECLARE
|
||||
v_users uuid[] := ARRAY[
|
||||
'40000000-0000-0000-0000-000000000001'::uuid,
|
||||
'40000000-0000-0000-0000-000000000002'::uuid,
|
||||
'40000000-0000-0000-0000-000000000003'::uuid
|
||||
];
|
||||
v_templates uuid[] := ARRAY[
|
||||
'50000000-0000-0000-0000-000000000001'::uuid,
|
||||
'50000000-0000-0000-0000-000000000002'::uuid,
|
||||
'50000000-0000-0000-0000-000000000003'::uuid,
|
||||
'50000000-0000-0000-0000-000000000004'::uuid,
|
||||
'50000000-0000-0000-0000-000000000005'::uuid,
|
||||
'50000000-0000-0000-0000-000000000006'::uuid,
|
||||
'50000000-0000-0000-0000-000000000007'::uuid,
|
||||
'50000000-0000-0000-0000-000000000008'::uuid
|
||||
];
|
||||
v_channels text[] := ARRAY['manual','auto','campaign','invite','cs','points'];
|
||||
|
||||
i int;
|
||||
j int;
|
||||
v_uc_id uuid;
|
||||
v_user uuid;
|
||||
v_tpl uuid;
|
||||
v_channel text;
|
||||
v_received_at timestamptz;
|
||||
v_expire_at timestamptz;
|
||||
v_order_id uuid;
|
||||
v_order_amount numeric;
|
||||
v_discount_amount numeric;
|
||||
v_used_at timestamptz;
|
||||
BEGIN
|
||||
-- 为最近 30 天生成多条券(部分使用,部分未使用,部分即将到期)
|
||||
FOR i IN 0..29 LOOP
|
||||
-- 当天发放 2~4 条券
|
||||
FOR j IN 1..(2 + (i % 3)) LOOP
|
||||
v_user := v_users[1 + (i + j) % array_length(v_users, 1)];
|
||||
v_tpl := v_templates[1 + (i + j) % array_length(v_templates, 1)];
|
||||
v_channel := v_channels[1 + (i + j) % array_length(v_channels, 1)];
|
||||
|
||||
v_received_at := (now() - (i || ' days')::interval);
|
||||
v_expire_at := v_received_at + interval '20 days';
|
||||
|
||||
-- 通过 coupon_code 幂等插入,并返回最终 id(避免 ON CONFLICT DO NOTHING 导致 v_uc_id 无对应记录)
|
||||
INSERT INTO public.user_coupons (
|
||||
id, user_id, template_id, coupon_code, status,
|
||||
received_at, expire_at, obtain_channel, merchant_id
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
v_user,
|
||||
v_tpl,
|
||||
'TESTCOUPON-' || i || '-' || j,
|
||||
1, -- 初始未使用
|
||||
v_received_at,
|
||||
v_expire_at,
|
||||
v_channel,
|
||||
'30000000-0000-0000-0000-000000000001'::uuid
|
||||
)
|
||||
ON CONFLICT (coupon_code) DO UPDATE
|
||||
SET user_id = EXCLUDED.user_id,
|
||||
template_id = EXCLUDED.template_id,
|
||||
received_at = EXCLUDED.received_at,
|
||||
expire_at = EXCLUDED.expire_at,
|
||||
obtain_channel= EXCLUDED.obtain_channel,
|
||||
merchant_id = EXCLUDED.merchant_id
|
||||
RETURNING id INTO v_uc_id;
|
||||
|
||||
-- 约一半的券会在 0~10 天内被使用,生成对应订单和使用记录
|
||||
IF (i + j) % 2 = 0 THEN
|
||||
v_order_id := gen_random_uuid();
|
||||
v_order_amount := 50 + (10 * ((i + j) % 5)); -- 50~90 的订单金额
|
||||
v_discount_amount := CASE
|
||||
WHEN (i + j) % 4 = 0 THEN 10
|
||||
WHEN (i + j) % 4 = 1 THEN 15
|
||||
WHEN (i + j) % 4 = 2 THEN 20
|
||||
ELSE 5
|
||||
END;
|
||||
v_used_at := v_received_at + ( (1 + (i % 5)) || ' days')::interval;
|
||||
|
||||
-- 插入订单
|
||||
INSERT INTO public.orders (
|
||||
id, user_id, merchant_id, total_amount, status, created_at, updated_at
|
||||
) VALUES (
|
||||
v_order_id,
|
||||
v_user,
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
v_order_amount,
|
||||
2, -- 已完成
|
||||
v_used_at,
|
||||
v_used_at
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 更新 user_coupons 为已使用(确认对应记录存在)
|
||||
UPDATE public.user_coupons
|
||||
SET status = 2,
|
||||
used_at = v_used_at,
|
||||
order_id = v_order_id
|
||||
WHERE id = v_uc_id;
|
||||
|
||||
-- 插入使用流水(仅当对应 user_coupon 存在时生效)
|
||||
INSERT INTO public.coupon_usage_logs (
|
||||
id, user_coupon_id, order_id, discount_amount, used_at,
|
||||
user_id, template_id, merchant_id, order_amount, created_at
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
v_uc_id,
|
||||
v_order_id,
|
||||
v_discount_amount,
|
||||
v_used_at,
|
||||
v_user,
|
||||
v_tpl,
|
||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||
v_order_amount,
|
||||
v_used_at
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 完成
|
||||
SELECT 'coupon test data seed completed!' AS message;
|
||||
|
||||
72
pages/mall/analytics/test/02_insert_delivery_test_data.sql
Normal file
72
pages/mall/analytics/test/02_insert_delivery_test_data.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
-- =============================================
|
||||
-- 配送效率分析 - 测试数据插入脚本
|
||||
-- 说明:
|
||||
-- - 用于生成配送员 + 近 30 天配送任务(已送达),便于配送效率分析页面展示趋势与排行。
|
||||
-- - 可重复执行:配送员按 real_name 唯一键去重;任务默认会持续累加。
|
||||
-- - 如需每次重置数据,请取消 TRUNCATE 注释。
|
||||
-- =============================================
|
||||
|
||||
-- For a clean test, you can uncomment these lines to clear old data.
|
||||
-- TRUNCATE public.ml_delivery_tasks, public.ml_delivery_drivers RESTART IDENTITY;
|
||||
|
||||
-- 1) Insert a pool of drivers.
|
||||
-- ON CONFLICT ensures this is safe to run multiple times.
|
||||
INSERT INTO public.ml_delivery_drivers (real_name, work_status, rating_avg, rating_count)
|
||||
VALUES
|
||||
('张配送', 1, 4.8, 32),
|
||||
('李骑手', 1, 4.5, 20),
|
||||
('王师傅', 2, 4.9, 150),
|
||||
('赵快递', 3, 4.7, 88),
|
||||
('刘哥', 1, 4.6, 54),
|
||||
('新人小陈', 1, 3.9, 12)
|
||||
ON CONFLICT (real_name) DO NOTHING;
|
||||
|
||||
|
||||
-- 2) Generate tasks for the last 30 days.
|
||||
DO $$
|
||||
DECLARE
|
||||
driver_ids UUID[];
|
||||
d_id UUID;
|
||||
i INT;
|
||||
j INT;
|
||||
num_tasks INT;
|
||||
assigned_time TIMESTAMPTZ;
|
||||
delivery_duration INT; -- in minutes
|
||||
BEGIN
|
||||
-- Get all driver IDs into an array
|
||||
SELECT array_agg(id) INTO driver_ids FROM public.ml_delivery_drivers;
|
||||
|
||||
IF array_length(driver_ids, 1) IS NULL THEN
|
||||
RAISE NOTICE 'No drivers found, skipping task generation.';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Loop for the last 30 days
|
||||
FOR i IN 0..29 LOOP
|
||||
-- Create a random number of tasks for each day
|
||||
num_tasks := floor(random() * 10 + 5)::INT; -- 5 to 14 tasks per day
|
||||
FOR j IN 1..num_tasks LOOP
|
||||
-- Pick a random driver
|
||||
d_id := driver_ids[floor(random() * array_length(driver_ids, 1) + 1)];
|
||||
|
||||
-- Generate a random time within the day (from 8am to 10pm)
|
||||
assigned_time := (NOW() - (i || ' days')::INTERVAL)::date
|
||||
+ '8 hours'::interval
|
||||
+ (floor(random() * 14 * 60) || ' minutes')::interval;
|
||||
|
||||
-- Generate random delivery duration (10 to 50 minutes)
|
||||
delivery_duration := floor(random() * 41 + 10);
|
||||
|
||||
INSERT INTO public.ml_delivery_tasks (order_id, driver_id, status, assigned_at, delivered_at, delivery_fee)
|
||||
VALUES (
|
||||
gen_random_uuid(), -- Generate a unique order_id for each task
|
||||
d_id,
|
||||
5, -- Status 'Delivered'
|
||||
assigned_time,
|
||||
assigned_time + (delivery_duration || ' minutes')::INTERVAL,
|
||||
floor(random() * 8 + 3) -- Fee between 3 and 10
|
||||
);
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
160
pages/mall/analytics/test/02_insert_market_trends_test_data.sql
Normal file
160
pages/mall/analytics/test/02_insert_market_trends_test_data.sql
Normal file
@@ -0,0 +1,160 @@
|
||||
-- =============================================
|
||||
-- 市场趋势页面测试数据 Seed(Supabase / Postgres)
|
||||
-- 说明:
|
||||
-- - 依赖表:users, merchants, categories, products, orders, order_items
|
||||
-- - 用于 /pages/mall/analytics/market-trends.uvue 联调
|
||||
-- - 目标:
|
||||
-- * 最近 30 天内至少 20 条订单
|
||||
-- * 覆盖多个商家、多类目、多价格区间
|
||||
-- =============================================
|
||||
|
||||
-- 1) 准备 2 个测试商家
|
||||
INSERT INTO public.merchants (id, shop_name, created_at)
|
||||
VALUES
|
||||
('31000000-0000-0000-0000-000000000001'::uuid, '市场趋势测试商家A', now() - interval '120 days'),
|
||||
('31000000-0000-0000-0000-000000000002'::uuid, '市场趋势测试商家B', now() - interval '120 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2) 准备 3 个测试用户
|
||||
INSERT INTO public.users (id, phone, email, nickname, created_at)
|
||||
VALUES
|
||||
('41000000-0000-0000-0000-000000000001'::uuid, '13910001001', 'market_user1@test.com', '市场用户1', now() - interval '60 days'),
|
||||
('41000000-0000-0000-0000-000000000002'::uuid, '13910001002', 'market_user2@test.com', '市场用户2', now() - interval '40 days'),
|
||||
('41000000-0000-0000-0000-000000000003'::uuid, '13910001003', 'market_user3@test.com', '市场用户3', now() - interval '20 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3) 准备 4 个商品分类
|
||||
-- 若 categories 表不存在,则创建一个最小版(仅供分析用)
|
||||
CREATE TABLE IF NOT EXISTS public.categories (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
created_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO public.categories (id, name, created_at)
|
||||
VALUES
|
||||
('32000000-0000-0000-0000-000000000001'::uuid, '生鲜食品', now() - interval '100 days'),
|
||||
('32000000-0000-0000-0000-000000000002'::uuid, '日用百货', now() - interval '100 days'),
|
||||
('32000000-0000-0000-0000-000000000003'::uuid, '数码家电', now() - interval '100 days'),
|
||||
('32000000-0000-0000-0000-000000000004'::uuid, '服饰鞋帽', now() - interval '100 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 4) 准备若干商品(分布在不同类目和商家)
|
||||
-- 如果 products 表还没有 category_id / stock 字段,这里按 mall.md 的设计进行补齐
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'products'
|
||||
AND column_name = 'category_id'
|
||||
) THEN
|
||||
ALTER TABLE public.products ADD COLUMN category_id uuid;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'products'
|
||||
AND column_name = 'stock'
|
||||
) THEN
|
||||
ALTER TABLE public.products ADD COLUMN stock integer DEFAULT 0;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO public.products (id, merchant_id, category_id, name, price, stock, status, created_at)
|
||||
VALUES
|
||||
('33000000-0000-0000-0000-000000000001'::uuid, '31000000-0000-0000-0000-000000000001', '32000000-0000-0000-0000-000000000001', '有机蔬菜礼包', 39.90, 1000, 1, now() - interval '90 days'),
|
||||
('33000000-0000-0000-0000-000000000002'::uuid, '31000000-0000-0000-0000-000000000001', '32000000-0000-0000-0000-000000000002', '纸巾家庭装', 19.90, 1000, 1, now() - interval '90 days'),
|
||||
('33000000-0000-0000-0000-000000000003'::uuid, '31000000-0000-0000-0000-000000000001', '32000000-0000-0000-0000-000000000003', '蓝牙耳机', 199.00, 500, 1, now() - interval '90 days'),
|
||||
('33000000-0000-0000-0000-000000000004'::uuid, '31000000-0000-0000-0000-000000000002', '32000000-0000-0000-0000-000000000004', '运动T恤', 79.00, 800, 1, now() - interval '90 days'),
|
||||
('33000000-0000-0000-0000-000000000005'::uuid, '31000000-0000-0000-0000-000000000002', '32000000-0000-0000-0000-000000000002', '洗衣液', 59.00, 800, 1, now() - interval '90 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 5) 生成最近 30 天订单 + 订单商品(至少 30 笔,满足 RPC 需要)
|
||||
DO $$
|
||||
DECLARE
|
||||
v_users uuid[] := ARRAY[
|
||||
'41000000-0000-0000-0000-000000000001'::uuid,
|
||||
'41000000-0000-0000-0000-000000000002'::uuid,
|
||||
'41000000-0000-0000-0000-000000000003'::uuid
|
||||
];
|
||||
v_merchants uuid[] := ARRAY[
|
||||
'31000000-0000-0000-0000-000000000001'::uuid,
|
||||
'31000000-0000-0000-0000-000000000002'::uuid
|
||||
];
|
||||
v_products uuid[] := ARRAY[
|
||||
'33000000-0000-0000-0000-000000000001'::uuid,
|
||||
'33000000-0000-0000-0000-000000000002'::uuid,
|
||||
'33000000-0000-0000-0000-000000000003'::uuid,
|
||||
'33000000-0000-0000-0000-000000000004'::uuid,
|
||||
'33000000-0000-0000-0000-000000000005'::uuid
|
||||
];
|
||||
|
||||
i int;
|
||||
j int;
|
||||
day_orders int;
|
||||
v_user uuid;
|
||||
v_merchant uuid;
|
||||
v_product uuid;
|
||||
v_order_id uuid;
|
||||
v_qty int;
|
||||
v_price numeric;
|
||||
v_total numeric;
|
||||
v_created_at timestamptz;
|
||||
BEGIN
|
||||
FOR i IN 0..29 LOOP
|
||||
-- 每天 1~3 笔订单
|
||||
day_orders := 1 + (i % 3);
|
||||
FOR j IN 1..day_orders LOOP
|
||||
v_user := v_users[1 + (i + j) % array_length(v_users, 1)];
|
||||
v_merchant := v_merchants[1 + (i + j) % array_length(v_merchants, 1)];
|
||||
v_product := v_products[1 + (i + j) % array_length(v_products, 1)];
|
||||
|
||||
v_qty := 1 + ((i + j) % 3); -- 1~3 件
|
||||
SELECT price INTO v_price FROM public.products WHERE id = v_product;
|
||||
IF v_price IS NULL THEN
|
||||
v_price := 50;
|
||||
END IF;
|
||||
v_total := v_price * v_qty;
|
||||
|
||||
v_created_at := (now() - (i || ' days')::interval)
|
||||
- interval '12 hours'
|
||||
+ ( (j * 4) || ' hours')::interval;
|
||||
|
||||
v_order_id := gen_random_uuid();
|
||||
|
||||
INSERT INTO public.orders (
|
||||
id, user_id, merchant_id, total_amount, status,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
v_order_id,
|
||||
v_user,
|
||||
v_merchant,
|
||||
v_total,
|
||||
2, -- 已完成
|
||||
v_created_at,
|
||||
v_created_at
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.order_items (
|
||||
id, order_id, product_id, quantity, price, total_amount, created_at
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
v_order_id,
|
||||
v_product,
|
||||
v_qty,
|
||||
v_price,
|
||||
v_total,
|
||||
v_created_at
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT 'market trends test data seed completed!' AS message;
|
||||
|
||||
172
pages/mall/analytics/test/02_insert_test_data.sql
Normal file
172
pages/mall/analytics/test/02_insert_test_data.sql
Normal file
@@ -0,0 +1,172 @@
|
||||
-- ============================================
|
||||
-- 数据分析实时大屏 - 测试数据插入脚本
|
||||
-- ============================================
|
||||
-- 注意:由于启用了 RLS(行级安全策略),直接插入数据可能会失败
|
||||
-- 解决方案:
|
||||
-- 1. 在 Supabase Dashboard 的 SQL Editor 中执行(使用 service_role 权限)
|
||||
-- 2. 或者临时禁用 RLS(见下方说明)
|
||||
-- ============================================
|
||||
|
||||
-- 方式一:临时禁用 RLS(仅用于测试数据插入,完成后会重新启用)
|
||||
-- 注意:在 Supabase Dashboard 的 SQL Editor 中执行时,通常不需要禁用 RLS
|
||||
-- 如果需要,可以取消下面的注释:
|
||||
|
||||
-- BEGIN;
|
||||
-- ALTER TABLE orders DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE user_sessions DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE users DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE products DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE merchants DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE order_items DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE page_views DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 清空现有测试数据(可选,谨慎使用)
|
||||
-- TRUNCATE TABLE orders, user_sessions, users, order_items, page_views CASCADE;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 插入测试用户
|
||||
-- ============================================
|
||||
INSERT INTO users (id, phone, email, nickname, last_login_at) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', '13800000001', 'user1@test.com', '测试用户1', NOW() - INTERVAL '2 minutes'),
|
||||
('22222222-2222-2222-2222-222222222222', '13800000002', 'user2@test.com', '测试用户2', NOW() - INTERVAL '5 minutes'),
|
||||
('33333333-3333-3333-3333-333333333333', '13800000003', 'user3@test.com', '测试用户3', NOW() - INTERVAL '10 minutes'),
|
||||
('44444444-4444-4444-4444-444444444444', '13800000004', 'user4@test.com', '测试用户4', NOW() - INTERVAL '1 hour'),
|
||||
('55555555-5555-5555-5555-555555555555', '13800000005', 'user5@test.com', '测试用户5', NOW() - INTERVAL '30 minutes'),
|
||||
('66666666-6666-6666-6666-666666666666', '13800000006', 'user6@test.com', '测试用户6', NOW() - INTERVAL '3 minutes'),
|
||||
('77777777-7777-7777-7777-777777777777', '13800000007', 'user7@test.com', '测试用户7', NOW() - INTERVAL '8 minutes'),
|
||||
('88888888-8888-8888-8888-888888888888', '13800000008', 'user8@test.com', '测试用户8', NOW() - INTERVAL '15 minutes')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 1.5 插入测试商家
|
||||
-- ============================================
|
||||
INSERT INTO merchants (id, shop_name) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', '测试商家A'),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', '测试商家B')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 1.6 插入测试商品(用于商品TOP)
|
||||
-- ============================================
|
||||
INSERT INTO products (id, merchant_id, name, price, sales, status) VALUES
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb1', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', '测试商品1', 258.00, 100, 1),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb2', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', '测试商品2', 399.00, 80, 1),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb3', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', '测试商品3', 128.50, 60, 1)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 2. 插入用户会话(在线用户)
|
||||
-- ============================================
|
||||
-- 最近5分钟内有活动的用户(在线用户)
|
||||
INSERT INTO user_sessions (id, user_id, session_token, last_active_at, is_active, ip_address) VALUES
|
||||
('c1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'token_001', NOW() - INTERVAL '1 minute', true, '192.168.1.101'),
|
||||
('c2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', 'token_002', NOW() - INTERVAL '2 minutes', true, '192.168.1.102'),
|
||||
('c3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', 'token_003', NOW() - INTERVAL '3 minutes', true, '192.168.1.103'),
|
||||
('c6666666-6666-6666-6666-666666666666', '66666666-6666-6666-6666-666666666666', 'token_006', NOW() - INTERVAL '4 minutes', true, '192.168.1.106'),
|
||||
('c7777777-7777-7777-7777-777777777777', '77777777-7777-7777-7777-777777777777', 'token_007', NOW() - INTERVAL '2 minutes', true, '192.168.1.107')
|
||||
ON CONFLICT (id) DO UPDATE SET last_active_at = EXCLUDED.last_active_at;
|
||||
|
||||
-- 今日访问的用户会话(用于转化率计算)
|
||||
INSERT INTO user_sessions (id, user_id, session_token, last_active_at, is_active, ip_address, created_at) VALUES
|
||||
('c8888888-8888-8888-8888-888888888888', '88888888-8888-8888-8888-888888888888', 'token_008', NOW() - INTERVAL '1 hour', true, '192.168.1.108', DATE_TRUNC('day', NOW()) + INTERVAL '2 hours'),
|
||||
('c9999999-9999-9999-9999-999999999999', '11111111-1111-1111-1111-111111111111', 'token_009', NOW() - INTERVAL '30 minutes', true, '192.168.1.109', DATE_TRUNC('day', NOW()) + INTERVAL '3 hours'),
|
||||
('caaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '22222222-2222-2222-2222-222222222222', 'token_010', NOW() - INTERVAL '1 hour', true, '192.168.1.110', DATE_TRUNC('day', NOW()) + INTERVAL '4 hours'),
|
||||
('cbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '33333333-3333-3333-3333-333333333333', 'token_011', NOW() - INTERVAL '2 hours', true, '192.168.1.111', DATE_TRUNC('day', NOW()) + INTERVAL '5 hours'),
|
||||
('cccccccc-cccc-cccc-cccc-cccccccccccc', '44444444-4444-4444-4444-444444444444', 'token_012', NOW() - INTERVAL '3 hours', true, '192.168.1.112', DATE_TRUNC('day', NOW()) + INTERVAL '6 hours')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 3. 插入今日订单(用于实时GMV和订单数统计)
|
||||
-- ============================================
|
||||
-- 今日已支付订单(status = 2)
|
||||
INSERT INTO orders (id, user_id, merchant_id, total_amount, status, created_at) VALUES
|
||||
-- 今日订单(最近几小时)
|
||||
('e1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 258.00, 2, NOW() - INTERVAL '1 hour'),
|
||||
('e2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 399.00, 2, NOW() - INTERVAL '2 hours'),
|
||||
('e3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 128.50, 2, NOW() - INTERVAL '30 minutes'),
|
||||
('e4444444-4444-4444-4444-444444444444', '44444444-4444-4444-4444-444444444444', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 599.00, 2, NOW() - INTERVAL '3 hours'),
|
||||
('e5555555-5555-5555-5555-555555555555', '55555555-5555-5555-5555-555555555555', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 89.90, 2, NOW() - INTERVAL '45 minutes'),
|
||||
('e6666666-6666-6666-6666-666666666666', '66666666-6666-6666-6666-666666666666', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 199.00, 2, NOW() - INTERVAL '1 hour 30 minutes'),
|
||||
('e7777777-7777-7777-7777-777777777777', '77777777-7777-7777-7777-777777777777', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 299.00, 2, NOW() - INTERVAL '2 hours 15 minutes'),
|
||||
('e8888888-8888-8888-8888-888888888888', '88888888-8888-8888-8888-888888888888', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 159.00, 2, NOW() - INTERVAL '4 hours'),
|
||||
('e9999999-9999-9999-9999-999999999999', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 459.00, 2, NOW() - INTERVAL '5 hours'),
|
||||
('eaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '22222222-2222-2222-2222-222222222222', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 79.90, 2, NOW() - INTERVAL '6 hours'),
|
||||
('ebbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '33333333-3333-3333-3333-333333333333', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 329.00, 2, NOW() - INTERVAL '7 hours'),
|
||||
('eccccccc-cccc-cccc-cccc-cccccccccccc', '44444444-4444-4444-4444-444444444444', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 189.00, 2, NOW() - INTERVAL '8 hours'),
|
||||
('eddddddd-dddd-dddd-dddd-dddddddddddd', '55555555-5555-5555-5555-555555555555', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 249.00, 2, NOW() - INTERVAL '9 hours'),
|
||||
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '66666666-6666-6666-6666-666666666666', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 99.00, 2, NOW() - INTERVAL '10 hours'),
|
||||
('efffffff-ffff-ffff-ffff-ffffffffffff', '77777777-7777-7777-7777-777777777777', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 369.00, 2, NOW() - INTERVAL '11 hours')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 4. 插入昨日同时段订单(用于增长率对比)
|
||||
-- ============================================
|
||||
-- 昨日同时段的订单(往前推24小时)
|
||||
INSERT INTO orders (id, user_id, merchant_id, total_amount, status, created_at) VALUES
|
||||
-- 昨日同时段订单
|
||||
('f1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 198.00, 2, NOW() - INTERVAL '25 hours'),
|
||||
('f2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 299.00, 2, NOW() - INTERVAL '26 hours'),
|
||||
('f3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 88.50, 2, NOW() - INTERVAL '24 hours 30 minutes'),
|
||||
('f4444444-4444-4444-4444-444444444444', '44444444-4444-4444-4444-444444444444', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 499.00, 2, NOW() - INTERVAL '27 hours'),
|
||||
('f5555555-5555-5555-5555-555555555555', '55555555-5555-5555-5555-555555555555', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 79.90, 2, NOW() - INTERVAL '24 hours 45 minutes'),
|
||||
('f6666666-6666-6666-6666-666666666666', '66666666-6666-6666-6666-666666666666', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 149.00, 2, NOW() - INTERVAL '25 hours 30 minutes'),
|
||||
('f7777777-7777-7777-7777-777777777777', '77777777-7777-7777-7777-777777777777', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 229.00, 2, NOW() - INTERVAL '26 hours 15 minutes'),
|
||||
('f8888888-8888-8888-8888-888888888888', '88888888-8888-8888-8888-888888888888', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2', 129.00, 2, NOW() - INTERVAL '28 hours'),
|
||||
('f9999999-9999-9999-9999-999999999999', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 359.00, 2, NOW() - INTERVAL '29 hours'),
|
||||
('faaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '22222222-2222-2222-2222-222222222222', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1', 69.90, 2, NOW() - INTERVAL '30 hours')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 5. 插入访问日志(用于转化率分析)
|
||||
-- ============================================
|
||||
-- 今日访问记录
|
||||
INSERT INTO page_views (id, user_id, path, source, created_at) VALUES
|
||||
('d1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '/pages/mall/analytics/index', 'direct', DATE_TRUNC('day', NOW()) + INTERVAL '1 hour'),
|
||||
('d2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', '/pages/mall/consumer/index', 'direct', DATE_TRUNC('day', NOW()) + INTERVAL '2 hours'),
|
||||
('d3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', '/pages/mall/consumer/index', 'search', DATE_TRUNC('day', NOW()) + INTERVAL '3 hours'),
|
||||
('d4444444-4444-4444-4444-444444444444', '44444444-4444-4444-4444-444444444444', '/pages/mall/consumer/index', 'social', DATE_TRUNC('day', NOW()) + INTERVAL '4 hours'),
|
||||
('d5555555-5555-5555-5555-555555555555', '55555555-5555-5555-5555-555555555555', '/pages/mall/consumer/index', 'ad', DATE_TRUNC('day', NOW()) + INTERVAL '5 hours'),
|
||||
('d6666666-6666-6666-6666-666666666666', '66666666-6666-6666-6666-666666666666', '/pages/mall/consumer/index', 'direct', DATE_TRUNC('day', NOW()) + INTERVAL '6 hours'),
|
||||
('d7777777-7777-7777-7777-777777777777', '77777777-7777-7777-7777-777777777777', '/pages/mall/consumer/index', 'search', DATE_TRUNC('day', NOW()) + INTERVAL '7 hours'),
|
||||
('d8888888-8888-8888-8888-888888888888', '88888888-8888-8888-8888-888888888888', '/pages/mall/consumer/index', 'direct', DATE_TRUNC('day', NOW()) + INTERVAL '8 hours'),
|
||||
('d9999999-9999-9999-9999-999999999999', '11111111-1111-1111-1111-111111111111', '/pages/mall/consumer/product-detail', 'search', DATE_TRUNC('day', NOW()) + INTERVAL '9 hours'),
|
||||
('daaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '22222222-2222-2222-2222-222222222222', '/pages/mall/consumer/product-detail', 'direct', DATE_TRUNC('day', NOW()) + INTERVAL '10 hours'),
|
||||
('dbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '33333333-3333-3333-3333-333333333333', '/pages/mall/consumer/product-detail', 'ad', DATE_TRUNC('day', NOW()) + INTERVAL '11 hours'),
|
||||
('dccccccc-cccc-cccc-cccc-cccccccccccc', '44444444-4444-4444-4444-444444444444', '/pages/mall/consumer/product-detail', 'social', DATE_TRUNC('day', NOW()) + INTERVAL '12 hours'),
|
||||
('dddddddd-dddd-dddd-dddd-dddddddddddd', '55555555-5555-5555-5555-555555555555', '/pages/mall/consumer/product-detail', 'search', DATE_TRUNC('day', NOW()) + INTERVAL '13 hours'),
|
||||
('deeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '66666666-6666-6666-6666-666666666666', '/pages/mall/consumer/product-detail', 'direct', DATE_TRUNC('day', NOW()) + INTERVAL '14 hours'),
|
||||
('dfffffff-ffff-ffff-ffff-ffffffffffff', '77777777-7777-7777-7777-777777777777', '/pages/mall/consumer/product-detail', 'ad', DATE_TRUNC('day', NOW()) + INTERVAL '15 hours')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 6. 插入订单商品关联(可选,用于详细分析)
|
||||
-- ============================================
|
||||
INSERT INTO order_items (id, order_id, product_id, quantity, price, total_amount) VALUES
|
||||
('a1111111-1111-1111-1111-111111111111', 'e1111111-1111-1111-1111-111111111111', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb1', 1, 258.00, 258.00),
|
||||
('a2222222-2222-2222-2222-222222222222', 'e2222222-2222-2222-2222-222222222222', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb2', 1, 399.00, 399.00),
|
||||
('a3333333-3333-3333-3333-333333333333', 'e3333333-3333-3333-3333-333333333333', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb3', 2, 128.50, 257.00)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 7. 重新启用 RLS(如果之前禁用了)
|
||||
-- ============================================
|
||||
-- 如果使用了方式一(临时禁用 RLS),请取消下面的注释来重新启用:
|
||||
|
||||
-- ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE user_sessions ENABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE products ENABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE merchants ENABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE order_items ENABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE page_views ENABLE ROW LEVEL SECURITY;
|
||||
-- COMMIT;
|
||||
|
||||
-- ============================================
|
||||
-- 完成
|
||||
-- ============================================
|
||||
-- 测试数据插入完成!
|
||||
-- 建议执行以下查询验证数据:
|
||||
-- SELECT COUNT(*) FROM users; -- 应该返回 8
|
||||
-- SELECT COUNT(*) FROM orders; -- 应该返回 25(15个今日 + 10个昨日)
|
||||
-- SELECT COUNT(*) FROM user_sessions; -- 应该返回 10
|
||||
-- SELECT COUNT(*) FROM page_views; -- 应该返回 15
|
||||
222
pages/mall/analytics/test/03_delivery_rls_policies.sql
Normal file
222
pages/mall/analytics/test/03_delivery_rls_policies.sql
Normal file
@@ -0,0 +1,222 @@
|
||||
-- =============================================
|
||||
-- 配送模块 - RLS(行级安全)与策略(按角色/用户隔离版)
|
||||
-- 修复说明:
|
||||
-- - Postgres 的 EXECUTE 只能执行「字符串」,因此 CREATE POLICY 必须包在字符串里。
|
||||
-- - 本脚本使用 format($pol$ ... $pol$) 生成字符串,避免引号转义问题。
|
||||
-- - 可重复执行:创建前检查 pg_policies。
|
||||
--
|
||||
-- 设计目标:
|
||||
-- 1) 关闭 UNRESTRICTED:启用 RLS,避免 API 公共可读写。
|
||||
-- 2) 最小可用:
|
||||
-- - 配送员(delivery)只能读写自己的配送员档案、以及自己的任务。
|
||||
-- - 管理员/分析员(admin/analytics)可读取全量数据用于后台与分析。
|
||||
-- - 其他角色默认无权限。
|
||||
--
|
||||
-- 前置假设:
|
||||
-- - public.ak_users 表存在,且字段:id(UUID), role(TEXT)
|
||||
-- - ml_delivery_drivers.user_id 可与 auth.uid() 对齐
|
||||
-- =============================================
|
||||
|
||||
-- 0) 开启 RLS
|
||||
ALTER TABLE public.ml_delivery_drivers ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.ml_delivery_tasks ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =========================
|
||||
-- ml_delivery_drivers 策略
|
||||
-- =========================
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 1.1 配送员只能查看自己的档案
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_drivers' AND policyname='delivery_can_select_own_driver_profile'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY delivery_can_select_own_driver_profile
|
||||
ON public.ml_delivery_drivers
|
||||
FOR SELECT
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND user_id = auth.uid()
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'delivery'
|
||||
)
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
|
||||
-- 1.2 配送员只能更新自己的档案
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_drivers' AND policyname='delivery_can_update_own_driver_profile'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY delivery_can_update_own_driver_profile
|
||||
ON public.ml_delivery_drivers
|
||||
FOR UPDATE
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND user_id = auth.uid()
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'delivery'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
user_id = auth.uid()
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
|
||||
-- 1.3 管理员/分析员可读全量(后台管理/报表)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_drivers' AND policyname='admin_analytics_can_select_all_delivery_drivers'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY admin_analytics_can_select_all_delivery_drivers
|
||||
ON public.ml_delivery_drivers
|
||||
FOR SELECT
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role IN ('admin','analytics')
|
||||
)
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
|
||||
-- 1.4 管理员可写全量(派单/改派/纠错)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_drivers' AND policyname='admin_can_manage_delivery_drivers'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY admin_can_manage_delivery_drivers
|
||||
ON public.ml_delivery_drivers
|
||||
FOR ALL
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'admin'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'admin'
|
||||
)
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =========================
|
||||
-- ml_delivery_tasks 策略
|
||||
-- =========================
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 2.1 配送员只能查看自己的任务
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_tasks' AND policyname='delivery_can_select_own_tasks'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY delivery_can_select_own_tasks
|
||||
ON public.ml_delivery_tasks
|
||||
FOR SELECT
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public.ml_delivery_drivers d
|
||||
JOIN public.ak_users u ON u.id = auth.uid()
|
||||
WHERE u.role = 'delivery'
|
||||
AND d.user_id = auth.uid()
|
||||
AND d.id = public.ml_delivery_tasks.driver_id
|
||||
)
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
|
||||
-- 2.2 配送员只能更新自己的任务
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_tasks' AND policyname='delivery_can_update_own_tasks'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY delivery_can_update_own_tasks
|
||||
ON public.ml_delivery_tasks
|
||||
FOR UPDATE
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public.ml_delivery_drivers d
|
||||
JOIN public.ak_users u ON u.id = auth.uid()
|
||||
WHERE u.role = 'delivery'
|
||||
AND d.user_id = auth.uid()
|
||||
AND d.id = public.ml_delivery_tasks.driver_id
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.ml_delivery_drivers d
|
||||
WHERE d.user_id = auth.uid()
|
||||
AND d.id = public.ml_delivery_tasks.driver_id
|
||||
)
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
|
||||
-- 2.3 管理员/分析员可读全量任务
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_tasks' AND policyname='admin_analytics_can_select_all_delivery_tasks'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY admin_analytics_can_select_all_delivery_tasks
|
||||
ON public.ml_delivery_tasks
|
||||
FOR SELECT
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role IN ('admin','analytics')
|
||||
)
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
|
||||
-- 2.4 管理员可写全量任务
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname='public' AND tablename='ml_delivery_tasks' AND policyname='admin_can_manage_delivery_tasks'
|
||||
) THEN
|
||||
EXECUTE format($pol$
|
||||
CREATE POLICY admin_can_manage_delivery_tasks
|
||||
ON public.ml_delivery_tasks
|
||||
FOR ALL
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'admin'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'admin'
|
||||
)
|
||||
)
|
||||
$pol$);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 完成
|
||||
SELECT 'delivery RLS policies applied' AS message;
|
||||
70
pages/mall/analytics/test/03_orders_analytics_policies.sql
Normal file
70
pages/mall/analytics/test/03_orders_analytics_policies.sql
Normal file
@@ -0,0 +1,70 @@
|
||||
-- =============================================
|
||||
-- Analytics Dashboard 专用:订单表 RLS 策略补充
|
||||
-- 目标:
|
||||
-- - 允许角色为 admin / analytics 的用户在 Supabase 中
|
||||
-- 通过 RPC 查询全量订单与订单明细(仅用于统计分析)。
|
||||
-- - 不影响普通消费者的订单隔离策略。
|
||||
-- 依赖:
|
||||
-- - public.ak_users 表存在,且包含字段:id(uuid), role(text)
|
||||
-- - 01_create_tables.sql 已经启用 orders / order_items 的 RLS
|
||||
-- =============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 如果 ak_users 表不存在,直接跳过(避免报错)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'ak_users'
|
||||
) THEN
|
||||
RAISE NOTICE 'ak_users table not found, skip analytics RLS policies for orders.';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- orders:允许 admin / analytics 角色读取全量订单用于分析
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = 'orders'
|
||||
AND policyname = 'analytics_can_select_all_orders'
|
||||
) THEN
|
||||
EXECUTE $pol$
|
||||
CREATE POLICY analytics_can_select_all_orders
|
||||
ON public.orders
|
||||
FOR SELECT
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid()
|
||||
AND u.role IN ('admin','analytics')
|
||||
)
|
||||
)
|
||||
$pol$;
|
||||
END IF;
|
||||
|
||||
-- order_items:允许 admin / analytics 查看任意订单的明细
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = 'order_items'
|
||||
AND policyname = 'analytics_can_select_all_order_items'
|
||||
) THEN
|
||||
EXECUTE $pol$
|
||||
CREATE POLICY analytics_can_select_all_order_items
|
||||
ON public.order_items
|
||||
FOR SELECT
|
||||
USING (
|
||||
auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.ak_users u
|
||||
WHERE u.id = auth.uid()
|
||||
AND u.role IN ('admin','analytics')
|
||||
)
|
||||
)
|
||||
$pol$;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT 'orders analytics RLS policies applied' AS message;
|
||||
|
||||
225
pages/mall/analytics/test/03_test_queries.sql
Normal file
225
pages/mall/analytics/test/03_test_queries.sql
Normal file
@@ -0,0 +1,225 @@
|
||||
-- ============================================
|
||||
-- 数据分析实时大屏 - 测试查询脚本
|
||||
-- ============================================
|
||||
-- 这些查询用于验证实时大屏的数据计算逻辑
|
||||
|
||||
-- ============================================
|
||||
-- 1. 测试实时GMV查询
|
||||
-- ============================================
|
||||
-- 今日GMV
|
||||
SELECT
|
||||
SUM(total_amount) as today_gmv,
|
||||
COUNT(*) as today_order_count
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
AND status = 2;
|
||||
|
||||
-- 昨日同时段GMV(当前时间往前推24小时)
|
||||
SELECT
|
||||
SUM(total_amount) as yesterday_gmv,
|
||||
COUNT(*) as yesterday_order_count
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW() - INTERVAL '1 day')
|
||||
AND created_at <= NOW() - INTERVAL '1 day'
|
||||
AND status = 2;
|
||||
|
||||
-- GMV增长率计算
|
||||
WITH today_stats AS (
|
||||
SELECT
|
||||
COALESCE(SUM(total_amount), 0) as gmv,
|
||||
COUNT(*) as orders
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
AND status = 2
|
||||
),
|
||||
yesterday_stats AS (
|
||||
SELECT
|
||||
COALESCE(SUM(total_amount), 0) as gmv,
|
||||
COUNT(*) as orders
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW() - INTERVAL '1 day')
|
||||
AND created_at <= NOW() - INTERVAL '1 day'
|
||||
AND status = 2
|
||||
)
|
||||
SELECT
|
||||
today_stats.gmv as today_gmv,
|
||||
yesterday_stats.gmv as yesterday_gmv,
|
||||
CASE
|
||||
WHEN yesterday_stats.gmv > 0 THEN
|
||||
ROUND((today_stats.gmv - yesterday_stats.gmv) / yesterday_stats.gmv * 100, 2)
|
||||
ELSE
|
||||
CASE WHEN today_stats.gmv > 0 THEN 100 ELSE 0 END
|
||||
END as gmv_growth_percent,
|
||||
today_stats.orders as today_orders,
|
||||
yesterday_stats.orders as yesterday_orders,
|
||||
CASE
|
||||
WHEN yesterday_stats.orders > 0 THEN
|
||||
ROUND((today_stats.orders - yesterday_stats.orders)::numeric / yesterday_stats.orders * 100, 2)
|
||||
ELSE
|
||||
CASE WHEN today_stats.orders > 0 THEN 100 ELSE 0 END
|
||||
END as order_growth_percent
|
||||
FROM today_stats, yesterday_stats;
|
||||
|
||||
-- ============================================
|
||||
-- 2. 测试在线用户查询
|
||||
-- ============================================
|
||||
-- 最近5分钟内有活动的用户(在线用户)
|
||||
SELECT COUNT(DISTINCT user_id) as online_users
|
||||
FROM user_sessions
|
||||
WHERE last_active_at >= NOW() - INTERVAL '5 minutes'
|
||||
AND is_active = true;
|
||||
|
||||
-- 备用方案:从用户表查询最近5分钟登录的用户
|
||||
SELECT COUNT(*) as online_users_from_users
|
||||
FROM users
|
||||
WHERE last_login_at >= NOW() - INTERVAL '5 minutes';
|
||||
|
||||
-- ============================================
|
||||
-- 3. 测试转化率查询
|
||||
-- ============================================
|
||||
-- 今日下单的唯一用户数
|
||||
SELECT COUNT(DISTINCT user_id) as unique_order_users
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
AND status = 2;
|
||||
|
||||
-- 今日访问用户数(从用户会话表)
|
||||
SELECT COUNT(DISTINCT user_id) as today_visitors
|
||||
FROM user_sessions
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW());
|
||||
|
||||
-- 转化率计算
|
||||
WITH order_users AS (
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
AND status = 2
|
||||
),
|
||||
visitors AS (
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM user_sessions
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
)
|
||||
SELECT
|
||||
order_users.count as order_users,
|
||||
visitors.count as visitors,
|
||||
CASE
|
||||
WHEN visitors.count > 0 THEN
|
||||
ROUND(order_users.count::numeric / visitors.count * 100, 2)
|
||||
ELSE 0
|
||||
END as conversion_rate_percent
|
||||
FROM order_users, visitors;
|
||||
|
||||
-- ============================================
|
||||
-- 4. 综合实时大屏数据查询
|
||||
-- ============================================
|
||||
WITH
|
||||
-- 今日统计
|
||||
today_stats AS (
|
||||
SELECT
|
||||
COALESCE(SUM(total_amount), 0) as gmv,
|
||||
COUNT(*) as orders,
|
||||
COUNT(DISTINCT user_id) as order_users
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
AND status = 2
|
||||
),
|
||||
-- 昨日同时段统计
|
||||
yesterday_stats AS (
|
||||
SELECT
|
||||
COALESCE(SUM(total_amount), 0) as gmv,
|
||||
COUNT(*) as orders,
|
||||
COUNT(DISTINCT user_id) as order_users
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW() - INTERVAL '1 day')
|
||||
AND created_at <= NOW() - INTERVAL '1 day'
|
||||
AND status = 2
|
||||
),
|
||||
-- 在线用户
|
||||
online_users AS (
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM user_sessions
|
||||
WHERE last_active_at >= NOW() - INTERVAL '5 minutes'
|
||||
AND is_active = true
|
||||
),
|
||||
-- 今日访问用户
|
||||
today_visitors AS (
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM user_sessions
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
),
|
||||
-- 昨日同时段访问用户
|
||||
yesterday_visitors AS (
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM user_sessions
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW() - INTERVAL '1 day')
|
||||
AND created_at <= NOW() - INTERVAL '1 day'
|
||||
)
|
||||
SELECT
|
||||
-- GMV数据
|
||||
today_stats.gmv as real_time_gmv,
|
||||
CASE
|
||||
WHEN yesterday_stats.gmv > 0 THEN
|
||||
ROUND((today_stats.gmv - yesterday_stats.gmv) / yesterday_stats.gmv * 100, 1)
|
||||
ELSE
|
||||
CASE WHEN today_stats.gmv > 0 THEN 100.0 ELSE 0.0 END
|
||||
END as gmv_growth,
|
||||
|
||||
-- 订单数据
|
||||
today_stats.orders as real_time_orders,
|
||||
CASE
|
||||
WHEN yesterday_stats.orders > 0 THEN
|
||||
ROUND((today_stats.orders - yesterday_stats.orders)::numeric / yesterday_stats.orders * 100, 1)
|
||||
ELSE
|
||||
CASE WHEN today_stats.orders > 0 THEN 100.0 ELSE 0.0 END
|
||||
END as order_growth,
|
||||
|
||||
-- 在线用户
|
||||
COALESCE(online_users.count, 0) as online_users,
|
||||
|
||||
-- 转化率
|
||||
CASE
|
||||
WHEN today_visitors.count > 0 THEN
|
||||
ROUND(today_stats.order_users::numeric / today_visitors.count * 100, 1)
|
||||
ELSE 0
|
||||
END as conversion_rate,
|
||||
CASE
|
||||
WHEN yesterday_visitors.count > 0 AND yesterday_stats.order_users > 0 THEN
|
||||
ROUND((today_stats.order_users::numeric / today_visitors.count * 100) -
|
||||
(yesterday_stats.order_users::numeric / yesterday_visitors.count * 100), 1)
|
||||
ELSE 0
|
||||
END as conversion_growth
|
||||
FROM today_stats, yesterday_stats, online_users, today_visitors, yesterday_visitors;
|
||||
|
||||
-- ============================================
|
||||
-- 5. 数据验证查询
|
||||
-- ============================================
|
||||
-- 检查今日订单数据
|
||||
SELECT
|
||||
DATE_TRUNC('hour', created_at) as hour,
|
||||
COUNT(*) as order_count,
|
||||
SUM(total_amount) as hour_gmv
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
AND status = 2
|
||||
GROUP BY DATE_TRUNC('hour', created_at)
|
||||
ORDER BY hour DESC;
|
||||
|
||||
-- 检查用户会话数据
|
||||
SELECT
|
||||
DATE_TRUNC('hour', created_at) as hour,
|
||||
COUNT(DISTINCT user_id) as unique_visitors
|
||||
FROM user_sessions
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
GROUP BY DATE_TRUNC('hour', created_at)
|
||||
ORDER BY hour DESC;
|
||||
|
||||
-- 检查最近活动的用户
|
||||
SELECT
|
||||
user_id,
|
||||
last_active_at,
|
||||
NOW() - last_active_at as time_ago
|
||||
FROM user_sessions
|
||||
WHERE last_active_at >= NOW() - INTERVAL '10 minutes'
|
||||
AND is_active = true
|
||||
ORDER BY last_active_at DESC;
|
||||
110
pages/mall/analytics/test/04_cleanup.sql
Normal file
110
pages/mall/analytics/test/04_cleanup.sql
Normal file
@@ -0,0 +1,110 @@
|
||||
-- ============================================
|
||||
-- 数据分析实时大屏 - 清理测试数据脚本
|
||||
-- ============================================
|
||||
-- 谨慎使用:此脚本会删除测试数据
|
||||
|
||||
-- 方式1:删除特定时间范围的测试数据(推荐)
|
||||
-- 删除今日的测试订单
|
||||
DELETE FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW())
|
||||
AND user_id IN (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'44444444-4444-4444-4444-444444444444',
|
||||
'55555555-5555-5555-5555-555555555555',
|
||||
'66666666-6666-6666-6666-666666666666',
|
||||
'77777777-7777-7777-7777-777777777777',
|
||||
'88888888-8888-8888-8888-888888888888'
|
||||
);
|
||||
|
||||
-- 删除昨日同时段的测试订单
|
||||
DELETE FROM orders
|
||||
WHERE created_at >= DATE_TRUNC('day', NOW() - INTERVAL '1 day')
|
||||
AND created_at <= NOW() - INTERVAL '1 day'
|
||||
AND user_id IN (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'44444444-4444-4444-4444-444444444444',
|
||||
'55555555-5555-5555-5555-555555555555',
|
||||
'66666666-6666-6666-6666-666666666666',
|
||||
'77777777-7777-7777-7777-777777777777',
|
||||
'88888888-8888-8888-8888-888888888888'
|
||||
);
|
||||
|
||||
-- 删除测试用户会话
|
||||
DELETE FROM user_sessions
|
||||
WHERE user_id IN (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'44444444-4444-4444-4444-444444444444',
|
||||
'55555555-5555-5555-5555-555555555555',
|
||||
'66666666-6666-6666-6666-666666666666',
|
||||
'77777777-7777-7777-7777-777777777777',
|
||||
'88888888-8888-8888-8888-888888888888'
|
||||
);
|
||||
|
||||
-- 删除测试访问日志
|
||||
DELETE FROM page_views
|
||||
WHERE user_id IN (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'44444444-4444-4444-4444-444444444444',
|
||||
'55555555-5555-5555-5555-555555555555',
|
||||
'66666666-6666-6666-6666-666666666666',
|
||||
'77777777-7777-7777-7777-777777777777',
|
||||
'88888888-8888-8888-8888-888888888888'
|
||||
);
|
||||
|
||||
-- 删除测试订单商品关联
|
||||
-- 说明:order_id 为 UUID,不能用 LIKE 前缀匹配;这里改为通过 orders 反查关联订单。
|
||||
DELETE FROM order_items
|
||||
WHERE order_id IN (
|
||||
SELECT id
|
||||
FROM orders
|
||||
WHERE user_id IN (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'44444444-4444-4444-4444-444444444444',
|
||||
'55555555-5555-5555-5555-555555555555',
|
||||
'66666666-6666-6666-6666-666666666666',
|
||||
'77777777-7777-7777-7777-777777777777',
|
||||
'88888888-8888-8888-8888-888888888888'
|
||||
)
|
||||
);
|
||||
|
||||
-- 删除测试用户(可选,如果这些用户只用于测试)
|
||||
-- 注意:如果用户有其他数据关联,可能需要先删除关联数据
|
||||
-- DELETE FROM users
|
||||
-- WHERE id IN (
|
||||
-- '11111111-1111-1111-1111-111111111111',
|
||||
-- '22222222-2222-2222-2222-222222222222',
|
||||
-- '33333333-3333-3333-3333-333333333333',
|
||||
-- '44444444-4444-4444-4444-444444444444',
|
||||
-- '55555555-5555-5555-5555-555555555555',
|
||||
-- '66666666-6666-6666-6666-666666666666',
|
||||
-- '77777777-7777-7777-7777-777777777777',
|
||||
-- '88888888-8888-8888-8888-888888888888'
|
||||
-- );
|
||||
|
||||
-- ============================================
|
||||
-- 方式2:完全清空表(非常谨慎使用)
|
||||
-- ============================================
|
||||
-- 取消下面的注释来完全清空表
|
||||
-- TRUNCATE TABLE orders, user_sessions, users, order_items, page_views CASCADE;
|
||||
|
||||
-- ============================================
|
||||
-- 方式3:删除测试表(如果表只用于测试)
|
||||
-- ============================================
|
||||
-- 取消下面的注释来删除表
|
||||
-- DROP TABLE IF EXISTS order_items CASCADE;
|
||||
-- DROP TABLE IF EXISTS page_views CASCADE;
|
||||
-- DROP TABLE IF EXISTS user_sessions CASCADE;
|
||||
-- DROP TABLE IF EXISTS orders CASCADE;
|
||||
-- DROP TABLE IF EXISTS users CASCADE;
|
||||
-- DROP TABLE IF EXISTS products CASCADE;
|
||||
-- DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;
|
||||
68
pages/mall/analytics/test/05_product_trend_rpc.sql
Normal file
68
pages/mall/analytics/test/05_product_trend_rpc.sql
Normal file
@@ -0,0 +1,68 @@
|
||||
-- ============================================
|
||||
-- 数据分析实时大屏 - 商品洞察:商品维度趋势 RPC
|
||||
-- ============================================
|
||||
-- 用途:用于“商品洞察”页面的【商品销售分析】图表(A2:商品维度趋势)
|
||||
-- 说明:按天聚合指定商品的 GMV/件数/订单数
|
||||
-- 依赖表:orders, order_items
|
||||
-- 依赖字段:
|
||||
-- orders.status = 2(已支付/已完成口径)
|
||||
-- order_items.quantity, order_items.total_amount
|
||||
--
|
||||
-- 执行方式:在 Supabase Dashboard -> SQL Editor 执行
|
||||
-- 权限:仅 authenticated 可执行
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION rpc_analytics_product_trend(
|
||||
p_start_date DATE,
|
||||
p_end_date DATE,
|
||||
p_product_id UUID
|
||||
)
|
||||
RETURNS TABLE (
|
||||
date DATE,
|
||||
gmv NUMERIC,
|
||||
qty INTEGER,
|
||||
orders 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 days AS (
|
||||
SELECT generate_series(p_start_date, p_end_date, interval '1 day')::date AS d
|
||||
),
|
||||
agg AS (
|
||||
SELECT
|
||||
DATE(o.created_at) AS d,
|
||||
COALESCE(SUM(oi.total_amount), 0)::NUMERIC AS gmv,
|
||||
COALESCE(SUM(oi.quantity), 0)::INTEGER AS qty,
|
||||
COUNT(DISTINCT oi.order_id)::INTEGER AS orders
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE DATE(o.created_at) >= p_start_date
|
||||
AND DATE(o.created_at) <= p_end_date
|
||||
AND o.status = 2
|
||||
AND oi.product_id = p_product_id
|
||||
GROUP BY DATE(o.created_at)
|
||||
)
|
||||
SELECT
|
||||
days.d AS date,
|
||||
COALESCE(agg.gmv, 0)::NUMERIC AS gmv,
|
||||
COALESCE(agg.qty, 0)::INTEGER AS qty,
|
||||
COALESCE(agg.orders, 0)::INTEGER AS orders
|
||||
FROM days
|
||||
LEFT JOIN agg ON agg.d = days.d
|
||||
ORDER BY days.d;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 授权收敛(只允许 authenticated 调用)
|
||||
REVOKE ALL ON FUNCTION rpc_analytics_product_trend(DATE, DATE, UUID) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION rpc_analytics_product_trend(DATE, DATE, UUID) TO authenticated;
|
||||
|
||||
SELECT 'rpc_analytics_product_trend created' AS message;
|
||||
171
pages/mall/analytics/test/06_market_trends_rpcs.sql
Normal file
171
pages/mall/analytics/test/06_market_trends_rpcs.sql
Normal file
@@ -0,0 +1,171 @@
|
||||
-- ============================================
|
||||
-- 数据分析模块 - 市场趋势页面所需 RPC 函数
|
||||
-- ============================================
|
||||
-- 用途:为“市场趋势”页面提供数据支持,包括行业对比、季节性趋势、竞争分析等。
|
||||
-- 执行方式:在 Supabase Dashboard -> SQL Editor 中执行。
|
||||
-- ============================================
|
||||
|
||||
-- 1. 行业对比分析 (按商品分类销售额)
|
||||
-- 依赖:products 表需要有 category_id, 且存在 categories 表。
|
||||
CREATE OR REPLACE FUNCTION rpc_analytics_category_sales(
|
||||
p_start_date DATE,
|
||||
p_end_date DATE
|
||||
)
|
||||
RETURNS TABLE (category_name TEXT, total_sales NUMERIC)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
COALESCE(c.name, '未分类') AS category_name,
|
||||
SUM(oi.total_amount) AS total_sales
|
||||
FROM order_items oi
|
||||
JOIN orders o ON oi.order_id = o.id
|
||||
LEFT JOIN products p ON oi.product_id = p.id
|
||||
LEFT JOIN categories c ON p.category_id = c.id
|
||||
WHERE o.created_at >= p_start_date AND o.created_at <= p_end_date AND o.status = 2
|
||||
GROUP BY COALESCE(c.name, '未分类')
|
||||
ORDER BY total_sales DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 2. 季节性趋势 (按月聚合GMV)
|
||||
CREATE OR REPLACE FUNCTION rpc_analytics_seasonal_trend(
|
||||
p_start_date DATE,
|
||||
p_end_date DATE
|
||||
)
|
||||
RETURNS TABLE (month TEXT, total_gmv NUMERIC)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
TO_CHAR(DATE_TRUNC('month', o.created_at), 'YYYY-MM') AS month,
|
||||
SUM(o.total_amount) AS total_gmv
|
||||
FROM orders o
|
||||
WHERE o.created_at >= p_start_date AND o.created_at <= p_end_date AND o.status = 2
|
||||
GROUP BY DATE_TRUNC('month', o.created_at)
|
||||
ORDER BY month;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 3. 竞争分析 (Top 5 商家市场份额)
|
||||
-- 使用 LANGUAGE sql,避免 OUT 参数 gmv 与列名 gmv 产生歧义
|
||||
CREATE OR REPLACE FUNCTION rpc_analytics_competition_share(
|
||||
p_start_date DATE,
|
||||
p_end_date DATE
|
||||
)
|
||||
RETURNS TABLE (merchant_name TEXT, gmv NUMERIC)
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
WITH merchant_sales AS (
|
||||
SELECT
|
||||
m.shop_name,
|
||||
SUM(o.total_amount) AS gmv
|
||||
FROM orders o
|
||||
JOIN merchants m ON o.merchant_id = m.id
|
||||
WHERE o.created_at >= p_start_date
|
||||
AND o.created_at <= p_end_date
|
||||
AND o.status = 2
|
||||
GROUP BY m.shop_name
|
||||
),
|
||||
top_merchants AS (
|
||||
SELECT
|
||||
shop_name,
|
||||
gmv
|
||||
FROM merchant_sales
|
||||
ORDER BY gmv DESC
|
||||
LIMIT 5
|
||||
),
|
||||
other_sales AS (
|
||||
SELECT
|
||||
'其他' AS shop_name,
|
||||
SUM(ms.gmv) AS gmv
|
||||
FROM merchant_sales ms
|
||||
WHERE ms.shop_name NOT IN (SELECT tm.shop_name FROM top_merchants tm)
|
||||
)
|
||||
SELECT shop_name::TEXT, gmv::NUMERIC FROM top_merchants
|
||||
UNION ALL
|
||||
SELECT shop_name::TEXT, gmv::NUMERIC FROM other_sales WHERE gmv > 0;
|
||||
$$;
|
||||
|
||||
-- 4. 市场整体趋势(按天聚合 GMV / 订单数 / 用户数 / 客单价)
|
||||
CREATE OR REPLACE FUNCTION rpc_analytics_market_trend_daily(
|
||||
p_start TIMESTAMPTZ,
|
||||
p_end TIMESTAMPTZ
|
||||
)
|
||||
RETURNS TABLE (
|
||||
day DATE,
|
||||
gmv NUMERIC,
|
||||
orders INTEGER,
|
||||
users INTEGER,
|
||||
avg_order_amount NUMERIC
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
DATE(o.created_at) AS day,
|
||||
COALESCE(SUM(o.total_amount), 0) AS gmv,
|
||||
COUNT(*)::INTEGER AS orders,
|
||||
COUNT(DISTINCT o.user_id)::INTEGER AS users,
|
||||
CASE
|
||||
WHEN COUNT(*) > 0 THEN ROUND(SUM(o.total_amount) / COUNT(*), 2)
|
||||
ELSE 0
|
||||
END AS avg_order_amount
|
||||
FROM orders o
|
||||
WHERE o.created_at >= p_start
|
||||
AND o.created_at < p_end
|
||||
AND o.status = 2
|
||||
GROUP BY DATE(o.created_at)
|
||||
ORDER BY day;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 5. 价格趋势分析(按天统计平均商品单价)
|
||||
CREATE OR REPLACE FUNCTION rpc_analytics_price_trend(
|
||||
p_start TIMESTAMPTZ,
|
||||
p_end TIMESTAMPTZ
|
||||
)
|
||||
RETURNS TABLE (
|
||||
day DATE,
|
||||
avg_price NUMERIC
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
DATE(o.created_at) AS day,
|
||||
CASE
|
||||
WHEN SUM(oi.quantity) > 0 THEN ROUND(SUM(oi.total_amount) / SUM(oi.quantity), 2)
|
||||
ELSE 0
|
||||
END AS avg_price
|
||||
FROM order_items oi
|
||||
JOIN orders o ON oi.order_id = o.id
|
||||
WHERE o.created_at >= p_start
|
||||
AND o.created_at < p_end
|
||||
AND o.status = 2
|
||||
GROUP BY DATE(o.created_at)
|
||||
ORDER BY day;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 授权
|
||||
REVOKE ALL ON FUNCTION rpc_analytics_category_sales(DATE, DATE) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION rpc_analytics_category_sales(DATE, DATE) TO authenticated;
|
||||
|
||||
REVOKE ALL ON FUNCTION rpc_analytics_seasonal_trend(DATE, DATE) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION rpc_analytics_seasonal_trend(DATE, DATE) TO authenticated;
|
||||
|
||||
REVOKE ALL ON FUNCTION rpc_analytics_competition_share(DATE, DATE) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION rpc_analytics_competition_share(DATE, DATE) TO authenticated;
|
||||
|
||||
REVOKE ALL ON FUNCTION rpc_analytics_market_trend_daily(TIMESTAMPTZ, TIMESTAMPTZ) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION rpc_analytics_market_trend_daily(TIMESTAMPTZ, TIMESTAMPTZ) TO authenticated;
|
||||
|
||||
REVOKE ALL ON FUNCTION rpc_analytics_price_trend(TIMESTAMPTZ, TIMESTAMPTZ) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION rpc_analytics_price_trend(TIMESTAMPTZ, TIMESTAMPTZ) TO authenticated;
|
||||
|
||||
SELECT 'Market trends RPCs created successfully.' AS message;
|
||||
405
pages/mall/analytics/test/07_custom_report_rpcs.sql
Normal file
405
pages/mall/analytics/test/07_custom_report_rpcs.sql
Normal file
@@ -0,0 +1,405 @@
|
||||
-- ============================================
|
||||
-- 07_custom_report_rpcs.sql
|
||||
-- 自定义报表创建与管理 RPC 定义
|
||||
-- ============================================
|
||||
-- 目标:
|
||||
-- 1) 为 `pages/mall/analytics/custom-report.uvue` 提供安全的数据服务
|
||||
-- 2) 确保用户记录存在,解决外键约束问题
|
||||
-- 3) 创建自定义报表并生成初始数据(metrics + rows)
|
||||
--
|
||||
-- 依赖前置脚本:
|
||||
-- - 01_create_tables.sql(users 表)
|
||||
-- - ANALYTICS_DB_SCHEMA.sql(analytics_reports / analytics_report_metrics / analytics_report_rows)
|
||||
--
|
||||
-- 使用说明:
|
||||
-- - 前端通过 supabase-js / UTS 调用 `rpc()` 访问本文件中的函数
|
||||
-- - 所有函数仅对 `authenticated` 角色开放执行权限
|
||||
-- ============================================
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 1. 确保用户记录存在(Upsert User)
|
||||
-- --------------------------------------------
|
||||
-- 说明:
|
||||
-- - 如果 users 表中不存在当前用户记录,则插入
|
||||
-- - 如果已存在,则更新最后登录时间等信息
|
||||
-- - 解决 analytics_reports.owner_user_id 外键约束问题
|
||||
CREATE OR REPLACE FUNCTION public.rpc_ensure_user_record(
|
||||
p_user_id uuid,
|
||||
p_email text DEFAULT NULL,
|
||||
p_phone text DEFAULT NULL,
|
||||
p_nickname text DEFAULT NULL
|
||||
)
|
||||
RETURNS uuid
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_user_id uuid;
|
||||
BEGIN
|
||||
-- 检查用户是否存在
|
||||
SELECT id INTO v_user_id
|
||||
FROM public.users
|
||||
WHERE id = p_user_id;
|
||||
|
||||
IF v_user_id IS NULL THEN
|
||||
-- 用户不存在,插入新记录
|
||||
INSERT INTO public.users (
|
||||
id,
|
||||
email,
|
||||
phone,
|
||||
nickname,
|
||||
last_login_at,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
p_user_id,
|
||||
p_email,
|
||||
p_phone,
|
||||
COALESCE(p_nickname, COALESCE(split_part(p_email, '@', 1), '用户')),
|
||||
NOW(),
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
RETURNING id INTO v_user_id;
|
||||
ELSE
|
||||
-- 用户已存在,更新信息
|
||||
UPDATE public.users
|
||||
SET
|
||||
email = COALESCE(p_email, email),
|
||||
phone = COALESCE(p_phone, phone),
|
||||
nickname = COALESCE(p_nickname, nickname),
|
||||
last_login_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = p_user_id;
|
||||
|
||||
v_user_id := p_user_id;
|
||||
END IF;
|
||||
|
||||
RETURN v_user_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_ensure_user_record(uuid,text,text,text) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_ensure_user_record(uuid,text,text,text) TO authenticated;
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 2. 创建自定义报表(含初始数据生成)
|
||||
-- --------------------------------------------
|
||||
-- 说明:
|
||||
-- - 创建 analytics_reports 记录
|
||||
-- - 根据 period 和选中的指标,生成 analytics_report_metrics
|
||||
-- - 根据 period 聚合 orders 数据,生成 analytics_report_rows
|
||||
CREATE OR REPLACE FUNCTION public.rpc_create_custom_report(
|
||||
p_title text,
|
||||
p_description text DEFAULT '',
|
||||
p_period text DEFAULT '30d', -- 7d/30d/90d/1y
|
||||
p_metrics text[] DEFAULT ARRAY['gmv', 'orders', 'users'], -- 选中的指标列表
|
||||
p_chart_type text DEFAULT 'line' -- 图表类型(暂不存储,仅用于后续扩展)
|
||||
)
|
||||
RETURNS uuid
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_user_id uuid;
|
||||
v_report_id uuid;
|
||||
v_start_date date;
|
||||
v_end_date date;
|
||||
v_metric_key text;
|
||||
v_metric_label text;
|
||||
v_metric_value numeric;
|
||||
v_total_gmv numeric := 0;
|
||||
v_total_orders integer := 0;
|
||||
v_total_users integer := 0;
|
||||
v_avg_order_amount numeric := 0;
|
||||
BEGIN
|
||||
-- 1. 获取当前用户 ID
|
||||
v_user_id := auth.uid();
|
||||
IF v_user_id IS NULL THEN
|
||||
RAISE EXCEPTION '用户未登录';
|
||||
END IF;
|
||||
|
||||
-- 2. 确保用户记录存在
|
||||
PERFORM public.rpc_ensure_user_record(
|
||||
v_user_id,
|
||||
NULL, -- email 从 auth.users 获取,这里不传
|
||||
NULL, -- phone 从 auth.users 获取,这里不传
|
||||
NULL -- nickname 从 auth.users 获取,这里不传
|
||||
);
|
||||
|
||||
-- 3. 计算时间范围
|
||||
v_end_date := CURRENT_DATE;
|
||||
CASE p_period
|
||||
WHEN '7d' THEN v_start_date := v_end_date - INTERVAL '7 days';
|
||||
WHEN '30d' THEN v_start_date := v_end_date - INTERVAL '30 days';
|
||||
WHEN '90d' THEN v_start_date := v_end_date - INTERVAL '90 days';
|
||||
WHEN '1y' THEN v_start_date := v_end_date - INTERVAL '1 year';
|
||||
ELSE v_start_date := v_end_date - INTERVAL '30 days'; -- 默认 30 天
|
||||
END CASE;
|
||||
|
||||
-- 4. 创建报表记录
|
||||
INSERT INTO public.analytics_reports (
|
||||
owner_user_id,
|
||||
title,
|
||||
description,
|
||||
type,
|
||||
period,
|
||||
date_start,
|
||||
date_end,
|
||||
status,
|
||||
generated_at,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
v_user_id,
|
||||
p_title,
|
||||
p_description,
|
||||
'custom',
|
||||
p_period,
|
||||
v_start_date,
|
||||
v_end_date,
|
||||
'ready',
|
||||
NOW(),
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
RETURNING id INTO v_report_id;
|
||||
|
||||
-- 5. 聚合订单数据,计算总指标
|
||||
SELECT
|
||||
COALESCE(SUM(o.total_amount), 0),
|
||||
COUNT(DISTINCT o.id),
|
||||
COUNT(DISTINCT o.user_id),
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT o.id) > 0
|
||||
THEN COALESCE(SUM(o.total_amount), 0) / COUNT(DISTINCT o.id)
|
||||
ELSE 0
|
||||
END
|
||||
INTO v_total_gmv, v_total_orders, v_total_users, v_avg_order_amount
|
||||
FROM public.orders o
|
||||
WHERE o.created_at >= v_start_date
|
||||
AND o.created_at <= v_end_date
|
||||
AND o.status = 2; -- 已完成订单
|
||||
|
||||
-- 6. 生成核心指标(analytics_report_metrics)
|
||||
-- GMV
|
||||
IF 'gmv' = ANY(p_metrics) THEN
|
||||
INSERT INTO public.analytics_report_metrics (
|
||||
report_id,
|
||||
metric_key,
|
||||
metric_label,
|
||||
metric_value_num,
|
||||
format,
|
||||
icon,
|
||||
color
|
||||
) VALUES (
|
||||
v_report_id,
|
||||
'gmv_total',
|
||||
'总GMV',
|
||||
v_total_gmv,
|
||||
'currency',
|
||||
'💰',
|
||||
'#4caf50'
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- 订单数
|
||||
IF 'orders' = ANY(p_metrics) THEN
|
||||
INSERT INTO public.analytics_report_metrics (
|
||||
report_id,
|
||||
metric_key,
|
||||
metric_label,
|
||||
metric_value_num,
|
||||
format,
|
||||
icon,
|
||||
color
|
||||
) VALUES (
|
||||
v_report_id,
|
||||
'orders_total',
|
||||
'总订单数',
|
||||
v_total_orders,
|
||||
'number',
|
||||
'📦',
|
||||
'#2196f3'
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- 用户数
|
||||
IF 'users' = ANY(p_metrics) THEN
|
||||
INSERT INTO public.analytics_report_metrics (
|
||||
report_id,
|
||||
metric_key,
|
||||
metric_label,
|
||||
metric_value_num,
|
||||
format,
|
||||
icon,
|
||||
color
|
||||
) VALUES (
|
||||
v_report_id,
|
||||
'users_total',
|
||||
'下单用户数',
|
||||
v_total_users,
|
||||
'number',
|
||||
'👥',
|
||||
'#ff9800'
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- 客单价
|
||||
IF 'avg_order' = ANY(p_metrics) THEN
|
||||
INSERT INTO public.analytics_report_metrics (
|
||||
report_id,
|
||||
metric_key,
|
||||
metric_label,
|
||||
metric_value_num,
|
||||
format,
|
||||
icon,
|
||||
color
|
||||
) VALUES (
|
||||
v_report_id,
|
||||
'avg_order_amount',
|
||||
'客单价',
|
||||
v_avg_order_amount,
|
||||
'currency',
|
||||
'💵',
|
||||
'#9c27b0'
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- 7. 生成明细行(analytics_report_rows)- 按天聚合
|
||||
INSERT INTO public.analytics_report_rows (
|
||||
report_id,
|
||||
row_date,
|
||||
gmv,
|
||||
orders,
|
||||
users,
|
||||
avg_order_amount
|
||||
)
|
||||
SELECT
|
||||
v_report_id,
|
||||
o.created_at::date AS row_date,
|
||||
COALESCE(SUM(o.total_amount), 0) AS gmv,
|
||||
COUNT(DISTINCT o.id) AS orders,
|
||||
COUNT(DISTINCT o.user_id) AS users,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT o.id) > 0
|
||||
THEN COALESCE(SUM(o.total_amount), 0) / COUNT(DISTINCT o.id)
|
||||
ELSE 0
|
||||
END AS avg_order_amount
|
||||
FROM public.orders o
|
||||
WHERE o.created_at >= v_start_date
|
||||
AND o.created_at <= v_end_date
|
||||
AND o.status = 2
|
||||
GROUP BY o.created_at::date
|
||||
ORDER BY o.created_at::date;
|
||||
|
||||
RETURN v_report_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_create_custom_report(text,text,text,text[],text) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_create_custom_report(text,text,text,text[],text) TO authenticated;
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 3. 更新自定义报表(仅更新基本信息)
|
||||
-- --------------------------------------------
|
||||
-- 说明:
|
||||
-- - 更新报表的标题、描述、周期
|
||||
-- - 不重新生成数据(如需重新生成,删除后重建)
|
||||
CREATE OR REPLACE FUNCTION public.rpc_update_custom_report(
|
||||
p_report_id uuid,
|
||||
p_title text,
|
||||
p_description text DEFAULT NULL,
|
||||
p_period text DEFAULT NULL
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_user_id uuid;
|
||||
BEGIN
|
||||
-- 1. 获取当前用户 ID
|
||||
v_user_id := auth.uid();
|
||||
IF v_user_id IS NULL THEN
|
||||
RAISE EXCEPTION '用户未登录';
|
||||
END IF;
|
||||
|
||||
-- 2. 更新报表(仅限所有者)
|
||||
UPDATE public.analytics_reports
|
||||
SET
|
||||
title = p_title,
|
||||
description = COALESCE(p_description, description),
|
||||
period = COALESCE(p_period, period),
|
||||
updated_at = NOW()
|
||||
WHERE id = p_report_id
|
||||
AND owner_user_id = v_user_id;
|
||||
|
||||
-- FOUND 是 PostgreSQL 的特殊变量,UPDATE 后自动设置
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION '报表不存在或无权限修改';
|
||||
END IF;
|
||||
|
||||
RETURN true;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_update_custom_report(uuid,text,text,text) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_update_custom_report(uuid,text,text,text) TO authenticated;
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 4. 删除自定义报表(级联删除相关数据)
|
||||
-- --------------------------------------------
|
||||
-- 说明:
|
||||
-- - 删除报表记录(CASCADE 会自动删除 metrics 和 rows)
|
||||
-- - 仅允许所有者删除
|
||||
CREATE OR REPLACE FUNCTION public.rpc_delete_custom_report(
|
||||
p_report_id uuid
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_user_id uuid;
|
||||
BEGIN
|
||||
-- 1. 获取当前用户 ID
|
||||
v_user_id := auth.uid();
|
||||
IF v_user_id IS NULL THEN
|
||||
RAISE EXCEPTION '用户未登录';
|
||||
END IF;
|
||||
|
||||
-- 2. 删除报表(仅限所有者,CASCADE 会自动删除 metrics 和 rows)
|
||||
DELETE FROM public.analytics_reports
|
||||
WHERE id = p_report_id
|
||||
AND owner_user_id = v_user_id
|
||||
AND type = 'custom';
|
||||
|
||||
-- FOUND 是 PostgreSQL 的特殊变量,DELETE 后自动设置
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION '报表不存在或无权限删除';
|
||||
END IF;
|
||||
|
||||
RETURN true;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_delete_custom_report(uuid) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_delete_custom_report(uuid) TO authenticated;
|
||||
|
||||
-- ============================================
|
||||
-- 完成提示
|
||||
-- ============================================
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'Custom report RPCs created successfully.';
|
||||
RAISE NOTICE 'Functions:';
|
||||
RAISE NOTICE ' - rpc_ensure_user_record(uuid, text, text, text)';
|
||||
RAISE NOTICE ' - rpc_create_custom_report(text, text, text, text[], text)';
|
||||
RAISE NOTICE ' - rpc_update_custom_report(uuid, text, text, text)';
|
||||
RAISE NOTICE ' - rpc_delete_custom_report(uuid)';
|
||||
END $$;
|
||||
853
pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql
Normal file
853
pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql
Normal file
@@ -0,0 +1,853 @@
|
||||
-- ============================================
|
||||
-- 数据分析模块数据库 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;
|
||||
463
pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql
Normal file
463
pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql
Normal file
@@ -0,0 +1,463 @@
|
||||
-- ============================================
|
||||
-- 数据分析模块测试数据 Seed(Supabase/Postgres)
|
||||
-- ============================================
|
||||
-- 用途:为 `pages/mall/analytics/*` 页面提供可联调的模拟数据
|
||||
-- 参考:`docs/ANALYTICS_DB_DESIGN.md`
|
||||
--
|
||||
-- 执行顺序:
|
||||
-- 1. 先执行基础业务表的 seed(users/merchants/products/orders等)
|
||||
-- 2. 再执行本文档(analytics_* 表 + 基础表补充数据)
|
||||
--
|
||||
-- ⚠️ 重要(RLS):
|
||||
-- 本脚本会写入已启用 RLS 的表(orders/user_sessions/page_views/analytics_*)。
|
||||
-- 请使用 Supabase Dashboard SQL Editor(等价于 postgres/service_role)执行,
|
||||
-- 不要用 anon/authenticated 直接执行,否则会被 RLS 策略拦截。
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 1. 基础业务表补充数据(如果还没有)
|
||||
-- ============================================
|
||||
|
||||
-- 1.1 确保有测试用户(分析师)
|
||||
-- 注意:如果 users 表已通过 02_insert_test_data.sql 插入数据,这里可以跳过或补充
|
||||
INSERT INTO users (id, phone, email, nickname, last_login_at, created_at)
|
||||
VALUES
|
||||
('00000000-0000-0000-0000-000000000001', '13800001001', 'analyst1@test.com', '分析师张三', NOW() - INTERVAL '1 day', NOW() - INTERVAL '30 days'),
|
||||
('00000000-0000-0000-0000-000000000002', '13800001002', 'analyst2@test.com', '分析师李四', NOW() - INTERVAL '2 days', NOW() - INTERVAL '20 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 1.2 确保有测试商家
|
||||
-- 注意:如果 merchants 表已通过 02_insert_test_data.sql 插入数据,这里可以跳过或补充
|
||||
INSERT INTO merchants (id, shop_name, created_at)
|
||||
VALUES
|
||||
('10000000-0000-0000-0000-000000000001', '测试商家A', NOW() - INTERVAL '60 days'),
|
||||
('10000000-0000-0000-0000-000000000002', '测试商家B', NOW() - INTERVAL '50 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 1.3 确保有测试商品
|
||||
-- 注意:如果 products 表已通过 02_insert_test_data.sql 插入数据,这里可以跳过或补充
|
||||
INSERT INTO products (id, merchant_id, name, price, sales, status, created_at)
|
||||
VALUES
|
||||
('20000000-0000-0000-0000-000000000001', '10000000-0000-0000-0000-000000000001', '测试商品1', 99.00, 50, 1, NOW() - INTERVAL '40 days'),
|
||||
('20000000-0000-0000-0000-000000000002', '10000000-0000-0000-0000-000000000001', '测试商品2', 199.00, 30, 1, NOW() - INTERVAL '35 days'),
|
||||
('20000000-0000-0000-0000-000000000003', '10000000-0000-0000-0000-000000000002', '测试商品3', 299.00, 20, 1, NOW() - INTERVAL '30 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 1.4 生成过去30天的测试订单(用于首页实时KPI和趋势)
|
||||
DO $$
|
||||
DECLARE
|
||||
i INTEGER;
|
||||
j INTEGER;
|
||||
order_date DATE;
|
||||
order_id UUID;
|
||||
user_id_val UUID := '00000000-0000-0000-0000-000000000001';
|
||||
merchant_id_val UUID := '10000000-0000-0000-0000-000000000001';
|
||||
product_id_val UUID := '20000000-0000-0000-0000-000000000001';
|
||||
BEGIN
|
||||
FOR i IN 0..29 LOOP
|
||||
order_date := CURRENT_DATE - (29 - i);
|
||||
|
||||
-- 每天生成 5-15 个订单
|
||||
FOR j IN 1..(5 + (i % 11)) LOOP
|
||||
order_id := gen_random_uuid();
|
||||
|
||||
-- 插入订单(状态2=已支付/已完成)
|
||||
INSERT INTO orders (
|
||||
id, user_id, merchant_id, status, total_amount, payment_method, created_at
|
||||
)
|
||||
VALUES (
|
||||
order_id,
|
||||
user_id_val,
|
||||
merchant_id_val,
|
||||
2, -- 已支付/已完成
|
||||
(50 + (j * 10) + (i * 2))::numeric(10,2),
|
||||
'alipay',
|
||||
order_date + (random() * INTERVAL '1 day')
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入订单商品
|
||||
INSERT INTO order_items (
|
||||
id, order_id, product_id, quantity, price, total_amount, created_at
|
||||
)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
order_id,
|
||||
product_id_val,
|
||||
1,
|
||||
(50 + (j * 10))::numeric(10,2),
|
||||
(50 + (j * 10))::numeric(10,2),
|
||||
order_date + (random() * INTERVAL '1 day')
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 1.5 插入 user_sessions(在线用户/访问用户统计)
|
||||
INSERT INTO user_sessions (id, user_id, created_at, last_active_at, is_active)
|
||||
VALUES
|
||||
(gen_random_uuid(), '00000000-0000-0000-0000-000000000001', NOW() - INTERVAL '10 minutes', NOW() - INTERVAL '1 minutes', TRUE),
|
||||
(gen_random_uuid(), '00000000-0000-0000-0000-000000000002', NOW() - INTERVAL '8 minutes', NOW() - INTERVAL '2 minutes', TRUE),
|
||||
(gen_random_uuid(), '00000000-0000-0000-0000-000000000001', NOW() - INTERVAL '20 minutes', NOW() - INTERVAL '6 minutes', FALSE)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 1.6 插入 page_views(流量来源统计)
|
||||
DO $$
|
||||
DECLARE
|
||||
i INTEGER;
|
||||
v_date DATE;
|
||||
v_source TEXT;
|
||||
BEGIN
|
||||
FOR i IN 0..29 LOOP
|
||||
v_date := CURRENT_DATE - (29 - i);
|
||||
v_source := CASE
|
||||
WHEN (i % 4) = 0 THEN 'direct'
|
||||
WHEN (i % 4) = 1 THEN 'search'
|
||||
WHEN (i % 4) = 2 THEN 'social'
|
||||
ELSE 'ad'
|
||||
END;
|
||||
INSERT INTO page_views (id, user_id, path, source, created_at)
|
||||
VALUES
|
||||
(gen_random_uuid(), '00000000-0000-0000-0000-000000000001', '/pages/mall/analytics/index', v_source, v_date + (random() * INTERVAL '1 day'))
|
||||
ON CONFLICT DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- 2. Analytics 表数据
|
||||
-- ============================================
|
||||
|
||||
-- 2.1 分析师偏好设置
|
||||
INSERT INTO analytics_user_preferences (id, user_id, default_period, timezone, currency, kpi_cards, created_at, updated_at)
|
||||
VALUES
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'7d',
|
||||
'Asia/Shanghai',
|
||||
'CNY',
|
||||
'["gmv", "orders", "users", "conversion"]'::jsonb,
|
||||
NOW() - INTERVAL '10 days',
|
||||
NOW()
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'30d',
|
||||
'Asia/Shanghai',
|
||||
'CNY',
|
||||
'["gmv", "orders"]'::jsonb,
|
||||
NOW() - INTERVAL '5 days',
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
default_period = EXCLUDED.default_period,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 2.2 报表定义(3个示例报表)
|
||||
INSERT INTO analytics_reports (
|
||||
id, owner_user_id, merchant_id, title, description, type, period,
|
||||
date_start, date_end, status, generated_at, created_at, updated_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
NULL,
|
||||
'销售报表 - 近7天',
|
||||
'展示近7天的销售趋势和核心指标',
|
||||
'sales',
|
||||
'7d',
|
||||
CURRENT_DATE - 7,
|
||||
CURRENT_DATE,
|
||||
'ready',
|
||||
NOW() - INTERVAL '1 hour',
|
||||
NOW() - INTERVAL '2 days',
|
||||
NOW() - INTERVAL '1 hour'
|
||||
),
|
||||
(
|
||||
'a0000000-0000-0000-0000-000000000002',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
NULL,
|
||||
'用户分析报表 - 近30天',
|
||||
'用户增长、活跃度、留存率分析',
|
||||
'users',
|
||||
'30d',
|
||||
CURRENT_DATE - 30,
|
||||
CURRENT_DATE,
|
||||
'ready',
|
||||
NOW() - INTERVAL '2 hours',
|
||||
NOW() - INTERVAL '5 days',
|
||||
NOW() - INTERVAL '2 hours'
|
||||
),
|
||||
(
|
||||
'a0000000-0000-0000-0000-000000000003',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'商家销售报表 - 近90天',
|
||||
'商家A的销售表现分析',
|
||||
'sales',
|
||||
'90d',
|
||||
CURRENT_DATE - 90,
|
||||
CURRENT_DATE,
|
||||
'ready',
|
||||
NOW() - INTERVAL '30 minutes',
|
||||
NOW() - INTERVAL '10 days',
|
||||
NOW() - INTERVAL '30 minutes'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2.3 报表核心指标(为第一个报表生成)
|
||||
INSERT INTO analytics_report_metrics (
|
||||
id, report_id, metric_key, metric_label, metric_value_num, metric_value_text,
|
||||
format, change_pct, icon, color, created_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'gmv',
|
||||
'GMV',
|
||||
125680.50,
|
||||
NULL,
|
||||
'currency',
|
||||
15.6,
|
||||
'money',
|
||||
'#3b82f6',
|
||||
NOW()
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'orders',
|
||||
'订单量',
|
||||
856,
|
||||
NULL,
|
||||
'number',
|
||||
12.3,
|
||||
'list',
|
||||
'#10b981',
|
||||
NOW()
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'conversion_rate',
|
||||
'转化率',
|
||||
3.45,
|
||||
NULL,
|
||||
'percent',
|
||||
0.8,
|
||||
'trend',
|
||||
'#f59e0b',
|
||||
NOW()
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'avg_order_amount',
|
||||
'客单价',
|
||||
146.82,
|
||||
NULL,
|
||||
'currency',
|
||||
-2.1,
|
||||
'wallet',
|
||||
'#8b5cf6',
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 2.4 报表明细行(趋势数据,为第一个报表生成过去7天的数据)
|
||||
DO $$
|
||||
DECLARE
|
||||
i INTEGER;
|
||||
row_date DATE;
|
||||
report_id_val UUID := 'a0000000-0000-0000-0000-000000000001';
|
||||
BEGIN
|
||||
FOR i IN 0..6 LOOP
|
||||
row_date := CURRENT_DATE - (6 - i);
|
||||
|
||||
INSERT INTO analytics_report_rows (
|
||||
id, report_id, row_date, gmv, orders, users, conversion, avg_order_amount, extra, created_at
|
||||
)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
report_id_val,
|
||||
row_date,
|
||||
(15000 + (i * 2000) + (random() * 3000))::numeric(10,2),
|
||||
(100 + (i * 15) + floor(random() * 30))::integer,
|
||||
(80 + (i * 10) + floor(random() * 20))::integer,
|
||||
(3.0 + (i * 0.1) + (random() * 0.5))::numeric(5,2),
|
||||
(140 + (i * 2) + (random() * 20))::numeric(10,2),
|
||||
'{}'::jsonb,
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 2.5 数据洞察(为第一个报表生成3条洞察)
|
||||
INSERT INTO analytics_insights (
|
||||
id, report_id, owner_user_id, type, impact, title, content, tags, created_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'positive',
|
||||
'high',
|
||||
'GMV持续增长',
|
||||
'近7天GMV较上周期增长15.6%,主要得益于新用户增长和促销活动',
|
||||
ARRAY['销售', '增长']::text[],
|
||||
NOW() - INTERVAL '1 hour'
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'warning',
|
||||
'medium',
|
||||
'客单价略有下降',
|
||||
'客单价较上周期下降2.1%,建议关注高价值商品推广',
|
||||
ARRAY['客单价', '预警']::text[],
|
||||
NOW() - INTERVAL '1 hour'
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'info',
|
||||
'low',
|
||||
'转化率稳定',
|
||||
'转化率保持在3.45%,与行业平均水平相当',
|
||||
ARRAY['转化率']::text[],
|
||||
NOW() - INTERVAL '1 hour'
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 2.6 报表收藏
|
||||
INSERT INTO analytics_report_favorites (id, user_id, report_id, created_at)
|
||||
VALUES
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
NOW() - INTERVAL '1 day'
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000002',
|
||||
NOW() - INTERVAL '2 days'
|
||||
)
|
||||
ON CONFLICT (user_id, report_id) DO NOTHING;
|
||||
|
||||
-- 2.7 导出任务历史
|
||||
INSERT INTO analytics_export_jobs (
|
||||
id, user_id, report_id, format, status, file_path, error_message, created_at, finished_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'xlsx',
|
||||
'done',
|
||||
'exports/report_001.xlsx',
|
||||
'',
|
||||
NOW() - INTERVAL '3 days',
|
||||
NOW() - INTERVAL '3 days' + INTERVAL '5 minutes'
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000002',
|
||||
'pdf',
|
||||
'done',
|
||||
'exports/report_002.pdf',
|
||||
'',
|
||||
NOW() - INTERVAL '1 day',
|
||||
NOW() - INTERVAL '1 day' + INTERVAL '2 minutes'
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000003',
|
||||
'csv',
|
||||
'running',
|
||||
NULL,
|
||||
'',
|
||||
NOW() - INTERVAL '10 minutes',
|
||||
NULL
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 3. 补充报表明细行(为其他报表生成数据)
|
||||
-- ============================================
|
||||
|
||||
-- 为第二个报表(用户分析报表)生成过去30天的趋势数据
|
||||
DO $$
|
||||
DECLARE
|
||||
i INTEGER;
|
||||
row_date DATE;
|
||||
report_id_val UUID := 'a0000000-0000-0000-0000-000000000002';
|
||||
BEGIN
|
||||
FOR i IN 0..29 LOOP
|
||||
row_date := CURRENT_DATE - (29 - i);
|
||||
|
||||
INSERT INTO analytics_report_rows (
|
||||
id, report_id, row_date, gmv, orders, users, conversion, avg_order_amount, extra, created_at
|
||||
)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
report_id_val,
|
||||
row_date,
|
||||
(18000 + (i * 500) + (random() * 2000))::numeric(10,2),
|
||||
(120 + (i * 3) + floor(random() * 20))::integer,
|
||||
(90 + (i * 2) + floor(random() * 15))::integer,
|
||||
(3.2 + (i * 0.05) + (random() * 0.3))::numeric(5,2),
|
||||
(150 + (i * 1) + (random() * 30))::numeric(10,2),
|
||||
'{}'::jsonb,
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 为第三个报表(商家销售报表)生成过去90天的趋势数据
|
||||
DO $$
|
||||
DECLARE
|
||||
i INTEGER;
|
||||
row_date DATE;
|
||||
report_id_val UUID := 'a0000000-0000-0000-0000-000000000003';
|
||||
BEGIN
|
||||
FOR i IN 0..89 LOOP
|
||||
row_date := CURRENT_DATE - (89 - i);
|
||||
|
||||
INSERT INTO analytics_report_rows (
|
||||
id, report_id, row_date, gmv, orders, users, conversion, avg_order_amount, extra, created_at
|
||||
)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
report_id_val,
|
||||
row_date,
|
||||
(20000 + (i * 100) + (random() * 3000))::numeric(10,2),
|
||||
(150 + (i * 1) + floor(random() * 30))::integer,
|
||||
(100 + (i * 1) + floor(random() * 20))::integer,
|
||||
(3.5 + (i * 0.01) + (random() * 0.4))::numeric(5,2),
|
||||
(130 + (i * 0.5) + (random() * 40))::numeric(10,2),
|
||||
'{}'::jsonb,
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- 完成
|
||||
-- ============================================
|
||||
|
||||
SELECT 'Analytics test data seed completed!' AS message;
|
||||
388
pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql
Normal file
388
pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql
Normal file
@@ -0,0 +1,388 @@
|
||||
-- ============================================
|
||||
-- 优惠券效果分析 RPC 集合(Supabase / Postgres)
|
||||
-- 说明:
|
||||
-- - 这些函数只依赖业务域表:
|
||||
-- coupon_templates, user_coupons, coupon_usage_logs, orders
|
||||
-- - 不创建/修改业务表结构,由业务侧 schema 负责。
|
||||
-- - 仅提供 Analytics Dashboard 所需的聚合统计。
|
||||
-- - 调用方:/pages/mall/analytics/coupon-analysis.uvue
|
||||
-- ============================================
|
||||
|
||||
-- 安全注意:
|
||||
-- - 函数使用 SECURITY DEFINER,并将执行权限收敛到 authenticated 角色。
|
||||
-- - 具体可见文件末尾的 REVOKE / GRANT 语句。
|
||||
|
||||
|
||||
-- 1) 概览 KPI:发放/使用/GMV 提升/ROI/整体到期情况
|
||||
CREATE OR REPLACE FUNCTION public.rpc_coupon_effectiveness_overview(
|
||||
p_start timestamptz,
|
||||
p_end timestamptz,
|
||||
p_merchant_id uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
total_issued numeric,
|
||||
total_used numeric,
|
||||
usage_rate numeric,
|
||||
gmv_increase numeric,
|
||||
issued_growth numeric,
|
||||
gmv_growth numeric,
|
||||
roi numeric,
|
||||
about_to_expire_cnt numeric
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_prev_start timestamptz;
|
||||
v_prev_end timestamptz;
|
||||
v_prev_issued numeric := 0;
|
||||
v_prev_gmv numeric := 0;
|
||||
v_discount_sum numeric := 0;
|
||||
BEGIN
|
||||
IF p_start IS NULL OR p_end IS NULL OR p_start >= p_end THEN
|
||||
RAISE EXCEPTION 'invalid period';
|
||||
END IF;
|
||||
|
||||
-- 上一周期窗口:长度与当前周期一致
|
||||
v_prev_start := p_start - (p_end - p_start);
|
||||
v_prev_end := p_start;
|
||||
|
||||
-- 当前周期:发放数量(user_coupons.received_at)
|
||||
SELECT
|
||||
COALESCE(COUNT(uc.id), 0)::numeric
|
||||
INTO total_issued
|
||||
FROM user_coupons uc
|
||||
JOIN coupon_templates ct ON ct.id = uc.template_id
|
||||
WHERE uc.received_at >= p_start
|
||||
AND uc.received_at < p_end
|
||||
AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id);
|
||||
|
||||
-- 当前周期:使用记录 + GMV / 优惠额
|
||||
SELECT
|
||||
COALESCE(COUNT(cul.id), 0)::numeric,
|
||||
COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric,
|
||||
COALESCE(SUM(cul.discount_amount), 0)::numeric
|
||||
INTO total_used, gmv_increase, v_discount_sum
|
||||
FROM coupon_usage_logs cul
|
||||
JOIN coupon_templates ct ON ct.id = cul.template_id
|
||||
LEFT JOIN orders o ON o.id = cul.order_id
|
||||
WHERE cul.used_at >= p_start
|
||||
AND cul.used_at < p_end
|
||||
AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id);
|
||||
|
||||
-- 使用率(0-100)
|
||||
IF total_issued > 0 THEN
|
||||
usage_rate := ROUND((total_used / total_issued) * 100.0, 2);
|
||||
ELSE
|
||||
usage_rate := 0;
|
||||
END IF;
|
||||
|
||||
-- 上一周期:发放与 GMV,用于增长率
|
||||
SELECT
|
||||
COALESCE(COUNT(uc.id), 0)::numeric
|
||||
INTO v_prev_issued
|
||||
FROM user_coupons uc
|
||||
JOIN coupon_templates ct ON ct.id = uc.template_id
|
||||
WHERE uc.received_at >= v_prev_start
|
||||
AND uc.received_at < v_prev_end
|
||||
AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id);
|
||||
|
||||
SELECT
|
||||
COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric
|
||||
INTO v_prev_gmv
|
||||
FROM coupon_usage_logs cul
|
||||
JOIN coupon_templates ct ON ct.id = cul.template_id
|
||||
LEFT JOIN orders o ON o.id = cul.order_id
|
||||
WHERE cul.used_at >= v_prev_start
|
||||
AND cul.used_at < v_prev_end
|
||||
AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id);
|
||||
|
||||
-- 发放增长率
|
||||
IF v_prev_issued > 0 THEN
|
||||
issued_growth := ROUND(((total_issued - v_prev_issued) / v_prev_issued) * 100.0, 2);
|
||||
ELSE
|
||||
issued_growth := CASE WHEN total_issued > 0 THEN 100.0 ELSE 0.0 END;
|
||||
END IF;
|
||||
|
||||
-- GMV 增长率
|
||||
IF v_prev_gmv > 0 THEN
|
||||
gmv_growth := ROUND(((gmv_increase - v_prev_gmv) / v_prev_gmv) * 100.0, 2);
|
||||
ELSE
|
||||
gmv_growth := CASE WHEN gmv_increase > 0 THEN 100.0 ELSE 0.0 END;
|
||||
END IF;
|
||||
|
||||
-- ROI = (GMV_with_coupon - discount_sum) / discount_sum
|
||||
IF v_discount_sum > 0 THEN
|
||||
roi := ROUND(((gmv_increase - v_discount_sum) / v_discount_sum) * 100.0, 2);
|
||||
ELSE
|
||||
roi := 0;
|
||||
END IF;
|
||||
|
||||
-- 未来 7 天内即将到期且未使用的券数量(整体概览)
|
||||
SELECT
|
||||
COALESCE(COUNT(uc2.id), 0)::numeric
|
||||
INTO about_to_expire_cnt
|
||||
FROM user_coupons uc2
|
||||
JOIN coupon_templates ct2 ON ct2.id = uc2.template_id
|
||||
WHERE uc2.status = 1 -- 假设 1 = unused(参考 mall.md)
|
||||
AND uc2.expire_at > now()
|
||||
AND uc2.expire_at <= now() + interval '7 days'
|
||||
AND (p_merchant_id IS NULL OR ct2.merchant_id = p_merchant_id);
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- 2) 按券类型统计:8 种券类型效果
|
||||
CREATE OR REPLACE FUNCTION public.rpc_coupon_type_stats(
|
||||
p_start timestamptz,
|
||||
p_end timestamptz,
|
||||
p_merchant_id uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
coupon_type integer,
|
||||
total_issued numeric,
|
||||
total_used numeric,
|
||||
usage_rate numeric,
|
||||
gmv_with_coupon numeric,
|
||||
amount_saved numeric
|
||||
)
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
SELECT
|
||||
ct.coupon_type::integer AS coupon_type,
|
||||
COALESCE(COUNT(DISTINCT uc.id), 0)::numeric AS total_issued,
|
||||
COALESCE(COUNT(DISTINCT cul.id), 0)::numeric AS total_used,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT uc.id) > 0
|
||||
THEN ROUND(
|
||||
(COUNT(DISTINCT cul.id)::numeric / COUNT(DISTINCT uc.id)::numeric) * 100.0,
|
||||
2
|
||||
)
|
||||
ELSE 0
|
||||
END AS usage_rate,
|
||||
COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric AS gmv_with_coupon,
|
||||
COALESCE(SUM(cul.discount_amount), 0)::numeric AS amount_saved
|
||||
FROM coupon_templates ct
|
||||
LEFT JOIN user_coupons uc
|
||||
ON uc.template_id = ct.id
|
||||
AND uc.received_at >= p_start
|
||||
AND uc.received_at < p_end
|
||||
LEFT JOIN coupon_usage_logs cul
|
||||
ON cul.template_id = ct.id
|
||||
AND cul.used_at >= p_start
|
||||
AND cul.used_at < p_end
|
||||
LEFT JOIN orders o
|
||||
ON o.id = cul.order_id
|
||||
WHERE (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id)
|
||||
GROUP BY ct.coupon_type
|
||||
ORDER BY total_issued DESC;
|
||||
$$;
|
||||
|
||||
|
||||
-- 3) 按发放渠道统计:主动领取/自动发放/活动/邀请/客服/积分
|
||||
CREATE OR REPLACE FUNCTION public.rpc_coupon_channel_stats(
|
||||
p_start timestamptz,
|
||||
p_end timestamptz,
|
||||
p_merchant_id uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
channel text,
|
||||
total_issued numeric,
|
||||
total_used numeric,
|
||||
usage_rate numeric,
|
||||
gmv_with_coupon numeric
|
||||
)
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
SELECT
|
||||
uc.obtain_channel::text AS channel,
|
||||
COALESCE(COUNT(DISTINCT uc.id), 0)::numeric AS total_issued,
|
||||
COALESCE(COUNT(DISTINCT cul.id), 0)::numeric AS total_used,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT uc.id) > 0
|
||||
THEN ROUND(
|
||||
(COUNT(DISTINCT cul.id)::numeric / COUNT(DISTINCT uc.id)::numeric) * 100.0,
|
||||
2
|
||||
)
|
||||
ELSE 0
|
||||
END AS usage_rate,
|
||||
COALESCE(SUM(COALESCE(cul.order_amount, o.total_amount)), 0)::numeric AS gmv_with_coupon
|
||||
FROM user_coupons uc
|
||||
JOIN coupon_templates ct
|
||||
ON ct.id = uc.template_id
|
||||
LEFT JOIN coupon_usage_logs cul
|
||||
ON cul.user_coupon_id = uc.id
|
||||
AND cul.used_at >= p_start
|
||||
AND cul.used_at < p_end
|
||||
LEFT JOIN orders o
|
||||
ON o.id = cul.order_id
|
||||
WHERE uc.received_at >= p_start
|
||||
AND uc.received_at < p_end
|
||||
AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id)
|
||||
AND uc.obtain_channel IS NOT NULL
|
||||
GROUP BY uc.obtain_channel
|
||||
ORDER BY total_issued DESC;
|
||||
$$;
|
||||
|
||||
|
||||
-- 4) 使用趋势:按天发放 vs 使用
|
||||
CREATE OR REPLACE FUNCTION public.rpc_coupon_trend_daily(
|
||||
p_start timestamptz,
|
||||
p_end timestamptz,
|
||||
p_merchant_id uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
day date,
|
||||
issued numeric,
|
||||
used numeric
|
||||
)
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
WITH days AS (
|
||||
SELECT generate_series(
|
||||
date_trunc('day', p_start)::date,
|
||||
date_trunc('day', p_end)::date,
|
||||
interval '1 day'
|
||||
)::date AS d
|
||||
),
|
||||
issued AS (
|
||||
SELECT
|
||||
uc_day::date AS d,
|
||||
COUNT(*)::numeric AS cnt
|
||||
FROM (
|
||||
SELECT DATE(uc.received_at) AS uc_day
|
||||
FROM user_coupons uc
|
||||
JOIN coupon_templates ct ON ct.id = uc.template_id
|
||||
WHERE uc.received_at >= p_start
|
||||
AND uc.received_at < p_end
|
||||
AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id)
|
||||
) t
|
||||
GROUP BY uc_day
|
||||
),
|
||||
used AS (
|
||||
SELECT
|
||||
cul_day::date AS d,
|
||||
COUNT(*)::numeric AS cnt
|
||||
FROM (
|
||||
SELECT DATE(cul.used_at) AS cul_day
|
||||
FROM coupon_usage_logs cul
|
||||
JOIN coupon_templates ct ON ct.id = cul.template_id
|
||||
WHERE cul.used_at >= p_start
|
||||
AND cul.used_at < p_end
|
||||
AND (p_merchant_id IS NULL OR ct.merchant_id = p_merchant_id)
|
||||
) t
|
||||
GROUP BY cul_day
|
||||
)
|
||||
SELECT
|
||||
d.d AS day,
|
||||
COALESCE(i.cnt, 0) AS issued,
|
||||
COALESCE(u.cnt, 0) AS used
|
||||
FROM days d
|
||||
LEFT JOIN issued i ON i.d = d.d
|
||||
LEFT JOIN used u ON u.d = d.d
|
||||
ORDER BY d.d;
|
||||
$$;
|
||||
|
||||
|
||||
-- 5) 转化效果:有券 vs 无券(GMV/订单数/客单价)
|
||||
CREATE OR REPLACE FUNCTION public.rpc_coupon_conversion_effect(
|
||||
p_start timestamptz,
|
||||
p_end timestamptz,
|
||||
p_merchant_id uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
metric text,
|
||||
with_coupon numeric,
|
||||
without_coupon numeric
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_with_orders numeric := 0;
|
||||
v_without_orders numeric := 0;
|
||||
v_with_gmv numeric := 0;
|
||||
v_without_gmv numeric := 0;
|
||||
BEGIN
|
||||
-- 有券订单集合
|
||||
SELECT
|
||||
COALESCE(COUNT(DISTINCT o.id), 0)::numeric,
|
||||
COALESCE(SUM(o.total_amount), 0)::numeric
|
||||
INTO v_with_orders, v_with_gmv
|
||||
FROM orders o
|
||||
WHERE o.created_at >= p_start
|
||||
AND o.created_at < p_end
|
||||
AND (p_merchant_id IS NULL OR o.merchant_id = p_merchant_id)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM coupon_usage_logs cul
|
||||
WHERE cul.order_id = o.id
|
||||
);
|
||||
|
||||
-- 无券订单集合
|
||||
SELECT
|
||||
COALESCE(COUNT(DISTINCT o.id), 0)::numeric,
|
||||
COALESCE(SUM(o.total_amount), 0)::numeric
|
||||
INTO v_without_orders, v_without_gmv
|
||||
FROM orders o
|
||||
WHERE o.created_at >= p_start
|
||||
AND o.created_at < p_end
|
||||
AND (p_merchant_id IS NULL OR o.merchant_id = p_merchant_id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM coupon_usage_logs cul
|
||||
WHERE cul.order_id = o.id
|
||||
);
|
||||
|
||||
-- GMV 行
|
||||
metric := 'GMV';
|
||||
with_coupon := v_with_gmv;
|
||||
without_coupon := v_without_gmv;
|
||||
RETURN NEXT;
|
||||
|
||||
-- 订单数 行
|
||||
metric := 'orders';
|
||||
with_coupon := v_with_orders;
|
||||
without_coupon := v_without_orders;
|
||||
RETURN NEXT;
|
||||
|
||||
-- 客单价 行
|
||||
metric := 'avg_order_amount';
|
||||
with_coupon := CASE WHEN v_with_orders > 0 THEN ROUND(v_with_gmv / v_with_orders, 2) ELSE 0 END;
|
||||
without_coupon := CASE WHEN v_without_orders > 0 THEN ROUND(v_without_gmv / v_without_orders, 2) ELSE 0 END;
|
||||
RETURN NEXT;
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 权限收敛:只允许 authenticated 角色调用
|
||||
-- ============================================
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_coupon_effectiveness_overview(timestamptz,timestamptz,uuid) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION public.rpc_coupon_type_stats(timestamptz,timestamptz,uuid) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION public.rpc_coupon_channel_stats(timestamptz,timestamptz,uuid) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION public.rpc_coupon_trend_daily(timestamptz,timestamptz,uuid) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION public.rpc_coupon_conversion_effect(timestamptz,timestamptz,uuid) FROM PUBLIC;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_coupon_effectiveness_overview(timestamptz,timestamptz,uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_coupon_type_stats(timestamptz,timestamptz,uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_coupon_channel_stats(timestamptz,timestamptz,uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_coupon_trend_daily(timestamptz,timestamptz,uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_coupon_conversion_effect(timestamptz,timestamptz,uuid) TO authenticated;
|
||||
|
||||
-- 完成
|
||||
SELECT 'Coupon analysis RPCs created successfully!' AS message;
|
||||
|
||||
342
pages/mall/analytics/test/DATA_DETAIL_RPCS.sql
Normal file
342
pages/mall/analytics/test/DATA_DETAIL_RPCS.sql
Normal file
@@ -0,0 +1,342 @@
|
||||
-- ============================================
|
||||
-- DATA_DETAIL_RPCS.sql
|
||||
-- 数据分析详情页专用 RPC 定义
|
||||
-- ============================================
|
||||
-- 目标:
|
||||
-- 1) 为 `pages/mall/analytics/data-detail.uvue` 提供统一的数据服务
|
||||
-- 2) 仅复用现有 analytics_* 表与业务表,不新增物理表
|
||||
-- 3) 权限完全依赖各表自身的 RLS 策略,本文件只负责函数与 GRANT
|
||||
--
|
||||
-- 依赖前置脚本:
|
||||
-- - 01_create_tables.sql
|
||||
-- - ../../user/test/USER_AUTH_SCHEMA.sql
|
||||
-- - ../../user/test/USER_AUTH_TRIGGER.sql
|
||||
-- - ANALYTICS_DB_SCHEMA.sql
|
||||
--
|
||||
-- 使用说明:
|
||||
-- - 前端通过 supabase-js / UTS 调用 `rpc()` 访问本文件中的函数
|
||||
-- - 所有函数仅对 `authenticated` 角色开放执行权限
|
||||
-- ============================================
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 1. 报表基础信息(用于初始化筛选器)
|
||||
-- --------------------------------------------
|
||||
-- 根据报表 ID 返回基础配置,包含标题、类型、时间范围等
|
||||
CREATE OR REPLACE FUNCTION public.rpc_data_detail_report_info(
|
||||
p_report_id uuid
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id uuid,
|
||||
title text,
|
||||
type text,
|
||||
period text,
|
||||
date_start date,
|
||||
date_end date,
|
||||
status text,
|
||||
merchant_id uuid
|
||||
)
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
SELECT
|
||||
r.id,
|
||||
r.title,
|
||||
r.type,
|
||||
r.period,
|
||||
r.date_start,
|
||||
r.date_end,
|
||||
r.status,
|
||||
r.merchant_id
|
||||
FROM public.analytics_reports r
|
||||
WHERE r.id = p_report_id
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_data_detail_report_info(uuid) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_data_detail_report_info(uuid) TO authenticated;
|
||||
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 2. 报表明细行(表格数据)
|
||||
-- --------------------------------------------
|
||||
-- 说明:
|
||||
-- - 以 analytics_report_rows 作为数据源
|
||||
-- - 可按日期 / GMV / 订单数 / 用户数排序
|
||||
-- - 维度信息通过 extra(JSONB) 透出,前端可自由解析
|
||||
CREATE OR REPLACE FUNCTION public.rpc_data_detail_rows(
|
||||
p_report_id uuid,
|
||||
p_sort_by text DEFAULT 'row_date', -- row_date | gmv | orders | users
|
||||
p_sort_dir text DEFAULT 'asc', -- asc | desc
|
||||
p_limit integer DEFAULT 200,
|
||||
p_offset integer DEFAULT 0
|
||||
)
|
||||
RETURNS TABLE (
|
||||
row_date date,
|
||||
gmv numeric,
|
||||
orders integer,
|
||||
users integer,
|
||||
conversion numeric,
|
||||
avg_order_amount numeric,
|
||||
extra jsonb
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- 统一的 LIMIT / OFFSET 处理
|
||||
IF lower(p_sort_by) = 'gmv' THEN
|
||||
IF lower(p_sort_dir) = 'desc' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.gmv DESC, r.row_date DESC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
ELSE
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.gmv ASC, r.row_date ASC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
END IF;
|
||||
|
||||
ELSIF lower(p_sort_by) = 'orders' THEN
|
||||
IF lower(p_sort_dir) = 'desc' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.orders DESC, r.row_date DESC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
ELSE
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.orders ASC, r.row_date ASC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
END IF;
|
||||
|
||||
ELSIF lower(p_sort_by) = 'users' THEN
|
||||
IF lower(p_sort_dir) = 'desc' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.users DESC, r.row_date DESC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
ELSE
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.users ASC, r.row_date ASC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
END IF;
|
||||
|
||||
ELSE
|
||||
-- 默认按日期排序
|
||||
IF lower(p_sort_dir) = 'desc' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.row_date DESC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
ELSE
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.row_date,
|
||||
r.gmv,
|
||||
r.orders,
|
||||
r.users,
|
||||
r.conversion,
|
||||
r.avg_order_amount,
|
||||
r.extra
|
||||
FROM public.analytics_report_rows r
|
||||
WHERE r.report_id = p_report_id
|
||||
ORDER BY r.row_date ASC
|
||||
LIMIT GREATEST(p_limit, 0)
|
||||
OFFSET GREATEST(p_offset, 0);
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_data_detail_rows(uuid,text,text,integer,integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_data_detail_rows(uuid,text,text,integer,integer) TO authenticated;
|
||||
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 3. 钻取指标列表(KPI / 汇总卡片)
|
||||
-- --------------------------------------------
|
||||
-- 说明:
|
||||
-- - 直接从 analytics_report_metrics 读取
|
||||
-- - 前端可根据 format 字段决定展示方式(数字 / 金额 / 百分比)
|
||||
CREATE OR REPLACE FUNCTION public.rpc_data_detail_drill_items(
|
||||
p_report_id uuid
|
||||
)
|
||||
RETURNS TABLE (
|
||||
metric_key text,
|
||||
metric_label text,
|
||||
metric_value_num numeric,
|
||||
metric_value_text text,
|
||||
format text,
|
||||
change_pct numeric,
|
||||
icon text,
|
||||
color text
|
||||
)
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
SELECT
|
||||
m.metric_key,
|
||||
m.metric_label,
|
||||
m.metric_value_num,
|
||||
m.metric_value_text,
|
||||
m.format,
|
||||
m.change_pct,
|
||||
m.icon,
|
||||
m.color
|
||||
FROM public.analytics_report_metrics m
|
||||
WHERE m.report_id = p_report_id
|
||||
ORDER BY m.metric_key
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_data_detail_drill_items(uuid) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_data_detail_drill_items(uuid) TO authenticated;
|
||||
|
||||
|
||||
-- --------------------------------------------
|
||||
-- 4. GMV 对比数据(当前周期 vs 对比周期)
|
||||
-- --------------------------------------------
|
||||
-- 说明:
|
||||
-- - 当前周期 = analytics_reports.period / date_start/date_end 所定义的范围
|
||||
-- - 对比周期 = 与当前周期长度相同的上一段时间
|
||||
-- - 聚合来源:orders
|
||||
CREATE OR REPLACE FUNCTION public.rpc_data_detail_compare_gmv(
|
||||
p_report_id uuid
|
||||
)
|
||||
RETURNS TABLE (
|
||||
day date,
|
||||
gmv_current numeric,
|
||||
gmv_previous numeric
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_date_start date;
|
||||
v_date_end date;
|
||||
v_period_len integer;
|
||||
BEGIN
|
||||
SELECT
|
||||
COALESCE(r.date_start, (now() - INTERVAL '7 days')::date),
|
||||
COALESCE(r.date_end, now()::date)
|
||||
INTO v_date_start, v_date_end
|
||||
FROM public.analytics_reports r
|
||||
WHERE r.id = p_report_id;
|
||||
|
||||
IF v_date_start IS NULL OR v_date_end IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
v_period_len := (v_date_end - v_date_start) + 1;
|
||||
|
||||
RETURN QUERY
|
||||
WITH cur AS (
|
||||
SELECT
|
||||
o.created_at::date AS day,
|
||||
SUM(o.total_amount) AS gmv
|
||||
FROM public.orders o
|
||||
WHERE o.created_at::date BETWEEN v_date_start AND v_date_end
|
||||
AND o.status = 2
|
||||
GROUP BY o.created_at::date
|
||||
),
|
||||
prev_range AS (
|
||||
SELECT
|
||||
(v_date_start - v_period_len) AS start_date,
|
||||
(v_date_start - 1) AS end_date
|
||||
),
|
||||
prev AS (
|
||||
SELECT
|
||||
o.created_at::date AS day,
|
||||
SUM(o.total_amount) AS gmv
|
||||
FROM public.orders o, prev_range pr
|
||||
WHERE o.created_at::date BETWEEN pr.start_date AND pr.end_date
|
||||
AND o.status = 2
|
||||
GROUP BY o.created_at::date
|
||||
),
|
||||
series AS (
|
||||
SELECT generate_series(v_date_start, v_date_end, INTERVAL '1 day')::date AS day
|
||||
)
|
||||
SELECT
|
||||
s.day,
|
||||
COALESCE(c.gmv, 0) AS gmv_current,
|
||||
COALESCE(p.gmv, 0) AS gmv_previous
|
||||
FROM series s
|
||||
LEFT JOIN cur c ON c.day = s.day
|
||||
LEFT JOIN prev p ON p.day = (s.day - v_period_len);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.rpc_data_detail_compare_gmv(uuid) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_data_detail_compare_gmv(uuid) TO authenticated;
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 文件结束
|
||||
-- ============================================
|
||||
|
||||
73
pages/mall/analytics/test/DELIVERY_ANALYSIS_RPCS.sql
Normal file
73
pages/mall/analytics/test/DELIVERY_ANALYSIS_RPCS.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- ============================================
|
||||
-- 配送效率分析(按 assigned_at 统计周期,接单 -> 送达)
|
||||
-- 依赖表:ml_delivery_tasks, ml_delivery_drivers
|
||||
-- ============================================
|
||||
|
||||
-- 1) 每日趋势:平均时效/平均配送费/总配送费/完成单量
|
||||
CREATE OR REPLACE FUNCTION public.rpc_delivery_efficiency_daily(
|
||||
p_start TIMESTAMPTZ,
|
||||
p_end TIMESTAMPTZ
|
||||
)
|
||||
RETURNS TABLE (
|
||||
day DATE,
|
||||
completed_orders INTEGER,
|
||||
avg_delivery_minutes NUMERIC,
|
||||
total_fee NUMERIC,
|
||||
avg_fee NUMERIC
|
||||
)
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
SELECT
|
||||
DATE(t.assigned_at) AS day,
|
||||
COUNT(*)::INTEGER AS completed_orders,
|
||||
AVG(EXTRACT(EPOCH FROM (t.delivered_at - t.assigned_at)) / 60.0) AS avg_delivery_minutes,
|
||||
COALESCE(SUM(t.delivery_fee), 0) AS total_fee,
|
||||
AVG(t.delivery_fee) AS avg_fee
|
||||
FROM public.ml_delivery_tasks t
|
||||
WHERE t.status = 5
|
||||
AND t.assigned_at >= p_start
|
||||
AND t.assigned_at < p_end
|
||||
AND t.delivered_at IS NOT NULL
|
||||
GROUP BY DATE(t.assigned_at)
|
||||
ORDER BY day;
|
||||
$$;
|
||||
|
||||
-- 2) Top N 配送员:按完成单量排序
|
||||
CREATE OR REPLACE FUNCTION public.rpc_delivery_efficiency_top_drivers(
|
||||
p_start TIMESTAMPTZ,
|
||||
p_end TIMESTAMPTZ,
|
||||
p_limit INTEGER DEFAULT 10
|
||||
)
|
||||
RETURNS TABLE (
|
||||
driver_id UUID,
|
||||
driver_name TEXT,
|
||||
orders INTEGER,
|
||||
rating_avg NUMERIC
|
||||
)
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
SELECT
|
||||
d.id AS driver_id,
|
||||
CAST(d.real_name AS TEXT) AS driver_name,
|
||||
COUNT(*)::INTEGER AS orders,
|
||||
COALESCE(d.rating_avg, 0) AS rating_avg
|
||||
FROM public.ml_delivery_tasks t
|
||||
JOIN public.ml_delivery_drivers d ON d.id = t.driver_id
|
||||
WHERE t.status = 5
|
||||
AND t.assigned_at >= p_start
|
||||
AND t.assigned_at < p_end
|
||||
AND t.delivered_at IS NOT NULL
|
||||
GROUP BY d.id, d.real_name, d.rating_avg
|
||||
ORDER BY orders DESC
|
||||
LIMIT p_limit;
|
||||
$$;
|
||||
|
||||
-- 建议:只允许 authenticated 调用(按你现有 analytics 的做法)
|
||||
REVOKE ALL ON FUNCTION public.rpc_delivery_efficiency_daily(TIMESTAMPTZ, TIMESTAMPTZ) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION public.rpc_delivery_efficiency_top_drivers(TIMESTAMPTZ, TIMESTAMPTZ, INTEGER) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_delivery_efficiency_daily(TIMESTAMPTZ, TIMESTAMPTZ) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.rpc_delivery_efficiency_top_drivers(TIMESTAMPTZ, TIMESTAMPTZ, INTEGER) TO authenticated;
|
||||
Reference in New Issue
Block a user