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

2
.gitignore vendored
View File

@@ -17,8 +17,6 @@ claude*
# Logs and databases
*.log
*.sql
*.sqlite
# Dependency directories
node_modules/

View File

@@ -0,0 +1,692 @@
-- =====================================================================================
-- 商城系统增量升级脚本 (ALTER方式)
-- 用于在现有数据库基础上添加商城功能
-- 表名前缀: ml_ (mall)
-- 复用表: ak_users (用户主表)
-- 兼容: PostgreSQL + Supabase
-- =====================================================================================
-- =====================================================================================
-- 1. 启用必要的扩展
-- =====================================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
-- =====================================================================================
-- 2. 检查并创建商城核心表(如果不存在)
-- =====================================================================================
-- 商城用户扩展信息表
CREATE TABLE IF NOT EXISTS public.ml_user_profiles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
user_type INTEGER DEFAULT 1 NOT NULL,
status INTEGER DEFAULT 1 NOT NULL,
real_name VARCHAR(100),
id_card VARCHAR(32),
business_license VARCHAR(100),
credit_score INTEGER DEFAULT 100,
verification_status INTEGER DEFAULT 0,
verification_data JSONB DEFAULT '{}',
preferences JSONB DEFAULT '{}',
emergency_contact VARCHAR(200),
service_areas JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_user_type CHECK (user_type IN (1,2,3,4,5)),
CONSTRAINT chk_ml_user_status CHECK (status IN (1,2,3,4)),
CONSTRAINT chk_ml_verification_status CHECK (verification_status IN (0,1,2)),
CONSTRAINT chk_ml_credit_score CHECK (credit_score >= 0 AND credit_score <= 1000)
);
-- 用户地址表
CREATE TABLE IF NOT EXISTS public.ml_user_addresses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
receiver_name VARCHAR(100) NOT NULL,
receiver_phone VARCHAR(32) NOT NULL,
province VARCHAR(100) NOT NULL,
city VARCHAR(100) NOT NULL,
district VARCHAR(100) NOT NULL,
street VARCHAR(200),
address_detail TEXT NOT NULL,
postal_code VARCHAR(16),
is_default BOOLEAN DEFAULT FALSE,
label VARCHAR(50),
latitude DECIMAL(10,7),
longitude DECIMAL(10,7),
delivery_instructions TEXT,
business_hours VARCHAR(100),
status INTEGER DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_address_status CHECK (status IN (1,2))
);
-- 商品分类表
CREATE TABLE IF NOT EXISTS public.ml_categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL,
parent_id UUID REFERENCES public.ml_categories(id),
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE,
description TEXT,
icon_url TEXT,
banner_url TEXT,
sort_order INTEGER DEFAULT 0,
level INTEGER DEFAULT 1,
path TEXT[],
is_active BOOLEAN DEFAULT TRUE,
seo_title VARCHAR(200),
seo_description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 品牌表
CREATE TABLE IF NOT EXISTS public.ml_brands (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
logo_url TEXT,
description TEXT,
website VARCHAR(500),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 商品表
CREATE TABLE IF NOT EXISTS public.ml_products (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL,
merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
category_id UUID NOT NULL REFERENCES public.ml_categories(id),
brand_id UUID REFERENCES public.ml_brands(id),
product_code VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(500) NOT NULL,
subtitle VARCHAR(1000),
description TEXT,
main_image_url TEXT,
image_urls JSONB DEFAULT '[]',
video_urls JSONB DEFAULT '[]',
base_price DECIMAL(12,2) NOT NULL CHECK (base_price >= 0),
market_price DECIMAL(12,2),
cost_price DECIMAL(12,2),
total_stock INTEGER DEFAULT 0 CHECK (total_stock >= 0),
available_stock INTEGER DEFAULT 0 CHECK (available_stock >= 0),
min_order_qty INTEGER DEFAULT 1 CHECK (min_order_qty > 0),
max_order_qty INTEGER,
weight DECIMAL(10,3),
dimensions JSONB,
status INTEGER DEFAULT 1,
is_featured BOOLEAN DEFAULT FALSE,
is_new BOOLEAN DEFAULT FALSE,
is_hot BOOLEAN DEFAULT FALSE,
view_count INTEGER DEFAULT 0,
sale_count INTEGER DEFAULT 0,
favorite_count INTEGER DEFAULT 0,
rating_avg DECIMAL(3,2) DEFAULT 0.00 CHECK (rating_avg >= 0 AND rating_avg <= 5),
rating_count INTEGER DEFAULT 0,
seo_title VARCHAR(200),
seo_description VARCHAR(500),
seo_keywords TEXT[],
slug VARCHAR(200) UNIQUE,
tags TEXT[],
attributes JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
published_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_ml_product_status CHECK (status IN (1,2,3,4))
);
-- 商品SKU表
CREATE TABLE IF NOT EXISTS public.ml_product_skus (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
sku_code VARCHAR(100) UNIQUE NOT NULL,
specifications JSONB DEFAULT '{}',
price DECIMAL(12,2) NOT NULL CHECK (price >= 0),
market_price DECIMAL(12,2),
cost_price DECIMAL(12,2),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
warning_stock INTEGER DEFAULT 10,
image_url TEXT,
weight DECIMAL(10,3),
status INTEGER DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_sku_status CHECK (status IN (1,2))
);
-- 店铺信息表
CREATE TABLE IF NOT EXISTS public.ml_shops (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL,
merchant_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
shop_name VARCHAR(200) NOT NULL,
shop_logo TEXT,
shop_banner TEXT,
description TEXT,
business_license VARCHAR(100),
contact_name VARCHAR(100),
contact_phone VARCHAR(32),
contact_email VARCHAR(200),
address JSONB,
business_hours JSONB,
status INTEGER DEFAULT 1,
product_count INTEGER DEFAULT 0,
order_count INTEGER DEFAULT 0,
rating_avg DECIMAL(3,2) DEFAULT 0.00,
rating_count INTEGER DEFAULT 0,
verified_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_shop_status CHECK (status IN (1,2,3))
);
-- 订单表
CREATE TABLE IF NOT EXISTS public.ml_orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL,
order_no VARCHAR(50) UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES public.ak_users(id),
merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
product_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
discount_amount DECIMAL(12,2) DEFAULT 0,
shipping_fee DECIMAL(12,2) DEFAULT 0,
total_amount DECIMAL(12,2) NOT NULL,
paid_amount DECIMAL(12,2) DEFAULT 0,
shipping_address JSONB NOT NULL,
order_status INTEGER DEFAULT 1,
payment_status INTEGER DEFAULT 1,
shipping_status INTEGER DEFAULT 1,
paid_at TIMESTAMP WITH TIME ZONE,
shipped_at TIMESTAMP WITH TIME ZONE,
delivered_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
remark TEXT,
merchant_memo TEXT,
cancel_reason TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_order_status CHECK (order_status IN (1,2,3,4,5,6,7)),
CONSTRAINT chk_ml_payment_status CHECK (payment_status IN (1,2,3,4)),
CONSTRAINT chk_ml_shipping_status CHECK (shipping_status IN (1,2,3,4))
);
-- 购物车表
CREATE TABLE IF NOT EXISTS public.ml_shopping_cart (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
sku_id UUID REFERENCES public.ml_product_skus(id) ON DELETE CASCADE,
quantity INTEGER NOT NULL CHECK (quantity > 0),
selected BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, product_id, sku_id)
);
-- =====================================================================================
-- 3. ALTER 语句:为现有表添加商城相关字段
-- =====================================================================================
-- 为 ak_users 表添加商城相关字段(如果不存在)
DO $$
BEGIN
-- 添加商城相关字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_status') THEN
ALTER TABLE public.ak_users ADD COLUMN mall_status INTEGER DEFAULT 1; -- 1:正常 2:禁用
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_type') THEN
ALTER TABLE public.ak_users ADD COLUMN mall_type INTEGER DEFAULT 1; -- 1:消费者 2:商家
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'last_login_ip') THEN
ALTER TABLE public.ak_users ADD COLUMN last_login_ip INET;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'total_orders') THEN
ALTER TABLE public.ak_users ADD COLUMN total_orders INTEGER DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'total_spent') THEN
ALTER TABLE public.ak_users ADD COLUMN total_spent DECIMAL(12,2) DEFAULT 0.00;
END IF;
RAISE NOTICE 'ak_users 表字段添加完成';
END $$;
-- =====================================================================================
-- 4. 创建索引
-- =====================================================================================
-- 用户扩展表索引
CREATE INDEX IF NOT EXISTS idx_ml_user_profiles_user_id ON public.ml_user_profiles(user_id);
CREATE INDEX IF NOT EXISTS idx_ml_user_profiles_type ON public.ml_user_profiles(user_type);
CREATE INDEX IF NOT EXISTS idx_ml_user_profiles_status ON public.ml_user_profiles(status);
-- 分类表索引
CREATE INDEX IF NOT EXISTS idx_ml_categories_cid ON public.ml_categories(cid);
CREATE INDEX IF NOT EXISTS idx_ml_categories_parent ON public.ml_categories(parent_id);
CREATE INDEX IF NOT EXISTS idx_ml_categories_slug ON public.ml_categories(slug);
CREATE INDEX IF NOT EXISTS idx_ml_categories_level ON public.ml_categories(level, sort_order);
-- 品牌表索引
CREATE INDEX IF NOT EXISTS idx_ml_brands_cid ON public.ml_brands(cid);
CREATE INDEX IF NOT EXISTS idx_ml_brands_name ON public.ml_brands(name);
-- 地址表索引
CREATE INDEX IF NOT EXISTS idx_ml_user_addresses_user_id ON public.ml_user_addresses(user_id);
CREATE INDEX IF NOT EXISTS idx_ml_user_addresses_default ON public.ml_user_addresses(user_id, is_default);
-- 商品表索引
CREATE INDEX IF NOT EXISTS idx_ml_products_cid ON public.ml_products(cid);
CREATE INDEX IF NOT EXISTS idx_ml_products_merchant ON public.ml_products(merchant_id, status);
CREATE INDEX IF NOT EXISTS idx_ml_products_category ON public.ml_products(category_id, status);
CREATE INDEX IF NOT EXISTS idx_ml_products_status ON public.ml_products(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_products_featured ON public.ml_products(is_featured, status);
CREATE INDEX IF NOT EXISTS idx_ml_products_price ON public.ml_products(base_price);
CREATE INDEX IF NOT EXISTS idx_ml_products_rating ON public.ml_products(rating_avg DESC, rating_count DESC);
CREATE INDEX IF NOT EXISTS idx_ml_products_tags ON public.ml_products USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_ml_products_slug ON public.ml_products(slug);
-- 店铺表索引
CREATE INDEX IF NOT EXISTS idx_ml_shops_cid ON public.ml_shops(cid);
CREATE INDEX IF NOT EXISTS idx_ml_shops_merchant ON public.ml_shops(merchant_id);
-- SKU表索引
CREATE INDEX IF NOT EXISTS idx_ml_product_skus_product ON public.ml_product_skus(product_id);
CREATE INDEX IF NOT EXISTS idx_ml_product_skus_code ON public.ml_product_skus(sku_code);
-- 订单表索引
CREATE INDEX IF NOT EXISTS idx_ml_orders_cid ON public.ml_orders(cid);
CREATE INDEX IF NOT EXISTS idx_ml_orders_user ON public.ml_orders(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_orders_merchant ON public.ml_orders(merchant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_orders_status ON public.ml_orders(order_status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_orders_no ON public.ml_orders(order_no);
-- 购物车表索引
CREATE INDEX IF NOT EXISTS idx_ml_shopping_cart_user ON public.ml_shopping_cart(user_id);
-- ak_users 表新增字段索引
CREATE INDEX IF NOT EXISTS idx_ak_users_mall_status ON public.ak_users(mall_status);
CREATE INDEX IF NOT EXISTS idx_ak_users_mall_type ON public.ak_users(mall_type);
CREATE INDEX IF NOT EXISTS idx_ak_users_total_orders ON public.ak_users(total_orders DESC);
-- =====================================================================================
-- 5. 创建序列(如果不存在)
-- =====================================================================================
CREATE SEQUENCE IF NOT EXISTS public.ml_order_seq START 1;
-- =====================================================================================
-- 6. 创建或替换触发器函数
-- =====================================================================================
-- 自动更新 updated_at 字段的函数
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 确保每个用户只有一个默认地址的触发器函数
CREATE OR REPLACE FUNCTION public.ensure_single_default_address()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.is_default = TRUE THEN
UPDATE public.ml_user_addresses
SET is_default = FALSE
WHERE user_id = NEW.user_id AND id != NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 商品库存更新触发器函数
CREATE OR REPLACE FUNCTION public.update_product_stock()
RETURNS TRIGGER AS $$
BEGIN
-- 更新商品总库存
UPDATE public.ml_products
SET
total_stock = (
SELECT COALESCE(SUM(stock), 0)
FROM public.ml_product_skus
WHERE product_id = COALESCE(NEW.product_id, OLD.product_id) AND status = 1
),
available_stock = (
SELECT COALESCE(SUM(stock), 0)
FROM public.ml_product_skus
WHERE product_id = COALESCE(NEW.product_id, OLD.product_id) AND status = 1
)
WHERE id = COALESCE(NEW.product_id, OLD.product_id);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- 订单状态变更处理函数
CREATE OR REPLACE FUNCTION public.handle_order_status_change()
RETURNS TRIGGER AS $$
BEGIN
-- 如果订单状态变为已付款
IF NEW.order_status = 2 AND OLD.order_status = 1 THEN
NEW.paid_at = NOW();
END IF;
-- 如果订单状态变为已发货
IF NEW.order_status = 3 AND OLD.order_status = 2 THEN
NEW.shipped_at = NOW();
END IF;
-- 如果订单状态变为已完成
IF NEW.order_status = 4 AND OLD.order_status = 3 THEN
NEW.delivered_at = NOW();
NEW.completed_at = NOW();
-- 更新用户统计数据
UPDATE public.ak_users
SET
total_orders = total_orders + 1,
total_spent = total_spent + NEW.total_amount
WHERE id = NEW.user_id;
-- 更新商品销量
UPDATE public.ml_products
SET sale_count = sale_count + (
SELECT SUM(quantity)
FROM public.ml_order_items
WHERE order_id = NEW.id
)
WHERE id IN (
SELECT product_id
FROM public.ml_order_items
WHERE order_id = NEW.id
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- =====================================================================================
-- 7. 创建触发器
-- =====================================================================================
-- 删除可能存在的同名触发器,然后重新创建
DROP TRIGGER IF EXISTS trigger_ml_user_profiles_updated_at ON public.ml_user_profiles;
CREATE TRIGGER trigger_ml_user_profiles_updated_at
BEFORE UPDATE ON public.ml_user_profiles
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS trigger_ml_user_addresses_updated_at ON public.ml_user_addresses;
CREATE TRIGGER trigger_ml_user_addresses_updated_at
BEFORE UPDATE ON public.ml_user_addresses
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS trigger_ml_products_updated_at ON public.ml_products;
CREATE TRIGGER trigger_ml_products_updated_at
BEFORE UPDATE ON public.ml_products
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS trigger_ml_product_skus_updated_at ON public.ml_product_skus;
CREATE TRIGGER trigger_ml_product_skus_updated_at
BEFORE UPDATE ON public.ml_product_skus
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS trigger_ml_shops_updated_at ON public.ml_shops;
CREATE TRIGGER trigger_ml_shops_updated_at
BEFORE UPDATE ON public.ml_shops
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS trigger_ml_orders_updated_at ON public.ml_orders;
CREATE TRIGGER trigger_ml_orders_updated_at
BEFORE UPDATE ON public.ml_orders
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS trigger_ml_shopping_cart_updated_at ON public.ml_shopping_cart;
CREATE TRIGGER trigger_ml_shopping_cart_updated_at
BEFORE UPDATE ON public.ml_shopping_cart
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS trigger_ml_single_default_address ON public.ml_user_addresses;
CREATE TRIGGER trigger_ml_single_default_address
BEFORE INSERT OR UPDATE ON public.ml_user_addresses
FOR EACH ROW EXECUTE FUNCTION public.ensure_single_default_address();
DROP TRIGGER IF EXISTS trigger_ml_update_product_stock ON public.ml_product_skus;
CREATE TRIGGER trigger_ml_update_product_stock
AFTER INSERT OR UPDATE OR DELETE ON public.ml_product_skus
FOR EACH ROW EXECUTE FUNCTION public.update_product_stock();
DROP TRIGGER IF EXISTS trigger_ml_order_status_change ON public.ml_orders;
CREATE TRIGGER trigger_ml_order_status_change
BEFORE UPDATE ON public.ml_orders
FOR EACH ROW EXECUTE FUNCTION public.handle_order_status_change();
-- =====================================================================================
-- 8. 创建实用函数
-- =====================================================================================
-- 生成订单号的函数
CREATE OR REPLACE FUNCTION public.generate_order_no()
RETURNS TEXT AS $$
DECLARE
order_no TEXT;
BEGIN
order_no := 'ML' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(NEXTVAL('ml_order_seq')::TEXT, 6, '0');
RETURN order_no;
END;
$$ LANGUAGE plpgsql;
-- 生成优惠券码的函数
CREATE OR REPLACE FUNCTION public.generate_coupon_code()
RETURNS TEXT AS $$
DECLARE
code TEXT;
chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
result TEXT := '';
i INTEGER;
BEGIN
FOR i IN 1..8 LOOP
result := result || substr(chars, (random() * length(chars))::integer + 1, 1);
END LOOP;
RETURN 'CP' || result;
END;
$$ LANGUAGE plpgsql;
-- 检查用户是否为认证商家
CREATE OR REPLACE FUNCTION public.is_verified_merchant(p_user_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
result BOOLEAN := FALSE;
BEGIN
SELECT (user_type = 2 AND verification_status = 1) INTO result
FROM public.ml_user_profiles
WHERE user_id = p_user_id;
RETURN COALESCE(result, FALSE);
END;
$$ LANGUAGE plpgsql;
-- 计算购物车总金额
CREATE OR REPLACE FUNCTION public.calculate_cart_total(p_user_id UUID)
RETURNS DECIMAL AS $$
DECLARE
total_amount DECIMAL := 0;
BEGIN
SELECT COALESCE(SUM(
CASE
WHEN c.sku_id IS NOT NULL THEN s.price * c.quantity
ELSE p.base_price * c.quantity
END
), 0) INTO total_amount
FROM public.ml_shopping_cart c
LEFT JOIN public.ml_product_skus s ON c.sku_id = s.id
LEFT JOIN public.ml_products p ON c.product_id = p.id
WHERE c.user_id = p_user_id
AND c.selected = TRUE
AND p.status = 1
AND (s.id IS NULL OR s.status = 1);
RETURN total_amount;
END;
$$ LANGUAGE plpgsql;
-- SEO友好的获取商品信息函数
CREATE OR REPLACE FUNCTION public.get_product_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
name VARCHAR,
slug VARCHAR,
description TEXT,
main_image_url TEXT,
base_price DECIMAL,
rating_avg DECIMAL,
sale_count INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
p.id,
p.cid,
p.name,
p.slug,
p.description,
p.main_image_url,
p.base_price,
p.rating_avg,
p.sale_count
FROM public.ml_products p
WHERE p.cid = p_cid AND p.status = 1;
END;
$$ LANGUAGE plpgsql;
-- =====================================================================================
-- 9. 创建视图
-- =====================================================================================
-- 商城用户完整信息视图
CREATE OR REPLACE VIEW public.ml_users_view AS
SELECT
u.id,
u.username,
u.email,
u.phone,
u.avatar_url,
u.gender,
u.birthday,
u.bio,
u.created_at as user_created_at,
u.updated_at as user_updated_at,
u.mall_status,
u.mall_type,
u.total_orders,
u.total_spent,
p.user_type,
p.status,
p.real_name,
p.credit_score,
p.verification_status,
CASE
WHEN p.user_type = 1 THEN '消费者'
WHEN p.user_type = 2 THEN '商家'
WHEN p.user_type = 3 THEN '配送员'
WHEN p.user_type = 4 THEN '客服'
WHEN p.user_type = 5 THEN '管理员'
ELSE '未知'
END as user_type_name
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles p ON u.id = p.user_id;
-- 商品详情视图
CREATE OR REPLACE VIEW public.ml_products_detail_view AS
SELECT
p.*,
c.cid as category_cid,
c.name as category_name,
c.path as category_path,
b.cid as brand_cid,
b.name as brand_name,
s.cid as shop_cid,
s.shop_name,
u.username as merchant_name,
CASE
WHEN p.status = 1 THEN '上架'
WHEN p.status = 2 THEN '下架'
WHEN p.status = 3 THEN '草稿'
WHEN p.status = 4 THEN '删除'
ELSE '未知'
END as status_name
FROM public.ml_products p
LEFT JOIN public.ml_categories c ON p.category_id = c.id
LEFT JOIN public.ml_brands b ON p.brand_id = b.id
LEFT JOIN public.ml_shops s ON p.merchant_id = s.merchant_id
LEFT JOIN public.ak_users u ON p.merchant_id = u.id;
-- =====================================================================================
-- 10. 初始化基础数据
-- =====================================================================================
-- 插入默认分类(如果不存在)
INSERT INTO public.ml_categories (id, name, slug, level, path)
SELECT * FROM (VALUES
(uuid_generate_v4(), '数码电器', 'digital', 1, ARRAY['数码电器']),
(uuid_generate_v4(), '服装鞋帽', 'fashion', 1, ARRAY['服装鞋帽']),
(uuid_generate_v4(), '家居用品', 'home', 1, ARRAY['家居用品']),
(uuid_generate_v4(), '食品饮料', 'food', 1, ARRAY['食品饮料']),
(uuid_generate_v4(), '美妆护肤', 'beauty', 1, ARRAY['美妆护肤'])
) AS v(id, name, slug, level, path)
WHERE NOT EXISTS (SELECT 1 FROM public.ml_categories WHERE slug = v.slug);
-- 为现有 ak_users 用户创建默认商城档案(如果不存在)
INSERT INTO public.ml_user_profiles (user_id, user_type, status)
SELECT
id,
1, -- 默认为消费者
1 -- 默认状态正常
FROM public.ak_users
WHERE id NOT IN (SELECT user_id FROM public.ml_user_profiles WHERE user_id IS NOT NULL);
-- =====================================================================================
-- 11. 完成提示
-- =====================================================================================
DO $$
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '商城系统增量升级完成!';
RAISE NOTICE '=======================================================';
RAISE NOTICE '✓ 扩展创建完成';
RAISE NOTICE '✓ 商城表结构创建/检查完成';
RAISE NOTICE '✓ ak_users 表字段添加完成';
RAISE NOTICE '✓ 索引创建完成';
RAISE NOTICE '✓ 触发器创建完成';
RAISE NOTICE '✓ 实用函数创建完成';
RAISE NOTICE '✓ 视图创建完成';
RAISE NOTICE '✓ 基础数据初始化完成';
RAISE NOTICE '=======================================================';
RAISE NOTICE '使用说明:';
RAISE NOTICE '1. 此脚本安全执行,不会覆盖现有数据';
RAISE NOTICE '2. 使用 IF NOT EXISTS 和 IF EXISTS 检查避免重复';
RAISE NOTICE '3. 为现有用户自动创建商城档案';
RAISE NOTICE '4. 所有新表前缀: ml_';
RAISE NOTICE '5. 复用表: ak_users';
RAISE NOTICE '=======================================================';
END $$;

View File

@@ -0,0 +1,734 @@
-- =====================================================================================
-- 商城系统字段增量添加脚本 (仅字段和索引)
-- 适用于已有表结构,仅添加缺失字段和索引的场景
-- =====================================================================================
-- =====================================================================================
-- 1. 为现有 ak_users 表添加商城字段
-- =====================================================================================
DO $$
BEGIN
-- 商城状态字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_status') THEN
ALTER TABLE public.ak_users ADD COLUMN mall_status INTEGER DEFAULT 1;
ALTER TABLE public.ak_users ADD CONSTRAINT chk_ak_users_mall_status CHECK (mall_status IN (1,2));
RAISE NOTICE '✓ 添加字段: ak_users.mall_status';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.mall_status';
END IF;
-- 商城用户类型字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_type') THEN
ALTER TABLE public.ak_users ADD COLUMN mall_type INTEGER DEFAULT 1;
ALTER TABLE public.ak_users ADD CONSTRAINT chk_ak_users_mall_type CHECK (mall_type IN (1,2,3));
RAISE NOTICE '✓ 添加字段: ak_users.mall_type';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.mall_type';
END IF;
-- 最后登录IP字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'last_login_ip') THEN
ALTER TABLE public.ak_users ADD COLUMN last_login_ip INET;
RAISE NOTICE '✓ 添加字段: ak_users.last_login_ip';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.last_login_ip';
END IF;
-- 总订单数字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'total_orders') THEN
ALTER TABLE public.ak_users ADD COLUMN total_orders INTEGER DEFAULT 0 CHECK (total_orders >= 0);
RAISE NOTICE '✓ 添加字段: ak_users.total_orders';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.total_orders';
END IF;
-- 总消费金额字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'total_spent') THEN
ALTER TABLE public.ak_users ADD COLUMN total_spent DECIMAL(12,2) DEFAULT 0.00 CHECK (total_spent >= 0);
RAISE NOTICE '✓ 添加字段: ak_users.total_spent';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.total_spent';
END IF;
-- 用户等级字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'user_level') THEN
ALTER TABLE public.ak_users ADD COLUMN user_level INTEGER DEFAULT 1 CHECK (user_level >= 1 AND user_level <= 10);
RAISE NOTICE '✓ 添加字段: ak_users.user_level';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.user_level';
END IF;
-- 积分字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'points') THEN
ALTER TABLE public.ak_users ADD COLUMN points INTEGER DEFAULT 0 CHECK (points >= 0);
RAISE NOTICE '✓ 添加字段: ak_users.points';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.points';
END IF;
-- 实名认证状态
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'verified_status') THEN
ALTER TABLE public.ak_users ADD COLUMN verified_status INTEGER DEFAULT 0 CHECK (verified_status IN (0,1,2));
RAISE NOTICE '✓ 添加字段: ak_users.verified_status';
ELSE
RAISE NOTICE '○ 字段已存在: ak_users.verified_status';
END IF;
RAISE NOTICE '>> ak_users 表字段检查完成';
END $$;
-- =====================================================================================
-- 2. 为现有商城表添加CID字段SEO优化必需
-- =====================================================================================
-- 为主要商城表添加cid自增字段
DO $$
BEGIN
-- 为 ml_categories 表添加 cid 字段
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_categories') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_categories' AND column_name = 'cid') THEN
-- 创建序列
CREATE SEQUENCE IF NOT EXISTS public.ml_categories_cid_seq;
-- 添加cid字段
ALTER TABLE public.ml_categories ADD COLUMN cid INTEGER UNIQUE DEFAULT nextval('public.ml_categories_cid_seq');
-- 设置序列所有者
ALTER SEQUENCE public.ml_categories_cid_seq OWNED BY public.ml_categories.cid;
-- 更新现有记录的cid值
UPDATE public.ml_categories SET cid = nextval('public.ml_categories_cid_seq') WHERE cid IS NULL;
RAISE NOTICE '✓ 添加字段: ml_categories.cid (自增SEO ID)';
ELSE
RAISE NOTICE '○ 字段已存在: ml_categories.cid';
END IF;
END IF;
-- 为 ml_brands 表添加 cid 字段
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_brands') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_brands' AND column_name = 'cid') THEN
CREATE SEQUENCE IF NOT EXISTS public.ml_brands_cid_seq;
ALTER TABLE public.ml_brands ADD COLUMN cid INTEGER UNIQUE DEFAULT nextval('public.ml_brands_cid_seq');
ALTER SEQUENCE public.ml_brands_cid_seq OWNED BY public.ml_brands.cid;
UPDATE public.ml_brands SET cid = nextval('public.ml_brands_cid_seq') WHERE cid IS NULL;
RAISE NOTICE '✓ 添加字段: ml_brands.cid (自增SEO ID)';
ELSE
RAISE NOTICE '○ 字段已存在: ml_brands.cid';
END IF;
END IF;
-- 为 ml_products 表添加 cid 字段
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_products') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'cid') THEN
CREATE SEQUENCE IF NOT EXISTS public.ml_products_cid_seq;
ALTER TABLE public.ml_products ADD COLUMN cid INTEGER UNIQUE DEFAULT nextval('public.ml_products_cid_seq');
ALTER SEQUENCE public.ml_products_cid_seq OWNED BY public.ml_products.cid;
UPDATE public.ml_products SET cid = nextval('public.ml_products_cid_seq') WHERE cid IS NULL;
RAISE NOTICE '✓ 添加字段: ml_products.cid (自增SEO ID)';
ELSE
RAISE NOTICE '○ 字段已存在: ml_products.cid';
END IF;
END IF;
-- 为 ml_shops 表添加 cid 字段
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_shops') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_shops' AND column_name = 'cid') THEN
CREATE SEQUENCE IF NOT EXISTS public.ml_shops_cid_seq;
ALTER TABLE public.ml_shops ADD COLUMN cid INTEGER UNIQUE DEFAULT nextval('public.ml_shops_cid_seq');
ALTER SEQUENCE public.ml_shops_cid_seq OWNED BY public.ml_shops.cid;
UPDATE public.ml_shops SET cid = nextval('public.ml_shops_cid_seq') WHERE cid IS NULL;
RAISE NOTICE '✓ 添加字段: ml_shops.cid (自增SEO ID)';
ELSE
RAISE NOTICE '○ 字段已存在: ml_shops.cid';
END IF;
END IF;
-- 为 ml_orders 表添加 cid 字段
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_orders') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_orders' AND column_name = 'cid') THEN
CREATE SEQUENCE IF NOT EXISTS public.ml_orders_cid_seq;
ALTER TABLE public.ml_orders ADD COLUMN cid INTEGER UNIQUE DEFAULT nextval('public.ml_orders_cid_seq');
ALTER SEQUENCE public.ml_orders_cid_seq OWNED BY public.ml_orders.cid;
UPDATE public.ml_orders SET cid = nextval('public.ml_orders_cid_seq') WHERE cid IS NULL;
RAISE NOTICE '✓ 添加字段: ml_orders.cid (自增SEO ID)';
ELSE
RAISE NOTICE '○ 字段已存在: ml_orders.cid';
END IF;
END IF;
RAISE NOTICE '>> CID 字段添加完成';
END $$;
-- =====================================================================================
-- 3. 为现有商城表添加其他字段(如果表存在的话)
-- =====================================================================================
-- 为 ml_products 表添加SEO和营销字段
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_products') THEN
-- SEO标题
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'seo_title') THEN
ALTER TABLE public.ml_products ADD COLUMN seo_title VARCHAR(200);
RAISE NOTICE '✓ 添加字段: ml_products.seo_title';
END IF;
-- SEO描述
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'seo_description') THEN
ALTER TABLE public.ml_products ADD COLUMN seo_description VARCHAR(500);
RAISE NOTICE '✓ 添加字段: ml_products.seo_description';
END IF;
-- SEO关键词
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'seo_keywords') THEN
ALTER TABLE public.ml_products ADD COLUMN seo_keywords TEXT[];
RAISE NOTICE '✓ 添加字段: ml_products.seo_keywords';
END IF;
-- URL slug
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'slug') THEN
ALTER TABLE public.ml_products ADD COLUMN slug VARCHAR(200) UNIQUE;
RAISE NOTICE '✓ 添加字段: ml_products.slug';
END IF;
-- 标签
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'tags') THEN
ALTER TABLE public.ml_products ADD COLUMN tags TEXT[];
RAISE NOTICE '✓ 添加字段: ml_products.tags';
END IF;
-- 是否特色商品
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'is_featured') THEN
ALTER TABLE public.ml_products ADD COLUMN is_featured BOOLEAN DEFAULT FALSE;
RAISE NOTICE '✓ 添加字段: ml_products.is_featured';
END IF;
-- 是否新品
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'is_new') THEN
ALTER TABLE public.ml_products ADD COLUMN is_new BOOLEAN DEFAULT FALSE;
RAISE NOTICE '✓ 添加字段: ml_products.is_new';
END IF;
-- 是否热销
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'is_hot') THEN
ALTER TABLE public.ml_products ADD COLUMN is_hot BOOLEAN DEFAULT FALSE;
RAISE NOTICE '✓ 添加字段: ml_products.is_hot';
END IF;
-- 浏览次数
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'view_count') THEN
ALTER TABLE public.ml_products ADD COLUMN view_count INTEGER DEFAULT 0;
RAISE NOTICE '✓ 添加字段: ml_products.view_count';
END IF;
-- 销售数量
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'sale_count') THEN
ALTER TABLE public.ml_products ADD COLUMN sale_count INTEGER DEFAULT 0;
RAISE NOTICE '✓ 添加字段: ml_products.sale_count';
END IF;
-- 收藏数量
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'favorite_count') THEN
ALTER TABLE public.ml_products ADD COLUMN favorite_count INTEGER DEFAULT 0;
RAISE NOTICE '✓ 添加字段: ml_products.favorite_count';
END IF;
RAISE NOTICE '>> ml_products 表字段检查完成';
ELSE
RAISE NOTICE '○ ml_products 表不存在,跳过字段添加';
END IF;
END $$;
-- 为 ml_categories 表添加SEO字段
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_categories') THEN
-- SEO标题
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_categories' AND column_name = 'seo_title') THEN
ALTER TABLE public.ml_categories ADD COLUMN seo_title VARCHAR(200);
RAISE NOTICE '✓ 添加字段: ml_categories.seo_title';
END IF;
-- SEO描述
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_categories' AND column_name = 'seo_description') THEN
ALTER TABLE public.ml_categories ADD COLUMN seo_description VARCHAR(500);
RAISE NOTICE '✓ 添加字段: ml_categories.seo_description';
END IF;
-- URL slug
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_categories' AND column_name = 'slug') THEN
ALTER TABLE public.ml_categories ADD COLUMN slug VARCHAR(200) UNIQUE;
RAISE NOTICE '✓ 添加字段: ml_categories.slug';
END IF;
RAISE NOTICE '>> ml_categories 表字段检查完成';
ELSE
RAISE NOTICE '○ ml_categories 表不存在,跳过字段添加';
END IF;
END $$;
-- =====================================================================================
-- 4. 创建CID字段索引SEO优化必需
-- =====================================================================================
-- 为CID字段创建索引
DO $$
BEGIN
-- ml_categories cid 索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_categories' AND column_name = 'cid') THEN
CREATE INDEX IF NOT EXISTS idx_ml_categories_cid ON public.ml_categories(cid);
RAISE NOTICE '✓ 创建索引: idx_ml_categories_cid';
END IF;
-- ml_brands cid 索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_brands' AND column_name = 'cid') THEN
CREATE INDEX IF NOT EXISTS idx_ml_brands_cid ON public.ml_brands(cid);
RAISE NOTICE '✓ 创建索引: idx_ml_brands_cid';
END IF;
-- ml_products cid 索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'cid') THEN
CREATE INDEX IF NOT EXISTS idx_ml_products_cid ON public.ml_products(cid);
RAISE NOTICE '✓ 创建索引: idx_ml_products_cid';
END IF;
-- ml_shops cid 索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_shops' AND column_name = 'cid') THEN
CREATE INDEX IF NOT EXISTS idx_ml_shops_cid ON public.ml_shops(cid);
RAISE NOTICE '✓ 创建索引: idx_ml_shops_cid';
END IF;
-- ml_orders cid 索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_orders' AND column_name = 'cid') THEN
CREATE INDEX IF NOT EXISTS idx_ml_orders_cid ON public.ml_orders(cid);
RAISE NOTICE '✓ 创建索引: idx_ml_orders_cid';
END IF;
RAISE NOTICE '>> CID 索引创建完成';
END $$;
-- =====================================================================================
-- 5. 创建索引(仅在字段存在时创建)
-- =====================================================================================
-- ak_users 表索引
DO $$
BEGIN
-- 商城状态索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_status') THEN
CREATE INDEX IF NOT EXISTS idx_ak_users_mall_status ON public.ak_users(mall_status);
RAISE NOTICE '✓ 创建索引: idx_ak_users_mall_status';
END IF;
-- 商城类型索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_type') THEN
CREATE INDEX IF NOT EXISTS idx_ak_users_mall_type ON public.ak_users(mall_type);
RAISE NOTICE '✓ 创建索引: idx_ak_users_mall_type';
END IF;
-- 订单数量索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'total_orders') THEN
CREATE INDEX IF NOT EXISTS idx_ak_users_total_orders ON public.ak_users(total_orders DESC);
RAISE NOTICE '✓ 创建索引: idx_ak_users_total_orders';
END IF;
-- 消费金额索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'total_spent') THEN
CREATE INDEX IF NOT EXISTS idx_ak_users_total_spent ON public.ak_users(total_spent DESC);
RAISE NOTICE '✓ 创建索引: idx_ak_users_total_spent';
END IF;
-- 用户等级索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'user_level') THEN
CREATE INDEX IF NOT EXISTS idx_ak_users_level ON public.ak_users(user_level);
RAISE NOTICE '✓ 创建索引: idx_ak_users_level';
END IF;
-- 积分索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'points') THEN
CREATE INDEX IF NOT EXISTS idx_ak_users_points ON public.ak_users(points DESC);
RAISE NOTICE '✓ 创建索引: idx_ak_users_points';
END IF;
-- 认证状态索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'verified_status') THEN
CREATE INDEX IF NOT EXISTS idx_ak_users_verified ON public.ak_users(verified_status);
RAISE NOTICE '✓ 创建索引: idx_ak_users_verified';
END IF;
RAISE NOTICE '>> ak_users 表索引创建完成';
END $$;
-- ml_products 表索引
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_products') THEN
-- slug 索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'slug') THEN
CREATE INDEX IF NOT EXISTS idx_ml_products_slug ON public.ml_products(slug);
RAISE NOTICE '✓ 创建索引: idx_ml_products_slug';
END IF;
-- 特色商品索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'is_featured') THEN
CREATE INDEX IF NOT EXISTS idx_ml_products_featured ON public.ml_products(is_featured, status);
RAISE NOTICE '✓ 创建索引: idx_ml_products_featured';
END IF;
-- 标签索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'tags') THEN
CREATE INDEX IF NOT EXISTS idx_ml_products_tags ON public.ml_products USING GIN(tags);
RAISE NOTICE '✓ 创建索引: idx_ml_products_tags (GIN)';
END IF;
-- 浏览量索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'view_count') THEN
CREATE INDEX IF NOT EXISTS idx_ml_products_view_count ON public.ml_products(view_count DESC);
RAISE NOTICE '✓ 创建索引: idx_ml_products_view_count';
END IF;
-- 销量索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'sale_count') THEN
CREATE INDEX IF NOT EXISTS idx_ml_products_sale_count ON public.ml_products(sale_count DESC);
RAISE NOTICE '✓ 创建索引: idx_ml_products_sale_count';
END IF;
RAISE NOTICE '>> ml_products 表索引创建完成';
ELSE
RAISE NOTICE '○ ml_products 表不存在,跳过索引创建';
END IF;
END $$;
-- ml_categories 表索引
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_categories') THEN
-- slug 索引
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_categories' AND column_name = 'slug') THEN
CREATE INDEX IF NOT EXISTS idx_ml_categories_slug ON public.ml_categories(slug);
RAISE NOTICE '✓ 创建索引: idx_ml_categories_slug';
END IF;
RAISE NOTICE '>> ml_categories 表索引创建完成';
ELSE
RAISE NOTICE '○ ml_categories 表不存在,跳过索引创建';
END IF;
END $$;
-- =====================================================================================
-- 6. 创建或更新约束
-- =====================================================================================
DO $$
BEGIN
-- ak_users 表约束检查
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_status') THEN
-- 检查约束是否存在,不存在则添加
IF NOT EXISTS (SELECT 1 FROM information_schema.check_constraints WHERE constraint_name = 'chk_ak_users_mall_status') THEN
ALTER TABLE public.ak_users ADD CONSTRAINT chk_ak_users_mall_status CHECK (mall_status IN (1,2));
RAISE NOTICE '✓ 添加约束: chk_ak_users_mall_status';
END IF;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'mall_type') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.check_constraints WHERE constraint_name = 'chk_ak_users_mall_type') THEN
ALTER TABLE public.ak_users ADD CONSTRAINT chk_ak_users_mall_type CHECK (mall_type IN (1,2,3));
RAISE NOTICE '✓ 添加约束: chk_ak_users_mall_type';
END IF;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = 'verified_status') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.check_constraints WHERE constraint_name = 'chk_ak_users_verified_status') THEN
ALTER TABLE public.ak_users ADD CONSTRAINT chk_ak_users_verified_status CHECK (verified_status IN (0,1,2));
RAISE NOTICE '✓ 添加约束: chk_ak_users_verified_status';
END IF;
END IF;
RAISE NOTICE '>> 约束检查完成';
END $$;
-- =====================================================================================
-- 7. 创建SEO相关函数
-- =====================================================================================
-- 根据 cid 获取商品信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_product_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
name VARCHAR,
slug VARCHAR,
description TEXT,
main_image_url TEXT,
base_price DECIMAL,
rating_avg DECIMAL,
sale_count INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
p.id,
p.cid,
p.name,
p.slug,
p.description,
p.main_image_url,
p.base_price,
p.rating_avg,
p.sale_count
FROM public.ml_products p
WHERE p.cid = p_cid AND p.status = 1;
END;
$$ LANGUAGE plpgsql;
-- 根据 cid 获取分类信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_category_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
name VARCHAR,
slug VARCHAR,
description TEXT,
icon_url TEXT,
path TEXT[]
) AS $$
BEGIN
RETURN QUERY
SELECT
c.id,
c.cid,
c.name,
c.slug,
c.description,
c.icon_url,
c.path
FROM public.ml_categories c
WHERE c.cid = p_cid AND c.is_active = TRUE;
END;
$$ LANGUAGE plpgsql;
-- 根据 cid 获取品牌信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_brand_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
name VARCHAR,
logo_url TEXT,
description TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
b.id,
b.cid,
b.name,
b.logo_url,
b.description
FROM public.ml_brands b
WHERE b.cid = p_cid AND b.is_active = TRUE;
END;
$$ LANGUAGE plpgsql;
-- 根据 cid 获取店铺信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_shop_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
shop_name VARCHAR,
description TEXT,
shop_logo TEXT,
rating_avg DECIMAL,
product_count INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
s.id,
s.cid,
s.shop_name,
s.description,
s.shop_logo,
s.rating_avg,
s.product_count
FROM public.ml_shops s
WHERE s.cid = p_cid AND s.status = 1;
END;
$$ LANGUAGE plpgsql;
-- 生成 SEO 友好的 URL 路径
CREATE OR REPLACE FUNCTION public.generate_seo_url(
p_type VARCHAR, -- 'product', 'category', 'brand', 'shop'
p_cid INTEGER,
p_slug VARCHAR DEFAULT NULL
)
RETURNS TEXT AS $$
DECLARE
url_path TEXT;
BEGIN
CASE p_type
WHEN 'product' THEN
url_path := '/product/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
WHEN 'category' THEN
url_path := '/category/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
WHEN 'brand' THEN
url_path := '/brand/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
WHEN 'shop' THEN
url_path := '/shop/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
ELSE
url_path := '/' || p_type || '/' || p_cid;
END CASE;
RETURN url_path;
END;
$$ LANGUAGE plpgsql;
-- 批量更新 slug 字段的函数
CREATE OR REPLACE FUNCTION public.update_seo_slugs()
RETURNS VOID AS $$
BEGIN
-- 更新商品 slug
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_products') THEN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_products' AND column_name = 'slug') THEN
UPDATE public.ml_products
SET slug = LOWER(REGEXP_REPLACE(TRIM(name), '[^a-zA-Z0-9\u4e00-\u9fa5]+', '-', 'g'))
WHERE slug IS NULL OR slug = '';
END IF;
END IF;
-- 更新分类 slug
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_categories') THEN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_categories' AND column_name = 'slug') THEN
UPDATE public.ml_categories
SET slug = LOWER(REGEXP_REPLACE(TRIM(name), '[^a-zA-Z0-9\u4e00-\u9fa5]+', '-', 'g'))
WHERE slug IS NULL OR slug = '';
END IF;
END IF;
RAISE NOTICE 'SEO slugs updated successfully';
END;
$$ LANGUAGE plpgsql;
-- =====================================================================================
-- 8. 创建基础函数(如果不存在)
-- =====================================================================================
-- 更新用户商城统计数据的函数
CREATE OR REPLACE FUNCTION public.update_user_mall_stats(p_user_id UUID)
RETURNS VOID AS $$
BEGIN
UPDATE public.ak_users
SET
total_orders = (
SELECT COUNT(*)
FROM public.ml_orders
WHERE user_id = p_user_id AND order_status = 4
),
total_spent = (
SELECT COALESCE(SUM(total_amount), 0)
FROM public.ml_orders
WHERE user_id = p_user_id AND order_status = 4
)
WHERE id = p_user_id;
END;
$$ LANGUAGE plpgsql;
-- 为用户计算等级的函数
CREATE OR REPLACE FUNCTION public.calculate_user_level(p_total_spent DECIMAL)
RETURNS INTEGER AS $$
BEGIN
CASE
WHEN p_total_spent >= 100000 THEN RETURN 10;
WHEN p_total_spent >= 50000 THEN RETURN 9;
WHEN p_total_spent >= 20000 THEN RETURN 8;
WHEN p_total_spent >= 10000 THEN RETURN 7;
WHEN p_total_spent >= 5000 THEN RETURN 6;
WHEN p_total_spent >= 2000 THEN RETURN 5;
WHEN p_total_spent >= 1000 THEN RETURN 4;
WHEN p_total_spent >= 500 THEN RETURN 3;
WHEN p_total_spent >= 100 THEN RETURN 2;
ELSE RETURN 1;
END CASE;
END;
$$ LANGUAGE plpgsql;
-- 批量更新用户等级的函数
CREATE OR REPLACE FUNCTION public.update_all_user_levels()
RETURNS INTEGER AS $$
DECLARE
affected_rows INTEGER := 0;
BEGIN
UPDATE public.ak_users
SET user_level = public.calculate_user_level(total_spent)
WHERE total_spent > 0;
GET DIAGNOSTICS affected_rows = ROW_COUNT;
RETURN affected_rows;
END;
$$ LANGUAGE plpgsql;
-- =====================================================================================
-- 9. 完成提示
-- =====================================================================================
DO $$
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '商城系统字段增量添加完成!';
RAISE NOTICE '=======================================================';
RAISE NOTICE '执行内容:';
RAISE NOTICE '✓ ak_users 表增加商城相关字段';
RAISE NOTICE '✓ 商城核心表增加 cid 自增字段 (SEO优化)';
RAISE NOTICE '✓ 现有商城表增加SEO和营销字段';
RAISE NOTICE '✓ 创建相应的索引 (包括CID索引)';
RAISE NOTICE '✓ 添加约束检查';
RAISE NOTICE '✓ 创建SEO相关函数';
RAISE NOTICE '✓ 创建实用函数';
RAISE NOTICE '=======================================================';
RAISE NOTICE '新增字段说明:';
RAISE NOTICE '• ak_users.mall_status: 商城状态 (1:正常 2:禁用)';
RAISE NOTICE '• ak_users.mall_type: 用户类型 (1:消费者 2:商家 3:其他)';
RAISE NOTICE '• ak_users.total_orders: 总订单数';
RAISE NOTICE '• ak_users.total_spent: 总消费金额';
RAISE NOTICE '• ak_users.user_level: 用户等级 (1-10)';
RAISE NOTICE '• ak_users.points: 用户积分';
RAISE NOTICE '• ak_users.verified_status: 认证状态 (0:未认证 1:已认证 2:认证失败)';
RAISE NOTICE '-------------------------------------------------------';
RAISE NOTICE 'CID 字段说明 (SEO优化):';
RAISE NOTICE '• ml_categories.cid: 分类SEO友好ID';
RAISE NOTICE '• ml_brands.cid: 品牌SEO友好ID';
RAISE NOTICE '• ml_products.cid: 商品SEO友好ID';
RAISE NOTICE '• ml_shops.cid: 店铺SEO友好ID';
RAISE NOTICE '• ml_orders.cid: 订单SEO友好ID';
RAISE NOTICE '-------------------------------------------------------';
RAISE NOTICE 'SEO 函数说明:';
RAISE NOTICE '• get_product_by_cid(cid): 根据CID获取商品信息';
RAISE NOTICE '• get_category_by_cid(cid): 根据CID获取分类信息';
RAISE NOTICE '• get_brand_by_cid(cid): 根据CID获取品牌信息';
RAISE NOTICE '• get_shop_by_cid(cid): 根据CID获取店铺信息';
RAISE NOTICE '• generate_seo_url(type, cid, slug): 生成SEO友好URL';
RAISE NOTICE '• update_seo_slugs(): 批量更新slug字段';
RAISE NOTICE '=======================================================';
RAISE NOTICE '使用建议:';
RAISE NOTICE '1. 此脚本可安全重复执行';
RAISE NOTICE '2. 使用 IF NOT EXISTS 检查避免重复操作';
RAISE NOTICE '3. 建议在测试环境先执行验证';
RAISE NOTICE '4. 可根据实际需要注释掉不需要的字段';
RAISE NOTICE '5. 执行后可调用 update_seo_slugs() 初始化slug字段';
RAISE NOTICE '=======================================================';
RAISE NOTICE 'SEO URL 示例:';
RAISE NOTICE '• 商品页面: /product/123/iphone-15-pro';
RAISE NOTICE '• 分类页面: /category/45/digital-electronics';
RAISE NOTICE '• 品牌页面: /brand/12/apple';
RAISE NOTICE '• 店铺页面: /shop/88/official-store';
RAISE NOTICE '=======================================================';
END $$;

View File

@@ -0,0 +1,868 @@
-- =====================================================================================
-- 商城系统数据库迁移脚本 (PostgreSQL + Supabase)
-- 用途: 在现有数据库基础上添加商城相关表和功能
-- 说明: 复用 ak_users 表,新增 ml_ 前缀的商城表
-- 执行方式: 直接在数据库中执行此脚本
-- =====================================================================================
-- 检查必要的扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
-- =====================================================================================
-- 1. 创建商城用户扩展表
-- =====================================================================================
-- 商城用户档案表
CREATE TABLE IF NOT EXISTS public.ml_user_profiles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
user_type INTEGER DEFAULT 1 NOT NULL, -- 1:消费者 2:商家 3:配送员 4:客服 5:管理员
status INTEGER DEFAULT 1 NOT NULL, -- 1:正常 2:冻结 3:注销 4:待审核
real_name VARCHAR(100), -- 真实姓名
id_card VARCHAR(32), -- 身份证号
business_license VARCHAR(100), -- 营业执照号
credit_score INTEGER DEFAULT 100, -- 信用分数 0-1000
verification_status INTEGER DEFAULT 0, -- 认证状态 0:未认证 1:已认证 2:认证失败
verification_data JSONB DEFAULT '{}', -- 认证相关数据
preferences JSONB DEFAULT '{}', -- 用户偏好设置
emergency_contact VARCHAR(200), -- 紧急联系人
service_areas JSONB, -- 服务区域(配送员)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_user_type CHECK (user_type IN (1,2,3,4,5)),
CONSTRAINT chk_ml_user_status CHECK (status IN (1,2,3,4)),
CONSTRAINT chk_ml_verification_status CHECK (verification_status IN (0,1,2)),
CONSTRAINT chk_ml_credit_score CHECK (credit_score >= 0 AND credit_score <= 1000)
);
COMMENT ON TABLE public.ml_user_profiles IS '商城用户扩展信息表';
-- 用户地址表
CREATE TABLE IF NOT EXISTS public.ml_user_addresses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
receiver_name VARCHAR(100) NOT NULL,
receiver_phone VARCHAR(32) NOT NULL,
province VARCHAR(100) NOT NULL,
city VARCHAR(100) NOT NULL,
district VARCHAR(100) NOT NULL,
street VARCHAR(200),
address_detail TEXT NOT NULL,
postal_code VARCHAR(16),
is_default BOOLEAN DEFAULT FALSE,
label VARCHAR(50), -- home/office/school/other
latitude DECIMAL(10,7),
longitude DECIMAL(10,7),
delivery_instructions TEXT,
business_hours VARCHAR(100),
status INTEGER DEFAULT 1, -- 1:正常 2:禁用
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_address_status CHECK (status IN (1,2))
);
COMMENT ON TABLE public.ml_user_addresses IS '用户地址表';
-- =====================================================================================
-- 2. 创建商品相关表
-- =====================================================================================
-- 商品分类表
CREATE TABLE IF NOT EXISTS public.ml_categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
parent_id UUID REFERENCES public.ml_categories(id),
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE,
description TEXT,
icon_url TEXT,
banner_url TEXT,
sort_order INTEGER DEFAULT 0,
level INTEGER DEFAULT 1,
path TEXT[], -- 分类路径
is_active BOOLEAN DEFAULT TRUE,
seo_title VARCHAR(200),
seo_description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE public.ml_categories IS '商品分类表';
-- 品牌表
CREATE TABLE IF NOT EXISTS public.ml_brands (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
name VARCHAR(200) NOT NULL,
logo_url TEXT,
description TEXT,
website VARCHAR(500),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE public.ml_brands IS '品牌表';
-- 商品表
CREATE TABLE IF NOT EXISTS public.ml_products (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
category_id UUID NOT NULL REFERENCES public.ml_categories(id),
brand_id UUID REFERENCES public.ml_brands(id),
product_code VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(500) NOT NULL,
subtitle VARCHAR(1000),
description TEXT,
main_image_url TEXT,
image_urls JSONB DEFAULT '[]',
video_urls JSONB DEFAULT '[]',
-- 价格信息
base_price DECIMAL(12,2) NOT NULL CHECK (base_price >= 0),
market_price DECIMAL(12,2),
cost_price DECIMAL(12,2),
-- 库存信息
total_stock INTEGER DEFAULT 0 CHECK (total_stock >= 0),
available_stock INTEGER DEFAULT 0 CHECK (available_stock >= 0),
min_order_qty INTEGER DEFAULT 1 CHECK (min_order_qty > 0),
max_order_qty INTEGER,
-- 基础属性
weight DECIMAL(10,3),
dimensions JSONB, -- {length, width, height}
-- 状态
status INTEGER DEFAULT 1, -- 1:上架 2:下架 3:草稿 4:删除
is_featured BOOLEAN DEFAULT FALSE,
is_new BOOLEAN DEFAULT FALSE,
is_hot BOOLEAN DEFAULT FALSE,
-- 统计
view_count INTEGER DEFAULT 0,
sale_count INTEGER DEFAULT 0,
favorite_count INTEGER DEFAULT 0,
rating_avg DECIMAL(3,2) DEFAULT 0.00 CHECK (rating_avg >= 0 AND rating_avg <= 5),
rating_count INTEGER DEFAULT 0,
-- SEO
seo_title VARCHAR(200),
seo_description VARCHAR(500),
seo_keywords TEXT[],
slug VARCHAR(200) UNIQUE,
-- 其他
tags TEXT[],
attributes JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
published_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_ml_product_status CHECK (status IN (1,2,3,4))
);
COMMENT ON TABLE public.ml_products IS '商品表';
-- 商品SKU表
CREATE TABLE IF NOT EXISTS public.ml_product_skus (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
sku_code VARCHAR(100) UNIQUE NOT NULL,
specifications JSONB DEFAULT '{}', -- 规格组合
price DECIMAL(12,2) NOT NULL CHECK (price >= 0),
market_price DECIMAL(12,2),
cost_price DECIMAL(12,2),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
warning_stock INTEGER DEFAULT 10, -- 库存预警
image_url TEXT,
weight DECIMAL(10,3),
status INTEGER DEFAULT 1, -- 1:正常 2:禁用
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_sku_status CHECK (status IN (1,2))
);
COMMENT ON TABLE public.ml_product_skus IS '商品SKU表';
-- 商品规格表
CREATE TABLE IF NOT EXISTS public.ml_product_specs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
spec_name VARCHAR(100) NOT NULL, -- 规格名称:颜色、尺寸等
spec_values JSONB NOT NULL DEFAULT '[]', -- 规格值数组
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE public.ml_product_specs IS '商品规格表';
-- =====================================================================================
-- 3. 创建店铺相关表
-- =====================================================================================
-- 店铺信息表
CREATE TABLE IF NOT EXISTS public.ml_shops (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
merchant_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
shop_name VARCHAR(200) NOT NULL,
shop_logo TEXT,
shop_banner TEXT,
description TEXT,
business_license VARCHAR(100),
contact_name VARCHAR(100),
contact_phone VARCHAR(32),
contact_email VARCHAR(200),
address JSONB, -- 店铺地址信息
business_hours JSONB, -- 营业时间
-- 状态
status INTEGER DEFAULT 1, -- 1:正常 2:暂停 3:关闭
-- 统计
product_count INTEGER DEFAULT 0,
order_count INTEGER DEFAULT 0,
rating_avg DECIMAL(3,2) DEFAULT 0.00,
rating_count INTEGER DEFAULT 0,
-- 认证信息
verified_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_shop_status CHECK (status IN (1,2,3))
);
COMMENT ON TABLE public.ml_shops IS '店铺信息表';
-- =====================================================================================
-- 4. 创建订单相关表
-- =====================================================================================
-- 订单表
CREATE TABLE IF NOT EXISTS public.ml_orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
order_no VARCHAR(50) UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES public.ak_users(id),
merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
-- 金额信息
product_amount DECIMAL(12,2) NOT NULL DEFAULT 0, -- 商品金额
discount_amount DECIMAL(12,2) DEFAULT 0, -- 优惠金额
shipping_fee DECIMAL(12,2) DEFAULT 0, -- 运费
total_amount DECIMAL(12,2) NOT NULL, -- 总金额
paid_amount DECIMAL(12,2) DEFAULT 0, -- 已付金额
-- 地址信息
shipping_address JSONB NOT NULL, -- 收货地址
-- 状态信息
order_status INTEGER DEFAULT 1, -- 1:待付款 2:待发货 3:待收货 4:已完成 5:已取消 6:退款中 7:已退款
payment_status INTEGER DEFAULT 1, -- 1:未付款 2:已付款 3:部分退款 4:全额退款
shipping_status INTEGER DEFAULT 1, -- 1:未发货 2:已发货 3:运输中 4:已送达
-- 时间信息
paid_at TIMESTAMP WITH TIME ZONE,
shipped_at TIMESTAMP WITH TIME ZONE,
delivered_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
-- 其他信息
remark TEXT, -- 买家备注
merchant_memo TEXT, -- 商家备注
cancel_reason TEXT, -- 取消原因
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_order_status CHECK (order_status IN (1,2,3,4,5,6,7)),
CONSTRAINT chk_ml_payment_status CHECK (payment_status IN (1,2,3,4)),
CONSTRAINT chk_ml_shipping_status CHECK (shipping_status IN (1,2,3,4))
);
COMMENT ON TABLE public.ml_orders IS '订单表';
-- 订单商品表
CREATE TABLE IF NOT EXISTS public.ml_order_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES public.ml_orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.ml_products(id),
sku_id UUID REFERENCES public.ml_product_skus(id),
product_name VARCHAR(500) NOT NULL,
sku_name VARCHAR(500),
specifications JSONB DEFAULT '{}',
image_url TEXT,
price DECIMAL(12,2) NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
total_amount DECIMAL(12,2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE public.ml_order_items IS '订单商品表';
-- =====================================================================================
-- 5. 创建购物车和营销相关表
-- =====================================================================================
-- 购物车表
CREATE TABLE IF NOT EXISTS public.ml_shopping_cart (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
sku_id UUID REFERENCES public.ml_product_skus(id) ON DELETE CASCADE,
quantity INTEGER NOT NULL CHECK (quantity > 0),
selected BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, product_id, sku_id)
);
COMMENT ON TABLE public.ml_shopping_cart IS '购物车表';
-- 优惠券模板表
CREATE TABLE IF NOT EXISTS public.ml_coupon_templates (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
merchant_id UUID REFERENCES public.ak_users(id), -- NULL表示平台券
name VARCHAR(200) NOT NULL,
description TEXT,
coupon_type INTEGER NOT NULL, -- 1:满减券 2:折扣券 3:免运费券
discount_type INTEGER NOT NULL, -- 1:固定金额 2:百分比
discount_value DECIMAL(12,2) NOT NULL, -- 优惠值
min_order_amount DECIMAL(12,2) DEFAULT 0, -- 最低订单金额
max_discount_amount DECIMAL(12,2), -- 最大优惠金额
total_quantity INTEGER, -- 总发放数量
per_user_limit INTEGER DEFAULT 1, -- 每用户限领数量
usage_limit INTEGER DEFAULT 1, -- 每张券使用次数限制
-- 适用范围
applicable_products JSONB DEFAULT '[]', -- 适用商品ID数组
applicable_categories JSONB DEFAULT '[]', -- 适用分类ID数组
-- 时间限制
start_time TIMESTAMP WITH TIME ZONE NOT NULL,
end_time TIMESTAMP WITH TIME ZONE NOT NULL,
status INTEGER DEFAULT 1, -- 1:正常 2:暂停 3:已结束
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_coupon_type CHECK (coupon_type IN (1,2,3)),
CONSTRAINT chk_ml_discount_type CHECK (discount_type IN (1,2)),
CONSTRAINT chk_ml_coupon_status CHECK (status IN (1,2,3))
);
COMMENT ON TABLE public.ml_coupon_templates IS '优惠券模板表';
-- 用户优惠券表
CREATE TABLE IF NOT EXISTS public.ml_user_coupons (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
template_id UUID NOT NULL REFERENCES public.ml_coupon_templates(id),
coupon_code VARCHAR(50) UNIQUE NOT NULL,
status INTEGER DEFAULT 1, -- 1:未使用 2:已使用 3:已过期
used_at TIMESTAMP WITH TIME ZONE,
order_id UUID REFERENCES public.ml_orders(id),
received_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expire_at TIMESTAMP WITH TIME ZONE NOT NULL,
CONSTRAINT chk_ml_user_coupon_status CHECK (status IN (1,2,3))
);
COMMENT ON TABLE public.ml_user_coupons IS '用户优惠券表';
-- =====================================================================================
-- 6. 创建配送和评价相关表
-- =====================================================================================
-- 配送员信息表
CREATE TABLE IF NOT EXISTS public.ml_delivery_drivers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
real_name VARCHAR(100) NOT NULL,
id_card VARCHAR(32) NOT NULL,
driver_license VARCHAR(50),
vehicle_type INTEGER, -- 1:电动车 2:摩托车 3:汽车
vehicle_number VARCHAR(20),
service_areas JSONB DEFAULT '[]', -- 服务区域
work_status INTEGER DEFAULT 1, -- 1:在线 2:忙碌 3:离线
current_lat DECIMAL(10,7),
current_lng DECIMAL(10,7),
rating_avg DECIMAL(3,2) DEFAULT 0.00,
rating_count INTEGER DEFAULT 0,
order_count INTEGER DEFAULT 0,
status INTEGER DEFAULT 1, -- 1:正常 2:暂停 3:离职
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_driver_vehicle_type CHECK (vehicle_type IN (1,2,3)),
CONSTRAINT chk_ml_driver_work_status CHECK (work_status IN (1,2,3)),
CONSTRAINT chk_ml_driver_status CHECK (status IN (1,2,3))
);
COMMENT ON TABLE public.ml_delivery_drivers IS '配送员信息表';
-- 配送任务表
CREATE TABLE IF NOT EXISTS public.ml_delivery_tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID UNIQUE NOT NULL REFERENCES public.ml_orders(id),
driver_id UUID REFERENCES public.ml_delivery_drivers(id),
pickup_address JSONB NOT NULL, -- 取货地址
delivery_address JSONB NOT NULL, -- 配送地址
distance DECIMAL(8,2), -- 配送距离(km)
estimated_time INTEGER, -- 预计配送时间(分钟)
delivery_fee DECIMAL(10,2) NOT NULL DEFAULT 0,
status INTEGER DEFAULT 1, -- 1:待接单 2:已接单 3:取货中 4:配送中 5:已送达 6:配送失败
-- 时间记录
assigned_at TIMESTAMP WITH TIME ZONE,
picked_at TIMESTAMP WITH TIME ZONE,
delivered_at TIMESTAMP WITH TIME ZONE,
-- 其他信息
delivery_code VARCHAR(10), -- 取货码
remark TEXT,
failure_reason TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_delivery_status CHECK (status IN (1,2,3,4,5,6))
);
COMMENT ON TABLE public.ml_delivery_tasks IS '配送任务表';
-- 商品评价表
CREATE TABLE IF NOT EXISTS public.ml_product_reviews (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES public.ml_orders(id),
order_item_id UUID NOT NULL REFERENCES public.ml_order_items(id),
user_id UUID NOT NULL REFERENCES public.ak_users(id),
product_id UUID NOT NULL REFERENCES public.ml_products(id),
merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
content TEXT,
images JSONB DEFAULT '[]', -- 评价图片
is_anonymous BOOLEAN DEFAULT FALSE,
-- 商家回复
merchant_reply TEXT,
merchant_replied_at TIMESTAMP WITH TIME ZONE,
status INTEGER DEFAULT 1, -- 1:正常 2:已删除 3:已隐藏
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_ml_review_status CHECK (status IN (1,2,3))
);
COMMENT ON TABLE public.ml_product_reviews IS '商品评价表';
-- =====================================================================================
-- 7. 创建用户行为和系统配置表
-- =====================================================================================
-- 用户收藏表
CREATE TABLE IF NOT EXISTS public.ml_user_favorites (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
target_type INTEGER NOT NULL, -- 1:商品 2:店铺
target_id UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, target_type, target_id),
CONSTRAINT chk_ml_favorite_type CHECK (target_type IN (1,2))
);
COMMENT ON TABLE public.ml_user_favorites IS '用户收藏表';
-- 用户浏览历史表
CREATE TABLE IF NOT EXISTS public.ml_browse_history (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
browse_duration INTEGER DEFAULT 0, -- 浏览时长(秒)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, product_id)
);
COMMENT ON TABLE public.ml_browse_history IS '用户浏览历史表';
-- 搜索记录表
CREATE TABLE IF NOT EXISTS public.ml_search_history (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES public.ak_users(id) ON DELETE CASCADE,
keyword VARCHAR(200) NOT NULL,
result_count INTEGER DEFAULT 0,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE public.ml_search_history IS '搜索记录表';
-- 系统配置表
CREATE TABLE IF NOT EXISTS public.ml_system_configs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
config_key VARCHAR(100) UNIQUE NOT NULL,
config_value JSONB,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE public.ml_system_configs IS '系统配置表';
-- 地区表
CREATE TABLE IF NOT EXISTS public.ml_regions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
parent_id UUID REFERENCES public.ml_regions(id),
name VARCHAR(100) NOT NULL,
code VARCHAR(20),
level INTEGER NOT NULL, -- 1:省份 2:城市 3:区县 4:街道
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE public.ml_regions IS '地区表';
-- =====================================================================================
-- 8. 创建索引
-- =====================================================================================
-- 用户扩展表索引
CREATE INDEX IF NOT EXISTS idx_ml_user_profiles_user_id ON public.ml_user_profiles(user_id);
CREATE INDEX IF NOT EXISTS idx_ml_user_profiles_type ON public.ml_user_profiles(user_type);
CREATE INDEX IF NOT EXISTS idx_ml_user_profiles_status ON public.ml_user_profiles(status);
-- 分类表索引
CREATE INDEX IF NOT EXISTS idx_ml_categories_cid ON public.ml_categories(cid);
CREATE INDEX IF NOT EXISTS idx_ml_categories_parent ON public.ml_categories(parent_id);
CREATE INDEX IF NOT EXISTS idx_ml_categories_slug ON public.ml_categories(slug);
CREATE INDEX IF NOT EXISTS idx_ml_categories_level ON public.ml_categories(level, sort_order);
-- 品牌表索引
CREATE INDEX IF NOT EXISTS idx_ml_brands_cid ON public.ml_brands(cid);
CREATE INDEX IF NOT EXISTS idx_ml_brands_name ON public.ml_brands(name);
-- 地址表索引
CREATE INDEX IF NOT EXISTS idx_ml_user_addresses_user_id ON public.ml_user_addresses(user_id);
CREATE INDEX IF NOT EXISTS idx_ml_user_addresses_default ON public.ml_user_addresses(user_id, is_default);
CREATE INDEX IF NOT EXISTS idx_ml_user_addresses_location ON public.ml_user_addresses(city, district);
-- 商品表索引
CREATE INDEX IF NOT EXISTS idx_ml_products_cid ON public.ml_products(cid);
CREATE INDEX IF NOT EXISTS idx_ml_products_merchant ON public.ml_products(merchant_id, status);
CREATE INDEX IF NOT EXISTS idx_ml_products_category ON public.ml_products(category_id, status);
CREATE INDEX IF NOT EXISTS idx_ml_products_status ON public.ml_products(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_products_featured ON public.ml_products(is_featured, status);
CREATE INDEX IF NOT EXISTS idx_ml_products_price ON public.ml_products(base_price);
CREATE INDEX IF NOT EXISTS idx_ml_products_rating ON public.ml_products(rating_avg DESC, rating_count DESC);
CREATE INDEX IF NOT EXISTS idx_ml_products_sale_count ON public.ml_products(sale_count DESC);
CREATE INDEX IF NOT EXISTS idx_ml_products_tags ON public.ml_products USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_ml_products_slug ON public.ml_products(slug);
-- 店铺表索引
CREATE INDEX IF NOT EXISTS idx_ml_shops_cid ON public.ml_shops(cid);
CREATE INDEX IF NOT EXISTS idx_ml_shops_merchant ON public.ml_shops(merchant_id);
-- SKU表索引
CREATE INDEX IF NOT EXISTS idx_ml_product_skus_product ON public.ml_product_skus(product_id);
CREATE INDEX IF NOT EXISTS idx_ml_product_skus_code ON public.ml_product_skus(sku_code);
-- 订单表索引
CREATE INDEX IF NOT EXISTS idx_ml_orders_cid ON public.ml_orders(cid);
CREATE INDEX IF NOT EXISTS idx_ml_orders_user ON public.ml_orders(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_orders_merchant ON public.ml_orders(merchant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_orders_status ON public.ml_orders(order_status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_orders_no ON public.ml_orders(order_no);
-- 订单商品表索引
CREATE INDEX IF NOT EXISTS idx_ml_order_items_order ON public.ml_order_items(order_id);
CREATE INDEX IF NOT EXISTS idx_ml_order_items_product ON public.ml_order_items(product_id);
-- 购物车表索引
CREATE INDEX IF NOT EXISTS idx_ml_shopping_cart_user ON public.ml_shopping_cart(user_id);
-- 优惠券模板表索引
CREATE INDEX IF NOT EXISTS idx_ml_coupon_templates_cid ON public.ml_coupon_templates(cid);
CREATE INDEX IF NOT EXISTS idx_ml_coupon_templates_merchant ON public.ml_coupon_templates(merchant_id);
-- 优惠券表索引
CREATE INDEX IF NOT EXISTS idx_ml_user_coupons_user ON public.ml_user_coupons(user_id, status);
CREATE INDEX IF NOT EXISTS idx_ml_user_coupons_code ON public.ml_user_coupons(coupon_code);
-- 收藏表索引
CREATE INDEX IF NOT EXISTS idx_ml_user_favorites_user ON public.ml_user_favorites(user_id, target_type);
CREATE INDEX IF NOT EXISTS idx_ml_user_favorites_target ON public.ml_user_favorites(target_type, target_id);
-- 浏览历史索引
CREATE INDEX IF NOT EXISTS idx_ml_browse_history_user ON public.ml_browse_history(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_browse_history_product ON public.ml_browse_history(product_id);
-- =====================================================================================
-- 9. 创建触发器函数
-- =====================================================================================
-- 自动更新 updated_at 字段的函数
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 创建触发器 (使用 DO 块避免重复创建错误)
DO $$
BEGIN
-- 用户档案更新触发器
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_user_profiles_updated_at') THEN
CREATE TRIGGER trigger_ml_user_profiles_updated_at
BEFORE UPDATE ON public.ml_user_profiles
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
-- 用户地址更新触发器
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_user_addresses_updated_at') THEN
CREATE TRIGGER trigger_ml_user_addresses_updated_at
BEFORE UPDATE ON public.ml_user_addresses
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
-- 商品更新触发器
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_products_updated_at') THEN
CREATE TRIGGER trigger_ml_products_updated_at
BEFORE UPDATE ON public.ml_products
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
-- SKU更新触发器
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_product_skus_updated_at') THEN
CREATE TRIGGER trigger_ml_product_skus_updated_at
BEFORE UPDATE ON public.ml_product_skus
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
-- 店铺更新触发器
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_shops_updated_at') THEN
CREATE TRIGGER trigger_ml_shops_updated_at
BEFORE UPDATE ON public.ml_shops
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
-- 订单更新触发器
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_orders_updated_at') THEN
CREATE TRIGGER trigger_ml_orders_updated_at
BEFORE UPDATE ON public.ml_orders
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
-- 购物车更新触发器
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_shopping_cart_updated_at') THEN
CREATE TRIGGER trigger_ml_shopping_cart_updated_at
BEFORE UPDATE ON public.ml_shopping_cart
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
END $$;
-- 确保每个用户只有一个默认地址的触发器
CREATE OR REPLACE FUNCTION public.ensure_single_default_address()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.is_default = TRUE THEN
UPDATE public.ml_user_addresses
SET is_default = FALSE
WHERE user_id = NEW.user_id AND id != NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_single_default_address') THEN
CREATE TRIGGER trigger_ml_single_default_address
BEFORE INSERT OR UPDATE ON public.ml_user_addresses
FOR EACH ROW EXECUTE FUNCTION public.ensure_single_default_address();
END IF;
END $$;
-- =====================================================================================
-- 10. 创建实用函数
-- =====================================================================================
-- 创建订单序列
CREATE SEQUENCE IF NOT EXISTS public.ml_order_seq START 1;
-- 生成订单号的函数
CREATE OR REPLACE FUNCTION public.generate_order_no()
RETURNS TEXT AS $$
DECLARE
order_no TEXT;
BEGIN
order_no := 'ML' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(NEXTVAL('ml_order_seq')::TEXT, 6, '0');
RETURN order_no;
END;
$$ LANGUAGE plpgsql;
-- 生成优惠券码的函数
CREATE OR REPLACE FUNCTION public.generate_coupon_code()
RETURNS TEXT AS $$
DECLARE
code TEXT;
chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
result TEXT := '';
i INTEGER;
BEGIN
FOR i IN 1..8 LOOP
result := result || substr(chars, (random() * length(chars))::integer + 1, 1);
END LOOP;
RETURN 'CP' || result;
END;
$$ LANGUAGE plpgsql;
-- 获取用户默认地址
CREATE OR REPLACE FUNCTION public.get_user_default_address(p_user_id UUID)
RETURNS TABLE (
id UUID,
receiver_name VARCHAR,
receiver_phone VARCHAR,
full_address TEXT,
latitude DECIMAL,
longitude DECIMAL
) AS $$
BEGIN
RETURN QUERY
SELECT
a.id,
a.receiver_name,
a.receiver_phone,
(a.province || ' ' || a.city || ' ' || a.district || ' ' || a.address_detail) as full_address,
a.latitude,
a.longitude
FROM public.ml_user_addresses a
WHERE a.user_id = p_user_id AND a.is_default = TRUE AND a.status = 1
LIMIT 1;
END;
$$ LANGUAGE plpgsql;
-- 检查用户是否为认证商家
CREATE OR REPLACE FUNCTION public.is_verified_merchant(p_user_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
result BOOLEAN := FALSE;
BEGIN
SELECT (user_type = 2 AND verification_status = 1) INTO result
FROM public.ml_user_profiles
WHERE user_id = p_user_id;
RETURN COALESCE(result, FALSE);
END;
$$ LANGUAGE plpgsql;
-- =====================================================================================
-- 11. 创建基础视图
-- =====================================================================================
-- 商城用户完整信息视图
CREATE OR REPLACE VIEW public.ml_users_view AS
SELECT
u.id,
u.username,
u.email,
u.phone,
u.avatar_url,
u.gender,
u.birthday,
u.bio,
u.created_at as user_created_at,
u.updated_at as user_updated_at,
p.user_type,
p.status,
p.real_name,
p.credit_score,
p.verification_status,
p.created_at as profile_created_at,
p.updated_at as profile_updated_at,
CASE
WHEN p.user_type = 1 THEN '消费者'
WHEN p.user_type = 2 THEN '商家'
WHEN p.user_type = 3 THEN '配送员'
WHEN p.user_type = 4 THEN '客服'
WHEN p.user_type = 5 THEN '管理员'
ELSE '未知'
END as user_type_name
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles p ON u.id = p.user_id;
COMMENT ON VIEW public.ml_users_view IS '商城用户完整信息视图';
-- =====================================================================================
-- 12. 插入初始配置数据
-- =====================================================================================
-- 插入系统配置
INSERT INTO public.ml_system_configs (config_key, config_value, description) VALUES
('shipping_fee', '{"default": 10, "free_threshold": 88}', '配送费配置'),
('platform_commission', '{"rate": 0.05}', '平台佣金配置'),
('coupon_settings', '{"max_per_user": 10}', '优惠券设置'),
('order_auto_confirm_days', '7', '订单自动确认天数')
ON CONFLICT (config_key) DO NOTHING;
-- 插入默认分类
INSERT INTO public.ml_categories (id, name, slug, level, path) VALUES
(uuid_generate_v4(), '数码电器', 'digital', 1, ARRAY['数码电器']),
(uuid_generate_v4(), '服装鞋帽', 'fashion', 1, ARRAY['服装鞋帽']),
(uuid_generate_v4(), '家居用品', 'home', 1, ARRAY['家居用品']),
(uuid_generate_v4(), '食品饮料', 'food', 1, ARRAY['食品饮料']),
(uuid_generate_v4(), '美妆护肤', 'beauty', 1, ARRAY['美妆护肤'])
ON CONFLICT (slug) DO NOTHING;
-- 为现有 ak_users 用户创建默认商城档案 (如果不存在)
INSERT INTO public.ml_user_profiles (user_id, user_type, status)
SELECT
id,
1, -- 默认为消费者
1 -- 默认状态正常
FROM public.ak_users
WHERE id NOT IN (SELECT user_id FROM public.ml_user_profiles WHERE user_id IS NOT NULL);
-- =====================================================================================
-- 13. 完成提示
-- =====================================================================================
DO $$
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '商城数据库迁移完成!';
RAISE NOTICE '=======================================================';
RAISE NOTICE '已创建表数量: 17 张商城表';
RAISE NOTICE '已创建索引: 30+ 个索引';
RAISE NOTICE '已创建触发器: 8 个触发器';
RAISE NOTICE '已创建函数: 6 个函数';
RAISE NOTICE '已创建视图: 1 个视图';
RAISE NOTICE '已插入基础配置和分类数据';
RAISE NOTICE '已为现有用户创建默认商城档案';
RAISE NOTICE '=======================================================';
RAISE NOTICE '表名前缀: ml_';
RAISE NOTICE '复用表: ak_users';
RAISE NOTICE '兼容: Supabase';
RAISE NOTICE '=======================================================';
END $$;

View File

@@ -0,0 +1,249 @@
-- ====================================================================
-- 角色字段统一说明
-- ====================================================================
-- 注意:角色信息统一存储在 ak_users.role 字段中
-- ml_user_profiles 表不再包含 role 字段,避免数据重复
-- 本脚本主要用于清理可能存在的重复字段和更新相关函数
-- ====================================================================
\echo '检查角色字段统一状态...'
BEGIN;
-- ====================================================================
-- 1. 安全检查
-- ====================================================================
-- 检查表是否存在
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_user_profiles') THEN
RAISE EXCEPTION '表 ml_user_profiles 不存在,请先运行完整数据库创建脚本';
END IF;
END $$;
-- 检查是否已经有 role 字段
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_user_profiles' AND column_name = 'role') THEN
RAISE NOTICE '检测到 role 字段已存在,跳过字段创建';
ELSE
RAISE NOTICE '开始添加 role 字段';
-- 添加 role 字段
ALTER TABLE public.ml_user_profiles
ADD COLUMN role TEXT DEFAULT 'customer';
END IF;
END $$;
-- ====================================================================
-- 2. 数据迁移
-- ====================================================================
-- 迁移现有 user_type 数据到 role 字段
UPDATE public.ml_user_profiles
SET role = CASE
WHEN user_type = 1 THEN 'customer' -- 消费者
WHEN user_type = 2 THEN 'merchant' -- 商家
WHEN user_type = 3 THEN 'delivery' -- 配送员
WHEN user_type = 4 THEN 'service' -- 客服
WHEN user_type = 5 THEN 'admin' -- 管理员
ELSE 'customer'
END
WHERE role = 'customer' OR role IS NULL;
-- 设置非空约束
ALTER TABLE public.ml_user_profiles
ALTER COLUMN role SET NOT NULL;
-- ====================================================================
-- 3. 约束和索引更新
-- ====================================================================
-- 添加新的约束
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.check_constraints WHERE constraint_name = 'chk_ml_user_role') THEN
ALTER TABLE public.ml_user_profiles
ADD CONSTRAINT chk_ml_user_role
CHECK (role IN ('customer', 'merchant', 'delivery', 'service', 'admin'));
RAISE NOTICE '已添加 role 字段约束';
END IF;
END $$;
-- 创建新索引
DROP INDEX IF EXISTS idx_ml_user_profiles_role;
CREATE INDEX idx_ml_user_profiles_role ON public.ml_user_profiles(role);
-- ====================================================================
-- 4. 同步 ak_users 表的 role 字段
-- ====================================================================
-- 同步 ak_users.role 字段
UPDATE public.ak_users
SET role = p.role,
updated_at = CURRENT_TIMESTAMP
FROM public.ml_user_profiles p
WHERE ak_users.id = p.user_id
AND (ak_users.role != p.role OR ak_users.role IS NULL);
-- ====================================================================
-- 5. 更新函数和视图
-- ====================================================================
-- 更新商家验证函数
CREATE OR REPLACE FUNCTION public.is_verified_merchant(user_uuid UUID)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result BOOLEAN := FALSE;
BEGIN
SELECT (role = 'merchant' AND verification_status = 1) INTO result
FROM public.ml_user_profiles
WHERE user_id = user_uuid;
RETURN COALESCE(result, FALSE);
END;
$$;
-- 更新用户信息视图
CREATE OR REPLACE VIEW public.ml_users_view AS
SELECT
u.id,
u.email,
u.username,
u.phone,
u.avatar_url,
u.status as user_status,
u.gender,
u.birthday,
u.bio,
u.created_at as user_created_at,
u.updated_at as user_updated_at,
p.role,
p.status,
p.real_name,
p.credit_score,
p.verification_status,
p.created_at as profile_created_at,
p.updated_at as profile_updated_at,
CASE
WHEN p.role = 'customer' THEN '消费者'
WHEN p.role = 'merchant' THEN '商家'
WHEN p.role = 'delivery' THEN '配送员'
WHEN p.role = 'service' THEN '客服'
WHEN p.role = 'admin' THEN '管理员'
ELSE '未知'
END as role_name
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles p ON u.id = p.user_id;
-- ====================================================================
-- 6. 更新字段注释
-- ====================================================================
COMMENT ON COLUMN public.ml_user_profiles.role IS '用户角色customer消费者, merchant商家, delivery配送员, service客服, admin管理员';
-- ====================================================================
-- 7. 验证迁移结果
-- ====================================================================
DO $$
DECLARE
total_users INTEGER;
migrated_users INTEGER;
role_stats RECORD;
BEGIN
-- 统计总用户数
SELECT COUNT(*) INTO total_users FROM public.ml_user_profiles;
-- 统计已迁移用户数
SELECT COUNT(*) INTO migrated_users
FROM public.ml_user_profiles
WHERE role IN ('customer', 'merchant', 'delivery', 'service', 'admin');
RAISE NOTICE '迁移完成:总用户 %, 已迁移 %', total_users, migrated_users;
-- 显示角色分布
RAISE NOTICE '角色分布统计:';
FOR role_stats IN
SELECT role, COUNT(*) as count
FROM public.ml_user_profiles
GROUP BY role
ORDER BY count DESC
LOOP
RAISE NOTICE ' %: % 用户', role_stats.role, role_stats.count;
END LOOP;
END $$;
COMMIT;
\echo '角色字段迁移完成!'
-- ====================================================================
-- 8. 可选:清理旧字段(请谨慎执行)
-- ====================================================================
/*
-- 警告:以下操作将永久删除 user_type 字段,请确保迁移成功后再执行
BEGIN;
-- 删除旧约束
ALTER TABLE public.ml_user_profiles DROP CONSTRAINT IF EXISTS chk_ml_user_type;
-- 删除旧索引
DROP INDEX IF EXISTS idx_ml_user_profiles_type;
-- 删除旧字段
ALTER TABLE public.ml_user_profiles DROP COLUMN IF EXISTS user_type;
COMMIT;
\echo '旧 user_type 字段清理完成';
*/
-- ====================================================================
-- 9. 回滚脚本(如需回滚,请执行以下命令)
-- ====================================================================
/*
-- 回滚到 user_type 字段(仅在必要时执行)
BEGIN;
-- 重新添加 user_type 字段
ALTER TABLE public.ml_user_profiles
ADD COLUMN user_type INTEGER DEFAULT 1;
-- 从 role 字段恢复数据
UPDATE public.ml_user_profiles
SET user_type = CASE
WHEN role = 'customer' THEN 1
WHEN role = 'merchant' THEN 2
WHEN role = 'delivery' THEN 3
WHEN role = 'service' THEN 4
WHEN role = 'admin' THEN 5
ELSE 1
END;
-- 设置非空约束
ALTER TABLE public.ml_user_profiles
ALTER COLUMN user_type SET NOT NULL;
-- 重新添加约束
ALTER TABLE public.ml_user_profiles
ADD CONSTRAINT chk_ml_user_type CHECK (user_type IN (1,2,3,4,5));
-- 重新创建索引
CREATE INDEX idx_ml_user_profiles_type ON public.ml_user_profiles(user_type);
-- 删除 role 字段
ALTER TABLE public.ml_user_profiles DROP CONSTRAINT IF EXISTS chk_ml_user_role;
ALTER TABLE public.ml_user_profiles DROP COLUMN IF EXISTS role;
COMMIT;
\echo '已回滚到 user_type 字段';
*/

View File

@@ -0,0 +1,207 @@
-- ====================================================================
-- 角色字段清理脚本 - Role Field Cleanup
-- ====================================================================
-- 目的:确保角色信息只存储在 ak_users.role 字段中
-- 清理 ml_user_profiles 表中可能存在的重复 role 字段
-- 兼容性Supabase + PostgreSQL 14+
-- ====================================================================
\echo '开始角色字段清理...'
BEGIN;
-- ====================================================================
-- 1. 检查并清理 ml_user_profiles 中的 role 字段
-- ====================================================================
-- 检查是否存在重复的 role 字段
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'ml_user_profiles'
AND column_name = 'role') THEN
RAISE NOTICE '发现 ml_user_profiles 表中存在 role 字段,开始清理...';
-- 如果 ak_users.role 字段为空,从 ml_user_profiles.role 迁移数据
UPDATE public.ak_users
SET role = COALESCE(ak_users.role, p.role),
updated_at = CURRENT_TIMESTAMP
FROM public.ml_user_profiles p
WHERE ak_users.id = p.user_id
AND (ak_users.role IS NULL OR ak_users.role = '');
-- 删除相关约束
ALTER TABLE public.ml_user_profiles DROP CONSTRAINT IF EXISTS chk_ml_user_role;
-- 删除相关索引
DROP INDEX IF EXISTS idx_ml_user_profiles_role;
-- 删除 role 字段
ALTER TABLE public.ml_user_profiles DROP COLUMN IF EXISTS role;
RAISE NOTICE '已删除 ml_user_profiles 表中的 role 字段';
ELSE
RAISE NOTICE 'ml_user_profiles 表中不存在 role 字段,无需清理';
END IF;
END $$;
-- ====================================================================
-- 2. 更新相关函数
-- ====================================================================
-- 更新商家验证函数
CREATE OR REPLACE FUNCTION public.is_verified_merchant(user_uuid UUID)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result BOOLEAN := FALSE;
BEGIN
SELECT (u.role = 'merchant' AND p.verification_status = 1) INTO result
FROM public.ml_user_profiles p
JOIN public.ak_users u ON p.user_id = u.id
WHERE p.user_id = user_uuid;
RETURN COALESCE(result, FALSE);
END;
$$;
-- 获取用户角色函数
CREATE OR REPLACE FUNCTION public.get_user_role(user_uuid UUID)
RETURNS TEXT
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
user_role TEXT;
BEGIN
SELECT role INTO user_role
FROM public.ak_users
WHERE id = user_uuid;
RETURN COALESCE(user_role, 'customer');
END;
$$;
-- 检查用户权限函数
CREATE OR REPLACE FUNCTION public.check_user_permission(user_uuid UUID, required_roles TEXT[])
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
user_role TEXT;
BEGIN
SELECT role INTO user_role
FROM public.ak_users
WHERE id = user_uuid;
RETURN user_role = ANY(required_roles);
END;
$$;
-- ====================================================================
-- 3. 更新视图
-- ====================================================================
-- 更新用户信息视图
CREATE OR REPLACE VIEW public.ml_users_view AS
SELECT
u.id,
u.email,
u.username,
u.phone,
u.avatar_url,
u.status as user_status,
u.gender,
u.birthday,
u.bio,
u.created_at as user_created_at,
u.updated_at as user_updated_at,
u.role,
p.status,
p.real_name,
p.credit_score,
p.verification_status,
p.created_at as profile_created_at,
p.updated_at as profile_updated_at,
CASE
WHEN u.role = 'customer' THEN '消费者'
WHEN u.role = 'merchant' THEN '商家'
WHEN u.role = 'delivery' THEN '配送员'
WHEN u.role = 'service' THEN '客服'
WHEN u.role = 'admin' THEN '管理员'
ELSE '未知'
END as role_name
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles p ON u.id = p.user_id;
-- 创建角色统计视图
CREATE OR REPLACE VIEW public.vw_role_statistics AS
SELECT
role,
COUNT(*) as user_count,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER() as percentage
FROM public.ak_users
WHERE role IS NOT NULL
GROUP BY role
ORDER BY user_count DESC;
-- ====================================================================
-- 4. 确保数据一致性
-- ====================================================================
-- 确保所有用户都有角色
UPDATE public.ak_users
SET role = 'customer'
WHERE role IS NULL OR role = '';
-- 确保角色字段有约束
DO $$
BEGIN
-- 检查约束是否存在
IF NOT EXISTS (SELECT 1 FROM information_schema.check_constraints
WHERE constraint_name = 'chk_ak_users_role') THEN
ALTER TABLE public.ak_users
ADD CONSTRAINT chk_ak_users_role
CHECK (role IN ('customer', 'merchant', 'delivery', 'service', 'admin'));
RAISE NOTICE '已添加 ak_users.role 字段约束';
END IF;
END $$;
-- 创建角色字段索引(如果不存在)
CREATE INDEX IF NOT EXISTS idx_ak_users_role ON public.ak_users(role);
COMMIT;
\echo '角色字段清理完成!'
-- ====================================================================
-- 验证结果
-- ====================================================================
-- 检查角色分布
SELECT '角色分布统计:' as info;
SELECT * FROM public.vw_role_statistics;
-- 检查是否还有重复字段
SELECT '字段检查:' as info;
SELECT
CASE
WHEN EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'ml_user_profiles'
AND column_name = 'role')
THEN '❌ ml_user_profiles.role 字段仍然存在'
ELSE '✅ ml_user_profiles.role 字段已清理'
END as ml_user_profiles_check,
CASE
WHEN EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'ak_users'
AND column_name = 'role')
THEN '✅ ak_users.role 字段存在'
ELSE '❌ ak_users.role 字段不存在'
END as ak_users_check;
SELECT '角色字段统一完成!角色信息统一存储在 ak_users.role 字段中。' as result;

View File

@@ -0,0 +1,287 @@
-- ====================================================================
-- 角色字段统一升级脚本 - Role Field Unification Upgrade
-- ====================================================================
-- 目的:将所有表的 user_type (INTEGER) 字段统一为 role (TEXT) 字段
-- 兼容性Supabase + PostgreSQL 14+
-- 执行顺序:在现有数据库基础上执行
-- ====================================================================
BEGIN;
-- ====================================================================
-- 1. 统一 ml_user_profiles 表的角色字段
-- ====================================================================
-- 1.1 添加新的 role 字段
ALTER TABLE public.ml_user_profiles
ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'customer';
-- 1.2 将现有 user_type 数据迁移到 role 字段
UPDATE public.ml_user_profiles
SET role = CASE
WHEN user_type = 1 THEN 'customer' -- 消费者
WHEN user_type = 2 THEN 'merchant' -- 商家
WHEN user_type = 3 THEN 'delivery' -- 配送员
WHEN user_type = 4 THEN 'service' -- 客服
WHEN user_type = 5 THEN 'admin' -- 管理员
ELSE 'customer'
END
WHERE role IS NULL OR role = 'customer';
-- 1.3 设置 role 字段约束
ALTER TABLE public.ml_user_profiles
ALTER COLUMN role SET NOT NULL;
ALTER TABLE public.ml_user_profiles
ADD CONSTRAINT IF NOT EXISTS chk_ml_user_role
CHECK (role IN ('customer', 'merchant', 'delivery', 'service', 'admin'));
-- 1.4 更新索引
DROP INDEX IF EXISTS idx_ml_user_profiles_type;
CREATE INDEX IF NOT EXISTS idx_ml_user_profiles_role ON public.ml_user_profiles(role);
-- 1.5 删除旧的 user_type 字段和约束(可选,建议在测试确认后执行)
-- ALTER TABLE public.ml_user_profiles DROP CONSTRAINT IF EXISTS chk_ml_user_type;
-- ALTER TABLE public.ml_user_profiles DROP COLUMN IF EXISTS user_type;
-- ====================================================================
-- 2. 更新相关函数中的字段引用
-- ====================================================================
-- 2.1 更新商家验证函数
CREATE OR REPLACE FUNCTION public.is_verified_merchant(user_uuid UUID)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result BOOLEAN := FALSE;
BEGIN
SELECT (role = 'merchant' AND verification_status = 1) INTO result
FROM public.ml_user_profiles
WHERE user_id = user_uuid;
RETURN COALESCE(result, FALSE);
END;
$$;
-- 2.2 更新用户信息视图
CREATE OR REPLACE VIEW public.vw_user_info AS
SELECT
u.id as user_id,
u.email,
u.username,
u.role as user_role,
u.status as user_status,
u.created_at as user_created_at,
p.cid as profile_cid,
p.role as profile_role,
p.status as profile_status,
p.real_name,
p.avatar_url,
p.phone,
p.credit_score,
p.verification_status,
p.created_at as profile_created_at,
CASE
WHEN p.role = 'customer' THEN '消费者'
WHEN p.role = 'merchant' THEN '商家'
WHEN p.role = 'delivery' THEN '配送员'
WHEN p.role = 'service' THEN '客服'
WHEN p.role = 'admin' THEN '管理员'
ELSE '未知'
END as role_name
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles p ON u.id = p.user_id;
-- ====================================================================
-- 3. 更新 RLS 策略中的角色检查
-- ====================================================================
-- 3.1 更新商品相关策略
DROP POLICY IF EXISTS "商家管理自己的商品" ON public.ml_products;
CREATE POLICY "商家管理自己的商品"
ON public.ml_products
FOR ALL
TO authenticated
USING (
merchant_id = auth.uid()
OR EXISTS (
SELECT 1 FROM public.ml_user_profiles p
WHERE p.user_id = auth.uid()
AND p.role IN ('admin', 'service')
)
);
-- 3.2 更新订单相关策略
DROP POLICY IF EXISTS "配送员查看分配的订单" ON public.ml_orders;
CREATE POLICY "配送员查看分配的订单"
ON public.ml_orders
FOR SELECT
TO authenticated
USING (
delivery_id = auth.uid()
OR EXISTS (
SELECT 1 FROM public.ml_user_profiles p
WHERE p.user_id = auth.uid()
AND p.role IN ('admin', 'service')
)
);
-- 3.3 更新用户资料策略
DROP POLICY IF EXISTS "用户管理自己的资料" ON public.ml_user_profiles;
CREATE POLICY "用户管理自己的资料"
ON public.ml_user_profiles
FOR ALL
TO authenticated
USING (
user_id = auth.uid()
OR EXISTS (
SELECT 1 FROM public.ml_user_profiles p
WHERE p.user_id = auth.uid()
AND p.role IN ('admin', 'service')
)
);
-- ====================================================================
-- 4. 更新字段注释
-- ====================================================================
COMMENT ON COLUMN public.ml_user_profiles.role IS '用户角色customer消费者, merchant商家, delivery配送员, service客服, admin管理员';
-- ====================================================================
-- 5. 创建角色辅助函数
-- ====================================================================
-- 5.1 获取用户角色函数
CREATE OR REPLACE FUNCTION public.get_user_role(user_uuid UUID)
RETURNS TEXT
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
user_role TEXT;
BEGIN
SELECT role INTO user_role
FROM public.ml_user_profiles
WHERE user_id = user_uuid;
RETURN COALESCE(user_role, 'customer');
END;
$$;
-- 5.2 检查用户权限函数
CREATE OR REPLACE FUNCTION public.check_user_permission(user_uuid UUID, required_roles TEXT[])
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
user_role TEXT;
BEGIN
SELECT role INTO user_role
FROM public.ml_user_profiles
WHERE user_id = user_uuid;
RETURN user_role = ANY(required_roles);
END;
$$;
-- 5.3 角色升级函数(将用户提升为商家等)
CREATE OR REPLACE FUNCTION public.upgrade_user_role(user_uuid UUID, new_role TEXT)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
-- 检查新角色是否有效
IF new_role NOT IN ('customer', 'merchant', 'delivery', 'service', 'admin') THEN
RAISE EXCEPTION '无效的角色类型: %', new_role;
END IF;
-- 更新用户角色
UPDATE public.ml_user_profiles
SET role = new_role,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = user_uuid;
-- 同步更新 ak_users 表的 role 字段
UPDATE public.ak_users
SET role = new_role,
updated_at = CURRENT_TIMESTAMP
WHERE id = user_uuid;
RETURN FOUND;
END;
$$;
-- ====================================================================
-- 6. 数据一致性检查
-- ====================================================================
-- 6.1 检查角色字段一致性
DO $$
DECLARE
inconsistent_count INTEGER;
BEGIN
SELECT COUNT(*) INTO inconsistent_count
FROM public.ak_users u
JOIN public.ml_user_profiles p ON u.id = p.user_id
WHERE u.role != p.role;
IF inconsistent_count > 0 THEN
RAISE NOTICE '发现 % 条记录的角色字段不一致,正在同步...', inconsistent_count;
-- 以 ml_user_profiles.role 为准同步到 ak_users.role
UPDATE public.ak_users
SET role = p.role,
updated_at = CURRENT_TIMESTAMP
FROM public.ml_user_profiles p
WHERE ak_users.id = p.user_id
AND ak_users.role != p.role;
RAISE NOTICE '角色字段同步完成';
ELSE
RAISE NOTICE '角色字段一致性检查通过';
END IF;
END;
$$;
-- ====================================================================
-- 7. 创建角色统计视图
-- ====================================================================
CREATE OR REPLACE VIEW public.vw_role_statistics AS
SELECT
role,
COUNT(*) as user_count,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER() as percentage
FROM public.ml_user_profiles
GROUP BY role
ORDER BY user_count DESC;
COMMIT;
-- ====================================================================
-- 执行验证
-- ====================================================================
-- 检查角色分布
SELECT '角色分布统计:' as info;
SELECT * FROM public.vw_role_statistics;
-- 检查索引
SELECT '索引检查:' as info;
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'ml_user_profiles'
AND indexname LIKE '%role%';
-- 检查约束
SELECT '约束检查:' as info;
SELECT conname, pg_get_constraintdef(oid) as definition
FROM pg_constraint
WHERE conrelid = 'public.ml_user_profiles'::regclass
AND conname LIKE '%role%';
SELECT '角色字段统一升级完成!' as result;

View File

@@ -0,0 +1,273 @@
-- 商城系统用户兼容性实施方案
-- 基于混合方案:复用 ak_users 主表 + 商城扩展表
-- 1. 商城用户扩展表
CREATE TABLE public.mall_user_profiles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
user_type INTEGER DEFAULT 1, -- 1:消费者 2:商家 3:配送员 4:客服 5:管理员
status INTEGER DEFAULT 1, -- 1:正常 2:冻结 3:注销 4:待审核
real_name VARCHAR(64), -- 真实姓名(商家认证、配送员必填)
id_card VARCHAR(32), -- 身份证号(商家认证、配送员必填)
credit_score INTEGER DEFAULT 100, -- 信用分数 0-1000
mall_role VARCHAR(32) DEFAULT 'consumer', -- 商城角色标识
verification_status INTEGER DEFAULT 0, -- 认证状态 0:未认证 1:已认证 2:认证失败
verification_data JSONB, -- 认证相关数据
business_license VARCHAR(128), -- 营业执照号(商家)
shop_category VARCHAR(64), -- 店铺类别(商家)
service_areas JSONB, -- 服务区域(配送员)
emergency_contact VARCHAR(128), -- 紧急联系人(配送员)
preferences JSONB, -- 用户偏好设置
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE public.mall_user_profiles IS '商城用户扩展信息表';
COMMENT ON COLUMN public.mall_user_profiles.user_id IS '关联ak_users表的用户ID';
COMMENT ON COLUMN public.mall_user_profiles.user_type IS '用户类型1消费者 2商家 3配送员 4客服 5管理员';
COMMENT ON COLUMN public.mall_user_profiles.status IS '用户状态1正常 2冻结 3注销 4待审核';
COMMENT ON COLUMN public.mall_user_profiles.credit_score IS '信用分数,影响交易权限';
COMMENT ON COLUMN public.mall_user_profiles.verification_status IS '认证状态0未认证 1已认证 2认证失败';
-- 创建索引
CREATE INDEX idx_mall_user_profiles_user_id ON public.mall_user_profiles(user_id);
CREATE INDEX idx_mall_user_profiles_user_type ON public.mall_user_profiles(user_type);
CREATE INDEX idx_mall_user_profiles_status ON public.mall_user_profiles(status);
CREATE INDEX idx_mall_user_profiles_mall_role ON public.mall_user_profiles(mall_role);
-- 2. 用户地址表
CREATE TABLE public.ak_user_addresses (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES public.ak_users(id) ON DELETE CASCADE,
receiver_name VARCHAR(64) NOT NULL, -- 收货人姓名
receiver_phone VARCHAR(32) NOT NULL, -- 收货人手机
province VARCHAR(64) NOT NULL, -- 省份
city VARCHAR(64) NOT NULL, -- 城市
district VARCHAR(64) NOT NULL, -- 区县
address_detail TEXT NOT NULL, -- 详细地址
postal_code VARCHAR(16), -- 邮编
is_default BOOLEAN DEFAULT false, -- 是否默认地址
label VARCHAR(32), -- 地址标签home/office/school/other
coordinates POINT, -- 经纬度坐标,用于配送距离计算
delivery_instructions TEXT, -- 配送说明
business_hours VARCHAR(128), -- 可配送时间(如9:00-18:00)
status INTEGER DEFAULT 1, -- 地址状态1正常 2禁用
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE public.ak_user_addresses IS '用户地址表';
COMMENT ON COLUMN public.ak_user_addresses.coordinates IS '经纬度坐标格式POINT(longitude latitude)';
COMMENT ON COLUMN public.ak_user_addresses.label IS '地址标签home家 office公司 school学校 other其他';
-- 创建索引
CREATE INDEX idx_user_addresses_user_id ON public.ak_user_addresses(user_id);
CREATE INDEX idx_user_addresses_city ON public.ak_user_addresses(city);
CREATE INDEX idx_user_addresses_district ON public.ak_user_addresses(district);
CREATE INDEX idx_user_addresses_is_default ON public.ak_user_addresses(is_default);
-- 创建地理位置索引(用于附近配送查询)
CREATE INDEX idx_user_addresses_coordinates ON public.ak_user_addresses USING GIST(coordinates);
-- 3. 用户收藏表
CREATE TABLE public.mall_user_favorites (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES public.ak_users(id) ON DELETE CASCADE,
target_type VARCHAR(32) NOT NULL, -- 收藏类型product/shop
target_id uuid NOT NULL, -- 目标ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE public.mall_user_favorites IS '用户收藏表';
COMMENT ON COLUMN public.mall_user_favorites.target_type IS '收藏类型product商品 shop店铺';
-- 创建索引和唯一约束
CREATE INDEX idx_mall_user_favorites_user_id ON public.mall_user_favorites(user_id);
CREATE INDEX idx_mall_user_favorites_target ON public.mall_user_favorites(target_type, target_id);
CREATE UNIQUE INDEX idx_mall_user_favorites_unique ON public.mall_user_favorites(user_id, target_type, target_id);
-- 4. 用户搜索历史表
CREATE TABLE public.mall_user_search_history (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES public.ak_users(id) ON DELETE CASCADE,
keyword VARCHAR(256) NOT NULL, -- 搜索关键词
search_count INTEGER DEFAULT 1, -- 搜索次数
last_search_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE public.mall_user_search_history IS '用户搜索历史表';
-- 创建索引
CREATE INDEX idx_mall_search_history_user_id ON public.mall_user_search_history(user_id);
CREATE INDEX idx_mall_search_history_keyword ON public.mall_user_search_history(keyword);
CREATE UNIQUE INDEX idx_mall_search_history_unique ON public.mall_user_search_history(user_id, keyword);
-- 5. 用户浏览历史表
CREATE TABLE public.mall_user_browse_history (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES public.ak_users(id) ON DELETE CASCADE,
product_id uuid NOT NULL, -- 浏览的商品ID
browse_count INTEGER DEFAULT 1, -- 浏览次数
browse_duration INTEGER DEFAULT 0, -- 浏览时长(秒)
last_browse_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE public.mall_user_browse_history IS '用户浏览历史表';
-- 创建索引
CREATE INDEX idx_mall_browse_history_user_id ON public.mall_user_browse_history(user_id);
CREATE INDEX idx_mall_browse_history_product_id ON public.mall_user_browse_history(product_id);
CREATE INDEX idx_mall_browse_history_last_browse ON public.mall_user_browse_history(last_browse_at);
CREATE UNIQUE INDEX idx_mall_browse_history_unique ON public.mall_user_browse_history(user_id, product_id);
-- 6. 触发器:确保每个用户只有一个默认地址
CREATE OR REPLACE FUNCTION ensure_single_default_address()
RETURNS TRIGGER AS $$
BEGIN
-- 如果新插入/更新的地址设为默认
IF NEW.is_default = true THEN
-- 将该用户的其他地址的默认状态设为false
UPDATE public.ak_user_addresses
SET is_default = false
WHERE user_id = NEW.user_id AND id != NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 创建触发器
CREATE TRIGGER trigger_ensure_single_default_address
BEFORE INSERT OR UPDATE ON public.ak_user_addresses
FOR EACH ROW
EXECUTE FUNCTION ensure_single_default_address();
-- 7. 触发器:自动更新 updated_at 字段
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 为相关表创建更新时间触发器
CREATE TRIGGER trigger_mall_user_profiles_updated_at
BEFORE UPDATE ON public.mall_user_profiles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_user_addresses_updated_at
BEFORE UPDATE ON public.ak_user_addresses
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 8. 数据迁移:为现有 ak_users 用户创建默认商城档案
INSERT INTO public.mall_user_profiles (user_id, user_type, status, mall_role)
SELECT
id,
1, -- 默认为消费者
1, -- 默认状态正常
'consumer' -- 默认角色消费者
FROM public.ak_users
WHERE id NOT IN (SELECT user_id FROM public.mall_user_profiles WHERE user_id IS NOT NULL);
-- 9. 创建视图:商城用户完整信息视图
CREATE VIEW public.mall_users_view AS
SELECT
u.id,
u.username,
u.email,
u.phone,
u.avatar_url,
u.gender,
u.birthday,
u.bio,
u.created_at as user_created_at,
u.updated_at as user_updated_at,
mp.user_type,
mp.status,
mp.real_name,
mp.credit_score,
mp.mall_role,
mp.verification_status,
mp.created_at as profile_created_at,
mp.updated_at as profile_updated_at
FROM public.ak_users u
INNER JOIN public.mall_user_profiles mp ON u.id = mp.user_id;
COMMENT ON VIEW public.mall_users_view IS '商城用户完整信息视图';
-- 10. 权限设置(根据实际需要调整)
-- 创建商城相关的RLS策略
ALTER TABLE public.mall_user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_user_addresses ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.mall_user_favorites ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.mall_user_search_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.mall_user_browse_history ENABLE ROW LEVEL SECURITY;
-- 用户只能访问自己的数据
CREATE POLICY mall_user_profiles_policy ON public.mall_user_profiles
FOR ALL USING (auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id));
CREATE POLICY user_addresses_policy ON public.ak_user_addresses
FOR ALL USING (auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id));
CREATE POLICY mall_user_favorites_policy ON public.mall_user_favorites
FOR ALL USING (auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id));
CREATE POLICY mall_user_search_history_policy ON public.mall_user_search_history
FOR ALL USING (auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id));
CREATE POLICY mall_user_browse_history_policy ON public.mall_user_browse_history
FOR ALL USING (auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id));
-- 11. 示例查询函数
-- 获取用户默认地址
CREATE OR REPLACE FUNCTION get_user_default_address(p_user_id uuid)
RETURNS TABLE (
id uuid,
receiver_name varchar,
receiver_phone varchar,
full_address text,
coordinates point
) AS $$
BEGIN
RETURN QUERY
SELECT
a.id,
a.receiver_name,
a.receiver_phone,
(a.province || a.city || a.district || a.address_detail) as full_address,
a.coordinates
FROM public.ak_user_addresses a
WHERE a.user_id = p_user_id AND a.is_default = true AND a.status = 1
LIMIT 1;
END;
$$ LANGUAGE plpgsql;
-- 检查用户是否为商城认证商家
CREATE OR REPLACE FUNCTION is_verified_merchant(p_user_id uuid)
RETURNS boolean AS $$
DECLARE
result boolean := false;
BEGIN
SELECT (user_type = 2 AND verification_status = 1) INTO result
FROM public.mall_user_profiles
WHERE user_id = p_user_id;
RETURN COALESCE(result, false);
END;
$$ LANGUAGE plpgsql;
-- 12. 完成提示
DO $$
BEGIN
RAISE NOTICE '商城用户兼容性方案部署完成!';
RAISE NOTICE '已创建表mall_user_profiles, ak_user_addresses, mall_user_favorites, mall_user_search_history, mall_user_browse_history';
RAISE NOTICE '已创建视图mall_users_view';
RAISE NOTICE '已设置触发器和RLS策略';
RAISE NOTICE '已为现有用户创建默认商城档案';
END $$;

View File

@@ -0,0 +1,110 @@
-- ===================================================================
-- 基于ak_contents的商品扩展方案不推荐但可行
-- ===================================================================
-- 1. 为ak_contents添加商品相关字段
DO $$
BEGIN
-- 添加内容类型字段区分资讯和商品
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='ak_contents' AND column_name='content_type') THEN
ALTER TABLE public.ak_contents ADD COLUMN content_type VARCHAR(50) DEFAULT 'article';
COMMENT ON COLUMN public.ak_contents.content_type IS '内容类型article=资讯product=商品';
END IF;
-- 添加商品特有字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='ak_contents' AND column_name='product_code') THEN
ALTER TABLE public.ak_contents ADD COLUMN product_code VARCHAR(50);
COMMENT ON COLUMN public.ak_contents.product_code IS '商品编码(仅商品类型使用)';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='ak_contents' AND column_name='base_price') THEN
ALTER TABLE public.ak_contents ADD COLUMN base_price DECIMAL(12,2);
COMMENT ON COLUMN public.ak_contents.base_price IS '商品基础价格(仅商品类型使用)';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='ak_contents' AND column_name='stock_quantity') THEN
ALTER TABLE public.ak_contents ADD COLUMN stock_quantity INTEGER DEFAULT 0;
COMMENT ON COLUMN public.ak_contents.stock_quantity IS '库存数量(仅商品类型使用)';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='ak_contents' AND column_name='merchant_id') THEN
ALTER TABLE public.ak_contents ADD COLUMN merchant_id UUID;
COMMENT ON COLUMN public.ak_contents.merchant_id IS '商家ID仅商品类型使用';
END IF;
END
$$;
-- 2. 创建商品特有的附加表
-- 商品SKU表
CREATE TABLE IF NOT EXISTS public.ak_product_skus (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_id UUID NOT NULL REFERENCES public.ak_contents(id) ON DELETE CASCADE,
sku_code VARCHAR(100) UNIQUE NOT NULL,
specification_values JSONB NOT NULL DEFAULT '{}',
price DECIMAL(12,2) NOT NULL,
stock_quantity INTEGER DEFAULT 0,
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 商品规格表
CREATE TABLE IF NOT EXISTS public.ak_product_specifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_id UUID NOT NULL REFERENCES public.ak_contents(id) ON DELETE CASCADE,
spec_name VARCHAR(100) NOT NULL,
spec_values TEXT[] NOT NULL,
is_required BOOLEAN DEFAULT false,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 商品属性表
CREATE TABLE IF NOT EXISTS public.ak_product_attributes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_id UUID NOT NULL REFERENCES public.ak_contents(id) ON DELETE CASCADE,
attribute_name VARCHAR(200) NOT NULL,
attribute_value TEXT NOT NULL,
attribute_group VARCHAR(100),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 3. 创建专门的商品视图
CREATE OR REPLACE VIEW public.vw_products AS
SELECT
id,
title as product_name,
content as product_description,
product_code,
base_price,
stock_quantity,
merchant_id,
category_id,
status,
tags,
published_at as listed_at,
created_at,
updated_at,
-- 排除资讯相关字段
NULL as ai_analysis_result,
NULL as translation_status
FROM public.ak_contents
WHERE content_type = 'product';
-- 4. 添加约束和索引
ALTER TABLE public.ak_contents ADD CONSTRAINT chk_product_code_unique
CHECK (content_type != 'product' OR product_code IS NOT NULL);
CREATE INDEX IF NOT EXISTS idx_ak_contents_product_type ON public.ak_contents(content_type) WHERE content_type = 'product';
CREATE INDEX IF NOT EXISTS idx_ak_contents_product_code ON public.ak_contents(product_code) WHERE content_type = 'product';
CREATE INDEX IF NOT EXISTS idx_ak_contents_merchant ON public.ak_contents(merchant_id) WHERE content_type = 'product';
-- 注意事项和限制
COMMENT ON VIEW public.vw_products IS '
使用ak_contents作为商品表的注意事项
1. 必须设置content_type=product来区分商品和资讯
2. 许多ak_contents的字段对商品无意义如AI翻译相关
3. 查询性能可能受影响,建议添加分区
4. 推荐使用独立的商品表设计以获得更好的架构
';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,666 @@
-- =====================================================================================
-- 商城系统 SEO 优化和安全策略脚本
-- 用途: 为商城系统添加 SEO 优化函数和 RLS 安全策略
-- 前置条件: 需要先执行 mall_migration.sql
-- =====================================================================================
-- =====================================================================================
-- 1. SEO 优化相关函数
-- =====================================================================================
-- 根据 cid 获取商品信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_product_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
name VARCHAR,
slug VARCHAR,
description TEXT,
main_image_url TEXT,
base_price DECIMAL,
rating_avg DECIMAL,
sale_count INTEGER,
category_name VARCHAR,
brand_name VARCHAR,
shop_name VARCHAR
) AS $$
BEGIN
RETURN QUERY
SELECT
p.id,
p.cid,
p.name,
p.slug,
p.description,
p.main_image_url,
p.base_price,
p.rating_avg,
p.sale_count,
c.name as category_name,
b.name as brand_name,
s.shop_name
FROM public.ml_products p
LEFT JOIN public.ml_categories c ON p.category_id = c.id
LEFT JOIN public.ml_brands b ON p.brand_id = b.id
LEFT JOIN public.ml_shops s ON p.merchant_id = s.merchant_id
WHERE p.cid = p_cid AND p.status = 1;
END;
$$ LANGUAGE plpgsql;
-- 根据 cid 获取分类信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_category_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
name VARCHAR,
slug VARCHAR,
description TEXT,
icon_url TEXT,
path TEXT[]
) AS $$
BEGIN
RETURN QUERY
SELECT
c.id,
c.cid,
c.name,
c.slug,
c.description,
c.icon_url,
c.path
FROM public.ml_categories c
WHERE c.cid = p_cid AND c.is_active = TRUE;
END;
$$ LANGUAGE plpgsql;
-- 根据 cid 获取品牌信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_brand_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
name VARCHAR,
logo_url TEXT,
description TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
b.id,
b.cid,
b.name,
b.logo_url,
b.description
FROM public.ml_brands b
WHERE b.cid = p_cid AND b.is_active = TRUE;
END;
$$ LANGUAGE plpgsql;
-- 根据 cid 获取店铺信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_shop_by_cid(p_cid INTEGER)
RETURNS TABLE (
id UUID,
cid INTEGER,
shop_name VARCHAR,
description TEXT,
shop_logo TEXT,
rating_avg DECIMAL,
product_count INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
s.id,
s.cid,
s.shop_name,
s.description,
s.shop_logo,
s.rating_avg,
s.product_count
FROM public.ml_shops s
WHERE s.cid = p_cid AND s.status = 1;
END;
$$ LANGUAGE plpgsql;
-- 生成 SEO 友好的 URL 路径
CREATE OR REPLACE FUNCTION public.generate_seo_url(
p_type VARCHAR, -- 'product', 'category', 'brand', 'shop'
p_cid INTEGER,
p_slug VARCHAR DEFAULT NULL
)
RETURNS TEXT AS $$
DECLARE
url_path TEXT;
BEGIN
CASE p_type
WHEN 'product' THEN
url_path := '/product/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
WHEN 'category' THEN
url_path := '/category/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
WHEN 'brand' THEN
url_path := '/brand/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
WHEN 'shop' THEN
url_path := '/shop/' || p_cid;
IF p_slug IS NOT NULL THEN
url_path := url_path || '/' || p_slug;
END IF;
ELSE
url_path := '/' || p_type || '/' || p_cid;
END CASE;
RETURN url_path;
END;
$$ LANGUAGE plpgsql;
-- 批量更新 slug 字段(用于现有数据)
CREATE OR REPLACE FUNCTION public.update_seo_slugs()
RETURNS VOID AS $$
BEGIN
-- 更新商品 slug
UPDATE public.ml_products
SET slug = LOWER(REGEXP_REPLACE(name, '[^a-zA-Z0-9\u4e00-\u9fa5]+', '-', 'g'))
WHERE slug IS NULL OR slug = '';
-- 更新分类 slug
UPDATE public.ml_categories
SET slug = LOWER(REGEXP_REPLACE(name, '[^a-zA-Z0-9\u4e00-\u9fa5]+', '-', 'g'))
WHERE slug IS NULL OR slug = '';
RAISE NOTICE 'SEO slugs updated successfully';
END;
$$ LANGUAGE plpgsql;
-- =====================================================================================
-- 2. 商业逻辑函数
-- =====================================================================================
-- 计算购物车总金额
CREATE OR REPLACE FUNCTION public.calculate_cart_total(p_user_id UUID)
RETURNS DECIMAL AS $$
DECLARE
total_amount DECIMAL := 0;
BEGIN
SELECT COALESCE(SUM(
CASE
WHEN s.id IS NOT NULL THEN s.price * c.quantity
ELSE p.base_price * c.quantity
END
), 0) INTO total_amount
FROM public.ml_shopping_cart c
LEFT JOIN public.ml_product_skus s ON c.sku_id = s.id
LEFT JOIN public.ml_products p ON c.product_id = p.id
WHERE c.user_id = p_user_id
AND c.selected = TRUE
AND p.status = 1
AND (s.id IS NULL OR s.status = 1);
RETURN total_amount;
END;
$$ LANGUAGE plpgsql;
-- 获取商品可用库存
CREATE OR REPLACE FUNCTION public.get_product_available_stock(p_product_id UUID, p_sku_id UUID DEFAULT NULL)
RETURNS INTEGER AS $$
DECLARE
stock_count INTEGER := 0;
BEGIN
IF p_sku_id IS NOT NULL THEN
-- 获取特定SKU库存
SELECT COALESCE(stock, 0) INTO stock_count
FROM public.ml_product_skus
WHERE id = p_sku_id AND product_id = p_product_id AND status = 1;
ELSE
-- 获取商品总库存
SELECT COALESCE(available_stock, 0) INTO stock_count
FROM public.ml_products
WHERE id = p_product_id AND status = 1;
END IF;
RETURN stock_count;
END;
$$ LANGUAGE plpgsql;
-- 商品库存更新触发器函数
CREATE OR REPLACE FUNCTION public.update_product_stock()
RETURNS TRIGGER AS $$
BEGIN
-- 更新商品总库存
IF TG_OP = 'DELETE' THEN
UPDATE public.ml_products
SET
total_stock = (
SELECT COALESCE(SUM(stock), 0)
FROM public.ml_product_skus
WHERE product_id = OLD.product_id AND status = 1
),
available_stock = (
SELECT COALESCE(SUM(stock), 0)
FROM public.ml_product_skus
WHERE product_id = OLD.product_id AND status = 1
)
WHERE id = OLD.product_id;
RETURN OLD;
ELSE
UPDATE public.ml_products
SET
total_stock = (
SELECT COALESCE(SUM(stock), 0)
FROM public.ml_product_skus
WHERE product_id = NEW.product_id AND status = 1
),
available_stock = (
SELECT COALESCE(SUM(stock), 0)
FROM public.ml_product_skus
WHERE product_id = NEW.product_id AND status = 1
)
WHERE id = NEW.product_id;
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
-- 订单状态变更时的处理
CREATE OR REPLACE FUNCTION public.handle_order_status_change()
RETURNS TRIGGER AS $$
BEGIN
-- 如果订单状态变为已付款
IF NEW.order_status = 2 AND (OLD.order_status IS NULL OR OLD.order_status = 1) THEN
NEW.paid_at = NOW();
END IF;
-- 如果订单状态变为已发货
IF NEW.order_status = 3 AND OLD.order_status = 2 THEN
NEW.shipped_at = NOW();
END IF;
-- 如果订单状态变为已完成
IF NEW.order_status = 4 AND OLD.order_status = 3 THEN
NEW.delivered_at = NOW();
NEW.completed_at = NOW();
-- 更新商品销量
UPDATE public.ml_products
SET sale_count = sale_count + (
SELECT SUM(quantity)
FROM public.ml_order_items
WHERE order_id = NEW.id
)
WHERE id IN (
SELECT product_id
FROM public.ml_order_items
WHERE order_id = NEW.id
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 创建库存更新触发器
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_update_product_stock') THEN
CREATE TRIGGER trigger_ml_update_product_stock
AFTER INSERT OR UPDATE OR DELETE ON public.ml_product_skus
FOR EACH ROW EXECUTE FUNCTION public.update_product_stock();
END IF;
END $$;
-- 创建订单状态变更触发器
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_ml_order_status_change') THEN
CREATE TRIGGER trigger_ml_order_status_change
BEFORE UPDATE ON public.ml_orders
FOR EACH ROW EXECUTE FUNCTION public.handle_order_status_change();
END IF;
END $$;
-- =====================================================================================
-- 3. 创建详细视图
-- =====================================================================================
-- 商品详情视图
CREATE OR REPLACE VIEW public.ml_products_detail_view AS
SELECT
p.*,
c.cid as category_cid,
c.name as category_name,
c.path as category_path,
b.cid as brand_cid,
b.name as brand_name,
s.cid as shop_cid,
s.shop_name,
u.username as merchant_name,
CASE
WHEN p.status = 1 THEN '上架'
WHEN p.status = 2 THEN '下架'
WHEN p.status = 3 THEN '草稿'
WHEN p.status = 4 THEN '删除'
ELSE '未知'
END as status_name
FROM public.ml_products p
LEFT JOIN public.ml_categories c ON p.category_id = c.id
LEFT JOIN public.ml_brands b ON p.brand_id = b.id
LEFT JOIN public.ml_shops s ON p.merchant_id = s.merchant_id
LEFT JOIN public.ak_users u ON p.merchant_id = u.id;
COMMENT ON VIEW public.ml_products_detail_view IS '商品详情视图';
-- 订单详情视图
CREATE OR REPLACE VIEW public.ml_orders_detail_view AS
SELECT
o.*,
u.username as customer_name,
u.phone as customer_phone,
m.username as merchant_name,
s.shop_name,
CASE
WHEN o.order_status = 1 THEN '待付款'
WHEN o.order_status = 2 THEN '待发货'
WHEN o.order_status = 3 THEN '待收货'
WHEN o.order_status = 4 THEN '已完成'
WHEN o.order_status = 5 THEN '已取消'
WHEN o.order_status = 6 THEN '退款中'
WHEN o.order_status = 7 THEN '已退款'
ELSE '未知'
END as order_status_name,
CASE
WHEN o.payment_status = 1 THEN '未付款'
WHEN o.payment_status = 2 THEN '已付款'
WHEN o.payment_status = 3 THEN '部分退款'
WHEN o.payment_status = 4 THEN '全额退款'
ELSE '未知'
END as payment_status_name
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
LEFT JOIN public.ak_users m ON o.merchant_id = m.id
LEFT JOIN public.ml_shops s ON o.merchant_id = s.merchant_id;
COMMENT ON VIEW public.ml_orders_detail_view IS '订单详情视图';
-- =====================================================================================
-- 4. RLS (Row Level Security) 策略
-- =====================================================================================
-- 启用 RLS
ALTER TABLE public.ml_user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_user_addresses ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_shopping_cart ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_user_favorites ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_browse_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_user_coupons ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_products ENABLE ROW LEVEL SECURITY;
-- 用户档案策略:用户只能访问自己的数据
DO $$
BEGIN
-- 删除可能存在的策略
DROP POLICY IF EXISTS ml_user_profiles_select_policy ON public.ml_user_profiles;
DROP POLICY IF EXISTS ml_user_profiles_insert_policy ON public.ml_user_profiles;
DROP POLICY IF EXISTS ml_user_profiles_update_policy ON public.ml_user_profiles;
DROP POLICY IF EXISTS ml_user_profiles_delete_policy ON public.ml_user_profiles;
-- 创建新策略
CREATE POLICY ml_user_profiles_select_policy ON public.ml_user_profiles
FOR SELECT USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_profiles_insert_policy ON public.ml_user_profiles
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_profiles_update_policy ON public.ml_user_profiles
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_profiles_delete_policy ON public.ml_user_profiles
FOR DELETE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
END $$;
-- 用户地址策略
DO $$
BEGIN
DROP POLICY IF EXISTS ml_user_addresses_select_policy ON public.ml_user_addresses;
DROP POLICY IF EXISTS ml_user_addresses_insert_policy ON public.ml_user_addresses;
DROP POLICY IF EXISTS ml_user_addresses_update_policy ON public.ml_user_addresses;
DROP POLICY IF EXISTS ml_user_addresses_delete_policy ON public.ml_user_addresses;
CREATE POLICY ml_user_addresses_select_policy ON public.ml_user_addresses
FOR SELECT USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_addresses_insert_policy ON public.ml_user_addresses
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_addresses_update_policy ON public.ml_user_addresses
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_addresses_delete_policy ON public.ml_user_addresses
FOR DELETE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
END $$;
-- 购物车策略
DO $$
BEGIN
DROP POLICY IF EXISTS ml_shopping_cart_select_policy ON public.ml_shopping_cart;
DROP POLICY IF EXISTS ml_shopping_cart_insert_policy ON public.ml_shopping_cart;
DROP POLICY IF EXISTS ml_shopping_cart_update_policy ON public.ml_shopping_cart;
DROP POLICY IF EXISTS ml_shopping_cart_delete_policy ON public.ml_shopping_cart;
CREATE POLICY ml_shopping_cart_select_policy ON public.ml_shopping_cart
FOR SELECT USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_shopping_cart_insert_policy ON public.ml_shopping_cart
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_shopping_cart_update_policy ON public.ml_shopping_cart
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_shopping_cart_delete_policy ON public.ml_shopping_cart
FOR DELETE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
END $$;
-- 订单策略:用户可以查看自己的订单,商家可以查看自己店铺的订单
DO $$
BEGIN
DROP POLICY IF EXISTS ml_orders_select_policy ON public.ml_orders;
DROP POLICY IF EXISTS ml_orders_insert_policy ON public.ml_orders;
DROP POLICY IF EXISTS ml_orders_update_policy ON public.ml_orders;
DROP POLICY IF EXISTS ml_orders_delete_policy ON public.ml_orders;
CREATE POLICY ml_orders_select_policy ON public.ml_orders
FOR SELECT USING (
auth.uid() IN (
SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
)
);
CREATE POLICY ml_orders_insert_policy ON public.ml_orders
FOR INSERT WITH CHECK (
auth.uid() IN (
SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
)
);
CREATE POLICY ml_orders_update_policy ON public.ml_orders
FOR UPDATE USING (
auth.uid() IN (
SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
)
);
CREATE POLICY ml_orders_delete_policy ON public.ml_orders
FOR DELETE USING (
auth.uid() IN (
SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
)
);
END $$;
-- 商品策略:所有人可以查看上架商品,商家只能管理自己的商品
DO $$
BEGIN
DROP POLICY IF EXISTS ml_products_select_policy ON public.ml_products;
DROP POLICY IF EXISTS ml_products_insert_policy ON public.ml_products;
DROP POLICY IF EXISTS ml_products_update_policy ON public.ml_products;
DROP POLICY IF EXISTS ml_products_delete_policy ON public.ml_products;
CREATE POLICY ml_products_select_policy ON public.ml_products
FOR SELECT USING (status = 1);
CREATE POLICY ml_products_insert_policy ON public.ml_products
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = merchant_id)
);
CREATE POLICY ml_products_update_policy ON public.ml_products
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = merchant_id)
);
CREATE POLICY ml_products_delete_policy ON public.ml_products
FOR DELETE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = merchant_id)
);
END $$;
-- 收藏策略
DO $$
BEGIN
DROP POLICY IF EXISTS ml_user_favorites_select_policy ON public.ml_user_favorites;
DROP POLICY IF EXISTS ml_user_favorites_insert_policy ON public.ml_user_favorites;
DROP POLICY IF EXISTS ml_user_favorites_update_policy ON public.ml_user_favorites;
DROP POLICY IF EXISTS ml_user_favorites_delete_policy ON public.ml_user_favorites;
CREATE POLICY ml_user_favorites_select_policy ON public.ml_user_favorites
FOR SELECT USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_favorites_insert_policy ON public.ml_user_favorites
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_favorites_update_policy ON public.ml_user_favorites
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_favorites_delete_policy ON public.ml_user_favorites
FOR DELETE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
END $$;
-- 浏览历史策略
DO $$
BEGIN
DROP POLICY IF EXISTS ml_browse_history_select_policy ON public.ml_browse_history;
DROP POLICY IF EXISTS ml_browse_history_insert_policy ON public.ml_browse_history;
DROP POLICY IF EXISTS ml_browse_history_update_policy ON public.ml_browse_history;
DROP POLICY IF EXISTS ml_browse_history_delete_policy ON public.ml_browse_history;
CREATE POLICY ml_browse_history_select_policy ON public.ml_browse_history
FOR SELECT USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_browse_history_insert_policy ON public.ml_browse_history
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_browse_history_update_policy ON public.ml_browse_history
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_browse_history_delete_policy ON public.ml_browse_history
FOR DELETE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
END $$;
-- 优惠券策略
DO $$
BEGIN
DROP POLICY IF EXISTS ml_user_coupons_select_policy ON public.ml_user_coupons;
DROP POLICY IF EXISTS ml_user_coupons_insert_policy ON public.ml_user_coupons;
DROP POLICY IF EXISTS ml_user_coupons_update_policy ON public.ml_user_coupons;
DROP POLICY IF EXISTS ml_user_coupons_delete_policy ON public.ml_user_coupons;
CREATE POLICY ml_user_coupons_select_policy ON public.ml_user_coupons
FOR SELECT USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_coupons_insert_policy ON public.ml_user_coupons
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_coupons_update_policy ON public.ml_user_coupons
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
CREATE POLICY ml_user_coupons_delete_policy ON public.ml_user_coupons
FOR DELETE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
END $$;
-- =====================================================================================
-- 5. 完成提示
-- =====================================================================================
DO $$
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE 'SEO 优化和安全策略配置完成!';
RAISE NOTICE '=======================================================';
RAISE NOTICE '已创建 SEO 函数: 6 个';
RAISE NOTICE '已创建业务函数: 4 个';
RAISE NOTICE '已创建详细视图: 2 个';
RAISE NOTICE '已配置 RLS 策略: 8 个表';
RAISE NOTICE '已创建库存和订单触发器';
RAISE NOTICE '=======================================================';
RAISE NOTICE '功能说明:';
RAISE NOTICE '- SEO 友好的 URL 生成';
RAISE NOTICE '- CID 基础的数据查询';
RAISE NOTICE '- 自动库存管理';
RAISE NOTICE '- 订单状态自动更新';
RAISE NOTICE '- 用户数据安全隔离';
RAISE NOTICE '=======================================================';
END $$;

View File

@@ -0,0 +1,452 @@
-- ===================================================================
-- 电商商城商品管理数据库设计
-- 基于PostgreSQL兼容现有ak_contents资讯系统
-- ===================================================================
-- ===================================================================
-- 1. 商品核心表
-- ===================================================================
-- 商品基础信息表
CREATE TABLE IF NOT EXISTS public.mall_products (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_code VARCHAR(50) UNIQUE NOT NULL, -- 商品编码
name VARCHAR(500) NOT NULL, -- 商品名称
subtitle VARCHAR(1000), -- 副标题/卖点
description TEXT, -- 商品描述
-- 商家信息
merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
brand_id UUID REFERENCES public.mall_brands(id),
-- 分类信息
category_id UUID NOT NULL REFERENCES public.mall_categories(id),
category_path TEXT[], -- 分类路径,便于查询
-- 基础属性
weight DECIMAL(10,3), -- 重量(kg)
dimensions JSONB, -- 尺寸信息 {长,宽,高}
-- 价格信息
base_price DECIMAL(12,2) NOT NULL, -- 基础价格
market_price DECIMAL(12,2), -- 市场价
cost_price DECIMAL(12,2), -- 成本价
-- 库存信息
stock_quantity INTEGER DEFAULT 0, -- 总库存
available_quantity INTEGER DEFAULT 0, -- 可用库存
reserved_quantity INTEGER DEFAULT 0, -- 预留库存
min_order_quantity INTEGER DEFAULT 1, -- 最小起订量
max_order_quantity INTEGER, -- 最大限购量
-- 状态信息
status VARCHAR(20) DEFAULT 'draft', -- 状态draft/active/inactive/deleted
is_featured BOOLEAN DEFAULT false, -- 是否精选
is_new BOOLEAN DEFAULT false, -- 是否新品
is_hot BOOLEAN DEFAULT false, -- 是否热卖
is_on_sale BOOLEAN DEFAULT false, -- 是否促销
-- 多媒体
main_image_url TEXT, -- 主图
image_urls TEXT[], -- 图片URL数组
video_urls TEXT[], -- 视频URL数组
-- SEO相关
seo_title VARCHAR(200), -- SEO标题
seo_description VARCHAR(500), -- SEO描述
seo_keywords TEXT[], -- SEO关键词
slug VARCHAR(200) UNIQUE, -- URL友好标识
-- 销售统计
view_count INTEGER DEFAULT 0, -- 浏览次数
sale_count INTEGER DEFAULT 0, -- 销售数量
favorite_count INTEGER DEFAULT 0, -- 收藏次数
rating_average DECIMAL(3,2) DEFAULT 0, -- 平均评分
rating_count INTEGER DEFAULT 0, -- 评分次数
-- 时间信息
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
published_at TIMESTAMP WITH TIME ZONE, -- 上架时间
sale_start_at TIMESTAMP WITH TIME ZONE, -- 开售时间
sale_end_at TIMESTAMP WITH TIME ZONE, -- 停售时间
-- 额外信息
tags TEXT[], -- 标签
attributes JSONB DEFAULT '{}', -- 自定义属性
notes TEXT, -- 内部备注
-- 约束
CONSTRAINT chk_price_positive CHECK (base_price >= 0),
CONSTRAINT chk_stock_non_negative CHECK (stock_quantity >= 0),
CONSTRAINT chk_available_stock CHECK (available_quantity >= 0),
CONSTRAINT chk_reserved_stock CHECK (reserved_quantity >= 0),
CONSTRAINT chk_rating_range CHECK (rating_average >= 0 AND rating_average <= 5)
);
-- 商品表索引
CREATE INDEX IF NOT EXISTS idx_mall_products_merchant ON public.mall_products(merchant_id, status);
CREATE INDEX IF NOT EXISTS idx_mall_products_category ON public.mall_products(category_id, status);
CREATE INDEX IF NOT EXISTS idx_mall_products_status ON public.mall_products(status, published_at DESC);
CREATE INDEX IF NOT EXISTS idx_mall_products_featured ON public.mall_products(is_featured, published_at DESC);
CREATE INDEX IF NOT EXISTS idx_mall_products_price ON public.mall_products(base_price, status);
CREATE INDEX IF NOT EXISTS idx_mall_products_sale_count ON public.mall_products(sale_count DESC);
CREATE INDEX IF NOT EXISTS idx_mall_products_rating ON public.mall_products(rating_average DESC, rating_count DESC);
CREATE INDEX IF NOT EXISTS idx_mall_products_code ON public.mall_products(product_code);
CREATE INDEX IF NOT EXISTS idx_mall_products_slug ON public.mall_products(slug);
CREATE INDEX IF NOT EXISTS idx_mall_products_tags ON public.mall_products USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_mall_products_category_path ON public.mall_products USING GIN(category_path);
COMMENT ON TABLE public.mall_products IS '商品基础信息表';
-- ===================================================================
-- 2. 商品SKU表
-- ===================================================================
-- 商品SKU表
CREATE TABLE IF NOT EXISTS public.mall_product_skus (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES public.mall_products(id) ON DELETE CASCADE,
sku_code VARCHAR(100) UNIQUE NOT NULL, -- SKU编码
-- 规格信息
specification_values JSONB NOT NULL DEFAULT '{}', -- 规格值 {"颜色":"红色","尺寸":"L"}
specification_text VARCHAR(500), -- 规格描述文本
-- 价格库存
price DECIMAL(12,2) NOT NULL, -- SKU价格
cost_price DECIMAL(12,2), -- SKU成本价
stock_quantity INTEGER DEFAULT 0, -- SKU库存
available_quantity INTEGER DEFAULT 0, -- SKU可用库存
reserved_quantity INTEGER DEFAULT 0, -- SKU预留库存
-- SKU属性
weight DECIMAL(10,3), -- SKU重量
barcode VARCHAR(50), -- 条形码
image_url TEXT, -- SKU图片
-- 状态
is_active BOOLEAN DEFAULT true, -- 是否启用
is_default BOOLEAN DEFAULT false, -- 是否默认SKU
-- 销售统计
sale_count INTEGER DEFAULT 0, -- 销售数量
-- 时间
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 约束
CONSTRAINT chk_sku_price_positive CHECK (price >= 0),
CONSTRAINT chk_sku_stock_non_negative CHECK (stock_quantity >= 0)
);
-- SKU表索引
CREATE INDEX IF NOT EXISTS idx_mall_product_skus_product ON public.mall_product_skus(product_id, is_active);
CREATE INDEX IF NOT EXISTS idx_mall_product_skus_code ON public.mall_product_skus(sku_code);
CREATE INDEX IF NOT EXISTS idx_mall_product_skus_barcode ON public.mall_product_skus(barcode);
CREATE INDEX IF NOT EXISTS idx_mall_product_skus_default ON public.mall_product_skus(product_id, is_default);
CREATE INDEX IF NOT EXISTS idx_mall_product_skus_spec ON public.mall_product_skus USING GIN(specification_values);
COMMENT ON TABLE public.mall_product_skus IS '商品SKU表';
-- ===================================================================
-- 3. 商品分类表
-- ===================================================================
-- 商品分类表
CREATE TABLE IF NOT EXISTS public.mall_categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(200) NOT NULL, -- 分类名称
slug VARCHAR(200) UNIQUE, -- URL友好标识
description TEXT, -- 分类描述
-- 层级关系
parent_id UUID REFERENCES public.mall_categories(id),
level INTEGER DEFAULT 0, -- 层级0=顶级
path TEXT, -- 路径:/1/2/3
sort_order INTEGER DEFAULT 0, -- 排序
-- 显示信息
icon_url TEXT, -- 分类图标
banner_url TEXT, -- 分类横幅
-- 状态
is_active BOOLEAN DEFAULT true, -- 是否启用
is_featured BOOLEAN DEFAULT false, -- 是否精选
-- 统计
product_count INTEGER DEFAULT 0, -- 商品数量
-- SEO
seo_title VARCHAR(200),
seo_description VARCHAR(500),
seo_keywords TEXT[],
-- 时间
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 自定义属性
attributes JSONB DEFAULT '{}'
);
-- 分类表索引
CREATE INDEX IF NOT EXISTS idx_mall_categories_parent ON public.mall_categories(parent_id, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_categories_level ON public.mall_categories(level, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_categories_active ON public.mall_categories(is_active, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_categories_featured ON public.mall_categories(is_featured, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_categories_slug ON public.mall_categories(slug);
COMMENT ON TABLE public.mall_categories IS '商品分类表';
-- ===================================================================
-- 4. 商品品牌表
-- ===================================================================
-- 商品品牌表
CREATE TABLE IF NOT EXISTS public.mall_brands (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(200) NOT NULL UNIQUE, -- 品牌名称
english_name VARCHAR(200), -- 英文名称
slug VARCHAR(200) UNIQUE, -- URL友好标识
description TEXT, -- 品牌描述
-- 品牌信息
logo_url TEXT, -- 品牌Logo
banner_url TEXT, -- 品牌横幅
website_url TEXT, -- 官网地址
origin_country VARCHAR(100), -- 品牌原产国
founded_year INTEGER, -- 创立年份
-- 状态
is_active BOOLEAN DEFAULT true, -- 是否启用
is_featured BOOLEAN DEFAULT false, -- 是否精选
-- 统计
product_count INTEGER DEFAULT 0, -- 商品数量
-- SEO
seo_title VARCHAR(200),
seo_description VARCHAR(500),
seo_keywords TEXT[],
-- 时间
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 排序
sort_order INTEGER DEFAULT 0
);
-- 品牌表索引
CREATE INDEX IF NOT EXISTS idx_mall_brands_active ON public.mall_brands(is_active, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_brands_featured ON public.mall_brands(is_featured, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_brands_slug ON public.mall_brands(slug);
COMMENT ON TABLE public.mall_brands IS '商品品牌表';
-- ===================================================================
-- 5. 商品规格相关表
-- ===================================================================
-- 规格名表(如:颜色、尺寸、款式等)
CREATE TABLE IF NOT EXISTS public.mall_specifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL, -- 规格名称:颜色、尺寸等
slug VARCHAR(100) UNIQUE, -- URL友好标识
type VARCHAR(50) DEFAULT 'select', -- 类型select/input/color/image
sort_order INTEGER DEFAULT 0, -- 排序
is_required BOOLEAN DEFAULT false, -- 是否必选
is_active BOOLEAN DEFAULT true, -- 是否启用
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 规格值表红色、蓝色、L、XL等
CREATE TABLE IF NOT EXISTS public.mall_specification_values (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
specification_id UUID NOT NULL REFERENCES public.mall_specifications(id) ON DELETE CASCADE,
value VARCHAR(200) NOT NULL, -- 规格值红色、L等
color_code VARCHAR(20), -- 颜色代码(仅颜色规格)
image_url TEXT, -- 规格值图片
sort_order INTEGER DEFAULT 0, -- 排序
is_active BOOLEAN DEFAULT true, -- 是否启用
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(specification_id, value)
);
-- 商品规格关联表
CREATE TABLE IF NOT EXISTS public.mall_product_specifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES public.mall_products(id) ON DELETE CASCADE,
specification_id UUID NOT NULL REFERENCES public.mall_specifications(id) ON DELETE CASCADE,
is_required BOOLEAN DEFAULT false, -- 该商品的该规格是否必选
sort_order INTEGER DEFAULT 0, -- 在该商品中的排序
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(product_id, specification_id)
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_mall_specifications_active ON public.mall_specifications(is_active, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_specification_values_spec ON public.mall_specification_values(specification_id, is_active, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_product_specifications_product ON public.mall_product_specifications(product_id, sort_order);
-- ===================================================================
-- 6. 商品详情相关表
-- ===================================================================
-- 商品详情内容表(富文本、图文混排)
CREATE TABLE IF NOT EXISTS public.mall_product_details (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES public.mall_products(id) ON DELETE CASCADE,
-- 详情内容
detail_type VARCHAR(50) DEFAULT 'rich_text', -- 类型rich_text/markdown/html
content TEXT, -- 详情内容
images TEXT[], -- 详情图片
-- 显示控制
section_title VARCHAR(200), -- 区块标题
sort_order INTEGER DEFAULT 0, -- 排序
is_active BOOLEAN DEFAULT true, -- 是否显示
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 商品参数表
CREATE TABLE IF NOT EXISTS public.mall_product_attributes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES public.mall_products(id) ON DELETE CASCADE,
-- 参数信息
attribute_name VARCHAR(200) NOT NULL, -- 参数名称
attribute_value TEXT NOT NULL, -- 参数值
attribute_group VARCHAR(100), -- 参数分组
-- 显示控制
sort_order INTEGER DEFAULT 0, -- 排序
is_key_attribute BOOLEAN DEFAULT false, -- 是否关键参数
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(product_id, attribute_name)
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_mall_product_details_product ON public.mall_product_details(product_id, sort_order);
CREATE INDEX IF NOT EXISTS idx_mall_product_attributes_product ON public.mall_product_attributes(product_id, attribute_group, sort_order);
-- ===================================================================
-- 7. 视图和函数
-- ===================================================================
-- 商品列表视图(包含完整信息)
CREATE OR REPLACE VIEW public.vw_mall_products_full AS
SELECT
p.*,
c.name as category_name,
c.path as category_full_path,
b.name as brand_name,
b.logo_url as brand_logo_url,
-- SKU汇总信息
(SELECT MIN(price) FROM public.mall_product_skus WHERE product_id = p.id AND is_active = true) as min_price,
(SELECT MAX(price) FROM public.mall_product_skus WHERE product_id = p.id AND is_active = true) as max_price,
(SELECT SUM(stock_quantity) FROM public.mall_product_skus WHERE product_id = p.id AND is_active = true) as total_stock,
-- 默认SKU信息
default_sku.id as default_sku_id,
default_sku.sku_code as default_sku_code,
default_sku.price as default_price,
default_sku.stock_quantity as default_stock
FROM public.mall_products p
LEFT JOIN public.mall_categories c ON p.category_id = c.id
LEFT JOIN public.mall_brands b ON p.brand_id = b.id
LEFT JOIN public.mall_product_skus default_sku ON p.id = default_sku.product_id AND default_sku.is_default = true
WHERE p.status != 'deleted';
COMMENT ON VIEW public.vw_mall_products_full IS '商品完整信息视图';
-- ===================================================================
-- 8. 触发器(维护统计数据)
-- ===================================================================
-- 更新商品SKU统计的触发器函数
CREATE OR REPLACE FUNCTION public.update_product_sku_stats()
RETURNS TRIGGER AS $$
BEGIN
-- 更新商品的库存统计
UPDATE public.mall_products
SET
stock_quantity = (
SELECT COALESCE(SUM(stock_quantity), 0)
FROM public.mall_product_skus
WHERE product_id = COALESCE(NEW.product_id, OLD.product_id) AND is_active = true
),
available_quantity = (
SELECT COALESCE(SUM(available_quantity), 0)
FROM public.mall_product_skus
WHERE product_id = COALESCE(NEW.product_id, OLD.product_id) AND is_active = true
),
updated_at = NOW()
WHERE id = COALESCE(NEW.product_id, OLD.product_id);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- 创建触发器
DO $$
BEGIN
DROP TRIGGER IF EXISTS trigger_update_product_sku_stats ON public.mall_product_skus;
CREATE TRIGGER trigger_update_product_sku_stats
AFTER INSERT OR UPDATE OR DELETE ON public.mall_product_skus
FOR EACH ROW EXECUTE FUNCTION public.update_product_sku_stats();
END
$$;
-- ===================================================================
-- 9. 初始化数据
-- ===================================================================
-- 插入基础商品分类
INSERT INTO public.mall_categories (name, slug, level, sort_order) VALUES
('服装鞋包', 'fashion', 0, 1),
('数码家电', 'electronics', 0, 2),
('食品生鲜', 'food', 0, 3),
('家居日用', 'home', 0, 4),
('美妆护肤', 'beauty', 0, 5),
('运动户外', 'sports', 0, 6),
('图书文娱', 'books', 0, 7),
('医药保健', 'health', 0, 8)
ON CONFLICT (slug) DO NOTHING;
-- 插入基础规格
INSERT INTO public.mall_specifications (name, slug, type, sort_order) VALUES
('颜色', 'color', 'color', 1),
('尺寸', 'size', 'select', 2),
('款式', 'style', 'select', 3),
('容量', 'capacity', 'select', 4),
('材质', 'material', 'select', 5)
ON CONFLICT (slug) DO NOTHING;
-- 输出完成信息
DO $$
BEGIN
RAISE NOTICE '商品管理数据库结构创建完成!';
RAISE NOTICE '已创建以下核心表:';
RAISE NOTICE '- mall_products: 商品基础信息';
RAISE NOTICE '- mall_product_skus: 商品SKU';
RAISE NOTICE '- mall_categories: 商品分类';
RAISE NOTICE '- mall_brands: 商品品牌';
RAISE NOTICE '- mall_specifications: 商品规格';
RAISE NOTICE '可以开始添加商品数据了!';
END
$$;

View File

@@ -0,0 +1,2 @@
-- Moved to doc_mall/create_mall_subscription_tables.sql
-- Keeping a small pointer file to avoid duplication.

View File

@@ -0,0 +1,47 @@
-- Optional guard to restrict non-admin updates on ml_user_subscriptions
-- Purpose: Allow normal users to toggle auto_renew and cancel_at_period_end only.
-- Admins can update any fields.
-- Dependencies: public.is_admin() from subscription_rls_policies.sql
begin;
-- Create or replace the guard function
create or replace function public.enforce_user_sub_update()
returns trigger
language plpgsql
as $$
begin
-- Admin can change anything
if public.is_admin() then
return new;
end if;
-- Owner can only toggle limited fields
if new.user_id = auth.uid() then
-- Revert disallowed fields to old values
new.status := old.status;
new.plan_id := old.plan_id;
new.start_date := old.start_date;
new.end_date := old.end_date;
new.next_billing_date := old.next_billing_date;
new.metadata := old.metadata;
-- Allow: auto_renew, cancel_at_period_end (and updated_at will be set by trigger)
return new;
end if;
-- Neither admin nor owner
raise exception 'Forbidden (not owner)';
end;
$$;
-- Recreate trigger (idempotent)
drop trigger if exists trg_enforce_user_sub_update on public.ml_user_subscriptions;
create trigger trg_enforce_user_sub_update
before update on public.ml_user_subscriptions
for each row execute function public.enforce_user_sub_update();
commit;
-- Usage:
-- 1) Ensure subscription tables and RLS policies are created (see create_mall_subscription_tables.sql, subscription_rls_policies.sql)
-- 2) Run this script to enforce column-level restrictions for non-admins

View File

@@ -0,0 +1,2 @@
-- Moved to doc_mall/subscription_rls_policies.sql
-- Keeping a small pointer file to avoid duplication.

View File

@@ -0,0 +1,332 @@
-- =====================================================================================
-- 商城系统数据库状态检查脚本
-- 分析现有数据库结构生成个性化ALTER建议
-- =====================================================================================
-- =====================================================================================
-- 1. 检查现有表结构
-- =====================================================================================
-- 检查 ak_users 表字段情况
DO $$
DECLARE
missing_fields TEXT[] := ARRAY[]::TEXT[];
existing_fields TEXT[] := ARRAY[]::TEXT[];
field_name TEXT;
field_names TEXT[] := ARRAY['mall_status', 'mall_type', 'last_login_ip', 'total_orders', 'total_spent', 'user_level', 'points', 'verified_status'];
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '检查 ak_users 表字段状态';
RAISE NOTICE '=======================================================';
FOREACH field_name IN ARRAY field_names LOOP
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = field_name) THEN
existing_fields := array_append(existing_fields, field_name);
RAISE NOTICE '✓ 字段已存在: %', field_name;
ELSE
missing_fields := array_append(missing_fields, field_name);
RAISE NOTICE '✗ 字段缺失: %', field_name;
END IF;
END LOOP;
RAISE NOTICE '-------------------------------------------------------';
RAISE NOTICE '已存在字段数量: %', array_length(existing_fields, 1);
RAISE NOTICE '缺失字段数量: %', array_length(missing_fields, 1);
IF array_length(missing_fields, 1) > 0 THEN
RAISE NOTICE '需要添加的字段: %', array_to_string(missing_fields, ', ');
ELSE
RAISE NOTICE 'ak_users 表所有商城字段均已存在';
END IF;
END $$;
-- 检查商城表存在情况
DO $$
DECLARE
table_name TEXT;
table_names TEXT[] := ARRAY['ml_user_profiles', 'ml_user_addresses', 'ml_categories', 'ml_brands', 'ml_products', 'ml_product_skus', 'ml_shops', 'ml_orders', 'ml_shopping_cart'];
existing_tables TEXT[] := ARRAY[]::TEXT[];
missing_tables TEXT[] := ARRAY[]::TEXT[];
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '检查商城核心表存在情况';
RAISE NOTICE '=======================================================';
FOREACH table_name IN ARRAY table_names LOOP
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = table_name) THEN
existing_tables := array_append(existing_tables, table_name);
RAISE NOTICE '✓ 表已存在: %', table_name;
ELSE
missing_tables := array_append(missing_tables, table_name);
RAISE NOTICE '✗ 表缺失: %', table_name;
END IF;
END LOOP;
RAISE NOTICE '-------------------------------------------------------';
RAISE NOTICE '已存在表数量: %', array_length(existing_tables, 1);
RAISE NOTICE '缺失表数量: %', array_length(missing_tables, 1);
IF array_length(missing_tables, 1) > 0 THEN
RAISE NOTICE '需要创建的表: %', array_to_string(missing_tables, ', ');
ELSE
RAISE NOTICE '所有商城核心表均已存在';
END IF;
END $$;
-- =====================================================================================
-- 2. 检查现有索引情况
-- =====================================================================================
-- 检查重要索引存在情况
DO $$
DECLARE
index_info RECORD;
missing_indexes TEXT[] := ARRAY[]::TEXT[];
existing_indexes TEXT[] := ARRAY[]::TEXT[];
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '检查重要索引存在情况';
RAISE NOTICE '=======================================================';
-- 定义重要索引列表
FOR index_info IN
SELECT * FROM (VALUES
('idx_ak_users_mall_status', 'ak_users', 'mall_status'),
('idx_ak_users_mall_type', 'ak_users', 'mall_type'),
('idx_ak_users_total_orders', 'ak_users', 'total_orders'),
('idx_ml_products_cid', 'ml_products', 'cid'),
('idx_ml_products_slug', 'ml_products', 'slug'),
('idx_ml_categories_cid', 'ml_categories', 'cid'),
('idx_ml_orders_cid', 'ml_orders', 'cid'),
('idx_ml_shops_cid', 'ml_shops', 'cid')
) AS t(index_name, table_name, column_name)
LOOP
-- 检查表是否存在
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = index_info.table_name) THEN
-- 检查索引是否存在
IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = index_info.index_name) THEN
existing_indexes := array_append(existing_indexes, index_info.index_name);
RAISE NOTICE '✓ 索引已存在: % (表: %)', index_info.index_name, index_info.table_name;
ELSE
missing_indexes := array_append(missing_indexes, index_info.index_name);
RAISE NOTICE '✗ 索引缺失: % (表: %)', index_info.index_name, index_info.table_name;
END IF;
ELSE
RAISE NOTICE '○ 表不存在,跳过索引检查: % (表: %)', index_info.index_name, index_info.table_name;
END IF;
END LOOP;
RAISE NOTICE '-------------------------------------------------------';
RAISE NOTICE '已存在索引数量: %', array_length(existing_indexes, 1);
RAISE NOTICE '缺失索引数量: %', array_length(missing_indexes, 1);
END $$;
-- =====================================================================================
-- 3. 检查扩展和函数
-- =====================================================================================
-- 检查必要的PostgreSQL扩展
DO $$
DECLARE
ext_name TEXT;
extensions TEXT[] := ARRAY['uuid-ossp', 'btree_gin'];
existing_ext TEXT[] := ARRAY[]::TEXT[];
missing_ext TEXT[] := ARRAY[]::TEXT[];
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '检查PostgreSQL扩展';
RAISE NOTICE '=======================================================';
FOREACH ext_name IN ARRAY extensions LOOP
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = ext_name) THEN
existing_ext := array_append(existing_ext, ext_name);
RAISE NOTICE '✓ 扩展已安装: %', ext_name;
ELSE
missing_ext := array_append(missing_ext, ext_name);
RAISE NOTICE '✗ 扩展缺失: %', ext_name;
END IF;
END LOOP;
IF array_length(missing_ext, 1) > 0 THEN
RAISE NOTICE '需要安装的扩展: %', array_to_string(missing_ext, ', ');
END IF;
END $$;
-- 检查商城相关函数
DO $$
DECLARE
func_name TEXT;
functions TEXT[] := ARRAY['generate_order_no', 'calculate_cart_total', 'update_user_mall_stats'];
existing_funcs TEXT[] := ARRAY[]::TEXT[];
missing_funcs TEXT[] := ARRAY[]::TEXT[];
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '检查商城相关函数';
RAISE NOTICE '=======================================================';
FOREACH func_name IN ARRAY functions LOOP
IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = func_name) THEN
existing_funcs := array_append(existing_funcs, func_name);
RAISE NOTICE '✓ 函数已存在: %', func_name;
ELSE
missing_funcs := array_append(missing_funcs, func_name);
RAISE NOTICE '✗ 函数缺失: %', func_name;
END IF;
END LOOP;
IF array_length(missing_funcs, 1) > 0 THEN
RAISE NOTICE '需要创建的函数: %', array_to_string(missing_funcs, ', ');
END IF;
END $$;
-- =====================================================================================
-- 4. 生成个性化建议
-- =====================================================================================
DO $$
DECLARE
ak_users_missing INTEGER := 0;
mall_tables_missing INTEGER := 0;
suggestion TEXT := '';
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '个性化升级建议';
RAISE NOTICE '=======================================================';
-- 统计ak_users缺失字段
SELECT COUNT(*) INTO ak_users_missing
FROM (VALUES ('mall_status'), ('mall_type'), ('total_orders'), ('total_spent')) AS t(field)
WHERE NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ak_users' AND column_name = t.field
);
-- 统计商城表缺失情况
SELECT COUNT(*) INTO mall_tables_missing
FROM (VALUES ('ml_products'), ('ml_categories'), ('ml_orders'), ('ml_shops')) AS t(table_name)
WHERE NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = t.table_name
);
-- 生成建议
IF ak_users_missing > 0 AND mall_tables_missing > 0 THEN
suggestion := '建议使用 mall_alter_upgrade.sql完整升级脚本';
ELSIF ak_users_missing > 0 AND mall_tables_missing = 0 THEN
suggestion := '建议使用 mall_fields_only_upgrade.sql仅字段升级脚本';
ELSIF ak_users_missing = 0 AND mall_tables_missing > 0 THEN
suggestion := '建议使用 mall_migration.sql表结构创建脚本';
ELSE
suggestion := '数据库结构已完整,建议检查数据完整性和权限配置';
END IF;
RAISE NOTICE '根据您的数据库状态分析:';
RAISE NOTICE '• ak_users 表缺失字段数: %', ak_users_missing;
RAISE NOTICE '• 缺失商城核心表数: %', mall_tables_missing;
RAISE NOTICE '';
RAISE NOTICE '推荐执行方案: %', suggestion;
-- 详细建议
RAISE NOTICE '';
RAISE NOTICE '详细执行步骤:';
IF ak_users_missing > 0 THEN
RAISE NOTICE '1. 先执行字段升级脚本为ak_users表添加商城字段';
END IF;
IF mall_tables_missing > 0 THEN
RAISE NOTICE '2. 执行表结构创建脚本建立商城核心表';
END IF;
RAISE NOTICE '3. 执行SEO和安全策略脚本mall_seo_security.sql';
RAISE NOTICE '4. 根据需要执行模拟数据插入脚本进行测试';
END $$;
-- =====================================================================================
-- 5. 生成具体的ALTER语句可选
-- =====================================================================================
-- 生成ak_users表缺失字段的ALTER语句
DO $$
DECLARE
alter_statements TEXT := '';
field_name TEXT;
field_configs TEXT[] := ARRAY[
'mall_status INTEGER DEFAULT 1 CHECK (mall_status IN (1,2))',
'mall_type INTEGER DEFAULT 1 CHECK (mall_type IN (1,2,3))',
'total_orders INTEGER DEFAULT 0 CHECK (total_orders >= 0)',
'total_spent DECIMAL(12,2) DEFAULT 0.00 CHECK (total_spent >= 0)',
'user_level INTEGER DEFAULT 1 CHECK (user_level >= 1 AND user_level <= 10)',
'points INTEGER DEFAULT 0 CHECK (points >= 0)',
'verified_status INTEGER DEFAULT 0 CHECK (verified_status IN (0,1,2))'
];
field_names TEXT[] := ARRAY['mall_status', 'mall_type', 'total_orders', 'total_spent', 'user_level', 'points', 'verified_status'];
i INTEGER;
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '生成ak_users表ALTER语句';
RAISE NOTICE '=======================================================';
FOR i IN 1..array_length(field_names, 1) LOOP
field_name := field_names[i];
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ak_users' AND column_name = field_name) THEN
alter_statements := alter_statements || format('ALTER TABLE public.ak_users ADD COLUMN %s;' || chr(10), field_configs[i]);
RAISE NOTICE '需要执行: ALTER TABLE public.ak_users ADD COLUMN %;', field_configs[i];
END IF;
END LOOP;
IF alter_statements = '' THEN
RAISE NOTICE 'ak_users表无需添加字段';
ELSE
RAISE NOTICE '';
RAISE NOTICE '完整ALTER脚本';
RAISE NOTICE '%', alter_statements;
END IF;
END $$;
-- =====================================================================================
-- 6. 数据完整性检查
-- =====================================================================================
DO $$
DECLARE
users_count INTEGER;
profiles_count INTEGER;
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '数据完整性检查';
RAISE NOTICE '=======================================================';
-- 检查用户表数据
SELECT COUNT(*) INTO users_count FROM public.ak_users;
RAISE NOTICE 'ak_users 表用户数量: %', users_count;
-- 检查用户档案表(如果存在)
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ml_user_profiles') THEN
SELECT COUNT(*) INTO profiles_count FROM public.ml_user_profiles;
RAISE NOTICE 'ml_user_profiles 表档案数量: %', profiles_count;
IF users_count > profiles_count THEN
RAISE NOTICE '注意: 有 % 个用户缺少商城档案,建议执行档案补充脚本', users_count - profiles_count;
END IF;
ELSE
RAISE NOTICE 'ml_user_profiles 表不存在';
END IF;
END $$;
-- =====================================================================================
-- 完成提示
-- =====================================================================================
DO $$
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '数据库状态检查完成!';
RAISE NOTICE '=======================================================';
RAISE NOTICE '请根据上述分析结果选择合适的升级脚本:';
RAISE NOTICE '';
RAISE NOTICE '• mall_alter_upgrade.sql - 完整升级(表+字段+索引+函数)';
RAISE NOTICE '• mall_fields_only_upgrade.sql - 仅字段升级(最小化修改)';
RAISE NOTICE '• mall_migration.sql - 完整建表(全新部署)';
RAISE NOTICE '• mall_seo_security.sql - SEO优化和安全策略';
RAISE NOTICE '';
RAISE NOTICE '建议在生产环境执行前先在测试环境验证!';
RAISE NOTICE '=======================================================';
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,113 @@
-- 商城数据库脚本验证测试
-- 这个脚本用于验证数据库创建和模拟数据插入是否正常工作
-- 1. 检查必要的扩展是否可用
DO $$
BEGIN
-- 检查 uuid-ossp 扩展
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'uuid-ossp') THEN
RAISE NOTICE 'uuid-ossp 扩展未安装,请先执行: CREATE EXTENSION IF NOT EXISTS "uuid-ossp";';
ELSE
RAISE NOTICE 'uuid-ossp 扩展已安装 ✓';
END IF;
-- 检查 pgcrypto 扩展
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto') THEN
RAISE NOTICE 'pgcrypto 扩展未安装,请先执行: CREATE EXTENSION IF NOT EXISTS "pgcrypto";';
ELSE
RAISE NOTICE 'pgcrypto 扩展已安装 ✓';
END IF;
END $$;
-- 2. 检查 ak_users 表是否存在
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'ak_users') THEN
RAISE NOTICE 'ak_users 表已存在 ✓';
-- 检查 ak_users 表结构
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'auth_id' AND data_type = 'uuid') THEN
RAISE NOTICE 'ak_users.auth_id 字段类型正确 (uuid) ✓';
ELSE
RAISE NOTICE 'ak_users.auth_id 字段类型可能不正确,应为 uuid 类型';
END IF;
ELSE
RAISE NOTICE 'ak_users 表不存在,需要先创建或从现有系统迁移';
END IF;
END $$;
-- 3. 语法验证 - 测试典型的 RLS 策略语法
DO $$
BEGIN
RAISE NOTICE '开始验证 RLS 策略语法...';
-- 测试 UUID 比较语法
BEGIN
-- 这个查询应该能正常解析
PERFORM 1 WHERE '00000000-0000-0000-0000-000000000000'::uuid = '00000000-0000-0000-0000-000000000000'::uuid;
RAISE NOTICE 'UUID 比较语法正确 ✓';
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'UUID 比较语法错误: %', SQLERRM;
END;
RAISE NOTICE 'RLS 策略语法验证完成 ✓';
END $$;
-- 4. 检查商城表是否已存在
DO $$
DECLARE
table_count INTEGER;
mall_tables TEXT[] := ARRAY[
'ml_user_profiles', 'ml_user_addresses', 'ml_shopping_cart',
'ml_merchants', 'ml_categories', 'ml_products', 'ml_product_images',
'ml_product_variants', 'ml_inventory', 'ml_orders', 'ml_order_items',
'ml_reviews', 'ml_user_behavior', 'ml_promotions', 'ml_coupons',
'ml_user_coupons', 'ml_delivery_info', 'ml_system_config'
];
tbl TEXT;
BEGIN
table_count := 0;
FOREACH tbl IN ARRAY mall_tables
LOOP
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = tbl) THEN
table_count := table_count + 1;
END IF;
END LOOP;
RAISE NOTICE '商城表检查: %/% 个表已存在', table_count, array_length(mall_tables, 1);
IF table_count = 0 THEN
RAISE NOTICE '商城表尚未创建,可以执行 complete_mall_database.sql';
ELSIF table_count = array_length(mall_tables, 1) THEN
RAISE NOTICE '所有商城表已存在 ✓';
ELSE
RAISE NOTICE '部分商城表已存在,建议检查现有表结构';
END IF;
END $$;
-- 5. 模拟数据检查
DO $$
DECLARE
user_count INTEGER;
profile_count INTEGER;
product_count INTEGER;
BEGIN
-- 检查用户数据
SELECT COUNT(*) INTO user_count FROM public.ak_users WHERE username IN ('admin', 'merchant1', 'merchant2', 'customer1', 'customer2', 'customer3', 'driver1', 'driver2');
RAISE NOTICE '测试用户数量: %', user_count;
-- 检查商城相关数据(如果表存在)
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'ml_user_profiles') THEN
SELECT COUNT(*) INTO profile_count FROM public.ml_user_profiles;
RAISE NOTICE '用户档案数量: %', profile_count;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'ml_products') THEN
SELECT COUNT(*) INTO product_count FROM public.ml_products;
RAISE NOTICE '商品数量: %', product_count;
END IF;
END $$;
-- 验证完成
SELECT '数据库验证测试完成' AS status;

View File

@@ -0,0 +1,113 @@
-- =================================================================-- 验证7检查临时表是否已清理
SELECT
'临时表清理检查' as check_type,
CASE
WHEN EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'temp_user_ids')
THEN '临时表仍存在'
ELSE '临时表已清理'
END as cleanup_status;
-- 验证8检查配送任务分配逻辑
SELECT
'配送任务分配检查' as check_type,
COUNT(DISTINCT dt.driver_id) as assigned_drivers,
COUNT(*) as total_tasks,
ROUND(AVG(tasks_per_driver.task_count), 2) as avg_tasks_per_driver
FROM public.ml_delivery_tasks dt
CROSS JOIN (
SELECT driver_id, COUNT(*) as task_count
FROM public.ml_delivery_tasks
GROUP BY driver_id
) as tasks_per_driver;============
-- mock_data_insert.sql 修复验证脚本
-- 用途: 验证修复后的模拟数据插入脚本是否能正常执行
-- =====================================================================================
-- 验证1检查商品价格数据完整性
SELECT
'商品价格检查' as check_type,
COUNT(*) as total_products,
COUNT(CASE WHEN base_price IS NULL THEN 1 END) as null_base_price_count,
COUNT(CASE WHEN base_price > 0 THEN 1 END) as valid_price_count
FROM public.ml_products;
-- 验证2检查SKU价格数据完整性
SELECT
'SKU价格检查' as check_type,
COUNT(*) as total_skus,
COUNT(CASE WHEN price IS NULL THEN 1 END) as null_price_count,
COUNT(CASE WHEN price > 0 THEN 1 END) as valid_price_count
FROM public.ml_product_skus;
-- 验证3测试商品-SKU价格查询逻辑
SELECT
'价格查询逻辑测试' as check_type,
p.name as product_name,
p.base_price,
s.price as sku_price,
COALESCE(s.price, p.base_price) as final_price,
CASE
WHEN s.price IS NOT NULL THEN 'SKU价格'
ELSE '基础价格'
END as price_source
FROM public.ml_products p
LEFT JOIN public.ml_product_skus s ON p.id = s.product_id
ORDER BY p.name, s.sku_code
LIMIT 10;
-- 验证4检查订单商品价格是否存在NULL值
SELECT
'订单商品价格检查' as check_type,
COUNT(*) as total_order_items,
COUNT(CASE WHEN price IS NULL THEN 1 END) as null_price_count,
COUNT(CASE WHEN price > 0 THEN 1 END) as valid_price_count,
MIN(price) as min_price,
MAX(price) as max_price
FROM public.ml_order_items;
-- 验证5检查订单关联的商家ID是否正确
SELECT
'订单商家关联检查' as check_type,
COUNT(DISTINCT o.merchant_id) as unique_merchants,
COUNT(*) as total_orders,
COUNT(CASE WHEN u.role = 'merchant' THEN 1 END) as valid_merchant_orders
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.merchant_id = u.id;
-- 验证6检查配送任务唯一性
SELECT
'配送任务唯一性检查' as check_type,
COUNT(*) as total_delivery_tasks,
COUNT(DISTINCT order_id) as unique_orders,
COUNT(*) - COUNT(DISTINCT order_id) as duplicate_order_count,
CASE
WHEN COUNT(*) = COUNT(DISTINCT order_id) THEN '✓ 无重复订单'
ELSE '✗ 存在重复订单配送任务'
END as uniqueness_status
FROM public.ml_delivery_tasks;
-- 验证7检查临时表是否已清理
SELECT
'临时表清理检查' as check_type,
CASE
WHEN EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'temp_user_ids')
THEN '临时表仍存在'
ELSE '临时表已清理'
END as cleanup_status;
-- 输出总体验证结果
DO $$
BEGIN
RAISE NOTICE '=======================================================';
RAISE NOTICE '模拟数据插入脚本修复验证完成';
RAISE NOTICE '=======================================================';
RAISE NOTICE '请检查以上查询结果:';
RAISE NOTICE '1. 商品和SKU价格应无NULL值';
RAISE NOTICE '2. 订单商品价格应无NULL值';
RAISE NOTICE '3. 订单应正确关联到商家用户';
RAISE NOTICE '4. 配送任务应无重复订单';
RAISE NOTICE '5. 临时表应已清理';
RAISE NOTICE '=======================================================';
RAISE NOTICE '如所有检查通过,说明修复有效';
RAISE NOTICE '=======================================================';
END $$;

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;

View File

@@ -0,0 +1,145 @@
-- ============================================
-- 用户登录 / 注册 - 核心用户资料表结构(创建版 / Create-only
-- ============================================
-- 用途创建核心业务用户资料表ak_users及其相关函数和 RLS 策略。
-- 特点:
-- 1. 不做 DROP/DELETE/TRUNCATE不清空数据
-- 2. 通过 IF NOT EXISTS + 系统表判断,实现可重复执行
-- 3. 职责单一:只负责 ak_users不涉及其他基础表
-- 4. 依赖应在基础表01_create_tables.sql之后执行
-- ============================================
-- ============================================
-- 1. 业务用户资料表 ak_users
-- ============================================
CREATE TABLE IF NOT EXISTS public.ak_users (
id uuid primary key,
username text,
email text,
gender text,
birthday date,
height_cm numeric,
weight_kg numeric,
bio text,
avatar_url text,
preferred_language text,
role text,
school_id text,
grade_id text,
class_id text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- 中文注释
COMMENT ON TABLE public.ak_users IS '业务用户资料表(与 auth.users 一一对应)';
COMMENT ON COLUMN public.ak_users.id IS '用户ID等于 auth.users.id';
COMMENT ON COLUMN public.ak_users.username IS '用户名/昵称';
COMMENT ON COLUMN public.ak_users.email IS '邮箱';
COMMENT ON COLUMN public.ak_users.gender IS '性别';
COMMENT ON COLUMN public.ak_users.birthday IS '生日';
COMMENT ON COLUMN public.ak_users.height_cm IS '身高(厘米)';
COMMENT ON COLUMN public.ak_users.weight_kg IS '体重(公斤)';
COMMENT ON COLUMN public.ak_users.bio IS '个人简介';
COMMENT ON COLUMN public.ak_users.avatar_url IS '头像地址';
COMMENT ON COLUMN public.ak_users.preferred_language IS '偏好语言';
COMMENT ON COLUMN public.ak_users.role IS '角色(如 customer/merchant/admin 等)';
COMMENT ON COLUMN public.ak_users.school_id IS '学校ID可选';
COMMENT ON COLUMN public.ak_users.grade_id IS '年级ID可选';
COMMENT ON COLUMN public.ak_users.class_id IS '班级ID可选';
COMMENT ON COLUMN public.ak_users.created_at IS '创建时间';
COMMENT ON COLUMN public.ak_users.updated_at IS '更新时间';
-- 为 ak_users 添加 updated_at 触发器
-- 注意:通用函数 update_updated_at_column() 在 01_create_tables.sql 中创建
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_ak_users_updated_at') THEN
EXECUTE 'CREATE TRIGGER update_ak_users_updated_at BEFORE UPDATE ON public.ak_users FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column()';
END IF;
END $$;
-- ============================================
-- 2. 行级安全策略RLS
-- ============================================
-- 启用 RLS
ALTER TABLE public.ak_users ENABLE ROW LEVEL SECURITY;
-- 仅允许本人读写自己的资料
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='ak_users' AND policyname='ak_users_self_select') THEN
EXECUTE 'CREATE POLICY "ak_users_self_select" ON public.ak_users FOR SELECT USING (auth.uid() = id)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='ak_users' AND policyname='ak_users_self_insert') THEN
EXECUTE 'CREATE POLICY "ak_users_self_insert" ON public.ak_users FOR INSERT WITH CHECK (auth.uid() = id)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='ak_users' AND policyname='ak_users_self_update') THEN
EXECUTE 'CREATE POLICY "ak_users_self_update" ON public.ak_users FOR UPDATE USING (auth.uid() = id)';
END IF;
END $$;
-- ============================================
-- 3. 相关函数
-- ============================================
-- 函数1手动初始化/更新用户资料(可选,供前端调用)
CREATE OR REPLACE FUNCTION public.upsert_user_profile(
p_user_id uuid,
p_email text,
p_username text default null
)
RETURNS public.ak_users
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_username text := coalesce(p_username, split_part(p_email, '@', 1), 'user');
v_result public.ak_users;
BEGIN
-- 插入或更新用户资料
INSERT INTO public.ak_users (id, email, username)
VALUES (p_user_id, p_email, v_username)
ON CONFLICT (id) DO UPDATE
SET
email = excluded.email,
username = coalesce(excluded.username, ak_users.username)
RETURNING * INTO v_result;
RETURN v_result;
END;
$$;
-- 函数2供 auth.users 触发器使用,自动创建用户资料
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
BEGIN
INSERT INTO public.ak_users (id, email, username)
VALUES (NEW.id, NEW.email, COALESCE(SPLIT_PART(NEW.email, '@', 1), 'user'))
ON CONFLICT (id) DO NOTHING;
EXCEPTION WHEN OTHERS THEN
-- 重要:不要因为业务表写入失败而阻断 auth.users 的注册事务
RAISE WARNING 'handle_new_user failed: %', SQLERRM;
END;
RETURN NEW;
END;
$$;
-- ============================================
-- 4. 函数授权
-- ============================================
-- upsert_user_profile 只允许已登录用户调用
REVOKE ALL ON FUNCTION public.upsert_user_profile(uuid, text, text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.upsert_user_profile(uuid, text, text) TO authenticated;
-- handle_new_user 是触发器函数,由系统内部调用,无需对任何角色授权

View File

@@ -0,0 +1,35 @@
-- ============================================
-- 用户登录 / 注册 测试数据
-- 位置pages/user/test/USER_AUTH_TEST_DATA.sql
-- 用途:
-- - 准备少量业务侧测试数据,方便前端联调登录/个人中心
-- - 注意Supabase 的 auth.users 建议通过实际“注册”流程生成,这里不直接插入
-- ============================================
-- 1. 如果你已经通过注册页创建了账号auth.users 中有记录),
-- 可以在 ak_users 中补充一条测试资料:
-- 把 '00000000-0000-0000-0000-000000000001' 替换成自己 auth.users 里的 id。
insert into public.ak_users (id, username, email, role)
values
('00000000-0000-0000-0000-000000000001', 'demo_user', 'demo@example.com', 'analyst')
on conflict (id) do update
set
username = excluded.username,
email = excluded.email,
role = excluded.role;
-- 2. 可选:补充 users / user_sessions 基础数据,方便分析模块统计演示
insert into public.users (id, email, nickname)
values
('00000000-0000-0000-0000-000000000001', 'demo@example.com', 'Demo 分析师')
on conflict (id) do update
set
email = excluded.email,
nickname = excluded.nickname;
insert into public.user_sessions (user_id, session_token, is_active)
values
('00000000-0000-0000-0000-000000000001', 'demo-session-token', true);

View File

@@ -0,0 +1,89 @@
-- ============================================
-- 自动创建 ak_users 记录的触发器
-- ============================================
-- 位置pages/user/test/USER_AUTH_TRIGGER.sql
-- 用途:当 auth.users 表中创建新用户时,自动在 ak_users 表中创建对应的业务资料记录
--
-- 执行方式:
-- 在 Supabase Dashboard 的 SQL Editor 中执行此文件
-- 需要 superuser 权限Dashboard 默认有)
-- ============================================
-- 触发器函数 `public.handle_new_user()` 的定义在 `USER_AUTH_SCHEMA.sql` 中完成。
-- 这里仅负责在 auth.users 上创建触发器(避免重复定义函数导致版本不一致)。
-- 兼容处理:如果 `public.handle_new_user()` 尚未创建(例如先执行了本文件),则在此处补齐创建。
-- 说明:这里采用“存在则跳过”的创建方式,不会覆盖已有实现。
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public'
AND p.proname = 'handle_new_user'
) THEN
EXECUTE $fn$
CREATE FUNCTION public.handle_new_user()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $body$
BEGIN
BEGIN
INSERT INTO public.ak_users (id, email, username)
VALUES (
NEW.id,
NEW.email,
COALESCE(SPLIT_PART(NEW.email, '@', 1), 'user')
)
ON CONFLICT (id) DO NOTHING;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'handle_new_user failed: %', SQLERRM;
END;
RETURN NEW;
END;
$body$;
$fn$;
END IF;
END $$;
-- 在 auth.users 表上创建触发器
-- 注意:这个触发器会在每次 auth.users 插入新记录时自动执行
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'on_auth_user_created') THEN
EXECUTE 'CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user()';
END IF;
END $$;
-- ============================================
-- 验证触发器
-- ============================================
-- 执行以下查询验证触发器是否创建成功:
-- SELECT * FROM pg_trigger WHERE tgname = 'on_auth_user_created';
-- SELECT * FROM pg_proc WHERE proname = 'handle_new_user';
-- ============================================
-- 测试触发器(可选)
-- ============================================
-- 注意:以下测试代码会创建一个测试用户,执行前请确认
--
-- 1. 通过 Supabase Auth API 注册一个新用户
-- 2. 检查 ak_users 表中是否自动创建了对应的记录
--
-- 或者手动测试(需要 admin 权限):
-- INSERT INTO auth.users (id, email, encrypted_password, email_confirmed_at, created_at, updated_at)
-- VALUES (
-- gen_random_uuid(),
-- 'test@example.com',
-- crypt('password', gen_salt('bf')),
-- NOW(),
-- NOW(),
-- NOW()
-- );
--
-- 然后检查 ak_users 表:
-- SELECT * FROM ak_users WHERE email = 'test@example.com';