mall数据库文件

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

View File

@@ -0,0 +1,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;

View 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 唯一(测试阶段允许 NULLNULL 不冲突)
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;

View 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 $$;

View 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;

View 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;
$$;

View File

@@ -0,0 +1,160 @@
-- =============================================
-- 市场趋势页面测试数据 SeedSupabase / 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;

View 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; -- 应该返回 2515个今日 + 10个昨日
-- SELECT COUNT(*) FROM user_sessions; -- 应该返回 10
-- SELECT COUNT(*) FROM page_views; -- 应该返回 15

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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.sqlusers 表)
-- - ANALYTICS_DB_SCHEMA.sqlanalytics_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 $$;

View File

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

View File

@@ -0,0 +1,463 @@
-- ============================================
-- 数据分析模块测试数据 SeedSupabase/Postgres
-- ============================================
-- 用途:为 `pages/mall/analytics/*` 页面提供可联调的模拟数据
-- 参考:`docs/ANALYTICS_DB_DESIGN.md`
--
-- 执行顺序:
-- 1. 先执行基础业务表的 seedusers/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;

View 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;

View 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;
-- ============================================
-- 文件结束
-- ============================================

View 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;