From 8f181b2b6ac1db06e3871f52778820aabd7cd3e1 Mon Sep 17 00:00:00 2001 From: comlibmb <1844410276@qq.com> Date: Fri, 30 Jan 2026 16:17:13 +0800 Subject: [PATCH] =?UTF-8?q?mall=E6=95=B0=E6=8D=AE=E5=BA=93=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 - mall_sql/migrations/mall_alter_upgrade.sql | 692 +++++++++ .../migrations/mall_fields_only_upgrade.sql | 734 +++++++++ mall_sql/migrations/mall_migration.sql | 868 +++++++++++ mall_sql/migrations/quick_role_migration.sql | 249 +++ mall_sql/migrations/role_field_cleanup.sql | 207 +++ .../migrations/role_field_unification.sql | 287 ++++ .../user_compatibility_implementation.sql | 273 ++++ .../schemas/ak_contents_product_extension.sql | 110 ++ mall_sql/schemas/complete_mall_database.sql | 1377 +++++++++++++++++ mall_sql/schemas/mall_seo_security.sql | 666 ++++++++ mall_sql/schemas/product_database.sql | 452 ++++++ .../create_mall_subscription_tables.sql | 2 + .../subscription_guard_trigger.sql | 47 + .../subscription_rls_policies.sql | 2 + mall_sql/tests/create_supabase_auth_users.sql | 0 mall_sql/tests/mall_database_check.sql | 332 ++++ mall_sql/tests/mock_data_insert.sql | 1180 ++++++++++++++ mall_sql/tests/validation_test.sql | 113 ++ mall_sql/tests/verify_mock_data_fix.sql | 113 ++ .../test/01_create_coupon_tables.sql | 223 +++ .../test/01_create_delivery_tables.sql | 283 ++++ .../mall/analytics/test/01_create_tables.sql | 303 ++++ .../test/02_insert_coupon_test_data.sql | 182 +++ .../test/02_insert_delivery_test_data.sql | 72 + .../02_insert_market_trends_test_data.sql | 160 ++ .../analytics/test/02_insert_test_data.sql | 172 ++ .../test/03_delivery_rls_policies.sql | 222 +++ .../test/03_orders_analytics_policies.sql | 70 + pages/mall/analytics/test/03_test_queries.sql | 225 +++ pages/mall/analytics/test/04_cleanup.sql | 110 ++ .../analytics/test/05_product_trend_rpc.sql | 68 + .../analytics/test/06_market_trends_rpcs.sql | 171 ++ .../analytics/test/07_custom_report_rpcs.sql | 405 +++++ .../analytics/test/ANALYTICS_DB_SCHEMA.sql | 853 ++++++++++ .../analytics/test/ANALYTICS_TEST_SEED.sql | 463 ++++++ .../analytics/test/COUPON_ANALYSIS_RPCS.sql | 388 +++++ .../mall/analytics/test/DATA_DETAIL_RPCS.sql | 342 ++++ .../analytics/test/DELIVERY_ANALYSIS_RPCS.sql | 73 + pages/user/test/USER_AUTH_SCHEMA.sql | 145 ++ pages/user/test/USER_AUTH_TEST_DATA.sql | 35 + pages/user/test/USER_AUTH_TRIGGER.sql | 89 ++ 42 files changed, 12758 insertions(+), 2 deletions(-) create mode 100644 mall_sql/migrations/mall_alter_upgrade.sql create mode 100644 mall_sql/migrations/mall_fields_only_upgrade.sql create mode 100644 mall_sql/migrations/mall_migration.sql create mode 100644 mall_sql/migrations/quick_role_migration.sql create mode 100644 mall_sql/migrations/role_field_cleanup.sql create mode 100644 mall_sql/migrations/role_field_unification.sql create mode 100644 mall_sql/migrations/user_compatibility_implementation.sql create mode 100644 mall_sql/schemas/ak_contents_product_extension.sql create mode 100644 mall_sql/schemas/complete_mall_database.sql create mode 100644 mall_sql/schemas/mall_seo_security.sql create mode 100644 mall_sql/schemas/product_database.sql create mode 100644 mall_sql/subscription/create_mall_subscription_tables.sql create mode 100644 mall_sql/subscription/subscription_guard_trigger.sql create mode 100644 mall_sql/subscription/subscription_rls_policies.sql create mode 100644 mall_sql/tests/create_supabase_auth_users.sql create mode 100644 mall_sql/tests/mall_database_check.sql create mode 100644 mall_sql/tests/mock_data_insert.sql create mode 100644 mall_sql/tests/validation_test.sql create mode 100644 mall_sql/tests/verify_mock_data_fix.sql create mode 100644 pages/mall/analytics/test/01_create_coupon_tables.sql create mode 100644 pages/mall/analytics/test/01_create_delivery_tables.sql create mode 100644 pages/mall/analytics/test/01_create_tables.sql create mode 100644 pages/mall/analytics/test/02_insert_coupon_test_data.sql create mode 100644 pages/mall/analytics/test/02_insert_delivery_test_data.sql create mode 100644 pages/mall/analytics/test/02_insert_market_trends_test_data.sql create mode 100644 pages/mall/analytics/test/02_insert_test_data.sql create mode 100644 pages/mall/analytics/test/03_delivery_rls_policies.sql create mode 100644 pages/mall/analytics/test/03_orders_analytics_policies.sql create mode 100644 pages/mall/analytics/test/03_test_queries.sql create mode 100644 pages/mall/analytics/test/04_cleanup.sql create mode 100644 pages/mall/analytics/test/05_product_trend_rpc.sql create mode 100644 pages/mall/analytics/test/06_market_trends_rpcs.sql create mode 100644 pages/mall/analytics/test/07_custom_report_rpcs.sql create mode 100644 pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql create mode 100644 pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql create mode 100644 pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql create mode 100644 pages/mall/analytics/test/DATA_DETAIL_RPCS.sql create mode 100644 pages/mall/analytics/test/DELIVERY_ANALYSIS_RPCS.sql create mode 100644 pages/user/test/USER_AUTH_SCHEMA.sql create mode 100644 pages/user/test/USER_AUTH_TEST_DATA.sql create mode 100644 pages/user/test/USER_AUTH_TRIGGER.sql diff --git a/.gitignore b/.gitignore index 46224298..d311b649 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,6 @@ claude* # Logs and databases *.log -*.sql -*.sqlite # Dependency directories node_modules/ diff --git a/mall_sql/migrations/mall_alter_upgrade.sql b/mall_sql/migrations/mall_alter_upgrade.sql new file mode 100644 index 00000000..6b82771f --- /dev/null +++ b/mall_sql/migrations/mall_alter_upgrade.sql @@ -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 $$; diff --git a/mall_sql/migrations/mall_fields_only_upgrade.sql b/mall_sql/migrations/mall_fields_only_upgrade.sql new file mode 100644 index 00000000..efeba845 --- /dev/null +++ b/mall_sql/migrations/mall_fields_only_upgrade.sql @@ -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 $$; diff --git a/mall_sql/migrations/mall_migration.sql b/mall_sql/migrations/mall_migration.sql new file mode 100644 index 00000000..2ccd0bba --- /dev/null +++ b/mall_sql/migrations/mall_migration.sql @@ -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 $$; diff --git a/mall_sql/migrations/quick_role_migration.sql b/mall_sql/migrations/quick_role_migration.sql new file mode 100644 index 00000000..0cc8284a --- /dev/null +++ b/mall_sql/migrations/quick_role_migration.sql @@ -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 字段'; +*/ diff --git a/mall_sql/migrations/role_field_cleanup.sql b/mall_sql/migrations/role_field_cleanup.sql new file mode 100644 index 00000000..6af83afb --- /dev/null +++ b/mall_sql/migrations/role_field_cleanup.sql @@ -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; diff --git a/mall_sql/migrations/role_field_unification.sql b/mall_sql/migrations/role_field_unification.sql new file mode 100644 index 00000000..fd2115a2 --- /dev/null +++ b/mall_sql/migrations/role_field_unification.sql @@ -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; diff --git a/mall_sql/migrations/user_compatibility_implementation.sql b/mall_sql/migrations/user_compatibility_implementation.sql new file mode 100644 index 00000000..688b7c7c --- /dev/null +++ b/mall_sql/migrations/user_compatibility_implementation.sql @@ -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 $$; diff --git a/mall_sql/schemas/ak_contents_product_extension.sql b/mall_sql/schemas/ak_contents_product_extension.sql new file mode 100644 index 00000000..24f5b82f --- /dev/null +++ b/mall_sql/schemas/ak_contents_product_extension.sql @@ -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. 推荐使用独立的商品表设计以获得更好的架构 +'; diff --git a/mall_sql/schemas/complete_mall_database.sql b/mall_sql/schemas/complete_mall_database.sql new file mode 100644 index 00000000..ba0a8cf5 --- /dev/null +++ b/mall_sql/schemas/complete_mall_database.sql @@ -0,0 +1,1377 @@ +-- ===================================================================================== +-- 电商商城系统完整数据库设�?(PostgreSQL + Supabase) +-- 表名前缀: ml_ (mall) +-- 复用�? ak_users (用户主表) +-- 包含: 表结构、索引、触发器、RLS策略、视图、函�? +-- ===================================================================================== + +-- ===================================================================================== +-- 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 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, + 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_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 '商城用户扩展信息表'; +COMMENT ON COLUMN public.ml_user_profiles.status IS '用户状态:1正常 2冻结 3注销 4待审核'; + +-- 用户地址�? +CREATE TABLE 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 '用户地址�?; + +-- ===================================================================================== +-- 3. 商品管理�? +-- ===================================================================================== + +-- 商品分类�? +CREATE TABLE 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 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 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 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 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 '商品规格�?; + +-- ===================================================================================== +-- 4. 店铺管理�? +-- ===================================================================================== + +-- 店铺信息�? +CREATE TABLE 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 '店铺信息�?; + +-- ===================================================================================== +-- 5. 订单管理�? +-- ===================================================================================== + +-- 订单�? +CREATE TABLE 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 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 '订单商品�?; + +-- ===================================================================================== +-- 6. 购物车表 +-- ===================================================================================== + +-- 购物车表 +CREATE TABLE 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 '购物车表'; + +-- ===================================================================================== +-- 7. 营销管理�? +-- ===================================================================================== + +-- 优惠券模板表 +CREATE TABLE 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 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 '用户优惠券表'; + +-- ===================================================================================== +-- 8. 配送管理表 +-- ===================================================================================== + +-- 配送员信息�? +CREATE TABLE 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 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 '配送任务表'; + +-- ===================================================================================== +-- 9. 评价管理�? +-- ===================================================================================== + +-- 商品评价�? +CREATE TABLE 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 '商品评价�?; + +-- ===================================================================================== +-- 10. 用户行为�? +-- ===================================================================================== + +-- 用户收藏�? +CREATE TABLE 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 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 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 '搜索记录�?; + +-- ===================================================================================== +-- 11. 系统配置�? +-- ===================================================================================== + +-- 系统配置�? +CREATE TABLE 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 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 '地区�?; + +-- ===================================================================================== +-- 12. 创建索引 +-- ===================================================================================== + +-- 用户扩展表索�? +CREATE INDEX idx_ml_user_profiles_user_id ON public.ml_user_profiles(user_id); + +CREATE INDEX idx_ml_user_profiles_status ON public.ml_user_profiles(status); + +-- 分类表索引 +CREATE INDEX idx_ml_categories_cid ON public.ml_categories(cid); +CREATE INDEX idx_ml_categories_parent ON public.ml_categories(parent_id); +CREATE INDEX idx_ml_categories_slug ON public.ml_categories(slug); +CREATE INDEX idx_ml_categories_level ON public.ml_categories(level, sort_order); + +-- 品牌表索引 +CREATE INDEX idx_ml_brands_cid ON public.ml_brands(cid); +CREATE INDEX idx_ml_brands_name ON public.ml_brands(name); + +-- 地址表索�? +CREATE INDEX idx_ml_user_addresses_user_id ON public.ml_user_addresses(user_id); +CREATE INDEX idx_ml_user_addresses_default ON public.ml_user_addresses(user_id, is_default); +CREATE INDEX idx_ml_user_addresses_location ON public.ml_user_addresses(city, district); + +-- 商品表索�? +CREATE INDEX idx_ml_products_cid ON public.ml_products(cid); +CREATE INDEX idx_ml_products_merchant ON public.ml_products(merchant_id, status); +CREATE INDEX idx_ml_products_category ON public.ml_products(category_id, status); +CREATE INDEX idx_ml_products_status ON public.ml_products(status, created_at DESC); +CREATE INDEX idx_ml_products_featured ON public.ml_products(is_featured, status); +CREATE INDEX idx_ml_products_price ON public.ml_products(base_price); +CREATE INDEX idx_ml_products_rating ON public.ml_products(rating_avg DESC, rating_count DESC); +CREATE INDEX idx_ml_products_sale_count ON public.ml_products(sale_count DESC); +CREATE INDEX idx_ml_products_tags ON public.ml_products USING GIN(tags); +CREATE INDEX idx_ml_products_slug ON public.ml_products(slug); + +-- 店铺表索引 +CREATE INDEX idx_ml_shops_cid ON public.ml_shops(cid); +CREATE INDEX idx_ml_shops_merchant ON public.ml_shops(merchant_id); + +-- SKU表索�? +CREATE INDEX idx_ml_product_skus_product ON public.ml_product_skus(product_id); +CREATE INDEX idx_ml_product_skus_code ON public.ml_product_skus(sku_code); + +-- 订单表索�? +CREATE INDEX idx_ml_orders_cid ON public.ml_orders(cid); +CREATE INDEX idx_ml_orders_user ON public.ml_orders(user_id, created_at DESC); +CREATE INDEX idx_ml_orders_merchant ON public.ml_orders(merchant_id, created_at DESC); +CREATE INDEX idx_ml_orders_status ON public.ml_orders(order_status, created_at DESC); +CREATE INDEX idx_ml_orders_no ON public.ml_orders(order_no); + +-- 订单商品表索�? +CREATE INDEX idx_ml_order_items_order ON public.ml_order_items(order_id); +CREATE INDEX idx_ml_order_items_product ON public.ml_order_items(product_id); + +-- 购物车表索引 +CREATE INDEX idx_ml_shopping_cart_user ON public.ml_shopping_cart(user_id); + +-- 优惠券模板表索引 +CREATE INDEX idx_ml_coupon_templates_cid ON public.ml_coupon_templates(cid); +CREATE INDEX idx_ml_coupon_templates_merchant ON public.ml_coupon_templates(merchant_id); + +-- 优惠券表索引 +CREATE INDEX idx_ml_user_coupons_user ON public.ml_user_coupons(user_id, status); +CREATE INDEX idx_ml_user_coupons_code ON public.ml_user_coupons(coupon_code); + +-- 收藏表索�? +CREATE INDEX idx_ml_user_favorites_user ON public.ml_user_favorites(user_id, target_type); +CREATE INDEX idx_ml_user_favorites_target ON public.ml_user_favorites(target_type, target_id); + +-- 浏览历史索引 +CREATE INDEX idx_ml_browse_history_user ON public.ml_browse_history(user_id, created_at DESC); +CREATE INDEX idx_ml_browse_history_product ON public.ml_browse_history(product_id); + +-- ===================================================================================== +-- 13. 触发器函�? +-- ===================================================================================== + +-- 自动更新 updated_at 字段的函�? +CREATE OR REPLACE FUNCTION public.update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- 为需要的表创�?updated_at 触发�? +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(); + +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(); + +CREATE TRIGGER trigger_ml_products_updated_at + BEFORE UPDATE ON public.ml_products + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +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(); + +CREATE TRIGGER trigger_ml_shops_updated_at + BEFORE UPDATE ON public.ml_shops + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +CREATE TRIGGER trigger_ml_orders_updated_at + BEFORE UPDATE ON public.ml_orders + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +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(); + +-- 确保每个用户只有一个默认地址的触发器 +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 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(); + +-- 商品库存更新触发�? +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 = 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; +$$ LANGUAGE plpgsql; + +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(); + +-- 订单状态变更时的处�? +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.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; + +CREATE TRIGGER trigger_ml_order_status_change + BEFORE UPDATE ON public.ml_orders + FOR EACH ROW EXECUTE FUNCTION public.handle_order_status_change(); + +-- ===================================================================================== +-- 14. 实用函数 +-- ===================================================================================== + +-- 生成订单号的函数 +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 SEQUENCE IF NOT EXISTS public.ml_order_seq START 1; + +-- 生成优惠券码的函�? +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 (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 = 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(s.price * c.quantity), 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; + +-- ===================================================================================== +-- 15. 创建视图 +-- ===================================================================================== + +-- 商城用户完整信息视图 +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.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; + +COMMENT ON VIEW public.ml_users_view IS '商城用户完整信息视图'; + +-- 商品详情视图 +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 '订单详情视图'; + +-- ===================================================================================== +-- 16. 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; + +-- 用户只能访问自己的数�? +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) + ); + +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) + ); + +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) + ); + +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) + ); + +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) + ); + +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) + ); + +-- 订单策略:用户可以查看自己的订单,商家可以查看自己店铺的订单 +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) + ) + ); + +-- 商品策略:所有人可以查看上架商品,商家只能管理自己的商品 +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) + ); + +-- ===================================================================================== +-- 17. 初始化数�? +-- ===================================================================================== + +-- 插入系统配置 +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', '订单自动确认天数'); + +-- 插入默认分类 +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['美妆护肤']); + +-- 为现有 ak_users 用户创建默认商城档案 +INSERT INTO public.ml_user_profiles (user_id, status) +SELECT + id, + 1 -- 默认状态正常 +FROM public.ak_users +WHERE id NOT IN (SELECT user_id FROM public.ml_user_profiles WHERE user_id IS NOT NULL); + +-- ===================================================================================== +-- 18. 完成提示 +-- ===================================================================================== + +DO $$ +BEGIN + RAISE NOTICE '======================================================='; + RAISE NOTICE '商城数据库创建完成!'; + RAISE NOTICE '======================================================='; + RAISE NOTICE '已创建表数量: 20+ 张表'; + RAISE NOTICE '已创建索�? 30+ 个索�?; + RAISE NOTICE '已创建触发器: 8 个触发器'; + RAISE NOTICE '已创建函�? 10+ 个函�?; + RAISE NOTICE '已创建视�? 3 个视�?; + RAISE NOTICE '已设置RLS策略: 多个策略'; + RAISE NOTICE '已为现有用户创建默认档案'; + RAISE NOTICE '======================================================='; + RAISE NOTICE '表名前缀: ml_'; + RAISE NOTICE '复用�? ak_users'; + RAISE NOTICE '兼容: Supabase'; + RAISE NOTICE '======================================================='; +END $$; + +-- ===================================================================================== +-- 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; diff --git a/mall_sql/schemas/mall_seo_security.sql b/mall_sql/schemas/mall_seo_security.sql new file mode 100644 index 00000000..39eaedeb --- /dev/null +++ b/mall_sql/schemas/mall_seo_security.sql @@ -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 $$; diff --git a/mall_sql/schemas/product_database.sql b/mall_sql/schemas/product_database.sql new file mode 100644 index 00000000..5c13e095 --- /dev/null +++ b/mall_sql/schemas/product_database.sql @@ -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 +$$; diff --git a/mall_sql/subscription/create_mall_subscription_tables.sql b/mall_sql/subscription/create_mall_subscription_tables.sql new file mode 100644 index 00000000..cc8242df --- /dev/null +++ b/mall_sql/subscription/create_mall_subscription_tables.sql @@ -0,0 +1,2 @@ +-- Moved to doc_mall/create_mall_subscription_tables.sql +-- Keeping a small pointer file to avoid duplication. \ No newline at end of file diff --git a/mall_sql/subscription/subscription_guard_trigger.sql b/mall_sql/subscription/subscription_guard_trigger.sql new file mode 100644 index 00000000..5464d863 --- /dev/null +++ b/mall_sql/subscription/subscription_guard_trigger.sql @@ -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 \ No newline at end of file diff --git a/mall_sql/subscription/subscription_rls_policies.sql b/mall_sql/subscription/subscription_rls_policies.sql new file mode 100644 index 00000000..1efcbd82 --- /dev/null +++ b/mall_sql/subscription/subscription_rls_policies.sql @@ -0,0 +1,2 @@ +-- Moved to doc_mall/subscription_rls_policies.sql +-- Keeping a small pointer file to avoid duplication. diff --git a/mall_sql/tests/create_supabase_auth_users.sql b/mall_sql/tests/create_supabase_auth_users.sql new file mode 100644 index 00000000..e69de29b diff --git a/mall_sql/tests/mall_database_check.sql b/mall_sql/tests/mall_database_check.sql new file mode 100644 index 00000000..a9e0f023 --- /dev/null +++ b/mall_sql/tests/mall_database_check.sql @@ -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 $$; diff --git a/mall_sql/tests/mock_data_insert.sql b/mall_sql/tests/mock_data_insert.sql new file mode 100644 index 00000000..d1d4b775 --- /dev/null +++ b/mall_sql/tests/mock_data_insert.sql @@ -0,0 +1,1180 @@ +-- ===================================================================================== +-- 商城系统模拟数据插入脚本 +-- 依赖: complete_mall_database.sql (必须先执行主数据库脚本) +-- 重要: 需要先在 Supabase Auth 中创建测试用户,建议使用 create_supabase_auth_users.js +-- 用途: 为商城系统生成测试数据,便于开发和测试 +-- ===================================================================================== + +-- ===================================================================================== +-- 0. 检查 Supabase Auth 用户环境 +-- ===================================================================================== + +-- 检查是否为 Supabase 环境 +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'auth') THEN + RAISE WARNING '当前环境不是 Supabase,将使用虚拟 auth_id'; + RAISE WARNING '如果是 Supabase 环境,请先使用以下方式创建测试用户:'; + RAISE WARNING '1. 执行 create_supabase_auth_users.js 脚本'; + RAISE WARNING '2. 或在 Supabase Dashboard 中手动创建用户'; + ELSE + RAISE NOTICE '检测到 Supabase 环境,将尝试关联真实 auth 用户'; + + -- 检查是否有测试用户 + IF EXISTS (SELECT 1 FROM auth.users WHERE email LIKE '%@mall.com') THEN + RAISE NOTICE '发现测试用户,将使用真实 auth_id'; + ELSE + RAISE WARNING '未发现测试用户,建议先执行 create_supabase_auth_users.js'; + RAISE WARNING '或手动在 Supabase Dashboard 创建以下测试用户:'; + RAISE WARNING '- admin@mall.com'; + RAISE WARNING '- merchant1@mall.com'; + RAISE WARNING '- merchant2@mall.com'; + RAISE WARNING '- customer1@mall.com'; + RAISE WARNING '- customer2@mall.com'; + RAISE WARNING '- customer3@mall.com'; + RAISE WARNING '- driver1@mall.com'; + RAISE WARNING '- driver2@mall.com'; + END IF; + END IF; +END $$; + +-- ===================================================================================== +-- 1. 清理现有测试数据 (可选,谨慎使用) +-- ===================================================================================== + +-- TRUNCATE TABLE public.ml_product_reviews CASCADE; +-- TRUNCATE TABLE public.ml_order_items CASCADE; +-- TRUNCATE TABLE public.ml_orders CASCADE; +-- TRUNCATE TABLE public.ml_shopping_cart CASCADE; +-- TRUNCATE TABLE public.ml_user_coupons CASCADE; +-- TRUNCATE TABLE public.ml_coupon_templates CASCADE; +-- TRUNCATE TABLE public.ml_product_skus CASCADE; +-- TRUNCATE TABLE public.ml_products CASCADE; +-- TRUNCATE TABLE public.ml_shops CASCADE; +-- TRUNCATE TABLE public.ml_brands CASCADE; +-- TRUNCATE TABLE public.ml_categories CASCADE; +-- TRUNCATE TABLE public.ml_user_addresses CASCADE; +-- TRUNCATE TABLE public.ml_user_profiles CASCADE; + +-- ===================================================================================== +-- 2. Supabase Auth 用户创建 + ak_users 数据 +-- ===================================================================================== + +-- 重要提示:在 Supabase 环境中,建议通过以下方式创建测试用户: +-- +-- 方法一:使用 Supabase Dashboard 手动创建用户 +-- 1. 进入 Supabase Dashboard -> Authentication -> Users +-- 2. 点击 "Add user" 创建以下测试用户: +-- - admin@mall.com (密码: Test123456!) +-- - merchant1@mall.com (密码: Test123456!) +-- - merchant2@mall.com (密码: Test123456!) +-- - customer1@mall.com (密码: Test123456!) +-- - customer2@mall.com (密码: Test123456!) +-- - customer3@mall.com (密码: Test123456!) +-- - driver1@mall.com (密码: Test123456!) +-- - driver2@mall.com (密码: Test123456!) +-- +-- 方法二:使用 Supabase Admin API (需要在服务端执行) +-- +-- 方法三:如果在本地开发环境,可以尝试直接插入 auth.users 表(仅限开发) + +-- 检查是否为 Supabase 环境 +DO $$ +DECLARE + is_supabase BOOLEAN := FALSE; + auth_user_rec RECORD; + test_emails TEXT[] := ARRAY[ + 'admin@mall.com', + 'merchant1@mall.com', + 'merchant2@mall.com', + 'customer1@mall.com', + 'customer2@mall.com', + 'customer3@mall.com', + 'driver1@mall.com', + 'driver2@mall.com' + ]; + email_addr TEXT; +BEGIN + -- 检查是否存在 auth schema (Supabase 特征) + SELECT EXISTS ( + SELECT 1 FROM information_schema.schemata WHERE schema_name = 'auth' + ) INTO is_supabase; + + IF is_supabase THEN + RAISE NOTICE '检测到 Supabase 环境,检查 auth.users 表中的测试用户...'; + + -- 检查测试用户是否存在 + FOREACH email_addr IN ARRAY test_emails LOOP + SELECT id, email INTO auth_user_rec + FROM auth.users + WHERE email = email_addr + LIMIT 1; + + IF FOUND THEN + RAISE NOTICE '✓ Auth用户已存在: % (ID: %)', auth_user_rec.email, auth_user_rec.id; + ELSE + RAISE NOTICE '✗ Auth用户不存在: %', email_addr; + RAISE NOTICE '请在 Supabase Dashboard 中创建此用户或使用 Admin API'; + END IF; + END LOOP; + + RAISE NOTICE '============================================'; + RAISE NOTICE '如果有缺失的用户,请按以下步骤操作:'; + RAISE NOTICE '1. 登录 Supabase Dashboard'; + RAISE NOTICE '2. 进入 Authentication -> Users'; + RAISE NOTICE '3. 点击 "Add user" 创建缺失的用户'; + RAISE NOTICE '4. 邮箱格式: user@mall.com, 密码: Test123456!'; + RAISE NOTICE '5. 重新执行此脚本'; + RAISE NOTICE '============================================'; + ELSE + RAISE NOTICE '非 Supabase 环境,将创建虚拟 auth_id...'; + END IF; +END $$; + +-- ===================================================================================== +-- 1. 插入用户数据 (ak_users) - 关联 Supabase Auth 用户 +-- ===================================================================================== + +-- 临时函数:获取或创建 auth_id +CREATE OR REPLACE FUNCTION get_or_create_auth_id(user_email TEXT) +RETURNS UUID AS $$ +DECLARE + auth_user_id UUID; +BEGIN + -- 尝试从 auth.users 获取真实用户ID + IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'auth') THEN + SELECT id INTO auth_user_id FROM auth.users WHERE email = user_email LIMIT 1; + + IF auth_user_id IS NOT NULL THEN + RETURN auth_user_id; + ELSE + RAISE WARNING '用户 % 在 auth.users 中不存在,使用虚拟UUID', user_email; + END IF; + END IF; + + -- 生成确定性虚拟UUID (基于邮箱) + RETURN uuid_generate_v5(uuid_ns_dns(), user_email); +END; +$$ LANGUAGE plpgsql; + +-- 插入系统管理员 +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('admin@mall.com'), + '系统管理员', + 'admin', + 'admin@mall.com', + '13800138000', + 'https://example.com/avatars/admin.jpg', + 'active', + 'web', + NOW() - INTERVAL '365 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'admin@mall.com'); + +-- 插入商家用户 +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('merchant1@mall.com'), + '数码专营店', + 'merchant', + 'merchant1@mall.com', + '13800138001', + 'https://example.com/avatars/merchant1.jpg', + 'active', + 'web', + NOW() - INTERVAL '300 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'merchant1@mall.com'); + +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('merchant2@mall.com'), + '时尚服饰店', + 'merchant', + 'merchant2@mall.com', + '13800138002', + 'https://example.com/avatars/merchant2.jpg', + 'active', + 'web', + NOW() - INTERVAL '200 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'merchant2@mall.com'); + +-- 插入普通用户 +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('customer1@mall.com'), + '张小明', + 'customer', + 'customer1@mall.com', + '13800138101', + 'https://example.com/avatars/customer1.jpg', + 'active', + 'mobile', + NOW() - INTERVAL '180 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'customer1@mall.com'); + +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('customer2@mall.com'), + '李小红', + 'customer', + 'customer2@mall.com', + '13800138102', + 'https://example.com/avatars/customer2.jpg', + 'active', + 'mobile', + NOW() - INTERVAL '120 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'customer2@mall.com'); + +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('customer3@mall.com'), + '王小华', + 'customer', + 'customer3@mall.com', + '13800138103', + 'https://example.com/avatars/customer3.jpg', + 'active', + 'web', + NOW() - INTERVAL '90 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'customer3@mall.com'); + +-- 插入配送员 +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('driver1@mall.com'), + '快递小哥1', + 'delivery', + 'driver1@mall.com', + '13800138201', + 'https://example.com/avatars/driver1.jpg', + 'active', + 'mobile', + NOW() - INTERVAL '150 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'driver1@mall.com'); + +INSERT INTO ak_users (auth_id, username, role, email, phone, avatar_url, status, registration_source, created_at, updated_at) +SELECT + get_or_create_auth_id('driver2@mall.com'), + '快递小哥2', + 'delivery', + 'driver2@mall.com', + '13800138202', + 'https://example.com/avatars/driver2.jpg', + 'active', + 'mobile', + NOW() - INTERVAL '100 days', + NOW() +WHERE NOT EXISTS (SELECT 1 FROM ak_users WHERE email = 'driver2@mall.com'); + +-- 清理临时函数 +DROP FUNCTION get_or_create_auth_id(TEXT); + +-- ===================================================================================== +-- 2. 获取用户ID (用于后续插入) +-- ===================================================================================== +DO $$ +DECLARE + admin_id UUID; + merchant1_id UUID; + merchant2_id UUID; + customer1_id UUID; + customer2_id UUID; + customer3_id UUID; + driver1_id UUID; + driver2_id UUID; +BEGIN + -- 获取用户ID(使用email字段) + SELECT id INTO admin_id FROM ak_users WHERE email = 'admin@mall.com'; + SELECT id INTO merchant1_id FROM ak_users WHERE email = 'merchant1@mall.com'; + SELECT id INTO merchant2_id FROM ak_users WHERE email = 'merchant2@mall.com'; + SELECT id INTO customer1_id FROM ak_users WHERE email = 'customer1@mall.com'; + SELECT id INTO customer2_id FROM ak_users WHERE email = 'customer2@mall.com'; + SELECT id INTO customer3_id FROM ak_users WHERE email = 'customer3@mall.com'; + SELECT id INTO driver1_id FROM ak_users WHERE email = 'driver1@mall.com'; + SELECT id INTO driver2_id FROM ak_users WHERE email = 'driver2@mall.com'; + + -- 创建临时表存储ID + CREATE TEMP TABLE IF NOT EXISTS temp_user_ids ( + role VARCHAR(20), + user_id UUID + ); + + INSERT INTO temp_user_ids VALUES + ('admin', admin_id), + ('merchant1', merchant1_id), + ('merchant2', merchant2_id), + ('customer1', customer1_id), + ('customer2', customer2_id), + ('customer3', customer3_id), + ('driver1', driver1_id), + ('driver2', driver2_id); +END $$; + +-- ===================================================================================== +-- 3. 用户档案数据 +-- ===================================================================================== + +-- 插入用户档案 +INSERT INTO public.ml_user_profiles (user_id, status, real_name, credit_score, verification_status, verification_data, preferences) +SELECT + t.user_id, + 1, -- 正常状态 + CASE + WHEN t.role = 'admin' THEN '系统管理员' + WHEN t.role = 'merchant1' THEN '张三丰' + WHEN t.role = 'merchant2' THEN '李四海' + WHEN t.role = 'customer1' THEN '王小明' + WHEN t.role = 'customer2' THEN '刘小红' + WHEN t.role = 'customer3' THEN '陈小华' + WHEN t.role = 'driver1' THEN '赵配送' + WHEN t.role = 'driver2' THEN '钱师傅' + END, + CASE + WHEN t.role = 'admin' THEN 1000 + WHEN t.role IN ('merchant1', 'merchant2') THEN 950 + WHEN t.role IN ('driver1', 'driver2') THEN 900 + ELSE 850 + END, + CASE + WHEN t.role IN ('admin', 'merchant1', 'merchant2') THEN 1 -- 已认证 + ELSE 0 -- 未认证 + END, + '{}', -- 认证数据 + '{"language": "zh-CN", "currency": "CNY", "notifications": true}' -- 偏好设置 +FROM temp_user_ids t +ON CONFLICT (user_id) DO UPDATE SET + real_name = EXCLUDED.real_name, + credit_score = EXCLUDED.credit_score, + verification_status = EXCLUDED.verification_status; + +-- ===================================================================================== +-- 4. 用户地址数据 +-- ===================================================================================== + +-- 插入用户地址 +INSERT INTO public.ml_user_addresses (user_id, receiver_name, receiver_phone, province, city, district, address_detail, is_default, label, latitude, longitude) +SELECT + t.user_id, + CASE + WHEN t.role = 'customer1' THEN '王小明' + WHEN t.role = 'customer2' THEN '刘小红' + WHEN t.role = 'customer3' THEN '陈小华' + WHEN t.role = 'merchant1' THEN '张三丰' + WHEN t.role = 'merchant2' THEN '李四海' + END, + CASE + WHEN t.role = 'customer1' THEN '13800138003' + WHEN t.role = 'customer2' THEN '13800138004' + WHEN t.role = 'customer3' THEN '13800138005' + WHEN t.role = 'merchant1' THEN '13800138001' + WHEN t.role = 'merchant2' THEN '13800138002' + END, + '北京市', + '朝阳区', + '望京街道', + CASE + WHEN t.role = 'customer1' THEN '望京SOHO T1座 1201室' + WHEN t.role = 'customer2' THEN '国贸大厦 A座 2301室' + WHEN t.role = 'customer3' THEN '三里屯太古里 B1-023' + WHEN t.role = 'merchant1' THEN '中关村创业大街 8号楼' + WHEN t.role = 'merchant2' THEN '亦庄经济开发区 科创园' + END, + TRUE, -- 默认地址 + 'home', + 39.9942 + (random() - 0.5) * 0.01, -- 北京纬度附近 + 116.3258 + (random() - 0.5) * 0.01 -- 北京经度附近 +FROM temp_user_ids t +WHERE t.role IN ('customer1', 'customer2', 'customer3', 'merchant1', 'merchant2'); + +-- 为部分用户添加第二个地址 +INSERT INTO public.ml_user_addresses (user_id, receiver_name, receiver_phone, province, city, district, address_detail, is_default, label, latitude, longitude) +SELECT + t.user_id, + '公司收货', + '010-12345678', + '北京市', + '海淀区', + '中关村街道', + '中关村大街1号 科技大厦', + FALSE, + 'office', + 39.9042, + 116.3958 +FROM temp_user_ids t +WHERE t.role IN ('customer1', 'customer2'); + +-- ===================================================================================== +-- 5. 商品分类数据 +-- ===================================================================================== + +-- 删除默认分类,插入更详细的分类 +DELETE FROM public.ml_categories; + +-- 一级分类 +INSERT INTO public.ml_categories (id, name, slug, level, path, sort_order, icon_url, description) VALUES +(uuid_generate_v4(), '数码电器', 'digital', 1, ARRAY['数码电器'], 1, 'icon-digital', '手机、电脑、家电等数码产品'), +(uuid_generate_v4(), '服装鞋帽', 'fashion', 1, ARRAY['服装鞋帽'], 2, 'icon-fashion', '男装、女装、鞋子、配饰'), +(uuid_generate_v4(), '家居用品', 'home', 1, ARRAY['家居用品'], 3, 'icon-home', '家具、装饰、生活用品'), +(uuid_generate_v4(), '食品饮料', 'food', 1, ARRAY['食品饮料'], 4, 'icon-food', '新鲜食材、零食、饮品'), +(uuid_generate_v4(), '美妆护肤', 'beauty', 1, ARRAY['美妆护肤'], 5, 'icon-beauty', '化妆品、护肤品、个人护理'), +(uuid_generate_v4(), '运动户外', 'sports', 1, ARRAY['运动户外'], 6, 'icon-sports', '运动器材、户外装备、健身用品'), +(uuid_generate_v4(), '图书文娱', 'books', 1, ARRAY['图书文娱'], 7, 'icon-books', '图书、音像、文具、玩具'), +(uuid_generate_v4(), '母婴用品', 'baby', 1, ARRAY['母婴用品'], 8, 'icon-baby', '婴儿用品、孕妇用品、儿童玩具'); + +-- 获取一级分类ID并创建二级分类 +DO $$ +DECLARE + digital_id UUID; + fashion_id UUID; + home_id UUID; + food_id UUID; +BEGIN + -- 获取一级分类ID + SELECT id INTO digital_id FROM public.ml_categories WHERE slug = 'digital'; + SELECT id INTO fashion_id FROM public.ml_categories WHERE slug = 'fashion'; + SELECT id INTO home_id FROM public.ml_categories WHERE slug = 'home'; + SELECT id INTO food_id FROM public.ml_categories WHERE slug = 'food'; + + -- 数码电器二级分类 + INSERT INTO public.ml_categories (parent_id, name, slug, level, path, sort_order) VALUES + (digital_id, '手机通讯', 'mobile', 2, ARRAY['数码电器', '手机通讯'], 1), + (digital_id, '电脑办公', 'computer', 2, ARRAY['数码电器', '电脑办公'], 2), + (digital_id, '家用电器', 'appliance', 2, ARRAY['数码电器', '家用电器'], 3), + (digital_id, '数码配件', 'accessories', 2, ARRAY['数码电器', '数码配件'], 4); + + -- 服装鞋帽二级分类 + INSERT INTO public.ml_categories (parent_id, name, slug, level, path, sort_order) VALUES + (fashion_id, '男装', 'mens-wear', 2, ARRAY['服装鞋帽', '男装'], 1), + (fashion_id, '女装', 'womens-wear', 2, ARRAY['服装鞋帽', '女装'], 2), + (fashion_id, '男鞋', 'mens-shoes', 2, ARRAY['服装鞋帽', '男鞋'], 3), + (fashion_id, '女鞋', 'womens-shoes', 2, ARRAY['服装鞋帽', '女鞋'], 4); + + -- 家居用品二级分类 + INSERT INTO public.ml_categories (parent_id, name, slug, level, path, sort_order) VALUES + (home_id, '家具', 'furniture', 2, ARRAY['家居用品', '家具'], 1), + (home_id, '家装', 'decoration', 2, ARRAY['家居用品', '家装'], 2), + (home_id, '厨具', 'kitchen', 2, ARRAY['家居用品', '厨具'], 3), + (home_id, '生活用品', 'daily', 2, ARRAY['家居用品', '生活用品'], 4); + + -- 食品饮料二级分类 + INSERT INTO public.ml_categories (parent_id, name, slug, level, path, sort_order) VALUES + (food_id, '新鲜水果', 'fruits', 2, ARRAY['食品饮料', '新鲜水果'], 1), + (food_id, '肉禽蛋类', 'meat', 2, ARRAY['食品饮料', '肉禽蛋类'], 2), + (food_id, '零食坚果', 'snacks', 2, ARRAY['食品饮料', '零食坚果'], 3), + (food_id, '酒水饮料', 'drinks', 2, ARRAY['食品饮料', '酒水饮料'], 4); +END $$; + +-- ===================================================================================== +-- 6. 品牌数据 +-- ===================================================================================== + +INSERT INTO public.ml_brands (name, logo_url, description, website) VALUES +('苹果', 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/apple/apple-original.svg', '创新科技品牌', 'https://www.apple.com'), +('华为', 'https://logo.clearbit.com/huawei.com', '全球领先的信息与通信技术解决方案供应商', 'https://www.huawei.com'), +('小米', 'https://logo.clearbit.com/mi.com', '专注于智能硬件和电子产品', 'https://www.mi.com'), +('三星', 'https://logo.clearbit.com/samsung.com', '全球知名电子产品制造商', 'https://www.samsung.com'), +('耐克', 'https://logo.clearbit.com/nike.com', '全球著名体育用品品牌', 'https://www.nike.com'), +('阿迪达斯', 'https://logo.clearbit.com/adidas.com', '德国运动用品制造商', 'https://www.adidas.com'), +('优衣库', 'https://logo.clearbit.com/uniqlo.com', '日本休闲服饰品牌', 'https://www.uniqlo.com'), +('宜家', 'https://logo.clearbit.com/ikea.com', '瑞典家居用品零售商', 'https://www.ikea.com'), +('美的', 'https://logo.clearbit.com/midea.com', '中国家电制造商', 'https://www.midea.com'), +('海尔', 'https://logo.clearbit.com/haier.com', '全球知名家电品牌', 'https://www.haier.com'); + +-- ===================================================================================== +-- 7. 店铺数据 +-- ===================================================================================== + +-- 获取商家用户ID并创建店铺 +INSERT INTO public.ml_shops (merchant_id, shop_name, shop_logo, shop_banner, description, business_license, contact_name, contact_phone, contact_email, address, business_hours, status, verified_at) +SELECT + t.user_id, + CASE + WHEN t.role = 'merchant1' THEN '张三丰数码专营店' + WHEN t.role = 'merchant2' THEN '李四海时尚小铺' + END, + CASE + WHEN t.role = 'merchant1' THEN 'https://api.dicebear.com/7.x/shapes/svg?seed=shop1' + WHEN t.role = 'merchant2' THEN 'https://api.dicebear.com/7.x/shapes/svg?seed=shop2' + END, + CASE + WHEN t.role = 'merchant1' THEN 'https://picsum.photos/800/200?random=1' + WHEN t.role = 'merchant2' THEN 'https://picsum.photos/800/200?random=2' + END, + CASE + WHEN t.role = 'merchant1' THEN '专业销售各类数码产品,品质保证,服务至上' + WHEN t.role = 'merchant2' THEN '时尚潮流服饰,让您成为街头最亮的星' + END, + CASE + WHEN t.role = 'merchant1' THEN '91110000MA0001234A' + WHEN t.role = 'merchant2' THEN '91110000MA0005678B' + END, + CASE + WHEN t.role = 'merchant1' THEN '张三丰' + WHEN t.role = 'merchant2' THEN '李四海' + END, + CASE + WHEN t.role = 'merchant1' THEN '13800138001' + WHEN t.role = 'merchant2' THEN '13800138002' + END, + CASE + WHEN t.role = 'merchant1' THEN 'merchant1@mall.com' + WHEN t.role = 'merchant2' THEN 'merchant2@mall.com' + END, + '{"province": "北京市", "city": "朝阳区", "district": "望京街道", "detail": "望京商业中心"}', + '{"monday": "09:00-21:00", "tuesday": "09:00-21:00", "wednesday": "09:00-21:00", "thursday": "09:00-21:00", "friday": "09:00-21:00", "saturday": "09:00-22:00", "sunday": "10:00-20:00"}', + 1, -- 正常状态 + NOW() - INTERVAL '30 days' -- 30天前认证 +FROM temp_user_ids t +WHERE t.role IN ('merchant1', 'merchant2'); + +-- ===================================================================================== +-- 8. 商品数据 +-- ===================================================================================== + +-- 获取分类和品牌ID +DO $$ +DECLARE + mobile_cat_id UUID; + computer_cat_id UUID; + mens_wear_cat_id UUID; + womens_wear_cat_id UUID; + furniture_cat_id UUID; + fruits_cat_id UUID; + + apple_brand_id UUID; + huawei_brand_id UUID; + xiaomi_brand_id UUID; + nike_brand_id UUID; + uniqlo_brand_id UUID; + ikea_brand_id UUID; + + merchant1_id UUID; + merchant2_id UUID; +BEGIN + -- 获取分类ID + SELECT id INTO mobile_cat_id FROM public.ml_categories WHERE slug = 'mobile'; + SELECT id INTO computer_cat_id FROM public.ml_categories WHERE slug = 'computer'; + SELECT id INTO mens_wear_cat_id FROM public.ml_categories WHERE slug = 'mens-wear'; + SELECT id INTO womens_wear_cat_id FROM public.ml_categories WHERE slug = 'womens-wear'; + SELECT id INTO furniture_cat_id FROM public.ml_categories WHERE slug = 'furniture'; + SELECT id INTO fruits_cat_id FROM public.ml_categories WHERE slug = 'fruits'; + + -- 获取品牌ID + SELECT id INTO apple_brand_id FROM public.ml_brands WHERE name = '苹果'; + SELECT id INTO huawei_brand_id FROM public.ml_brands WHERE name = '华为'; + SELECT id INTO xiaomi_brand_id FROM public.ml_brands WHERE name = '小米'; + SELECT id INTO nike_brand_id FROM public.ml_brands WHERE name = '耐克'; + SELECT id INTO uniqlo_brand_id FROM public.ml_brands WHERE name = '优衣库'; + SELECT id INTO ikea_brand_id FROM public.ml_brands WHERE name = '宜家'; + + -- 获取商家ID + SELECT user_id INTO merchant1_id FROM temp_user_ids WHERE role = 'merchant1'; + SELECT user_id INTO merchant2_id FROM temp_user_ids WHERE role = 'merchant2'; + + -- 插入商品数据 - 商家1的数码产品 + INSERT INTO public.ml_products ( + merchant_id, category_id, brand_id, product_code, name, subtitle, description, + main_image_url, image_urls, base_price, market_price, total_stock, available_stock, + weight, status, is_featured, is_new, is_hot, tags, slug + ) VALUES + -- iPhone 15 Pro + (merchant1_id, mobile_cat_id, apple_brand_id, 'IP15P-001', + 'iPhone 15 Pro 256GB 深空黑色', + 'A17 Pro芯片,钛金属设计,专业级摄像头系统', + '全新iPhone 15 Pro采用航空级钛金属设计,搭载A17 Pro芯片,配备专业级摄像头系统,支持5G网络。48MP主摄像头,2倍变焦,4K视频录制。', + 'https://picsum.photos/600/600?random=10', + '["https://picsum.photos/600/600?random=11", "https://picsum.photos/600/600?random=12", "https://picsum.photos/600/600?random=13"]', + 8999.00, 9999.00, 50, 45, 0.187, 1, true, true, true, + ARRAY['手机', '苹果', 'iPhone', '5G', '新品'], 'iphone-15-pro-256gb-black'), + + -- 华为 Mate 60 Pro + (merchant1_id, mobile_cat_id, huawei_brand_id, 'HW-M60P-001', + '华为 Mate 60 Pro 512GB 雅川青', + '卫星通话,昆仑玻璃,超聚光夜拍', + '华为Mate 60 Pro支持卫星通话功能,采用昆仑玻璃,配备超聚光夜拍摄像头,5000万像素主摄,支持100倍数字变焦。', + 'https://picsum.photos/600/600?random=20', + '["https://picsum.photos/600/600?random=21", "https://picsum.photos/600/600?random=22"]', + 6999.00, 7999.00, 30, 28, 0.225, 1, true, false, true, + ARRAY['手机', '华为', 'Mate', '卫星通话'], 'huawei-mate-60-pro-512gb'), + + -- 小米笔记本 + (merchant1_id, computer_cat_id, xiaomi_brand_id, 'XM-NB-001', + '小米笔记本 Pro 14 增强版', + '2.8K OLED屏幕,12代酷睿处理器', + '14英寸2.8K OLED全面屏,120Hz刷新率,第12代Intel Core处理器,16GB内存,512GB固态硬盘,雷电4接口。', + 'https://picsum.photos/600/600?random=30', + '["https://picsum.photos/600/600?random=31", "https://picsum.photos/600/600?random=32"]', + 5999.00, 6999.00, 20, 18, 1.4, 1, false, true, false, + ARRAY['笔记本', '小米', 'OLED', '办公'], 'xiaomi-notebook-pro-14'); + + -- 插入商品数据 - 商家2的时尚产品 + INSERT INTO public.ml_products ( + merchant_id, category_id, brand_id, product_code, name, subtitle, description, + main_image_url, image_urls, base_price, market_price, total_stock, available_stock, + weight, status, is_featured, is_new, is_hot, tags, slug + ) VALUES + -- 耐克运动鞋 + (merchant2_id, mens_wear_cat_id, nike_brand_id, 'NK-AIR-001', + 'Nike Air Max 270 男士运动鞋', + '全掌气垫,舒适透气,时尚百搭', + 'Nike Air Max 270采用全新的Air Max气垫设计,提供卓越的缓震效果。透气网面鞋身,轻量化设计,适合日常运动和休闲穿着。', + 'https://picsum.photos/600/600?random=40', + '["https://picsum.photos/600/600?random=41", "https://picsum.photos/600/600?random=42"]', + 899.00, 1099.00, 100, 95, 0.8, 1, true, false, true, + ARRAY['运动鞋', '耐克', '气垫', '男鞋'], 'nike-air-max-270-mens'), + + -- 优衣库T恤 + (merchant2_id, mens_wear_cat_id, uniqlo_brand_id, 'UQ-TEE-001', + 'UNIQLO 优质棉圆领T恤(短袖)', + '100%纯棉,柔软舒适,多色可选', + '采用100%精选纯棉制作,触感柔软,吸汗透气。简约设计,多种颜色可选,是日常穿搭的必备单品。', + 'https://picsum.photos/600/600?random=50', + '["https://picsum.photos/600/600?random=51", "https://picsum.photos/600/600?random=52"]', + 59.00, 79.00, 200, 180, 0.2, 1, false, false, false, + ARRAY['T恤', '优衣库', '纯棉', '基础款'], 'uniqlo-cotton-tshirt'), + + -- 女装连衣裙 + (merchant2_id, womens_wear_cat_id, uniqlo_brand_id, 'UQ-DRESS-001', + 'UNIQLO 女装雪纺连衣裙', + '轻盈雪纺面料,优雅飘逸,职场通勤', + '采用轻盈雪纺面料,版型优雅,适合职场通勤和日常聚会。A字版型修饰身形,多种花色可选。', + 'https://picsum.photos/600/600?random=60', + '["https://picsum.photos/600/600?random=61", "https://picsum.photos/600/600?random=62"]', + 299.00, 399.00, 80, 75, 0.3, 1, true, true, false, + ARRAY['连衣裙', '优衣库', '雪纺', '女装'], 'uniqlo-chiffon-dress'); +END $$; + +-- ===================================================================================== +-- 9. 商品SKU数据 +-- ===================================================================================== + +-- 为iPhone添加SKU +INSERT INTO public.ml_product_skus (product_id, sku_code, specifications, price, market_price, stock, image_url) +SELECT + p.id, + 'IP15P-' || color.code || '-' || storage.code, + jsonb_build_object('颜色', color.name, '存储容量', storage.name), + p.base_price + storage.price_diff, + p.market_price + storage.price_diff, + 15, + 'https://picsum.photos/400/400?random=' || (10 + color.id) +FROM public.ml_products p +CROSS JOIN (VALUES + (1, 'BLK', '深空黑色', 0), + (2, 'WHT', '白色钛金属', 0), + (3, 'BLU', '蓝色钛金属', 0) +) AS color(id, code, name, price_diff) +CROSS JOIN (VALUES + (1, '128G', '128GB', -1000), + (2, '256G', '256GB', 0), + (3, '512G', '512GB', 1500) +) AS storage(id, code, name, price_diff) +WHERE p.product_code = 'IP15P-001'; + +-- 为运动鞋添加SKU +INSERT INTO public.ml_product_skus (product_id, sku_code, specifications, price, market_price, stock, image_url) +SELECT + p.id, + 'NK-AIR-' || color.code || '-' || size.code, + jsonb_build_object('颜色', color.name, '尺码', size.name), + p.base_price, + p.market_price, + 10, + 'https://picsum.photos/400/400?random=' || (40 + color.id) +FROM public.ml_products p +CROSS JOIN (VALUES + (1, 'BLK', '黑色'), + (2, 'WHT', '白色'), + (3, 'RED', '红色') +) AS color(id, code, name) +CROSS JOIN (VALUES + (1, '40', '40码'), + (2, '41', '41码'), + (3, '42', '42码'), + (4, '43', '43码'), + (5, '44', '44码') +) AS size(id, code, name) +WHERE p.product_code = 'NK-AIR-001'; + +-- 为T恤添加SKU +INSERT INTO public.ml_product_skus (product_id, sku_code, specifications, price, market_price, stock, image_url) +SELECT + p.id, + 'UQ-TEE-' || color.code || '-' || size.code, + jsonb_build_object('颜色', color.name, '尺码', size.name), + p.base_price, + p.market_price, + 25, + 'https://picsum.photos/400/400?random=' || (50 + color.id) +FROM public.ml_products p +CROSS JOIN (VALUES + (1, 'WHT', '白色'), + (2, 'BLK', '黑色'), + (3, 'GRY', '灰色'), + (4, 'NVY', '深蓝色') +) AS color(id, code, name) +CROSS JOIN (VALUES + (1, 'S', 'S'), + (2, 'M', 'M'), + (3, 'L', 'L'), + (4, 'XL', 'XL') +) AS size(id, code, name) +WHERE p.product_code = 'UQ-TEE-001'; + +-- ===================================================================================== +-- 10. 购物车数据 +-- ===================================================================================== + +-- 为消费者添加购物车数据 +INSERT INTO public.ml_shopping_cart (user_id, product_id, sku_id, quantity, selected) +SELECT + customer.user_id, + p.id, + s.id, + CASE + WHEN customer.role = 'customer1' THEN 1 + WHEN customer.role = 'customer2' THEN 2 + ELSE 1 + END, + true +FROM temp_user_ids customer +CROSS JOIN public.ml_products p +LEFT JOIN public.ml_product_skus s ON p.id = s.product_id +WHERE customer.role IN ('customer1', 'customer2', 'customer3') + AND p.status = 1 + AND (s.id IS NULL OR s.status = 1) + AND random() < 0.3 -- 30%的概率添加到购物车 +LIMIT 15; + +-- ===================================================================================== +-- 11. 优惠券数据 +-- ===================================================================================== + +-- 创建优惠券模板 +INSERT INTO public.ml_coupon_templates ( + merchant_id, name, description, coupon_type, discount_type, discount_value, + min_order_amount, max_discount_amount, total_quantity, per_user_limit, + start_time, end_time, status +) VALUES +-- 平台券 +(NULL, '新用户专享券', '新用户注册即送50元无门槛券', 1, 1, 50.00, 0, 50.00, 1000, 1, + NOW() - INTERVAL '1 day', NOW() + INTERVAL '30 days', 1), +(NULL, '满200减30', '全平台满200元减30元', 1, 1, 30.00, 200.00, 30.00, 500, 3, + NOW() - INTERVAL '1 day', NOW() + INTERVAL '15 days', 1), +(NULL, '9折优惠券', '全平台9折优惠,最高减100元', 2, 2, 0.9, 100.00, 100.00, 200, 1, + NOW() - INTERVAL '1 day', NOW() + INTERVAL '7 days', 1), + +-- 商家券 +((SELECT user_id FROM temp_user_ids WHERE role = 'merchant1'), + '数码专营店满1000减100', '店铺满1000元减100元', 1, 1, 100.00, 1000.00, 100.00, 100, 2, + NOW() - INTERVAL '1 day', NOW() + INTERVAL '20 days', 1), +((SELECT user_id FROM temp_user_ids WHERE role = 'merchant2'), + '时尚小铺免运费券', '店铺订单免运费', 3, 1, 15.00, 0, 15.00, 50, 1, + NOW() - INTERVAL '1 day', NOW() + INTERVAL '10 days', 1); + +-- 为用户发放优惠券 +INSERT INTO public.ml_user_coupons (user_id, template_id, coupon_code, status, expire_at) +SELECT + customer.user_id, + t.id, + public.generate_coupon_code(), + 1, -- 未使用 + t.end_time +FROM temp_user_ids customer +CROSS JOIN public.ml_coupon_templates t +WHERE customer.role IN ('customer1', 'customer2', 'customer3') + AND t.status = 1 + AND random() < 0.6; -- 60%的概率获得优惠券 + +-- ===================================================================================== +-- 12. 订单数据 +-- ===================================================================================== + +-- 生成订单数据 +DO $$ +DECLARE + customer_rec RECORD; + selected_merchant_id UUID; + product_rec RECORD; + order_id UUID; + order_no TEXT; + total_amount DECIMAL; +BEGIN + -- 为每个客户生成2-4个订单 + FOR customer_rec IN + SELECT user_id, role FROM temp_user_ids WHERE role LIKE 'customer%' + LOOP + FOR i IN 1..FLOOR(2 + random() * 3)::INTEGER LOOP + -- 生成订单号 + order_no := public.generate_order_no(); + + -- 随机选择商家 + SELECT user_id INTO selected_merchant_id FROM temp_user_ids + WHERE role LIKE 'merchant%' + ORDER BY random() LIMIT 1; + + -- 计算订单总金额(这里简化,实际应该根据商品计算) + total_amount := 100 + random() * 2000; + + -- 创建订单 + INSERT INTO public.ml_orders ( + id, order_no, user_id, merchant_id, product_amount, shipping_fee, total_amount, + shipping_address, order_status, payment_status, shipping_status, + paid_at, shipped_at, delivered_at, + remark, created_at + ) VALUES ( + uuid_generate_v4(), order_no, customer_rec.user_id, selected_merchant_id, + total_amount - 10, 10.00, total_amount, + jsonb_build_object( + 'receiver_name', '收货人姓名', + 'receiver_phone', '13800138000', + 'province', '北京市', + 'city', '朝阳区', + 'district', '望京街道', + 'address_detail', '望京SOHO T1座 1201室' + ), + CASE + WHEN random() < 0.6 THEN 4 -- 已完成 + WHEN random() < 0.8 THEN 3 -- 待收货 + WHEN random() < 0.9 THEN 2 -- 待发货 + ELSE 1 -- 待付款 + END, + CASE + WHEN random() < 0.8 THEN 2 -- 已付款 + ELSE 1 -- 未付款 + END, + CASE + WHEN random() < 0.7 THEN 4 -- 已送达 + WHEN random() < 0.85 THEN 3 -- 运输中 + WHEN random() < 0.9 THEN 2 -- 已发货 + ELSE 1 -- 未发货 + END, + CASE WHEN random() < 0.8 THEN NOW() - INTERVAL '1 day' * FLOOR(random() * 10) END, + CASE WHEN random() < 0.7 THEN NOW() - INTERVAL '1 day' * FLOOR(random() * 8) END, + CASE WHEN random() < 0.6 THEN NOW() - INTERVAL '1 day' * FLOOR(random() * 5) END, + '请尽快发货,谢谢!', + NOW() - INTERVAL '1 day' * FLOOR(random() * 30) + ) RETURNING id INTO order_id; + + -- 为订单添加商品 + FOR product_rec IN + SELECT + p.id as product_id, + s.id as sku_id, + p.name, + COALESCE(s.price, p.base_price) as price, -- 使用SKU价格,如果没有则使用基础价格 + COALESCE(s.image_url, p.main_image_url) as image_url -- 使用SKU图片,如果没有则使用主图 + FROM public.ml_products p + LEFT JOIN public.ml_product_skus s ON p.id = s.product_id + WHERE p.merchant_id = selected_merchant_id + AND p.status = 1 + AND (s.id IS NULL OR s.status = 1) + ORDER BY random() + LIMIT FLOOR(1 + random() * 3)::INTEGER + LOOP + DECLARE + item_quantity INTEGER; + item_price DECIMAL; + BEGIN + item_quantity := FLOOR(1 + random() * 2)::INTEGER; + item_price := product_rec.price; + + INSERT INTO public.ml_order_items ( + order_id, product_id, sku_id, product_name, + price, quantity, total_amount, image_url + ) VALUES ( + order_id, product_rec.product_id, product_rec.sku_id, product_rec.name, + item_price, item_quantity, + item_price * item_quantity, + product_rec.image_url + ); + END; + END LOOP; + END LOOP; + END LOOP; +END $$; + +-- ===================================================================================== +-- 13. 商品评价数据 +-- ===================================================================================== + +-- 为已完成的订单生成评价 +INSERT INTO public.ml_product_reviews ( + order_id, order_item_id, user_id, product_id, merchant_id, + rating, content, images, is_anonymous, status, created_at +) +SELECT + o.id, + oi.id, + o.user_id, + oi.product_id, + o.merchant_id, + FLOOR(3 + random() * 3)::INTEGER, -- 3-5星评价 + CASE + WHEN random() < 0.3 THEN '商品质量很好,物流很快,推荐购买!' + WHEN random() < 0.6 THEN '包装完好,商品和描述一致,满意的购物体验。' + WHEN random() < 0.8 THEN '性价比不错,会再次购买的。' + ELSE '商品不错,快递也很快,好评!' + END, + CASE + WHEN random() < 0.3 THEN '["https://picsum.photos/300/300?random=' || FLOOR(random() * 100)::INTEGER || '"]' + ELSE '[]' + END::JSONB, + random() < 0.1, -- 10%匿名评价 + 1, -- 正常状态 + o.delivered_at + INTERVAL '1 day' * FLOOR(random() * 10) +FROM public.ml_orders o +JOIN public.ml_order_items oi ON o.id = oi.order_id +WHERE o.order_status = 4 -- 已完成的订单 + AND o.delivered_at IS NOT NULL + AND random() < 0.7; -- 70%的概率有评价 + +-- ===================================================================================== +-- 14. 用户行为数据 +-- ===================================================================================== + +-- 用户收藏数据 +INSERT INTO public.ml_user_favorites (user_id, target_type, target_id) +SELECT + customer.user_id, + 1, -- 商品收藏 + p.id +FROM temp_user_ids customer +CROSS JOIN public.ml_products p +WHERE customer.role LIKE 'customer%' + AND p.status = 1 + AND random() < 0.2 -- 20%的概率收藏 +ON CONFLICT (user_id, target_type, target_id) DO NOTHING; + +-- 店铺收藏 +INSERT INTO public.ml_user_favorites (user_id, target_type, target_id) +SELECT + customer.user_id, + 2, -- 店铺收藏 + s.merchant_id +FROM temp_user_ids customer +CROSS JOIN public.ml_shops s +WHERE customer.role LIKE 'customer%' + AND random() < 0.3 -- 30%的概率收藏店铺 +ON CONFLICT (user_id, target_type, target_id) DO NOTHING; + +-- 浏览历史数据 +INSERT INTO public.ml_browse_history (user_id, product_id, browse_duration, created_at, updated_at) +SELECT + customer.user_id, + p.id, + FLOOR(10 + random() * 300)::INTEGER, -- 10-300秒浏览时长 + NOW() - INTERVAL '1 day' * FLOOR(random() * 30), -- 30天内的浏览记录 + NOW() - INTERVAL '1 day' * FLOOR(random() * 30) +FROM temp_user_ids customer +CROSS JOIN public.ml_products p +WHERE customer.role LIKE 'customer%' + AND p.status = 1 + AND random() < 0.4 -- 40%的概率有浏览记录 +ON CONFLICT (user_id, product_id) DO UPDATE SET + browse_duration = EXCLUDED.browse_duration, + updated_at = EXCLUDED.updated_at; + +-- 搜索历史数据 +INSERT INTO public.ml_search_history (user_id, keyword, result_count, created_at) +SELECT + customer.user_id, + keyword.word, + FLOOR(1 + random() * 50)::INTEGER, -- 1-50个结果 + NOW() - INTERVAL '1 day' * FLOOR(random() * 30) +FROM temp_user_ids customer +CROSS JOIN (VALUES + ('iPhone'), ('华为手机'), ('笔记本电脑'), ('运动鞋'), + ('T恤'), ('连衣裙'), ('耳机'), ('充电器'), ('数据线'), ('手机壳') +) AS keyword(word) +WHERE customer.role LIKE 'customer%' + AND random() < 0.3; -- 30%的概率有搜索记录 + +-- ===================================================================================== +-- 15. 配送数据 +-- ===================================================================================== + +-- 配送员信息 +INSERT INTO public.ml_delivery_drivers ( + user_id, real_name, id_card, driver_license, vehicle_type, vehicle_number, + service_areas, work_status, current_lat, current_lng, status +) +SELECT + t.user_id, + CASE + WHEN t.role = 'driver1' THEN '赵配送' + WHEN t.role = 'driver2' THEN '钱师傅' + END, + CASE + WHEN t.role = 'driver1' THEN '110101199001011234' + WHEN t.role = 'driver2' THEN '110101199002022345' + END, + CASE + WHEN t.role = 'driver1' THEN 'D110101199001011234' + WHEN t.role = 'driver2' THEN 'D110101199002022345' + END, + 1, -- 电动车 + CASE + WHEN t.role = 'driver1' THEN '京A12345' + WHEN t.role = 'driver2' THEN '京A67890' + END, + '["朝阳区", "海淀区", "东城区"]'::JSONB, + 1, -- 在线 + 39.9042 + (random() - 0.5) * 0.01, + 116.4074 + (random() - 0.5) * 0.01, + 1 -- 正常状态 +FROM temp_user_ids t +WHERE t.role LIKE 'driver%'; + +-- 配送任务(为部分已发货订单创建配送任务) +INSERT INTO public.ml_delivery_tasks ( + order_id, driver_id, pickup_address, delivery_address, + distance, estimated_time, delivery_fee, status, + assigned_at, picked_at, delivered_at, delivery_code +) +SELECT DISTINCT ON (o.id) -- 确保每个订单只有一个配送任务 + o.id, + d.id, + jsonb_build_object( + 'name', '商家仓库', + 'phone', '010-12345678', + 'address', '北京市朝阳区望京商业中心' + ), + o.shipping_address, + ROUND((5 + random() * 15)::NUMERIC, 2), -- 5-20公里 + FLOOR(20 + random() * 40)::INTEGER, -- 20-60分钟 + CASE + WHEN (o.shipping_address->>'district') = '朝阳区' THEN 8.00 + ELSE 12.00 + END, + CASE + WHEN o.order_status >= 4 THEN 5 -- 已送达 + WHEN o.order_status >= 3 THEN 4 -- 配送中 + WHEN o.shipping_status >= 2 THEN 2 -- 已接单 + ELSE 1 -- 待接单 + END, + o.shipped_at, + CASE WHEN o.order_status >= 3 THEN o.shipped_at + INTERVAL '30 minutes' END, + CASE WHEN o.order_status >= 4 THEN o.delivered_at END, + LPAD(FLOOR(random() * 10000)::TEXT, 4, '0') -- 4位取货码 +FROM public.ml_orders o +CROSS JOIN public.ml_delivery_drivers d +WHERE o.shipping_status >= 2 -- 已发货的订单 + AND random() < 0.8 -- 80%的概率有配送任务 + AND NOT EXISTS ( -- 确保订单尚未有配送任务 + SELECT 1 FROM public.ml_delivery_tasks dt WHERE dt.order_id = o.id + ) +ORDER BY o.id, random() -- 每个订单随机选择一个配送员 +LIMIT 50; -- 限制配送任务数量,避免过多数据 + +-- ===================================================================================== +-- 16. 更新统计数据 +-- ===================================================================================== + +-- 更新商品统计数据 +UPDATE public.ml_products SET + view_count = FLOOR(100 + random() * 9900)::INTEGER, + sale_count = ( + SELECT COALESCE(SUM(oi.quantity), 0) + FROM public.ml_order_items oi + JOIN public.ml_orders o ON oi.order_id = o.id + WHERE oi.product_id = ml_products.id + AND o.order_status = 4 + ), + favorite_count = ( + SELECT COUNT(*) + FROM public.ml_user_favorites f + WHERE f.target_type = 1 + AND f.target_id = ml_products.id + ), + rating_avg = ( + SELECT COALESCE(AVG(rating), 0) + FROM public.ml_product_reviews r + WHERE r.product_id = ml_products.id + AND r.status = 1 + ), + rating_count = ( + SELECT COUNT(*) + FROM public.ml_product_reviews r + WHERE r.product_id = ml_products.id + AND r.status = 1 + ); + +-- 更新店铺统计数据 +UPDATE public.ml_shops SET + product_count = ( + SELECT COUNT(*) + FROM public.ml_products p + WHERE p.merchant_id = ml_shops.merchant_id + AND p.status = 1 + ), + order_count = ( + SELECT COUNT(*) + FROM public.ml_orders o + WHERE o.merchant_id = ml_shops.merchant_id + AND o.order_status = 4 + ), + rating_avg = ( + SELECT COALESCE(AVG(r.rating), 0) + FROM public.ml_product_reviews r + WHERE r.merchant_id = ml_shops.merchant_id + AND r.status = 1 + ), + rating_count = ( + SELECT COUNT(*) + FROM public.ml_product_reviews r + WHERE r.merchant_id = ml_shops.merchant_id + AND r.status = 1 + ); + +-- 更新配送员统计数据 +UPDATE public.ml_delivery_drivers SET + order_count = ( + SELECT COUNT(*) + FROM public.ml_delivery_tasks dt + WHERE dt.driver_id = ml_delivery_drivers.id + AND dt.status = 5 + ), + rating_avg = 4.5 + random() * 0.5, -- 4.5-5.0星评价 + rating_count = FLOOR(10 + random() * 90)::INTEGER; -- 10-100个评价 + +-- ===================================================================================== +-- 17. 清理临时数据 +-- ===================================================================================== + +DROP TABLE IF EXISTS temp_user_ids; + +-- ===================================================================================== +-- 18. 完成提示 +-- ===================================================================================== + +DO $$ +BEGIN + RAISE NOTICE '======================================================='; + RAISE NOTICE '商城系统模拟数据插入完成!'; + RAISE NOTICE '======================================================='; + RAISE NOTICE '已创建数据概况:'; + RAISE NOTICE '- 测试用户: 8个 (管理员1个, 商家2个, 消费者3个, 配送员2个)'; + RAISE NOTICE '- 用户地址: 7个'; + RAISE NOTICE '- 商品分类: 多级分类体系'; + RAISE NOTICE '- 品牌: 10个知名品牌'; + RAISE NOTICE '- 店铺: 2个商家店铺'; + RAISE NOTICE '- 商品: 6个商品 + 多规格SKU'; + RAISE NOTICE '- 购物车: 随机购物车数据'; + RAISE NOTICE '- 优惠券: 5个优惠券模板 + 用户优惠券'; + RAISE NOTICE '- 订单: 多个测试订单 + 订单商品'; + RAISE NOTICE '- 商品评价: 基于完成订单的评价'; + RAISE NOTICE '- 用户行为: 收藏、浏览、搜索记录'; + RAISE NOTICE '- 配送数据: 配送员 + 配送任务'; + RAISE NOTICE '======================================================='; + RAISE NOTICE '数据状态: 包含各种业务场景的测试数据'; + RAISE NOTICE '建议: 可根据实际需要调整数据量和内容'; + RAISE NOTICE '======================================================='; +END $$; diff --git a/mall_sql/tests/validation_test.sql b/mall_sql/tests/validation_test.sql new file mode 100644 index 00000000..d9681f59 --- /dev/null +++ b/mall_sql/tests/validation_test.sql @@ -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; diff --git a/mall_sql/tests/verify_mock_data_fix.sql b/mall_sql/tests/verify_mock_data_fix.sql new file mode 100644 index 00000000..fdfb11e8 --- /dev/null +++ b/mall_sql/tests/verify_mock_data_fix.sql @@ -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 $$; diff --git a/pages/mall/analytics/test/01_create_coupon_tables.sql b/pages/mall/analytics/test/01_create_coupon_tables.sql new file mode 100644 index 00000000..d5d1b8b0 --- /dev/null +++ b/pages/mall/analytics/test/01_create_coupon_tables.sql @@ -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; + diff --git a/pages/mall/analytics/test/01_create_delivery_tables.sql b/pages/mall/analytics/test/01_create_delivery_tables.sql new file mode 100644 index 00000000..2b114c95 --- /dev/null +++ b/pages/mall/analytics/test/01_create_delivery_tables.sql @@ -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; diff --git a/pages/mall/analytics/test/01_create_tables.sql b/pages/mall/analytics/test/01_create_tables.sql new file mode 100644 index 00000000..bee0c648 --- /dev/null +++ b/pages/mall/analytics/test/01_create_tables.sql @@ -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 $$; diff --git a/pages/mall/analytics/test/02_insert_coupon_test_data.sql b/pages/mall/analytics/test/02_insert_coupon_test_data.sql new file mode 100644 index 00000000..7b9197da --- /dev/null +++ b/pages/mall/analytics/test/02_insert_coupon_test_data.sql @@ -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; + diff --git a/pages/mall/analytics/test/02_insert_delivery_test_data.sql b/pages/mall/analytics/test/02_insert_delivery_test_data.sql new file mode 100644 index 00000000..47afed51 --- /dev/null +++ b/pages/mall/analytics/test/02_insert_delivery_test_data.sql @@ -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; +$$; diff --git a/pages/mall/analytics/test/02_insert_market_trends_test_data.sql b/pages/mall/analytics/test/02_insert_market_trends_test_data.sql new file mode 100644 index 00000000..b016afe8 --- /dev/null +++ b/pages/mall/analytics/test/02_insert_market_trends_test_data.sql @@ -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; + diff --git a/pages/mall/analytics/test/02_insert_test_data.sql b/pages/mall/analytics/test/02_insert_test_data.sql new file mode 100644 index 00000000..e17f5973 --- /dev/null +++ b/pages/mall/analytics/test/02_insert_test_data.sql @@ -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 diff --git a/pages/mall/analytics/test/03_delivery_rls_policies.sql b/pages/mall/analytics/test/03_delivery_rls_policies.sql new file mode 100644 index 00000000..e5e73c7a --- /dev/null +++ b/pages/mall/analytics/test/03_delivery_rls_policies.sql @@ -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; diff --git a/pages/mall/analytics/test/03_orders_analytics_policies.sql b/pages/mall/analytics/test/03_orders_analytics_policies.sql new file mode 100644 index 00000000..6bf438ea --- /dev/null +++ b/pages/mall/analytics/test/03_orders_analytics_policies.sql @@ -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; + diff --git a/pages/mall/analytics/test/03_test_queries.sql b/pages/mall/analytics/test/03_test_queries.sql new file mode 100644 index 00000000..6619faaf --- /dev/null +++ b/pages/mall/analytics/test/03_test_queries.sql @@ -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; diff --git a/pages/mall/analytics/test/04_cleanup.sql b/pages/mall/analytics/test/04_cleanup.sql new file mode 100644 index 00000000..e9dfa01e --- /dev/null +++ b/pages/mall/analytics/test/04_cleanup.sql @@ -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; diff --git a/pages/mall/analytics/test/05_product_trend_rpc.sql b/pages/mall/analytics/test/05_product_trend_rpc.sql new file mode 100644 index 00000000..830a4bf9 --- /dev/null +++ b/pages/mall/analytics/test/05_product_trend_rpc.sql @@ -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; diff --git a/pages/mall/analytics/test/06_market_trends_rpcs.sql b/pages/mall/analytics/test/06_market_trends_rpcs.sql new file mode 100644 index 00000000..1ef59c18 --- /dev/null +++ b/pages/mall/analytics/test/06_market_trends_rpcs.sql @@ -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; diff --git a/pages/mall/analytics/test/07_custom_report_rpcs.sql b/pages/mall/analytics/test/07_custom_report_rpcs.sql new file mode 100644 index 00000000..74c8ee3d --- /dev/null +++ b/pages/mall/analytics/test/07_custom_report_rpcs.sql @@ -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 $$; diff --git a/pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql b/pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql new file mode 100644 index 00000000..89fc0e15 --- /dev/null +++ b/pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql @@ -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; diff --git a/pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql b/pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql new file mode 100644 index 00000000..91b6f425 --- /dev/null +++ b/pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql @@ -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; diff --git a/pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql b/pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql new file mode 100644 index 00000000..65e5bb50 --- /dev/null +++ b/pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql @@ -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; + diff --git a/pages/mall/analytics/test/DATA_DETAIL_RPCS.sql b/pages/mall/analytics/test/DATA_DETAIL_RPCS.sql new file mode 100644 index 00000000..5bb969b9 --- /dev/null +++ b/pages/mall/analytics/test/DATA_DETAIL_RPCS.sql @@ -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; + + +-- ============================================ +-- 文件结束 +-- ============================================ + diff --git a/pages/mall/analytics/test/DELIVERY_ANALYSIS_RPCS.sql b/pages/mall/analytics/test/DELIVERY_ANALYSIS_RPCS.sql new file mode 100644 index 00000000..7e22fe05 --- /dev/null +++ b/pages/mall/analytics/test/DELIVERY_ANALYSIS_RPCS.sql @@ -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; diff --git a/pages/user/test/USER_AUTH_SCHEMA.sql b/pages/user/test/USER_AUTH_SCHEMA.sql new file mode 100644 index 00000000..e44292e3 --- /dev/null +++ b/pages/user/test/USER_AUTH_SCHEMA.sql @@ -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 是触发器函数,由系统内部调用,无需对任何角色授权 diff --git a/pages/user/test/USER_AUTH_TEST_DATA.sql b/pages/user/test/USER_AUTH_TEST_DATA.sql new file mode 100644 index 00000000..c0d560d1 --- /dev/null +++ b/pages/user/test/USER_AUTH_TEST_DATA.sql @@ -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); + diff --git a/pages/user/test/USER_AUTH_TRIGGER.sql b/pages/user/test/USER_AUTH_TRIGGER.sql new file mode 100644 index 00000000..7ecd7ccd --- /dev/null +++ b/pages/user/test/USER_AUTH_TRIGGER.sql @@ -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';