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