consumer模块完成90%,前端完成supabase对接

This commit is contained in:
2026-02-03 17:11:50 +08:00
parent b6200cda28
commit 8a535e3f38
69 changed files with 5020 additions and 33273 deletions

View File

@@ -2,11 +2,15 @@
// 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.62
// Kong HTTP Port: 8000
//export const SUPA_URL: string = 'http://localhost:18000'
//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
export const SUPA_URL: string = 'http://192.168.1.61:18000'
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
// WebSocket 实时连接(内网使用 ws:// 而非 wss://
export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
//export const WS_URL: string = 'ws://localhost:18000/realtime/v1/websocket'
// 备用配置(已注释,如需切换可取消注释)
// 开发环境 - 其他内网地址

View File

@@ -1,71 +0,0 @@
-- Mall Software Subscription Tables
-- PostgreSQL DDL; adjust schema name as needed (default public)
-- Plans
create table if not exists ml_subscription_plans (
id uuid primary key default gen_random_uuid(),
plan_code text not null unique,
name text not null,
description text,
features jsonb,
price numeric(12,2) not null,
currency text default 'CNY',
billing_period text not null check (billing_period in ('monthly','yearly')),
trial_days int default 0,
is_active boolean default true,
sort_order int default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_ml_subscription_plans_active on ml_subscription_plans(is_active) where is_active = true;
create index if not exists idx_ml_subscription_plans_sort on ml_subscription_plans(sort_order);
-- User Subscriptions
create table if not exists ml_user_subscriptions (
id uuid primary key default gen_random_uuid(),
user_id uuid not null,
plan_id uuid not null references ml_subscription_plans(id) on delete restrict,
status text not null default 'active' check (status in ('trial','active','past_due','canceled','expired')),
start_date timestamptz not null default now(),
end_date timestamptz,
next_billing_date timestamptz,
auto_renew boolean not null default true,
cancel_at_period_end boolean not null default false,
metadata jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_ml_user_subscriptions_user on ml_user_subscriptions(user_id);
create index if not exists idx_ml_user_subscriptions_plan on ml_user_subscriptions(plan_id);
create index if not exists idx_ml_user_subscriptions_status on ml_user_subscriptions(status);
-- updated_at trigger helper (idempotent)
create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $fn$
begin
new.updated_at = now();
return new;
end;
$fn$;
-- Recreate triggers safely
drop trigger if exists trg_ml_subscription_plans_updated on ml_subscription_plans;
create trigger trg_ml_subscription_plans_updated
before update on ml_subscription_plans
for each row execute function public.set_updated_at();
drop trigger if exists trg_ml_user_subscriptions_updated on ml_user_subscriptions;
create trigger trg_ml_user_subscriptions_updated
before update on ml_user_subscriptions
for each row execute function public.set_updated_at();
-- Optional: basic RLS scaffolding (customize policies per project standards)
-- alter table ml_user_subscriptions enable row level security;
-- create policy rls_ml_user_subscriptions_owner on ml_user_subscriptions
-- using (user_id::text = current_setting('app.user_id', true));
-- Done

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,332 +0,0 @@
-- =====================================================================================
-- 商城系统数据库状态检查脚本
-- 分析现有数据库结构生成个性化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 $$;

View File

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

View File

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

View File

@@ -1,666 +0,0 @@
-- =====================================================================================
-- 商城系统 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 $$;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
# Payment Page Documentation
**File Path:** `pages/mall/consumer/payment.uvue`
## 1. Overview
The Payment Page is the final step in the checkout process. It presents the user with the order total, allows selection of a payment method, and processes the transaction. It is fully integrated with the Supabase backend to ensure order status consistency.
## 2. Page Parameters (`onLoad`)
The page expects the following parameters when navigating to it:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `orderId` | String | **Yes** | The UUID of the order created in the previous step. |
| `amount` | Number | No | The total amount to pay. Defaults to fetching from DB if omitted. |
| `productAmount` | Number | No | Subtotal for products (for display). |
| `deliveryFee` | Number | No | Shipping cost (for display). |
| `discountAmount` | Number | No | Any discounts applied (for display). |
## 3. Data Integration (Supabase)
The page uses `supabaseService.uts` to interact with the database.
### 3.1 Loading Order Information
- **Method:** `supabaseService.getOrderDetail(orderId)`
- **Behavior:** Fetches `ml_orders` data.
- **Failover:** If DB load fails, falls back to `options.amount` or generates a mock Order No.
### 3.2 Loading User Balance
- **Method:** `supabaseService.getUserBalance()`
- **Behavior:** Fetches the current user's wallet balance to determine if "Balance Payment" is viable.
### 3.3 Processing Payment
- **Method:** `supabaseService.payOrder(orderId, method, amount)`
- **Updates Performed:**
- Sets `order_status` to `2` (Pending Delivery / Paid).
- Sets `payment_status` to `1` (Success).
- Sets `payment_method` (e.g., 'wechat', 'balance').
- Sets `payment_time`.
## 4. Key Features
### 4.1 Payment Methods
Supported methods (configured in `loadPaymentMethods`):
1. **WeChat Pay** (Simulated SDK)
2. **Alipay** (Simulated SDK)
3. **Balance Payment**
- Checks if `userBalance >= amount`.
- Requires 6-digit password input.
- Password verification is currently simulated.
4. **Bank Card** (Simulated)
### 4.2 User Interaction
- **Password Input:** A custom 6-digit dot display and numeric keypad overlay appear when "Balance Payment" is selected.
- **Interception:** `onBackPress` is intercepted to warn the user that cancelling will save the order as "Pending Payment".
## 5. State Management
- **Success Flow:**
1. `confirmPayment` validates inputs (balance, password).
2. Calls `payOrder` API.
3. Updates local storage `orders` cache (backup).
4. Emits `orderUpdated` global event (for Profile page refresh).
5. Redirects to `/pages/mall/consumer/payment-success`.
- **Cancellation Flow:**
1. User attempts to go back.
2. Modal prompts for confirmation.
3. On confirm, acts as "Pending Payment" (Status remains `1`).
4. Returns to previous page.
## 6. Future Improvements (TODO)
- **Real Payment SDK:** Integrate actual `uni.requestPayment` for WeChat/Alipay.
- **Server-Side Verification:** Move password verification to a Supabase Edge Function instead of frontend simulation.
- **Inventory Lock:** Ensure inventory is deducted atomically on the server side (currently simulated or implicit).

View File

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

View File

@@ -1,119 +0,0 @@
-- Subscription RLS and permissions
-- Purpose: Ensure admins can read/write ml_user_subscriptions and ml_subscription_plans;
-- consumers can only access their own subscriptions; everyone can read active plans.
-- Notes:
-- - Designed for Supabase (auth.uid(), auth.jwt()).
-- - Adjust table/column names if they differ in your DB.
-- 1) Helper: identify admin users
-- Prefer JWT app_metadata.role = 'admin' if you set it; fallback to ak_users.user_type = 5
-- (5 corresponds to ADMIN per MALL_USER_TYPE).
create or replace function public.is_admin()
returns boolean
language sql
stable
as $$
select coalesce(
-- Check custom claim from JWT: { app_metadata: { role: 'admin' } }
((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
-- Fallback: ak_users.user_type = 5 (cast to text for compatibility), match user by id as text
or exists (
select 1 from public.ak_users u
where u.id::text = auth.uid()::text
and u.user_type::text = '5'
)
, false);
$$;
comment on function public.is_admin is 'Returns true if current JWT/app user is admin by claim or ak_users.user_type=5.';
-- 2) Enable RLS on subscription tables
alter table if exists public.ml_subscription_plans enable row level security;
alter table if exists public.ml_user_subscriptions enable row level security;
grant select on table public.ml_subscription_plans to anon, authenticated;
grant select, insert, update, delete on table public.ml_subscription_plans to authenticated; -- limited by RLS
grant select, insert, update, delete on table public.ml_user_subscriptions to authenticated; -- limited by RLS
-- 4) Policies for ml_subscription_plans
-- 4.1 Everyone can read active plans
drop policy if exists ml_plans_select_active on public.ml_subscription_plans;
create policy ml_plans_select_active
on public.ml_subscription_plans
for select
to anon, authenticated
using (is_active = true);
-- 4.2 Admin can do anything
drop policy if exists ml_plans_admin_all on public.ml_subscription_plans;
create policy ml_plans_admin_all
on public.ml_subscription_plans
for all
to authenticated
using (public.is_admin())
with check (public.is_admin());
-- 5) Policies for ml_user_subscriptions
-- 5.1 Users can see their own subscriptions
drop policy if exists ml_user_subs_select_own on public.ml_user_subscriptions;
create policy ml_user_subs_select_own
on public.ml_user_subscriptions
for select
to authenticated
using (user_id = auth.uid());
-- 5.2 Users can create their own subscriptions (checkout)
drop policy if exists ml_user_subs_insert_own on public.ml_user_subscriptions;
create policy ml_user_subs_insert_own
on public.ml_user_subscriptions
for insert
to authenticated
with check (user_id = auth.uid());
-- 5.3 Users may update their own records (e.g., auto_renew, cancel_at_period_end)
-- NOTE: This allows updating any columns; for stricter control, add a BEFORE UPDATE trigger
-- that restricts column changes for non-admins.
drop policy if exists ml_user_subs_update_own on public.ml_user_subscriptions;
create policy ml_user_subs_update_own
on public.ml_user_subscriptions
for update
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());
-- 5.4 Admin can do anything on user subscriptions
drop policy if exists ml_user_subs_admin_all on public.ml_user_subscriptions;
create policy ml_user_subs_admin_all
on public.ml_user_subscriptions
for all
to authenticated
using (public.is_admin())
with check (public.is_admin());
-- 6) Optional: Trigger to limit non-admin updates to specific fields
-- Uncomment if you want to enforce column-level restrictions
-- create or replace function public.enforce_user_sub_update()
-- returns trigger language plpgsql as $$
-- begin
-- if public.is_admin() then
-- return new; -- admins can change anything
-- end if;
-- -- Only allow toggling auto_renew and cancel_at_period_end for owners
-- if new.user_id = auth.uid() then
-- 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;
-- -- allow: auto_renew, cancel_at_period_end
-- return new;
-- end if;
-- raise exception 'Forbidden';
-- end $$;
-- 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();
-- 7) Safety: ensure no rows are exposed to non-auth users except active plans via select policy above.
-- Admins authenticate as normal users with admin claim or ak_users.user_type=5.

File diff suppressed because one or more lines are too long

View File

@@ -21,7 +21,7 @@
<text class="action-icon">📝</text>
</view>
<view class="action-item" @click.stop="deleteAddress(item.id)">
<text class="action-icon"><EFBFBD></text>
<text class="action-icon">🗑</text>
</view>
</view>
</view>

View File

@@ -57,7 +57,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
const orderId = ref('')
const orderItemId = ref('') // Optional, if refunding specific item
@@ -92,19 +92,22 @@ onMounted(() => {
const loadOrderInfo = async () => {
try {
const { data, error } = await supa
.from('orders')
.select('actual_amount, delivery_fee')
.eq('id', orderId.value)
.single()
const orderData = await supabaseService.getOrderDetail(orderId.value)
if (error == null && data != null) {
maxAmount.value = data['actual_amount'] as number
deliveryFee.value = data['delivery_fee'] as number
if (orderData != null) {
const total = Number(orderData['total_amount'] ?? 0)
const shipping = Number(orderData['shipping_fee'] ?? 0)
maxAmount.value = total
deliveryFee.value = shipping
refundAmount.value = maxAmount.value.toString()
}
} catch (err) {
console.error('加载订单信息失败', err)
uni.showToast({
title: '加载订单失败',
icon: 'none'
})
}
}
@@ -132,53 +135,29 @@ const submitRefund = async () => {
submitting.value = true
try {
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
// 1. Create Refund Record
/* const { data, error } = await supa
.from('refunds')
.insert({
user_id: userId,
const result = await supabaseService.createRefund({
order_id: orderId.value,
refund_no: 'REF' + Date.now(),
refund_type: refundType.value,
refund_reason: refundReason.value,
refund_amount: amount,
description: description.value,
status: 1, // 待处理
status_history: [{
status: 1,
remark: '用户提交申请',
created_at: new Date().toISOString()
}]
description: description.value
})
if (error != null) throw error */
// MOCK SUBMIT
await new Promise(resolve => setTimeout(resolve, 1000))
// 2. Update Order Status (Optional, e.g. to "After-sales")
// Assuming status 6 is "After-sales/Refund"
/*
await supa
.from('orders')
.update({ status: 6 })
.eq('id', orderId.value)
*/
if (result.success) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.redirectTo({
url: '/pages/mall/consumer/refund'
})
// Go back to orders listing focused on refund type?
// or stay here? User probably wants to see list.
// Since profile redirects "Refunds" to orders list, let's go there.
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: result.message, icon: 'none' })
}
} catch (err) {
console.error('提交退款失败', err)
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
uni.showToast({ title: '提交异常', icon: 'none' })
} finally {
submitting.value = false
}

View File

@@ -1,810 +0,0 @@
<!-- 购物车页面 -->
<template>
<view class="cart-page">
<!-- 顶部栏 -->
<view class="cart-header">
<view class="header-title">
<text class="title-text">购物车</text>
</view>
<view v-if="selectedCount > 0" class="edit-btn" @click="toggleEditMode">
<text class="edit-text">{{ isEditMode ? '完成' : '编辑' }}</text>
</view>
</view>
<!-- 购物车为空 -->
<view v-if="cartItems.length === 0" class="empty-cart">
<text class="empty-icon">🛒</text>
<text class="empty-text">购物车还是空的</text>
<text class="empty-subtext">快去挑选心仪的商品吧</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 购物车列表 -->
<scroll-view v-else direction="vertical" class="cart-content">
<!-- 商品列表 -->
<view v-for="(item, index) in cartItems" :key="item.id" class="cart-item">
<view class="item-selector" @click="toggleSelectItem(item)">
<view :class="['select-icon', { selected: item.selected }]">
<text v-if="item.selected" class="icon-text">✓</text>
</view>
</view>
<image class="item-image" :src="item.product_image || '/static/default-product.png'" />
<view class="item-info">
<text class="item-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="item-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="item-price-row">
<text class="item-price">¥{{ item.price }}</text>
<view class="quantity-control">
<view class="quantity-btn minus" @click="decreaseQuantity(item)">-</view>
<text class="quantity-text">{{ item.quantity }}</text>
<view class="quantity-btn plus" @click="increaseQuantity(item)">+</view>
</view>
</view>
</view>
<view v-if="isEditMode" class="delete-btn" @click="removeItem(item, index)">
<text class="delete-text">删除</text>
</view>
</view>
<!-- 推荐商品 -->
<view v-if="recommendProducts.length > 0" class="recommend-section">
<view class="section-header">
<text class="section-title">猜你喜欢</text>
</view>
<view class="recommend-grid">
<view v-for="product in recommendProducts" :key="product.id" class="recommend-item" @click="viewProduct(product)">
<image class="recommend-image" :src="getProductFirstImage(product)" />
<text class="recommend-name">{{ product.name }}</text>
<text class="recommend-price">¥{{ product.price }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部结算栏 -->
<view v-if="cartItems.length > 0" class="bottom-bar">
<view class="select-all" @click="toggleSelectAll">
<view :class="['all-select-icon', { selected: isAllSelected }]">
<text v-if="isAllSelected" class="icon-text">✓</text>
</view>
<text class="select-all-text">全选</text>
</view>
<view v-if="!isEditMode" class="settlement-info">
<view class="total-price">
<text class="total-label">合计:</text>
<text class="total-value">¥{{ totalPrice.toFixed(2) }}</text>
</view>
<text class="total-desc">已选{{ selectedCount }}件</text>
</view>
<view v-if="isEditMode" class="edit-actions">
<view class="delete-all-btn" @click="removeSelected">
<text class="delete-all-text">删除({{ selectedCount }})</text>
</view>
</view>
<view v-else class="settle-btn" :class="{ disabled: selectedCount === 0 }" @click="goToCheckout">
<text class="settle-text">去结算</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import type { ProductType } from '@/types/mall-types.uts'
import supa from '@/components/supadb/aksupainstance.uts'
type CartItemType = {
id: string
user_id: string
product_id: string
sku_id: string
product_name: string
product_image: string
sku_specifications: any
price: number
quantity: number
selected: boolean
}
const cartItems = ref<Array<CartItemType>>([])
const recommendProducts = ref<Array<ProductType>>([])
const isEditMode = ref<boolean>(false)
const isLoading = ref<boolean>(false)
// 计算属性
const selectedCount = computed(() => {
return cartItems.value.filter(item => item.selected).length
})
const totalPrice = computed(() => {
return cartItems.value
.filter(item => item.selected)
.reduce((total, item) => total + (item.price * item.quantity), 0)
})
const isAllSelected = computed(() => {
return cartItems.value.length > 0 && cartItems.value.every(item => item.selected)
})
// 生命周期
onMounted(() => {
loadCartItems()
loadRecommendProducts()
})
// 加载购物车商品
const loadCartItems = async () => {
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
uni.navigateTo({
url: '/pages/user/login'
})
return
}
try {
const { data, error } = await supa
.from('shopping_cart')
.select(`
id,
user_id,
product_id,
sku_id,
quantity,
products (
name,
price,
images
),
product_skus (
specifications,
price as sku_price
)
`)
.eq('user_id', userId)
if (error !== null) {
console.error('加载购物车失败:', error)
return
}
const items: CartItemType[] = []
const cartData = data ?? []
for (let i = 0; i < cartData.length; i++) {
const item = cartData[i]
const product = item.products as any
const sku = item.product_skus as any
items.push({
id: item.id,
user_id: item.user_id,
product_id: item.product_id,
sku_id: item.sku_id,
product_name: product?.name || '未知商品',
product_image: product?.images?.[0] || '/static/default-product.png',
sku_specifications: sku?.specifications,
price: sku?.sku_price || product?.price || 0,
quantity: item.quantity,
selected: false
})
}
cartItems.value = items
} catch (err) {
console.error('加载购物车异常:', err)
}
}
// 加载推荐商品
const loadRecommendProducts = async () => {
try {
const { data, error } = await supa
.from('products')
.select('*')
.eq('status', 1)
.order('sales', { ascending: false })
.limit(6)
if (error !== null) {
console.error('加载推荐商品失败:', error)
return
}
recommendProducts.value = data ?? []
} catch (err) {
console.error('加载推荐商品异常:', err)
}
}
// 获取当前用户ID
const getCurrentUserId = (): string | null => {
// 这里应该从全局状态或storage中获取
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || null
}
// 获取商品第一张图片
const getProductFirstImage = (product: ProductType): string => {
return product.images?.[0] || '/static/default-product.png'
}
// 获取规格文本
const getSpecText = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs)
.map(key => `${key}: ${specs[key]}`)
.join('; ')
}
return String(specs)
}
// 切换编辑模式
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
}
// 切换商品选择
const toggleSelectItem = (item: CartItemType) => {
item.selected = !item.selected
cartItems.value = [...cartItems.value]
}
// 全选/取消全选
const toggleSelectAll = () => {
const newSelectedState = !isAllSelected.value
cartItems.value.forEach(item => {
item.selected = newSelectedState
})
cartItems.value = [...cartItems.value]
}
// 增加数量
const increaseQuantity = async (item: CartItemType) => {
if (isLoading.value) return
isLoading.value = true
try {
const newQuantity = item.quantity + 1
const { error } = await supa
.from('shopping_cart')
.update({ quantity: newQuantity })
.eq('id', item.id)
if (error !== null) {
console.error('更新数量失败:', error)
uni.showToast({
title: '更新失败',
icon: 'none'
})
return
}
item.quantity = newQuantity
cartItems.value = [...cartItems.value]
} catch (err) {
console.error('更新数量异常:', err)
} finally {
isLoading.value = false
}
}
// 减少数量
const decreaseQuantity = async (item: CartItemType) => {
if (item.quantity <= 1) {
removeItem(item, cartItems.value.indexOf(item))
return
}
if (isLoading.value) return
isLoading.value = true
try {
const newQuantity = item.quantity - 1
const { error } = await supa
.from('shopping_cart')
.update({ quantity: newQuantity })
.eq('id', item.id)
if (error !== null) {
console.error('更新数量失败:', error)
uni.showToast({
title: '更新失败',
icon: 'none'
})
return
}
item.quantity = newQuantity
cartItems.value = [...cartItems.value]
} catch (err) {
console.error('更新数量异常:', err)
} finally {
isLoading.value = false
}
}
// 移除单个商品
const removeItem = async (item: CartItemType, index: number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除这个商品吗?',
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supa
.from('shopping_cart')
.delete()
.eq('id', item.id)
if (error !== null) {
console.error('删除商品失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
return
}
cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value]
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('删除商品异常:', err)
}
}
}
})
}
// 移除选中商品
const removeSelected = () => {
const selectedItems = cartItems.value.filter(item => item.selected)
if (selectedItems.length === 0) {
uni.showToast({
title: '请选择要删除的商品',
icon: 'none'
})
return
}
uni.showModal({
title: '批量删除',
content: `确定要删除选中的${selectedItems.length}件商品吗?`,
success: async (res) => {
if (res.confirm) {
const deletePromises = selectedItems.map(item =>
supa
.from('shopping_cart')
.delete()
.eq('id', item.id)
)
try {
await Promise.all(deletePromises)
cartItems.value = cartItems.value.filter(item => !item.selected)
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (err) {
console.error('批量删除异常:', err)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}
})
}
// 查看商品详情
const viewProduct = (product: ProductType) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${product.id}`
})
}
// 去逛逛
const goShopping = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
// 去结算
const goToCheckout = () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择要结算的商品',
icon: 'none'
})
return
}
const selectedItems = cartItems.value.filter(item => item.selected)
const productIds = selectedItems.map(item => ({
product_id: item.product_id,
sku_id: item.sku_id,
quantity: item.quantity
}))
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
success: (res) => {
res.eventChannel.emit('acceptData', {
selectedItems: productIds,
totalAmount: totalPrice.value
})
}
})
}
</script>
<style scoped>
.cart-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.cart-header {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
flex: 1;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.edit-btn {
padding: 5px 10px;
}
.edit-text {
color: #007aff;
font-size: 14px;
}
.empty-cart {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 20px;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #007aff;
color: #ffffff;
padding: 10px 40px;
border-radius: 25px;
font-size: 14px;
border: none;
}
.cart-content {
flex: 1;
}
.cart-item {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
display: flex;
align-items: center;
position: relative;
}
.item-selector {
margin-right: 10px;
}
.select-icon {
width: 20px;
height: 20px;
border: 1px solid #cccccc;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.select-icon.selected {
background-color: #007aff;
border-color: #007aff;
}
.icon-text {
color: #ffffff;
font-size: 12px;
}
.item-image {
width: 80px;
height: 80px;
border-radius: 5px;
margin-right: 10px;
}
.item-info {
flex: 1;
min-height: 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-name {
font-size: 14px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
}
.item-spec {
font-size: 12px;
color: #999999;
margin-bottom: 10px;
}
.item-price-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-price {
font-size: 16px;
color: #ff4757;
font-weight: bold;
}
.quantity-control {
display: flex;
align-items: center;
border: 1px solid #e5e5e5;
border-radius: 15px;
overflow: hidden;
}
.quantity-btn {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #666666;
background-color: #f8f8f8;
}
.quantity-btn.minus {
border-right: 1px solid #e5e5e5;
}
.quantity-btn.plus {
border-left: 1px solid #e5e5e5;
}
.quantity-text {
width: 40px;
text-align: center;
font-size: 14px;
color: #333333;
}
.delete-btn {
position: absolute;
right: 15px;
bottom: 15px;
padding: 5px 10px;
background-color: #ff4757;
border-radius: 12px;
}
.delete-text {
color: #ffffff;
font-size: 12px;
}
.recommend-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.section-header {
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.recommend-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.recommend-item {
width: 48%;
margin-bottom: 15px;
}
.recommend-image {
width: 100%;
height: 120px;
border-radius: 5px;
margin-bottom: 8px;
}
.recommend-name {
font-size: 13px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.recommend-price {
font-size: 14px;
color: #ff4757;
font-weight: bold;
}
.bottom-bar {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.select-all {
display: flex;
align-items: center;
}
.all-select-icon {
width: 20px;
height: 20px;
border: 1px solid #cccccc;
border-radius: 10px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.all-select-icon.selected {
background-color: #007aff;
border-color: #007aff;
}
.select-all-text {
font-size: 14px;
color: #333333;
}
.settlement-info {
flex: 1;
margin-left: 20px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.total-price {
display: flex;
align-items: baseline;
margin-bottom: 5px;
}
.total-label {
font-size: 14px;
color: #333333;
}
.total-value {
font-size: 18px;
color: #ff4757;
font-weight: bold;
}
.total-desc {
font-size: 12px;
color: #999999;
}
.edit-actions {
flex: 1;
margin-left: 20px;
display: flex;
justify-content: flex-end;
}
.delete-all-btn {
background-color: #ff4757;
padding: 8px 20px;
border-radius: 15px;
}
.delete-all-text {
color: #ffffff;
font-size: 14px;
}
.settle-btn {
background-color: #007aff;
padding: 10px 30px;
border-radius: 20px;
margin-left: 20px;
}
.settle-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
.settle-text {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -168,7 +168,7 @@
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem } from '@/utils/supabaseService.uts'
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
// 响应式数据
const cartItems = ref<any[]>([])
@@ -176,78 +176,22 @@ const recommendProducts = ref<any[]>([])
const loading = ref<boolean>(false)
const statusBarHeight = ref(0)
const isManageMode = ref(false)
const mockRecommendProducts = [
{
id: 'rec_001',
shopId: 'shop_rec_1',
shopName: '潮流运动旗舰店',
name: '运动保温杯',
price: 59,
image: 'https://picsum.photos/100/100?random=11',
specification: '颜色:星空黑 | 容量500ml | 材质304不锈钢',
specDetails: {
color: '星空黑',
capacity: '500ml',
material: '304不锈钢'
}
},
{
id: 'rec_002',
shopId: 'shop_rec_2',
shopName: '智能家居生活馆',
name: '声波电动牙刷',
price: 129,
image: 'https://picsum.photos/100/100?random=12',
specification: '颜色:珍珠白 | 刷头敏感型×2 | 续航30天',
specDetails: {
color: '珍珠白',
brushHead: '敏感型×2',
batteryLife: '30天'
}
},
{
id: 'rec_003',
shopId: 'shop_rec_3',
shopName: '健康防护专家店',
name: '医用护理口罩',
price: 29.9,
image: 'https://picsum.photos/100/100?random=13',
specification: '规格:三层防护 | 数量50只独立装 | 执行标准YY0469',
specDetails: {
layers: '三层防护',
quantity: '50只',
standard: 'YY0469'
}
},
{
id: 'rec_004',
shopId: 'shop_rec_4',
shopName: '户外运动装备店',
name: '专业护膝',
price: 45,
image: 'https://picsum.photos/100/100?random=14',
specification: '尺码L码 | 材质:记忆棉+弹力布 | 适用:篮球/跑步',
specDetails: {
size: 'L码',
material: '记忆棉+弹力布',
suitableFor: '篮球/跑步'
}
}
]
const updatingItems = ref<Set<string>>(new Set()) // Track items being updated to prevent race conditions
// 计算属性
const cartGroups = computed(() => {
const groups = new Map<string, any>()
cartItems.value.forEach(item => {
if (!groups.has(item.shopId)) {
groups.set(item.shopId, {
// Build a unique key for the shop
const shopKey = item.shopId || 'unknown'
if (!groups.has(shopKey)) {
groups.set(shopKey, {
shopId: item.shopId,
shopName: item.shopName,
shopName: item.shopName || '商城优选', // Better default name
items: [] as any[]
})
}
const group = groups.get(item.shopId)
const group = groups.get(shopKey)
if (group) {
group.items.push(item)
}
@@ -304,35 +248,59 @@ const loadCartData = async () => {
const supabaseCartItems = await supabaseService.getCartItems()
// 转换数据格式以匹配前端界面
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem) => ({
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem) => {
// 调试日志:打印每条商品数据的关键字段
console.log(`CartItem raw: id=${item.id}, shop_id=${item.shop_id}, shop_name=${item.shop_name}, name=${item.product_name}, price=${item.product_price}`);
return {
id: item.id,
shopId: item.shop_id || 'unknown_shop',
shopName: item.shop_name || '未知店铺',
name: item.product_name || '商品',
price: item.product_price || 0,
image: item.product_image || '/static/product1.jpg',
spec: item.product_specification || '默认规格',
// 关键修复确保shopId有值如果后端返回null/undefined使用'default_shop'作为分组键
shopId: (item.shop_id != null && item.shop_id !== '') ? item.shop_id : 'default_shop',
// 关键修复确保shopName有值
shopName: (item.shop_name != null && item.shop_name !== '') ? item.shop_name : '商城优选',
name: item.product_name || '未知商品',
price: item.product_price != null ? item.product_price : 0,
image: item.product_image || '/static/images/default-product.png',
spec: item.product_specification || '标准规格',
quantity: item.quantity || 1,
selected: item.selected || false,
productId: item.product_id // 保留productId用于后续操作
}))
productId: item.product_id,
skuId: item.sku_id,
merchantId: item.merchant_id
}
})
console.log('Transformed items count:', transformedItems.length);
cartItems.value = transformedItems
// 加载推荐商品(暂时保持Mock数据
recommendProducts.value = [...mockRecommendProducts]
// 加载推荐商品(优先获取推荐位商品,如果没有则通过搜索获取热销商品
let recommends = await supabaseService.getRecommendedProducts(6)
// 如果没有设置推荐商品,则获取热销商品作为补充
if (recommends.length === 0) {
const hotResp = await supabaseService.searchProducts('', 1, 6, 'sales')
recommends = hotResp.data
}
if (recommends.length > 0) {
recommendProducts.value = recommends.map((p: Product) => {
return {
id: p.id,
shopId: p.merchant_id || 'unknown',
shopName: p.shop_name || '商城推荐',
name: p.name,
price: p.base_price,
image: p.main_image_url || '/static/images/default-product.png',
specification: '', // 推荐列表不显示详细规格
specDetails: {}
}
})
} else {
recommendProducts.value = []
}
} catch (error) {
console.error('加载购物车数据失败:', error)
// 如果API调用失败尝试从本地存储加载
const cartData = uni.getStorageSync('cart')
if (cartData) {
try {
cartItems.value = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
cartItems.value = []
}
}
} finally {
loading.value = false
}
@@ -340,6 +308,7 @@ const loadCartData = async () => {
// 商品操作 - 更新选中状态到Supabase
const toggleSelect = async (itemId: string) => {
// 乐观更新
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
const newSelected = !cartItems.value[index].selected
@@ -353,15 +322,17 @@ const toggleSelect = async (itemId: string) => {
// 恢复状态
cartItems.value[index].selected = !newSelected
cartItems.value = [...cartItems.value]
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
}
}
}
const toggleShopSelect = async (shopId: string) => {
// 查找该组是否已存在,并判断目标状态
const group = cartGroups.value.find((g: any) => g.shopId === shopId)
if (!group) return
// 检查当前是否全选
// 检查当前是否全选: 如果所有都选中,则目标是全不选(false);否则全选(true)
const isAllShopSelected = (group.items as any[]).every((item: any) => item.selected)
const newState = !isAllShopSelected
@@ -370,19 +341,28 @@ const toggleShopSelect = async (shopId: string) => {
.filter(item => item.shopId === shopId)
.map(item => item.id)
// 批量更新到Supabase
const success = await supabaseService.batchUpdateCartItemSelection(shopItemIds, newState)
if (success) {
// 更新本地状态
// 乐观更新本地状态
const oldStates = new Map<string, boolean>()
cartItems.value.forEach(item => {
if (item.shopId === shopId) {
oldStates.set(item.id, item.selected)
item.selected = newState
}
})
cartItems.value = [...cartItems.value]
} else {
// 批量更新到Supabase
const success = await supabaseService.batchUpdateCartItemSelection(shopItemIds, newState)
if (!success) {
console.error('批量更新店铺商品选中状态失败')
// 回滚
cartItems.value.forEach(item => {
if (item.shopId === shopId && oldStates.has(item.id)) {
item.selected = oldStates.get(item.id)!
}
})
cartItems.value = [...cartItems.value]
uni.showToast({
title: '操作失败',
icon: 'none'
@@ -391,20 +371,26 @@ const toggleShopSelect = async (shopId: string) => {
}
const toggleSelectAll = async () => {
// 目标状态:如果当前全选,则取消全选;否则全选
const newSelectedState = !allSelected.value
// 乐观更新
const oldItems = JSON.parse(JSON.stringify(cartItems.value))
const selectedItems = cartItems.value.map(item => ({
...item,
selected: newSelectedState
}))
cartItems.value = selectedItems
// 更新到Supabase
const itemIds = cartItems.value.map(item => item.id)
if (itemIds.length === 0) return
const success = await supabaseService.batchUpdateCartItemSelection(itemIds, newSelectedState)
if (success) {
cartItems.value = selectedItems
} else {
if (!success) {
console.error('批量更新选中状态失败')
cartItems.value = oldItems
uni.showToast({
title: '操作失败',
icon: 'none'
@@ -413,38 +399,50 @@ const toggleSelectAll = async () => {
}
const increaseQuantity = async (itemId: string) => {
if (updatingItems.value.has(itemId)) return
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
updatingItems.value.add(itemId)
const newQuantity = cartItems.value[index].quantity + 1
cartItems.value[index].quantity = newQuantity
cartItems.value = [...cartItems.value]
// 更新到Supabase
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
updatingItems.value.delete(itemId)
if (!success) {
console.error('更新商品数量失败')
// 恢复状态
cartItems.value[index].quantity = newQuantity - 1
cartItems.value = [...cartItems.value]
uni.showToast({ title: '更新失败', icon: 'none' })
}
}
}
const decreaseQuantity = async (itemId: string) => {
if (updatingItems.value.has(itemId)) return
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
if (cartItems.value[index].quantity > 1) {
updatingItems.value.add(itemId)
const newQuantity = cartItems.value[index].quantity - 1
cartItems.value[index].quantity = newQuantity
cartItems.value = [...cartItems.value]
// 更新到Supabase
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
updatingItems.value.delete(itemId)
if (!success) {
console.error('更新商品数量失败')
// 恢复状态
cartItems.value[index].quantity = newQuantity + 1
cartItems.value = [...cartItems.value]
uni.showToast({ title: '更新失败', icon: 'none' })
}
} else {
// 数量为1时询问是否删除
@@ -583,14 +581,17 @@ const goToCheckout = () => {
return
}
// 获取选中的商品 (直接过滤cartItems不依赖cartGroups)
// 获取选中的商品 (直接过滤cartItems不依赖cartGroups,确保扁平化传递)
const selectedItems = cartItems.value
.filter(item => item.selected)
.map(item => ({
id: item.id,
product_id: item.id, // 使用商品ID作为product_id
sku_id: item.id, // 使用商品ID作为sku_id
product_id: item.productId || item.id,
sku_id: item.skuId || item.id,
product_name: item.name,
shop_id: item.shopId, // 关键保留shopId用于分组
shop_name: item.shopName, // 关键保留shopName
merchant_id: item.merchantId,
product_image: item.image,
sku_specifications: item.spec,
price: Number(item.price), // 确保是数字
@@ -599,13 +600,20 @@ const goToCheckout = () => {
// 关键修复:将结算数据写入 Storage确保 checkout 页面能稳定获取
uni.setStorageSync('checkout_type', 'cart')
// 使用纯JSON序列化防止复杂对象引发的问题
try {
uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
} catch (e) {
console.error('存储结算数据失败', e)
uni.showToast({ title: '系统异常,请重试', icon: 'none' })
return
}
// 跳转到结算页面并传递数据
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
success: (res) => {
// 通过eventChannel传递数据
// 通过eventChannel传递数据 (作为备份)
res.eventChannel.emit('acceptData', {
selectedItems: selectedItems
})
@@ -1186,50 +1194,59 @@ const goToCheckout = () => {
background-color: white;
margin: 10px;
border-radius: 12px;
padding: 15px;
padding: 10px 15px; /* 减小内边距 */
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.action-bar-content {
display: flex;
flex-direction: row;
flex-direction: row; /* 强制横向 */
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
}
.action-left, .action-right {
.action-left {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
flex-shrink: 0;
}
.action-right {
display: flex;
flex-direction: row; /* 强制横向 */
align-items: center;
justify-content: flex-end;
flex: 1;
min-width: 0; /* 防止溢出 */
min-width: 0;
}
/* 合计信息区域 - 自适应横向排列 */
/* 合计信息区域 */
.total-info {
display: flex;
flex-direction: row; /* 强制横向 */
align-items: center;
margin-right: 12px;
flex-shrink: 0;
margin-right: 10px;
flex-shrink: 1; /* 允许压缩 */
overflow: hidden;
}
.total-text {
font-size: 14px;
color: #333;
margin-right: 5px;
margin-right: 2px;
white-space: nowrap;
flex-shrink: 0;
}
.total-price {
font-size: 18px;
font-size: 16px;
color: #ff5000;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 结算按钮 */
@@ -1238,20 +1255,22 @@ const goToCheckout = () => {
color: white;
border: none;
border-radius: 25px;
padding: 8px 20px;
padding: 6px 16px; /* 减小按钮内边距 */
font-size: 14px;
white-space: nowrap;
flex-shrink: 0;
margin: 0; /* 移除可能的margin */
}
.delete-btn {
background-color: #ff3b30; /* 红色删除按钮 */
padding: 8px 25px;
background-color: #ff3b30;
padding: 6px 20px;
}
/* 全选区域 */
.select-all {
display: flex;
flex-direction: row; /* 强制横向 */
align-items: center;
}
@@ -1265,25 +1284,27 @@ const goToCheckout = () => {
/* 响应式调整 */
/* 手机端小屏幕优化 */
@media screen and (max-width: 375px) {
.action-bar-content {
gap: 8px;
.cart-action-bar {
padding: 10px;
margin: 10px 5px; /* 减小外边距增加可用宽度 */
}
.total-text {
font-size: 13px;
font-size: 12px;
}
.total-price {
font-size: 16px;
font-size: 15px;
}
.checkout-btn, .delete-btn {
padding: 8px 15px;
font-size: 13px;
padding: 6px 12px;
font-size: 12px;
}
.select-all-text {
font-size: 13px;
margin-left: 4px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -48,7 +48,12 @@
</scroll-view>
<!-- 右侧商品列表 -->
<scroll-view scroll-y class="product-content">
<scroll-view
scroll-y
class="product-content"
@scrolltolower="loadMore"
:lower-threshold="50"
>
<!-- 分类标题 -->
<view class="category-header">
<text class="category-title">{{ currentCategoryName }}</text>
@@ -63,30 +68,29 @@
class="product-card"
@click="navigateToProduct(product)"
>
<view class="product-badge" v-if="product.badge">{{ product.badge }}</view>
<view class="product-badge" v-if="product.is_hot">热销</view>
<image
class="product-image"
:src="product.image"
:src="product.main_image_url"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.specification }}</text>
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
<text class="price-value">{{ product.base_price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
¥{{ product.market_price }}
</text>
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.manufacturer }}</text>
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sales }}</text>
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
</view>
</view>
@@ -123,6 +127,8 @@ const activePrimary = ref<string>('')
const cartCount = ref(3)
const hasMore = ref(true)
const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
const currentPage = ref(1)
const loading = ref(false)
// 获取当前分类信息
const currentCategoryName = ref('')
@@ -166,17 +172,29 @@ const loadCategories = async () => {
// 加载商品数据
const loadProducts = async () => {
if (loading.value) return
if (!activePrimary.value) {
console.warn('activePrimary为空无法加载商品')
return
}
loading.value = true
try {
if (activePrimary.value) {
console.log('开始加载商品分类ID:', activePrimary.value)
const response = await supabaseService.getProductsByCategory(activePrimary.value)
console.log('开始加载商品分类ID:', activePrimary.value, '页码:', currentPage.value)
const response = await supabaseService.getProductsByCategory(activePrimary.value, currentPage.value)
console.log('商品加载结果:', {
dataCount: response.data.length,
total: response.total,
hasmore: response.hasmore
hasmore: response.hasmore,
page: currentPage.value
})
if (currentPage.value === 1) {
productList.value = response.data
} else {
productList.value.push(...response.data)
}
hasMore.value = response.hasmore
// 更新当前分类信息
@@ -184,19 +202,25 @@ const loadProducts = async () => {
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description || ''
console.log('当前分类信息:', category.name, '描述:', category.description)
} else {
console.warn('未找到对应的分类信息分类ID:', activePrimary.value)
}
console.log('商品列表加载完成,数量:', productList.value.length)
} else {
console.warn('activePrimary为空无法加载商品')
}
console.log('商品列表加载完成,当前总数量:', productList.value.length)
} catch (error) {
console.error('加载商品数据失败:', error)
if (currentPage.value === 1) {
productList.value = []
}
} finally {
loading.value = false
}
}
// 加载更多
const loadMore = () => {
if (hasMore.value && !loading.value) {
currentPage.value++
loadProducts()
}
}
// 页面加载时处理参数 - 这是处理分类切换的主要入口
@@ -382,15 +406,15 @@ const selectPrimaryCategory = async (categoryId: string) => {
console.log('准备加载商品数据...')
// 加载对应商品 - 使用 Supabase 服务
const response = await supabaseService.getProductsByCategory(categoryId)
productList.value = response.data
hasMore.value = response.hasmore
// 重置分页并加载
currentPage.value = 1
hasMore.value = true
await loadProducts()
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', response.data.length)
console.log('商品列表:', response.data)
console.log('商品数量:', productList.value.length)
console.log('商品列表:', productList.value)
// 验证数据是否已正确更新
console.log('数据更新验证:')
@@ -403,55 +427,42 @@ const selectPrimaryCategory = async (categoryId: string) => {
}
// 添加到购物车
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' })
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_default',
shopName: product.shopName || product.manufacturer || '自营店铺',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
const success = await supabaseService.addToCart(product.id, 1)
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
cartCount.value++
} else {
uni.showToast({
title: '添加失败,请先登录',
icon: 'none'
})
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.showToast({ title: '操作失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
// 导航函数
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
const navigateToCart = () => uni.navigateTo({ url: '/pages/mall/consumer/cart' })
const navigateToProduct = (product: any) => {
const id = product.id
const price = (product.base_price || 0).toString()
const originalPrice = (product.market_price || '').toString()
const name = encodeURIComponent(product.name || '')
const image = encodeURIComponent(product.main_image_url || '')
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}&name=${encodeURIComponent(product.name)}&image=${encodeURIComponent(product.image || '')}`
url: `/pages/mall/consumer/product-detail?id=${id}&productId=${id}&price=${price}&originalPrice=${originalPrice}&name=${name}&image=${image}`
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -120,6 +120,7 @@
<script setup lang="uts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
// 响应式数据
const messages = ref<any[]>([])
@@ -168,16 +169,25 @@ const mockMessages = [
// 生命周期
onMounted(() => {
loadChatHistory()
// 模拟客服自动回复
setTimeout(() => {
addReceivedMessage('查询到您的订单正在打包中,预计今天下午发货')
}, 3000)
})
// 加载聊天记录
const loadChatHistory = () => {
messages.value = [...mockMessages]
const loadChatHistory = async () => {
const rawMsgs = await supabaseService.getUserChatMessages()
messages.value = rawMsgs.reverse().map((m: ChatMessage) => {
const date = new Date(m.created_at || new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
// Use explicit 'as' casting to avoid type errors if needed, though map handles it
const msg : any = {
id: m.id,
type: m.is_from_user ? 'sent' : 'received',
content: m.content,
time: timeStr
}
return msg
})
// 滚动到底部
setTimeout(() => {
@@ -186,7 +196,7 @@ const loadChatHistory = () => {
}
// 发送消息
const sendMessage = () => {
const sendMessage = async () => {
const content = inputMessage.value.trim()
if (!content) return
@@ -204,6 +214,9 @@ const sendMessage = () => {
// 滚动到底部
scrollToBottom()
// Backend Save
await supabaseService.sendChatMessage(content)
// 模拟客服回复2秒后
setTimeout(() => {
simulateCustomerReply()
@@ -211,7 +224,7 @@ const sendMessage = () => {
}
// 模拟客服回复
const simulateCustomerReply = () => {
const simulateCustomerReply = async () => {
const replies = [
'好的,已为您记录',
'这个问题需要进一步核实',
@@ -221,6 +234,9 @@ const simulateCustomerReply = () => {
]
const randomReply = replies[Math.floor(Math.random() * replies.length)]
await supabaseService.simulateServiceReply(randomReply)
addReceivedMessage(randomReply)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,15 +20,22 @@
</view>
</view>
<!-- 商品列表 -->
<!-- 商品列表 (按店铺分组) -->
<view class="products-section">
<!-- 调试信息 -->
<view v-if="checkoutItems.length > 0" class="debug-info">
<text class="debug-text">调试:共{{ checkoutItems.length }}件商品,总价计算:{{ totalAmount }}</text>
<text class="debug-text">共 {{ checkoutItems.length }} 件商品</text>
</view>
<view v-if="checkoutItems.length > 0">
<view v-for="item in checkoutItems" :key="item.id" class="product-item">
<image class="product-image" :src="item.product_image" />
<view v-if="shopGroups.length > 0">
<view v-for="group in shopGroups" :key="group.shopId" class="shop-group">
<view class="shop-header">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ group.shopName }}</text>
</view>
<view v-for="item in group.items" :key="item.id" class="product-item">
<image class="product-image" :src="item.product_image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
@@ -38,6 +45,17 @@
</view>
</view>
</view>
<!-- 店铺小计 -->
<view class="shop-subtotal">
<text class="subtotal-label">配送方式</text>
<text class="subtotal-value">快递 免邮</text>
</view>
<view class="shop-subtotal">
<text class="subtotal-text">小计: </text>
<text class="subtotal-price">¥{{ getGroupTotal(group) }}</text>
</view>
</view>
</view>
<view v-else class="no-products">
<text class="no-products-text">暂无商品信息</text>
@@ -286,6 +304,8 @@ type CheckoutItemType = {
sku_specifications: any
price: number
quantity: number
shop_id?: string
shop_name?: string
}
type DeliveryOptionType = {
@@ -329,6 +349,32 @@ const showSaveConfirm = ref<boolean>(false)
const smartAddressInput = ref<string>('')
// 计算属性 - 修复价格同步问题
// 按店铺分组商品
const shopGroups = computed(() => {
const groups = new Map<string, any>()
checkoutItems.value.forEach(item => {
// 使用类型断言访问可能的额外属性
const rawItem = item as any
const shopId = rawItem.shop_id || 'unknown'
if (!groups.has(shopId)) {
groups.set(shopId, {
shopId: shopId,
shopName: rawItem.shop_name || '商城优选',
merchant_id: rawItem.merchant_id || rawItem.shop_id,
items: [] as any[]
})
}
groups.get(shopId).items.push(item)
})
return Array.from(groups.values())
})
const getGroupTotal = (group: any) => {
return group.items.reduce((sum: number, item: any) => {
return sum + (Number(item.price) * Number(item.quantity))
}, 0).toFixed(2)
}
const totalAmount = computed(() => {
console.log('计算商品总价checkoutItems:', checkoutItems.value)
if (!checkoutItems.value || checkoutItems.value.length === 0) {
@@ -405,34 +451,38 @@ watch(checkoutItems, (newItems) => {
// 页面加载时监听eventChannel
onLoad(() => {
// 优先检查Storage中是否有"立即购买"的数据
let dataLoaded = false
// 优先检查Storage中是否有结算数据 (支持 buy_now 和 cart 两种模式)
const checkoutType = uni.getStorageSync('checkout_type')
if (checkoutType === 'buy_now') {
console.log('检测到立即购买模式从Storage加载数据')
if (checkoutType === 'buy_now' || checkoutType === 'cart') {
console.log(`检测到结算模式(${checkoutType})从Storage加载数据`)
const itemsStr = uni.getStorageSync('checkout_items')
if (itemsStr) {
try {
const items = JSON.parse(itemsStr as string)
console.log('从Storage加载的商品数据:', items)
if (items && Array.isArray(items) && items.length > 0) {
processCheckoutItems(items)
// 清除Storage避免污染下次进入刷新页面时可能需要保留暂时不清除或者在离开页面时清除
// uni.removeStorageSync('checkout_type')
// uni.removeStorageSync('checkout_items')
loadDefaultAddress()
return // 成功加载,直接返回
dataLoaded = true
}
} catch (e) {
console.error('解析立即购买数据失败', e)
console.error('解析结算数据失败', e)
}
}
}
// 如果没有从checkout_items加载到数据则尝试从通用购物车Storage加载 (回退方案)
if (!dataLoaded) {
console.log('未找到预结算数据,尝试从购物车本地存储加载')
loadFromLocalStorage()
} else {
// 如果已经加载了数据还需要单独加载地址因为loadFromLocalStorage通常会附带加载地址
loadDefaultAddress()
}
// 从上一页获取数据
const eventChannel = uni.getEventChannel ? uni.getEventChannel() : null
// 默认先尝试从本地存储加载(确保有数据)
loadFromLocalStorage()
if (eventChannel) {
eventChannel.on('acceptData', (data: any) => {
console.log('接收到商品数据:', data)
@@ -539,9 +589,6 @@ const loadFromLocalStorage = () => {
if (selectedCartItems.length > 0) {
// 转换为CheckoutItemType格式
const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => {
// 确保价格和数量是数字
let price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
if (isNaN(price)) price = 0
let quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
@@ -553,7 +600,7 @@ const loadFromLocalStorage = () => {
product_name: item.name || '',
product_image: item.image || '',
sku_specifications: item.spec ? { spec: item.spec } : {},
price: price,
price: Number(item.price) || 0,
quantity: quantity
}
})
@@ -1247,8 +1294,7 @@ const submitOrder = async () => {
uni.showLoading({ title: '提交中...' })
try {
const userId = getCurrentUserId()
// 确保使用当前登录用户ID (如果本地存储为空,可能需要处理)
const userId = supabaseService.getCurrentUserId()
if (!userId) {
uni.hideLoading()
uni.showToast({
@@ -1258,59 +1304,77 @@ const submitOrder = async () => {
return
}
// 准备订单项数据
// 注意:需根据 checkoutItems 的实际结构转换为 createOrder 需要的 CartItem 结构
// 假设 checkoutItems 已经包含了 product_id, quantity, price, name, image 等字段
const orderItems = checkoutItems.value.map((item: any): any => ({
id: item.id || '', // 这是一个临时ID或者购物车IDcreateOrder 中会使用 product_id
product_id: item.product_id || item.id, // 确保有 product_id
// 准备按店铺分组数据
const groups = shopGroups.value.map((group: any): any => {
return {
merchant_id: group.merchant_id || group.shopId,
shopName: group.shopName,
items: group.items.map((item: any): any => ({
id: item.id, // 用于清理购物车
product_id: item.product_id,
sku_id: item.sku_id,
quantity: item.quantity,
price: item.price,
product_name: item.name,
product_image: item.image,
spec: item.spec,
checked: true
product_name: item.product_name,
product_image: item.product_image,
specifications: item.sku_specifications // 保持原始对象createOrder 会处理序列化
}))
}
})
// 调用 Supabase 服务创建订单
const result = await supabaseService.createOrder(
userId,
selectedAddress.value!.id, // 地址ID
actualAmount.value, // 实付金额
orderItems
)
// 调用 Supabase 服务创建多店铺订单
const result = await supabaseService.createOrdersByShop({
shipping_address: selectedAddress.value,
shopGroups: groups,
deliveryFee: deliveryFee.value,
discountAmount: discountAmount.value
})
uni.hideLoading()
if (result.success) {
// 清除购买的商品 (如果来自购物车,应该在 createOrder 成功后清除,或者这里手动清除本地存储)
// 这里我们假设购物车清理逻辑可能在 createOrder 后端处理,或者需要在这里清除本地
// 清除结算商品
try {
uni.removeStorageSync('checkout_items')
uni.removeStorageSync('checkout_type')
} catch(e) {
console.error('清除结算商品失败', e)
}
const activeOrderId = result.data as string
const orderIds = result.orderIds
// 跳转支付页面
if (orderIds.length === 1) {
// 单个订单跳转支付
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${activeOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
url: `/pages/mall/consumer/payment?orderId=${orderIds[0]}&amount=${actualAmount.value}`
})
} else {
throw new Error(result.error)
// 多个订单跳转到订单列表
uni.showToast({
title: `成功创建${orderIds.length}个订单`,
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/mall/consumer/orders'
})
}, 1500)
}
} else {
throw new Error(result.error || '创建订单失败')
}
} catch (err: any) {
uni.hideLoading()
console.error('创建订单失败:', err)
console.error('提交订单错误:', err)
uni.showToast({
title: err.message || '订单创建失败',
title: err.message || '提交订单失败',
icon: 'none'
})
}
}
// 生成订单号
const generateOrderNo = (): string => {
const date = new Date()
@@ -1463,6 +1527,62 @@ const goBack = () => {
text-align: center;
}
.shop-group {
background-color: #fff;
margin: 10px 0;
border-radius: 12px;
padding: 10px;
}
.shop-header {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.shop-icon {
font-size: 18px;
margin-right: 8px;
}
.shop-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.shop-subtotal {
display: flex;
justify-content: flex-end; /* 右对齐 */
align-items: center;
padding-top: 10px;
margin-top: 5px;
border-top: 1px dashed #f0f0f0;
font-size: 14px;
}
.subtotal-label {
color: #666;
margin-right: 10px;
}
.subtotal-value {
color: #333;
}
.subtotal-text {
color: #333;
margin-right: 5px;
}
.subtotal-price {
color: #ff4757;
font-weight: bold;
font-size: 16px;
}
.product-item {
display: flex;
padding: 15px 0;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
# 功能与页面状态详解
本文档详细记录了 Consumer App 各个功能模块的实现状态与业务逻辑。
> **状态图例**: ✅ 正常 (已对接真实 DB) | 🚧 开发中 | ❌ 未开始
## 🛍️ 核心购物流程
| 功能模块 | 页面路径 | 状态 | 备注 |
| :--- | :--- | :--- | :--- |
| **首页** | `pages/mall/consumer/index.uvue` | ✅ 正常 | 金刚区、Banner、推荐商品流。 |
| **分类** | `pages/mall/consumer/category.uvue` | ✅ 正常 | 一级/二级分类联动,跳转搜索结果。 |
| **商品详情** | `pages/mall/consumer/product-detail.uvue` | ✅ 正常 | SKU 选择、加入购物车、立即购买。 |
| **购物车** | `pages/mall/consumer/cart.uvue` | ✅ 正常 | 数量增减、勾选计算、结算校验。 |
| **结算页** | `pages/mall/consumer/checkout.uvue` | ✅ 正常 | 选择地址、运费计算、创建订单。 |
| **收银台** | `pages/mall/consumer/payment.uvue` | ✅ 正常 | 模拟支付流程,更新订单为“待发货”。 |
## 👤 个人中心 (Profile)
**文件**: `pages/mall/consumer/profile.uvue`
* **资产看板**: 实时加载积分、余额、优惠券数量。
* **订单看板**: 待支付、待发货、待收货、退款/售后(跳转至 `refund.uvue`)。
* **服务矩阵**:
* **地址管理**: `address-list.uvue` (CRUD 正常)
* **我的收藏**: `favorites.uvue` (商品/店铺收藏 正常)
* **浏览足迹**: `footprint.uvue` (按日期分组 正常)
* **在线客服**: 跳转至 `chat.uvue`
* **消息通知**: 跳转至 `messages.uvue`
## 📦 订单管理体系
**列表页**: `pages/mall/consumer/orders.uvue`
* **状态筛选**: 全部 / 待支付 / 待发货 / 待收货 / 已完成。
* **核心操作**:
* **去支付**: 跳转收银台。
* **确认收货**: 变更状态为已完成。
* **申请售后**: 跳转 `apply-refund.uvue` (带入订单信息)。
**详情页**: `pages/mall/consumer/order-detail.uvue`
* **信息展示**: 完整的地址、商品规格、金额明细、时间线。
* **业务状态**: 根据 `order_status` 动态展示可操作按钮。
## 🔄 售后服务体系 (Refunds)
**申请页**: `pages/mall/consumer/apply-refund.uvue`
* **功能**: 支持仅退款/退货退款。
* **逻辑**: 自动获取订单最大可退金额,防止超额申请。
* **提交**: 数据写入 `ml_refunds` 表。
**记录页**: `pages/mall/consumer/refund.uvue`
* **列表**: 展示所有历史售后申请及其当前状态。
* **进度**: 可视化展示审核进度 (目前模拟进度条)。
## 💬 社交与互动
| 功能模块 | 页面路径 | 状态 | 说明 |
| :--- | :--- | :--- | :--- |
| **在线客服** | `pages/mall/consumer/chat.uvue` | ✅ 正常 | 支持文本/表情发送,历史记录持久化。 |
| **消息中心** | `pages/mall/consumer/messages.uvue` | ✅ 正常 | 聚合系统通知订单消息与客服消息。 |
| **商品评价** | `pages/mall/consumer/review.uvue` | ✅ 正常 | 支持星级评分与文本评价。 |

View File

@@ -0,0 +1,44 @@
# 测试数据生成指南 (Mock Data Guide)
为了有效测试消费者端前端功能,我们编写了 SQL 脚本来向数据库填充真实的模拟数据。
## 📂 脚本位置
所有脚本均位于 `doc_mall/consumer/sql/` 目录下。
## 🛠 使用说明
### 1. 修复现有数据问题 (优先级最高)
**脚本**: `fix_order_items_data.sql`
* **适用场景**: 如果您的订单列表中,商品显示为空白图片或缺失名称。
* **功能**:
*`ml_product_skus``ml_products` 表自动回填缺失的 `image_url`
* 修正占位符形式的 `product_name`
* 补充缺失的 `specifications` (如:规格参数)。
### 2. 生成新的测试订单
**脚本**: `add_mock_orders_corrected.sql`
* **适用场景**: 为测试用户 (`test@mall.com`) 创建一批全新的订单数据。
* **生成数据包含**:
* 1x **待支付** 订单
* 1x **待发货** 订单 (已支付)
* 1x **待收货** 订单 (已发货)
* 1x **已完成** 订单
* **注意**: 该脚本会随机选取数据库中现有的真实商品,确保数据关联正确无误。
### 3. 生成评价测试数据
**脚本**: `add_mock_reviews_for_test_user.sql`
* **适用场景**: 测试“我的评价”列表或商品详情页的评价展示。
* **功能**: 创建已完成的订单,并自动为其添加一条带图片的 5 星好评。
## 🧪 建议测试流程
1. **运行** `add_mock_orders_corrected.sql`
2. **打开 App** > 个人中心 (Profile) > 我的订单。
3. **验证**:
* 各状态标签页下是否有对应的订单。
* 商品图片和名称是否显示正常。
* 在“待收货”或“已完成”订单上点击**申请售后**,验证是否跳转正确。

View File

@@ -0,0 +1,62 @@
# 商城消费者端 (Consumer) 开发文档
本文档包含了商城消费者前端模块的详细开发指南和状态说明。
## 📂 项目结构
消费者端模块位于 `pages/mall/consumer/` 目录下,基于 UniApp x (UTS/UVUE) 开发。
### 核心页面清单
| 页面文件 | 描述 | 当前状态 |
|-----------|-------------|--------|
| `index.uvue` | 首页 (商城门面) | ✅ 正常 |
| `category.uvue` | 商品分类浏览 | ✅ 正常 |
| `cart.uvue` | 购物车 | ✅ 正常 |
| `profile.uvue` | 个人中心 (用户主页) | ✅ 正常 |
| `orders.uvue` | 订单列表管理 | ✅ 正常 |
| `order-detail.uvue` | 订单详情页 | ✅ 正常 |
| `apply-refund.uvue` | 申请售后 (退款/退货) | ✅ 正常 |
| `refund.uvue` | 售后记录列表 | ✅ 正常 |
| `chat.uvue` | 在线客服 | ✅ 正常 |
| `messages.uvue` | 消息通知中心 | ✅ 正常 |
### 关键功能实现
1. **用户个人中心**:
* 可视化展示用户状态(积分、余额)。
* 订单状态快捷入口(待支付、待发货、待收货等)。
* **我的服务**: 优惠券、地址、收藏夹。
* **新功能**: 已集成“评价”入口(跳转至待评价订单)和“退款/售后”入口(跳转至售后记录页)。
2. **订单管理**:
* 多状态标签页切换(全部、待支付、待收货、已完成)。
* **操作**: 支付、取消、提醒发货、确认收货、评价、**申请售后**。
*`supabaseService` 后端服务实时交互。
3. **售后系统**:
* 独立页面 `apply-refund.uvue`:支持仅退款/退货退款,关联原订单金额。
* 售后列表 `refund.uvue`:查看历史退款记录及进度。
4. **客服与消息**:
* 在线客服 `chat.uvue`:支持文本/表情发送,消息持久化存储。
* 消息中心 `messages.uvue`:聚合系统通知与客服消息。
## 🛠 技术栈
* **框架**: UniApp x (Vue 3 + UTS)
* **后端**: Supabase (PostgreSQL)
* **语言**: UTS (TypeScript 方言)
* **样式**: SCSS / UVUE Styles
## 🚀 快速开始
1. **环境准备**: 确保已安装 HBuilderX 并配置好 UniApp x 插件。
2. **数据库**: 运行 `doc_mall/consumer/sql/` 下的 SQL 脚本初始测试数据。
3. **运行**: 在 HBuilderX 中打开项目,运行到 Web 浏览器或 App 模拟器。
## 📚 文档索引
* [功能与页面状态详解](./FEATURES_&_PAGES.md)
* [Supabase 集成与数据库架构](./SUPABASE_INTEGRATION.md)
* [测试数据生成指南](./MOCK_DATA_GUIDE.md)

View File

@@ -0,0 +1,121 @@
# Supabase 集成与数据库架构 (Consumer App)
本文档详细描述了消费者端 (Consumer App) 涉及的所有数据库集成点、核心表结构以及 `supabaseService.uts` 提供的 API 服务。
> **更新时间**: 2026-02-03
> **状态**: 已完成核心业务闭环 (订单、支付、售后、客服、足迹等)
## 🗄️ 核心数据架构
消费者端业务依赖以下核心数据库表:
### 1. 交易与订单 (Orders & Transactions)
| 表名 | 描述 | 关键字段 |
| :--- | :--- | :--- |
| `ml_orders` | 订单主表 | `id`, `user_id`, `merchant_id`, `order_status` (1:待付, 2:待发, 3:待收, 4:完成, 5:取消), `total_amount` |
| `ml_order_items` | 订单商品明细 | `order_id`, `product_id`, `image_url` (快照), `specifications` (快照) |
| `ml_refunds` | **[新增]** 售后/退款申请 | `order_id`, `reason_category`, `refund_amount`, `status` (0:待审, 1:同意, 2:拒绝), `refund_type` (1:仅退款, 2:退货退款) |
### 2. 互动与消息 (Interaction & Communication)
| 表名 | 描述 | 关键字段 |
| :--- | :--- | :--- |
| `ml_chat_messages` | **[新增]** 客服聊天记录 | `session_id`, `sender_id`, `receiver_id`, `content`, `msg_type`, `is_from_user` |
| `ml_notifications` | 消息通知 | `type` (system/order/promotion), `title`, `is_read` |
| `ml_product_reviews` | 商品评价 | `order_id`, `product_id`, `rating`, `content`, `images` |
### 3. 用户行为 (User Behavior)
| 表名 | 描述 | 关键字段 |
| :--- | :--- | :--- |
| `ml_browsing_history` | 足迹/浏览记录 | `user_id`, `product_id`, `view_time` |
| `ml_favorites` | 收藏夹 | `user_id`, `target_id`, `type` (1:商品, 2:店铺) |
| `ml_user_addresses` | 收货地址 | `user_id`, `receiver_name`, `phone`, `province`... |
---
## 🔌 API 服务层 (`utils/supabaseService.uts`)
所有后端交互通过单例 `supabaseService` 进行,主要模块如下:
### 1. 售后/退款服务 (Refunds)
> **状态**: ✅ 已集成 (apply-refund.uvue)
```typescript
// 创建退款/售后申请
async createRefund(data: {
order_id: string,
refund_type: number,
refund_amount: number,
reason_category: string,
description: string,
images: string[]
}): Promise<boolean>
```
### 2. 在线客服/消息服务 (Chat & Messages)
> **状态**: ✅ 已集成 (chat.uvue, messages.uvue)
```typescript
// 获取当前用户的聊天记录
async getUserChatMessages(): Promise<ChatMessage[]>
// 发送聊天消息 (持久化到 ml_chat_messages)
async sendChatMessage(content: string, type: string = 'text'): Promise<boolean>
// (测试用) 模拟客服自动回复
async simulateServiceReply(content: string): Promise<boolean>
```
### 3. 订单与支付 (Orders & Payment)
> **状态**: ✅ 已集成 (checkout.uvue, payment.uvue, orders.uvue)
```typescript
// 创建订单 (由购物车或直接购买触发)
async createOrder(orderData: any): Promise<string | null>
// 获取订单详情 (包含商品明细)
async getOrderDetail(orderId: string): Promise<any | null>
// 支付订单 (模拟支付,更新订单状态 1->2记录支付时间)
async payOrder(orderId: string, paymentMethod: string, amount: number): Promise<boolean>
// 确认收货 (3->4)
async confirmReceipt(orderId: string): Promise<Result>
```
### 4. 商品与搜索 (Products)
> **状态**: ✅ 已集成 (search.uvue, product-detail.uvue)
```typescript
// 搜索商品 (支持关键词、分类、价格排序、销量排序)
async searchProducts(keyword: string, page: number, pageSize: number, sort: string, asc: boolean): Promise<PaginatedResponse<Product>>
// 获取足迹
async getFootprints(): Promise<any[]>
```
---
## 📊 页面集成状态一览表
| 页面模块 | 文件路径 | 数据源状态 | 说明 |
| :--- | :--- | :--- | :--- |
| **首页** | `pages/mall/consumer/index.uvue` | ✅ Real DB | 金刚区、推荐商品已接入 |
| **搜索** | `pages/mall/consumer/search.uvue` | ✅ Real DB | 关键词搜索、排序、分页正常 |
| **购物车** | `pages/mall/consumer/cart.uvue` | ✅ Real DB | 加减购、结算校验正常 |
| **结算台** | `pages/mall/consumer/checkout.uvue` | ✅ Real DB | 地址选择、订单创建正常 |
| **收银台** | `pages/mall/consumer/payment.uvue` | ✅ Real DB | 读取待付金额,更新支付状态 |
| **订单列表** | `pages/mall/consumer/orders.uvue` | ✅ Real DB | 状态筛选 (全部/待付/待收/退款) 正常 |
| **订单详情** | `pages/mall/consumer/order-detail.uvue` | ✅ Real DB | 地址、商品、金额展示正常 |
| **申请售后** | `pages/mall/consumer/apply-refund.uvue` | ✅ Real DB | **[本次完成]** 关联订单金额,提交至 `ml_refunds` |
| **在线客服** | `pages/mall/consumer/chat.uvue` | ✅ Real DB | **[本次完成]** 消息收发持久化,支持历史记录 |
| **消息中心** | `pages/mall/consumer/messages.uvue` | ✅ Real DB | 能够统计未读客服消息数 |
| **我的评价** | `pages/mall/consumer/review.uvue` | ✅ Real DB | 提交评价至 `ml_product_reviews` |
## 🛠️ 下一步维护建议
1. **异常处理**: 目前部分接口在网络异常时仅打印 `console.error`,建议增加全局统一的 Toasts 提示。
2. **图片上传**: 目前退款和评价中的图片上传依赖 Mock 或简单路径,需对接真实的 OSS/Supabase Storage 文件上传。
3. **实时消息**: 目前 `chat.uvue` 使用 polling (轮询) 或手动刷新Supabase 支持 Realtime Subscription后续可升级为 WebSocket 实时推送。

View File

@@ -65,26 +65,69 @@ const loadFavorites = async () => {
// Map response
favorites.value = res.map((item: any): Product => {
const prod = item.ml_products
let prod: any = null
if (item instanceof UTSJSONObject) {
prod = item.get('ml_products')
} else {
prod = item['ml_products']
}
let image = '/static/default-product.png'
if (prod) {
if (prod.main_image_url) image = prod.main_image_url
else if (prod.image_url) image = prod.image_url
else if (prod.image_urls) {
// Try parse
let id = ''
let name = '未知商品'
let price = 0
let sales = 0
if (prod != null) {
if (prod instanceof UTSJSONObject) {
id = prod.getString('id') || ''
name = prod.getString('name') || '未知商品'
price = prod.getNumber('base_price') || 0
image = prod.getString('main_image_url') || image
sales = prod.getNumber('sale_count') || 0
// 如果 main_image_url 为空,尝试解析 image_urls
if (image === '/static/default-product.png') {
const imgUrls = prod.getString('image_urls')
if (imgUrls) {
try {
const arr = JSON.parse(prod.image_urls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0]
const arr = JSON.parse(imgUrls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0] as string
} catch(e) {}
}
}
} else {
id = (prod['id'] as string) || ''
name = (prod['name'] as string) || '未知商品'
price = (prod['base_price'] as number) || 0
image = (prod['main_image_url'] as string) || image
sales = (prod['sale_count'] as number) || 0
if (image === '/static/default-product.png') {
const imgUrls = prod['image_urls'] as string
if (imgUrls) {
try {
const arr = JSON.parse(imgUrls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0] as string
} catch(e) {}
}
}
}
} else {
// 如果没取到商品,尝试直接从 item 取 target_id
if (item instanceof UTSJSONObject) {
id = item.getString('target_id') || ''
} else {
id = (item['target_id'] as string) || ''
}
}
return {
id: prod?.id || item.target_id,
name: prod?.name || '未知商品',
price: prod?.price || 0,
id: id,
name: name,
price: price,
image: image,
sales: prod?.sales || 0,
sales: sales,
shopId: '',
shopName: ''
}
@@ -109,8 +152,11 @@ const removeFavorite = async (id: string) => {
content: '确定要取消收藏该商品吗?',
success: async (res) => {
if (res.confirm) {
const success = await supabaseService.toggleFavorite(id) // Toggle removes if exists
if (success) {
// toggleFavorite 返回最新的状态true=已收藏false=未收藏
const isStillFavorite = await supabaseService.toggleFavorite(id)
if (!isStillFavorite) {
// 现在的状态是"未收藏",说明取消成功
// Remove from local list
const index = favorites.value.findIndex(item => item.id === id)
if (index !== -1) {
@@ -120,6 +166,11 @@ const removeFavorite = async (id: string) => {
title: '已取消收藏',
icon: 'none'
})
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}
}

View File

@@ -83,6 +83,7 @@
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type FootprintType = {
id: string
@@ -131,10 +132,70 @@ onMounted(() => {
})
// 加载足迹数据
const loadFootprints = (loadMore: boolean = false) => {
const loadFootprints = async (loadMore: boolean = false) => {
isLoading.value = true
// 从本地存储获取足迹数据
try {
const remoteData = await supabaseService.getFootprints()
if (remoteData.length > 0) {
console.log('获取到远程足迹数据:', remoteData.length)
// 使用远程数据
footprints.value = remoteData.map((item: any): FootprintType => {
let id = ''
let name = ''
let price = 0
let original_price = 0
let image = ''
let sales = 0
let shopId = ''
let shopName = ''
let viewTime = 0
if (item instanceof UTSJSONObject) {
id = item.getString('id') || ''
name = item.getString('name') || ''
price = item.getNumber('price') || 0
original_price = item.getNumber('original_price') || 0
image = item.getString('image') || ''
sales = item.getNumber('sales') || 0
shopId = item.getString('shopId') || ''
shopName = item.getString('shopName') || ''
viewTime = item.getNumber('viewTime') || 0
} else {
id = (item['id'] as string) || ''
name = (item['name'] as string) || ''
price = (item['price'] as number) || 0
original_price = (item['original_price'] as number) || 0
image = (item['image'] as string) || ''
sales = (item['sales'] as number) || 0
shopId = (item['shopId'] as string) || ''
shopName = (item['shopName'] as string) || ''
viewTime = (item['viewTime'] as number) || 0
}
return {
id: id,
name: name,
price: price,
original_price: original_price,
image: image,
sales: sales,
shopId: shopId,
shopName: shopName,
viewTime: viewTime,
selected: false
} as FootprintType
})
// 更新本地缓存
const dataToSave = footprints.value.map(item => {
const { selected, ...rest } = item
return rest
})
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
} else {
// 如果远程为空,尝试加载本地
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
@@ -150,6 +211,23 @@ const loadFootprints = (loadMore: boolean = false) => {
} else {
footprints.value = []
}
}
} catch (e) {
console.error('加载足迹失败', e)
// 失败时加载本地
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
footprints.value = data.map(item => ({
...item,
selected: false
}))
} catch (err) {
footprints.value = []
}
}
}
isLoading.value = false
hasMore.value = false // 本地存储一次性加载完
@@ -262,9 +340,18 @@ const deleteSelected = () => {
uni.showModal({
title: '确认删除',
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 从列表中移除
uni.showLoading({ title: '删除中' })
// 远程删除
for (const item of selectedItems) {
await supabaseService.deleteFootprint(item.id)
}
uni.hideLoading()
// 从列表移除
footprints.value = footprints.value.filter(item => !item.selected)
// 保存回本地存储

View File

@@ -39,52 +39,14 @@
<scroll-view
scroll-y
class="main-scroll"
:style="{ height: scrollHeight + 'px' }"
refresher-enabled
:refresher-triggered="refreshing"
:lower-threshold="50"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
@scroll="handleScroll"
>
<!-- 智能健康卡片 -->
<view class="smart-health-card" :style="{ marginTop: (statusBarHeight + 44 + 10) + 'px' }">
<view class="health-content">
<view class="health-header">
<text class="health-title">智能健康助手</text>
<text class="health-subtitle">根据您的健康数据推荐</text>
</view>
<view class="health-tips">
<text class="tip-item">💡 按时用药提醒</text>
<text class="tip-item">📋 健康记录跟踪</text>
<text class="tip-item">🩺 在线问诊咨询</text>
</view>
</view>
</view>
<!-- 智能分类网格 - 完全响应式 -->
<view class="smart-categories">
<view class="section-header">
<text class="section-title">智能分类</text>
<text class="section-desc">快速定位所需药品</text>
</view>
<view class="category-grid">
<view
v-for="category in categories"
:key="category.id"
class="category-card"
@click="switchCategory(category)"
:style="{ '--card-color': category.color }"
>
<view class="card-icon">
<text>{{ category.icon }}</text>
</view>
<text class="card-name">{{ category.name }}</text>
<text class="card-desc">{{ category.desc }}</text>
</view>
</view>
</view>
<!-- 健康资讯轮播 -->
<!-- 健康资讯轮播 (Moved Up) -->
<view class="health-news">
<view class="news-header">
<text class="news-title">健康资讯</text>
@@ -120,8 +82,48 @@
</swiper>
</view>
<!-- 智能服务入口 -->
<view class="smart-services">
<!-- 智能健康卡片 (Hidden) -->
<!-- <view class="smart-health-card" :style="{ marginTop: (statusBarHeight + 44 + 10) + 'px' }">
<view class="health-content">
<view class="health-header">
<text class="health-title">智能健康助手</text>
<text class="health-subtitle">根据您的健康数据推荐</text>
</view>
<view class="health-tips">
<text class="tip-item">💡 按时用药提醒</text>
<text class="tip-item">📋 健康记录跟踪</text>
<text class="tip-item">🩺 在线问诊咨询</text>
</view>
</view>
</view> -->
<!-- 智能分类网格 - 完全响应式 -->
<view class="smart-categories">
<view class="section-header">
<text class="section-title">智能分类</text>
<text class="section-desc">快速定位所需药品</text>
</view>
<view class="category-grid">
<view
v-for="category in categories"
:key="category.id"
class="category-card"
@click="switchCategory(category)"
:style="{ '--card-color': category.color }"
>
<view class="card-icon">
<text>{{ category.icon }}</text>
</view>
<text class="card-name">{{ category.name }}</text>
<text class="card-desc">{{ category.desc }}</text>
</view>
</view>
</view>
<!-- 健康资讯轮播 (Original Position - Removed) -->
<!-- 智能服务入口 (Hidden) -->
<!-- <view class="smart-services">
<view class="services-grid">
<view class="service-card" @click="navigateToConsultation">
<view class="service-icon" style="background: #2196F3;">
@@ -152,14 +154,14 @@
<text class="service-desc">健康管理助手</text>
</view>
</view>
</view>
</view> -->
<!-- 热销药品专区 -->
<view class="hot-products">
<view class="section-header">
<view class="title-section">
<text class="section-icon">🔥</text>
<text class="section-title">热销品</text>
<text class="section-title">热销品</text>
</view>
<view class="sort-tabs">
<text
@@ -180,30 +182,30 @@
class="product-card"
@click="navigateToProduct(product)"
>
<view class="product-badge" v-if="product.badge">{{ product.badge }}</view>
<view class="product-badge" v-if="product.is_hot">热销</view>
<image
class="product-image"
:src="product.image"
:src="product.main_image_url"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.specification }}</text>
<!-- spec is omitted if not available -->
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
<text class="price-value">{{ product.base_price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
¥{{ product.market_price }}
</text>
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.manufacturer }}</text>
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sales }}</text>
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
</view>
@@ -216,10 +218,14 @@
</view>
</view>
</view>
<!-- 加载状态提示 -->
<view class="load-more-status" v-if="loading || showLoadMore">
<text class="loading-text">正在加载更多商品...</text>
</view>
</view>
<!-- 家庭常备药 -->
<view class="family-medicine">
<!-- 家庭常备药 (Hidden) -->
<!-- <view class="family-medicine">
<view class="section-header">
<view class="title-section">
<text class="section-icon">🏠</text>
@@ -242,12 +248,12 @@
<text class="family-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view> -->
<!-- 智能推荐模块已隐藏 -->
<!-- 健康提醒 -->
<view class="health-reminder">
<!-- 健康提醒 (Hidden) -->
<!-- <view class="health-reminder">
<view class="reminder-content">
<text class="reminder-icon">⏰</text>
<view class="reminder-text">
@@ -258,7 +264,7 @@
<text class="action-text">查看</text>
</view>
</view>
</view>
</view> -->
<!-- 底部安全区域 -->
<view class="safe-area"></view>
@@ -280,7 +286,7 @@ const refreshing = ref(false)
const loading = ref(false)
const isFirstShow = ref(true)
const hasMore = ref(true)
const activeSort = ref('sales')
const activeSort = ref('recommend') // 默认展示智能推荐
const activeFilter = ref('recommend')
const currentPage = ref(1)
@@ -290,6 +296,7 @@ const recommendedProducts = ref<Product[]>([])
// 屏幕尺寸检测
const isMobile = ref(false)
const showLoadMore = ref(false)
// 导航栏显示控制
const showNavbar = ref(true)
@@ -358,12 +365,12 @@ const loadCategories = async () => {
}
// 获取热销商品(根据当前排序方式)
const loadHotProducts = async () => {
const loadHotProducts = async (targetLimit: number = 6) => {
try {
let products: Product[] = []
const limit = 6
const limit = targetLimit
console.log('加载热销商品,当前排序方式:', activeSort.value)
console.log('加载热销商品,当前排序方式:', activeSort.value, 'limit:', limit)
switch (activeSort.value) {
case 'sales':
@@ -522,9 +529,8 @@ const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 计算滚动区域高度 - 使用整个窗口高度
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight
// 计算滚动区域高度 - 不再需要手动计算,使用 Flex 布局自动撑开
// scrollHeight.value = windowHeight - 50
// 检测屏幕尺寸
const screenWidth = systemInfo.screenWidth || systemInfo.windowWidth
@@ -609,6 +615,7 @@ const switchCategory = (category: any) => {
// 切换排序
const switchSort = (sortId: string) => {
activeSort.value = sortId
hasMore.value = true // 重置加载更多状态
// 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts()
}
@@ -641,75 +648,77 @@ const onRefresh = () => {
// 加载更多
const loadMore = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
try {
// 增加限制以加载更多推荐商品
const currentLimit = recommendedProducts.value.length + 6
await loadRecommendedProducts(currentLimit)
// 假设如果返回的商品数量小于请求的限制,则没有更多数据
if (recommendedProducts.value.length < currentLimit) {
hasMore.value = false
console.log('=== 触发触底事件 ===')
if (loading.value) {
console.log('正在加载中,跳过')
return
}
showLoadMore.value = true
loading.value = true
try {
// 获取当前热销商品的数量
const currentCount = hotProducts.value.length
const nextLimit = currentCount + 6
console.log('开始加载更多,当前数量:', currentCount, '目标数量:', nextLimit)
// 加载更多热销商品
await loadHotProducts(nextLimit)
// 检查是否还有更多数据
if (hotProducts.value.length === currentCount) {
hasMore.value = false
uni.showToast({
title: '加载完成',
icon: 'success'
})
} catch (error) {
console.error('加载更多失败:', error)
uni.showToast({
title: '加载失败',
title: '没有更多了',
icon: 'none'
})
} else {
// 还有数据,或者是刚加载了一批
/* uni.showToast({
title: '加载完成',
icon: 'success'
}) */
}
} catch (error) {
console.error('加载更多失败:', error)
} finally {
loading.value = false
// 稍微延迟隐藏加载条,让用户看到
setTimeout(() => {
showLoadMore.value = false
}, 500)
}
}
// 添加到购物车
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' })
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_default',
shopName: product.shopName || product.manufacturer || '自营店铺',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
// 尝试调用 Supabase 服务添加
const success = await supabaseService.addToCart(product.id, 1)
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
// 失败(如未登录),回退到本地存储或提示登录
// 这里简单提示失败
uni.showToast({
title: '添加失败,请先登录',
icon: 'none'
})
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.showToast({
title: '操作异常',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
// 导航函数
@@ -718,18 +727,16 @@ const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
const navigateToProduct = (product: any) => {
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
// 移除 URLSearchParams 内部的 encodeURIComponent因为 append 会自动编码
// 或者直接构建 URL 字符串以确保兼容性
const name = product.name || ''
const image = product.image || '/static/product1.jpg'
const price = product.price?.toString() || '0'
const originalPrice = (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()
// 使用 main_image_url
const image = product.main_image_url || product.image || '/static/product1.jpg'
const price = (product.base_price || product.price || 0).toString()
const originalPrice = (product.market_price || product.original_price || (parseFloat(price) * 1.2).toFixed(2))?.toString()
// 手动构建URL避免双重编码问题
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
url: `/pages/mall/consumer/product-detail?id=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
})
}
const navigateToCategory = (item: any) => {
@@ -754,7 +761,8 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.medic-home {
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
background: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
line-height: 1.5;
@@ -762,6 +770,12 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
flex-direction: column;
}
.main-scroll {
flex: 1;
height: 1px; /* 让 flex 生效并允许滚动 */
width: 100%;
}
/* 智能导航栏 - 重新设计布局 */
.smart-navbar {
position: fixed;
@@ -1222,13 +1236,17 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
/* 产品网格 */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
display: block;
column-count: 2;
column-gap: 10px;
margin-top: 20px;
min-height: 500px; /* 确保有足够高度触发滚动 */
padding-bottom: 20px;
}
.product-card {
break-inside: avoid;
margin-bottom: 10px;
background: #f8f9fa;
border-radius: 12px;
overflow: hidden;
@@ -1796,6 +1814,19 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
display: none; /* 隐藏描述 */
}
.load-more-status {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.loading-text {
color: #888;
font-size: 14px;
}
.products-grid,
.recommend-grid {
grid-template-columns: repeat(2, 1fr); /* 手机端调整为双列显示 */
@@ -1964,7 +1995,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(2, 1fr);
column-count: 2;
}
.recommend-grid {
@@ -2031,7 +2062,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(3, 1fr);
column-count: 3;
}
.recommend-grid {
@@ -2068,7 +2099,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(4, 1fr);
column-count: 4;
}
.recommend-grid {
@@ -2092,7 +2123,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.products-grid {
grid-template-columns: repeat(4, 1fr);
column-count: 4;
}
.recommend-grid {

View File

@@ -180,30 +180,30 @@
class="product-card"
@click="navigateToProduct(product)"
>
<view class="product-badge" v-if="product.badge">{{ product.badge }}</view>
<view class="product-badge" v-if="product.is_hot">热销</view>
<image
class="product-image"
:src="product.image"
:src="product.main_image_url"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.specification }}</text>
<!-- spec is omitted if not available -->
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
<text class="price-value">{{ product.base_price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
¥{{ product.market_price }}
</text>
</view>
<view class="product-meta">
<text class="manufacturer">{{ product.manufacturer }}</text>
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text>
<view class="sales-info">
<text class="sales-count">已售{{ product.sales }}</text>
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
</view>
@@ -271,12 +271,14 @@ import { ref, reactive, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import supabaseService from '@/utils/supabaseService.uts'
import type { Product, Category } from '@/utils/supabaseService.uts'
import { getCurrentUser } from '@/utils/store.uts'
// 响应式数据
const statusBarHeight = ref(0)
const scrollHeight = ref(0)
const refreshing = ref(false)
const loading = ref(false)
const isFirstShow = ref(true)
const hasMore = ref(true)
const activeSort = ref('sales')
const activeFilter = ref('recommend')
@@ -334,13 +336,13 @@ const healthNews = [
const loadCategories = async () => {
try {
const categoriesData = await supabaseService.getCategories()
// 映射字段:将description映射为desc保持与原有结构兼容
// 映射字段:根据ml_categories表结构映射
categories.value = categoriesData.map((cat: any) => ({
id: cat.id,
name: cat.name,
icon: cat.icon || '📦',
desc: cat.description || cat.desc || '',
color: cat.color || '#4CAF50'
icon: cat.icon_url || '📦', // 使用icon_url字段
desc: cat.description || '', // 使用description字段
color: '#4CAF50' // 默认颜色表中可能没有color字段
}))
} catch (error) {
console.error('加载分类数据失败:', error)
@@ -408,6 +410,13 @@ const loadRecommendedProducts = async (limit: number = 6) => {
// 初始化数据
const initData = async () => {
// 首先确保用户资料已加载
try {
await getCurrentUser()
console.log('主页初始化:用户资料加载完成')
} catch (error) {
console.error('加载用户资料失败:', error)
}
await loadCategories()
await loadHotProducts()
await loadRecommendedProducts()
@@ -489,6 +498,22 @@ onShow(() => {
// 让分类页面在成功读取后自行清除
// 这样可以确保分类页面能正确读取到传递的数据
// 每次页面显示时尝试更新用户资料
if (!isFirstShow.value) {
getCurrentUser().then(profile => {
if (profile) {
console.log('主页onShow用户资料更新成功')
} else {
console.log('主页onShow用户资料为空可能未登录')
}
}).catch(error => {
console.error('主页onShow加载用户资料失败:', error)
})
} else {
isFirstShow.value = false
console.log('主页首次显示跳过onShow中的用户资料检查交由initData处理')
}
console.log('=== index页面onShow执行完成 ===')
})
@@ -645,46 +670,33 @@ const loadMore = async () => {
}
// 添加到购物车
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' })
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_default',
shopName: product.shopName || product.manufacturer || '自营店铺',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
// 尝试调用 Supabase 服务添加
const success = await supabaseService.addToCart(product.id, 1)
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
// 失败(如未登录),回退到本地存储或提示登录
// 这里简单提示失败
uni.showToast({
title: '添加失败,请先登录',
icon: 'none'
})
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.showToast({
title: '操作异常',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
// 导航函数
@@ -693,18 +705,16 @@ const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
const navigateToProduct = (product: any) => {
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
// 移除 URLSearchParams 内部的 encodeURIComponent因为 append 会自动编码
// 或者直接构建 URL 字符串以确保兼容性
const name = product.name || ''
const image = product.image || '/static/product1.jpg'
const price = product.price?.toString() || '0'
const originalPrice = (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()
// 使用 main_image_url
const image = product.main_image_url || product.image || '/static/product1.jpg'
const price = (product.base_price || product.price || 0).toString()
const originalPrice = (product.market_price || product.original_price || (parseFloat(price) * 1.2).toFixed(2))?.toString()
// 手动构建URL避免双重编码问题
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
url: `/pages/mall/consumer/product-detail?id=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
})
}
const navigateToCategory = (item: any) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,634 +0,0 @@
<!-- pages/mall/consumer/messages.uvue -->
<template>
<view class="messages-page">
<!-- 顶部标题栏 -->
<view class="messages-header">
<text class="header-title">消息</text>
<view class="header-actions">
<text class="action-icon" @click="clearAllUnread">📝</text>
</view>
</view>
<!-- 消息分类标签 -->
<view class="message-tabs">
<view
v-for="tab in messageTabs"
:key="tab.id"
:class="['tab-item', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.unread > 0" class="tab-badge">{{ tab.unread }}</text>
</view>
</view>
<!-- 消息列表 -->
<scroll-view
scroll-y
class="messages-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<!-- 系统通知 -->
<view v-if="activeTab === 'system'" class="message-section">
<view
v-for="message in systemMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="viewSystemMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📢</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
</view>
</view>
</view>
<!-- 订单消息 -->
<view v-if="activeTab === 'order'" class="message-section">
<view
v-for="message in orderMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="viewOrderMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">📦</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<text class="order-info" v-if="message.order_no">订单号: {{ message.order_no }}</text>
</view>
</view>
</view>
<!-- 客服消息 -->
<view v-if="activeTab === 'service'" class="message-section">
<view
v-for="message in serviceMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="startCustomerService(message)"
>
<view class="message-icon-wrapper">
<image
v-if="message.avatar"
class="message-avatar"
:src="message.avatar"
mode="aspectFill"
/>
<text v-else class="message-icon">💁</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
</view>
</view>
</view>
<!-- 优惠活动 -->
<view v-if="activeTab === 'promo'" class="message-section">
<view
v-for="message in promoMessages"
:key="message.id"
:class="['message-item', { unread: !message.read }]"
@click="viewPromoMessage(message)"
>
<view class="message-icon-wrapper">
<text class="message-icon">🎁</text>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ message.time }}</text>
</view>
<text class="message-preview">{{ message.content }}</text>
<view v-if="message.coupon" class="coupon-tag">
<text class="coupon-text">{{ message.coupon }}优惠券</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && currentMessages.length === 0" class="empty-messages">
<text class="empty-icon">💬</text>
<text class="empty-title">暂无消息</text>
<text class="empty-desc">暂时没有新消息</text>
</view>
</scroll-view>
<!-- 底部固定按钮 -->
<view class="floating-action">
<button class="action-button" @click="contactCustomerService">
<text class="button-icon">💁</text>
<text class="button-text">联系客服</text>
</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
// 响应式数据
const activeTab = ref<string>('system')
const refreshing = ref<boolean>(false)
const loading = ref<boolean>(false)
const unreadCount = ref<number>(5)
// 消息分类标签
const messageTabs = reactive([
{ id: 'system', name: '系统通知', unread: 3 },
{ id: 'order', name: '订单消息', unread: 2 },
{ id: 'service', name: '客服消息', unread: 0 },
{ id: 'promo', name: '优惠活动', unread: 1 }
])
// Mock 系统通知数据
const systemMessages = reactive([
{
id: 'sys001',
title: '系统维护通知',
content: '平台将于今晚23:00-01:00进行系统维护届时部分功能可能无法使用。',
time: '2023-11-23 15:30',
read: false,
type: 'system'
},
{
id: 'sys002',
title: '隐私政策更新',
content: '我们已更新隐私政策,请查阅相关条款。',
time: '2023-11-22 10:15',
read: true,
type: 'system'
},
{
id: 'sys003',
title: '账户安全提醒',
content: '检测到您的账户在异地登录,如果不是您本人操作,请及时修改密码。',
time: '2023-11-21 18:45',
read: false,
type: 'system'
}
])
// Mock 订单消息数据
const orderMessages = reactive([
{
id: 'order001',
title: '订单发货通知',
content: '您的订单202311230001已发货点击查看物流信息。',
time: '2023-11-23 14:20',
read: false,
type: 'order',
order_no: '202311230001'
},
{
id: 'order002',
title: '订单支付成功',
content: '您的订单202311220001支付成功商家正在备货中。',
time: '2023-11-22 09:30',
read: false,
type: 'order',
order_no: '202311220001'
},
{
id: 'order003',
title: '订单确认收货',
content: '您的订单202311210001已完成期待您的评价。',
time: '2023-11-21 16:15',
read: true,
type: 'order',
order_no: '202311210001'
}
])
// Mock 客服消息数据
const serviceMessages = reactive([
{
id: 'service001',
title: '在线客服',
content: '您好,有什么可以帮助您的吗?',
time: '2023-11-23 10:05',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=1'
},
{
id: 'service002',
title: '售后客服',
content: '关于您申请的退款,已处理完成。',
time: '2023-11-22 15:20',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=2'
}
])
// Mock 优惠活动数据
const promoMessages = reactive([
{
id: 'promo001',
title: '新人专享券',
content: '您有一张新人专享优惠券已到账有效期3天。',
time: '2023-11-23 08:00',
read: false,
type: 'promo',
coupon: '50元'
},
{
id: 'promo002',
title: '双11大促',
content: '双11狂欢购物节全场满300减50。',
time: '2023-11-22 12:30',
read: true,
type: 'promo',
coupon: '满300减50'
}
])
// 计算当前显示的消息
const currentMessages = computed(() => {
switch (activeTab.value) {
case 'system': return systemMessages
case 'order': return orderMessages
case 'service': return serviceMessages
case 'promo': return promoMessages
default: return []
}
})
// 生命周期
onMounted(() => {
loadMessages()
})
// 加载消息
const loadMessages = () => {
loading.value = true
setTimeout(() => {
// 这里应该调用API获取消息数据
loading.value = false
}, 800)
}
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
}
// 查看系统消息
const viewSystemMessage = (message: any) => {
message.read = true
uni.navigateTo({
url: `/pages/mall/consumer/message-detail?id=${message.id}&type=system`
})
}
// 查看订单消息
const viewOrderMessage = (message: any) => {
message.read = true
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${message.order_no}`
})
}
// 联系客服
const startCustomerService = (message: any) => {
uni.navigateTo({
url: '/pages/mall/consumer/chat'
})
}
// 查看优惠活动
const viewPromoMessage = (message: any) => {
message.read = true
uni.navigateTo({
url: `/pages/mall/consumer/coupons`
})
}
// 联系客服
const contactCustomerService = () => {
uni.navigateTo({
url: '/pages/mall/consumer/chat'
})
}
// 清除所有未读
const clearAllUnread = () => {
uni.showModal({
title: '确认操作',
content: '确定要标记所有消息为已读吗?',
success: (res) => {
if (res.confirm) {
// 标记所有消息为已读
systemMessages.forEach(msg => msg.read = true)
orderMessages.forEach(msg => msg.read = true)
serviceMessages.forEach(msg => msg.read = true)
promoMessages.forEach(msg => msg.read = true)
// 更新标签未读数
messageTabs.forEach(tab => tab.unread = 0)
uni.showToast({
title: '已标记所有消息为已读',
icon: 'success'
})
}
}
})
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadMessages()
refreshing.value = false
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1000)
}
</script>
<style>
.messages-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 头部 */
.messages-header {
background-color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.header-actions .action-icon {
font-size: 20px;
color: #666;
}
/* 消息分类标签 */
.message-tabs {
background-color: white;
display: flex;
padding: 0 15px;
border-bottom: 1px solid #eee;
}
.tab-item {
flex: 1;
padding: 15px 5px;
text-align: center;
position: relative;
border-bottom: 2px solid transparent;
}
.tab-item.active {
color: #ff5000;
border-bottom-color: #ff5000;
font-weight: bold;
}
.tab-name {
font-size: 14px;
}
.tab-badge {
position: absolute;
top: 8px;
right: 8px;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
min-width: 16px;
text-align: center;
}
/* 消息内容区 */
.messages-content {
flex: 1;
padding-bottom: 80px; /* 为底部按钮留出空间 */
}
/* 消息项 */
.message-section {
padding: 10px;
}
.message-item {
background-color: white;
border-radius: 10px;
padding: 15px;
margin-bottom: 10px;
display: flex;
align-items: flex-start;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.message-item.unread {
background-color: #fff8f6;
border-left: 3px solid #ff5000;
}
.message-icon-wrapper {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
flex-shrink: 0;
}
.message-icon {
font-size: 24px;
}
.message-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
}
.message-content {
flex: 1;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.message-title {
font-size: 16px;
color: #333;
font-weight: bold;
flex: 1;
margin-right: 10px;
}
.message-time {
font-size: 12px;
color: #999;
white-space: nowrap;
}
.message-preview {
font-size: 14px;
color: #666;
line-height: 1.4;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.order-info {
font-size: 12px;
color: #ff5000;
background-color: #fff0e8;
padding: 3px 8px;
border-radius: 4px;
display: inline-block;
}
.coupon-tag {
display: inline-block;
background-color: #ff5000;
color: white;
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
margin-top: 5px;
}
.coupon-text {
font-size: 12px;
}
/* 空状态 */
.empty-messages {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
}
/* 底部浮动按钮 */
.floating-action {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 100;
}
.action-button {
background: linear-gradient(135deg, #ff5000, #ff9500);
color: white;
border: none;
border-radius: 25px;
padding: 12px 20px;
display: flex;
align-items: center;
box-shadow: 0 4px 12px rgba(255, 80, 0, 0.3);
}
.button-icon {
font-size: 18px;
margin-right: 8px;
}
.button-text {
font-size: 14px;
font-weight: bold;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.tab-name {
font-size: 12px;
}
.message-title {
font-size: 14px;
}
.message-preview {
font-size: 13px;
}
}
@media screen and (min-width: 415px) {
.message-item {
padding: 20px;
}
.message-icon-wrapper {
width: 60px;
height: 60px;
border-radius: 30px;
}
.message-icon {
font-size: 28px;
}
}
</style>

View File

@@ -43,8 +43,8 @@
<!-- 客服消息 -->
<view v-if="activeTab === 'service'" class="message-section">
<!-- 在线客服卡片 -->
<view class="customer-service-info">
<!-- 在线客服卡片 (hidden) -->
<!-- <view class="customer-service-info">
<view class="service-header">
<text class="service-title">康乐医药在线客服</text>
<text class="service-status online">在线</text>
@@ -69,7 +69,7 @@
<text class="category-name">药品配送</text>
</view>
</view>
</view>
</view> -->
<!-- 客服消息列表 -->
<view
@@ -214,6 +214,7 @@
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
import { supabaseService, type Notification, type ChatMessage } from '@/utils/supabaseService.uts'
// 响应式数据
const activeTab = ref<string>('service')
@@ -222,6 +223,7 @@ const loading = ref<boolean>(false)
const unreadCount = ref<number>(12)
const statusBarHeight = ref(0)
const scrollTop = ref(0)
const scrollHeight = ref(0)
// 初始化页面布局数据
const initPage = () => {
@@ -242,182 +244,11 @@ const messageTabs = reactive([
])
// Mock 客服消息数据
const serviceMessages = reactive([
{
id: 'service001',
title: '康乐医药在线客服',
role: '官方客服',
content: '您好,我是康乐医药在线客服,有什么可以帮助您的吗?',
lastMessage: '请问有什么药品需要咨询?',
time: '刚刚',
read: false,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service1',
online: true,
unreadCount: 3,
tags: ['在线', '专业药师'],
icon: '👨‍⚕️',
color: '#4CAF50'
},
{
id: 'service002',
title: '处方药咨询',
role: '药师',
content: '关于您的处方药咨询,我们已经收到,请提供处方照片。',
lastMessage: '已收到您的处方,正在审核中...',
time: '10:30',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service2',
online: true,
unreadCount: 0,
tags: ['处方药', '审核'],
icon: '💊',
color: '#2196F3'
},
{
id: 'service003',
title: '药品配送服务',
role: '配送客服',
content: '您的订单预计今天下午送达,请保持电话畅通。',
lastMessage: '配送员正在路上预计30分钟内送达',
time: '09:45',
read: false,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service3',
online: true,
unreadCount: 1,
tags: ['配送中', '今日达'],
icon: '🚚',
color: '#FF9800'
},
{
id: 'service004',
title: '用药指导',
role: '临床药师',
content: '关于您咨询的药品服用方法,建议饭后半小时服用。',
lastMessage: '记得按时服药,如有不适及时联系',
time: '昨天',
read: true,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service4',
online: false,
unreadCount: 0,
tags: ['用药指导', '专业'],
icon: '📋',
color: '#9C27B0'
},
{
id: 'service005',
title: '售后服务中心',
role: '售后专员',
content: '您申请的药品退换货已受理,我们会尽快处理。',
lastMessage: '退款将在3-5个工作日内退回原账户',
time: '前天',
read: false,
type: 'service',
avatar: 'https://picsum.photos/50/50?random=service5',
online: true,
unreadCount: 2,
tags: ['售后', '退换货'],
icon: '🔄',
color: '#F44336'
}
])
// Mock 系统通知数据
const systemMessages = reactive([
{
id: 'sys001',
title: '系统维护通知',
content: '平台将于今晚23:00-01:00进行系统维护届时部分功能可能无法使用。',
time: '2023-11-23 15:30',
read: false,
type: 'system',
important: true
},
{
id: 'sys002',
title: '隐私政策更新',
content: '我们已更新隐私政策,请查阅相关条款。',
time: '2023-11-22 10:15',
read: true,
type: 'system',
important: false
},
{
id: 'sys003',
title: '账户安全提醒',
content: '检测到您的账户在异地登录,如果不是您本人操作,请及时修改密码。',
time: '2023-11-21 18:45',
read: false,
type: 'system',
important: true
}
])
// Mock 订单消息数据
const orderMessages = reactive([
{
id: 'order001',
title: '订单发货通知',
content: '您的订单202311230001已发货点击查看物流信息。',
time: '2023-11-23 14:20',
read: false,
type: 'order',
order_no: '202311230001',
status: 'shipping',
statusText: '配送中'
},
{
id: 'order002',
title: '订单支付成功',
content: '您的订单202311220001支付成功商家正在备货中。',
time: '2023-11-22 09:30',
read: false,
type: 'order',
order_no: '202311220001',
status: 'processing',
statusText: '处理中'
},
{
id: 'order003',
title: '订单确认收货',
content: '您的订单202311210001已完成期待您的评价。',
time: '2023-11-21 16:15',
read: true,
type: 'order',
order_no: '202311210001',
status: 'completed',
statusText: '已完成'
}
])
const serviceMessages = reactive<any[]>([])
const systemMessages = reactive<any[]>([])
const orderMessages = reactive<any[]>([])
// Mock 优惠活动数据
const promoMessages = reactive([
{
id: 'promo001',
title: '新人专享券',
content: '您有一张新人专享优惠券已到账有效期3天。',
time: '2023-11-23 08:00',
read: false,
type: 'promo',
coupon: '50元',
expiry: '2023-11-26',
claimed: false
},
{
id: 'promo002',
title: '双11大促',
content: '双11狂欢购物节全场满300减50。',
time: '2023-11-22 12:30',
read: true,
type: 'promo',
coupon: '满300减50',
expiry: '2023-11-30',
claimed: false
}
])
const promoMessages = reactive<any[]>([])
// 计算当前显示的消息
const currentMessages = computed(() => {
@@ -432,18 +263,134 @@ const currentMessages = computed(() => {
// 生命周期
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
loadMessages()
})
// 简单的日期格式化
const formatTime = (isoString: string): string => {
if (!isoString) return ''
try {
return isoString.split('T')[0]
} catch(e) {
return isoString
}
}
// 加载消息
const loadMessages = () => {
const loadMessages = async () => {
loading.value = true
setTimeout(() => {
// 模拟加载消息数据
try {
// 清空现有Mock数据
serviceMessages.length = 0
systemMessages.length = 0
orderMessages.length = 0
promoMessages.length = 0
// 1. 获取通知 (系统、订单、优惠)
const notes = await supabaseService.getUserNotifications()
notes.forEach((note: Notification) => {
// 这里使用 any 类型构建对象,以匹配 reactive 数组的结构
const item = {
id: note.id,
title: note.title,
content: note.content,
time: formatTime(note.created_at || ''),
read: note.is_read,
type: note.type, // 'system', 'order', 'promotion' => 'promo'
// 默认填充字段以避免渲染报错
avatar: note.icon_url,
important: note.type === 'system', // 简单逻辑
coupon: '点击查看',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: '',
role: '',
lastMessage: '',
online: false,
unreadCount: 0,
tags: [],
icon: '',
color: ''
}
if (note.type === 'system') {
systemMessages.push(item)
} else if (note.type === 'order') {
orderMessages.push(item)
} else if (note.type === 'promotion') {
// map type 'promotion' to 'promo' for tab
item.type = 'promo'
promoMessages.push(item)
}
})
// 2. 获取客服消息 (Chat)
const chats = await supabaseService.getUserChatMessages()
if (chats.length > 0) {
// 简单处理:将最新一条显示为"在线客服"会话
const lastMsg = chats[0]
serviceMessages.push({
id: lastMsg.id,
title: '在线客服',
role: '客服专员',
content: lastMsg.content,
lastMessage: lastMsg.content,
time: formatTime(lastMsg.created_at || ''),
read: lastMsg.is_read,
type: 'service',
avatar: '/static/icons/service-avatar.png',
online: true,
unreadCount: chats.filter((m: ChatMessage) => !m.is_read && !m.is_from_user).length,
tags: ['官方客服'],
icon: '👩‍💼',
color: '#2196F3',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: ''
})
} else {
// 如果没有真实数据,保留一个默认客服入口
serviceMessages.push({
id: 'default_service',
title: '在线客服',
role: '智能助手',
content: '有问题请随时联系我们',
lastMessage: '欢迎咨询',
time: '刚刚',
read: true,
type: 'service',
avatar: '/static/icons/service-avatar.png',
online: true,
unreadCount: 0,
tags: ['自动回复'],
icon: '🤖',
color: '#2196F3',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: ''
})
}
} catch (e) {
console.error('加载消息失败', e)
} finally {
updateUnreadCount()
loading.value = false
}, 800)
}
}
// 更新未读数量

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -115,15 +115,18 @@
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click="remindShipping(order.id)">提醒发货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">确认收货</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click="goReview(order)">评价</button>
<button class="action-btn refund" @click.stop="onApplyRefund(order)">申请售后</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
@@ -213,6 +216,7 @@ onLoad((options) => {
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
else if (type === 'refund') activeTab.value = 'all' // 申请售后默认显示全部
}
})
@@ -241,8 +245,8 @@ const loadOrders = async () => {
id: item.product_id,
name: item.product_name,
price: item.price,
image: item.product_image,
spec: item.spec || '',
image: item.image_url || '/static/default-product.png',
spec: item.specifications ? (typeof item.specifications === 'string' ? item.specifications : Object.values(item.specifications).join(' ')) : '',
quantity: item.quantity
}))
}))
@@ -451,24 +455,50 @@ const viewLogistics = (orderId: string) => {
})
}
const confirmReceipt = (orderId: string) => {
const confirmReceipt = async (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showLoading({ title: '处理中...' })
try {
const result = await supabaseService.confirmReceipt(orderId)
uni.hideLoading()
if (result.success) {
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新订单状态
// 更新本地状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
}
// 跳转到评价页面
setTimeout(() => {
const order = orders.value.find(o => o.id === orderId)
if (order) {
goReview(order)
}
}, 1000)
} else {
uni.showToast({
title: result.error || '确认收货失败',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '系统异常',
icon: 'none'
})
}
}
}
})
@@ -503,6 +533,12 @@ const viewOrderDetail = (orderId: string) => {
})
}
const onApplyRefund = (order: any) => {
uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${order.id}`
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })

View File

@@ -1,996 +0,0 @@
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
<view class="orders-header">
<text class="header-title">我的订单</text>
<view class="header-actions">
<text class="search-icon" @click="navigateToSearch">🔍</text>
</view>
</view>
<!-- 订单状态筛选 -->
<view class="order-tabs">
<scroll-view scroll-x class="tab-scroll" :show-scrollbar="false">
<view class="tab-container">
<view
v-for="tab in orderTabs"
:key="tab.id"
:class="['tab-item', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 订单列表 -->
<scroll-view
scroll-y
class="orders-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<!-- 空状态 -->
<view v-if="!loading && orders.length === 0" class="empty-orders">
<text class="empty-icon">📦</text>
<text class="empty-title">暂无订单</text>
<text class="empty-desc">去逛逛,发现心仪的商品</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 订单列表 -->
<view v-else class="order-list">
<view
v-for="order in orders"
:key="order.id"
class="order-card"
>
<!-- 订单头部 -->
<view class="order-header">
<text class="order-no">订单号:{{ order.order_no }}</text>
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
</view>
<!-- 订单商品 -->
<view class="order-products">
<view
v-for="product in order.products"
:key="product.id"
class="order-product"
@click="navigateToProduct(product)"
>
<image
class="product-image"
:src="product.image"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.spec }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-quantity">×{{ product.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info">
<view class="info-row">
<text class="info-label">商品合计</text>
<text class="info-value">¥{{ order.product_amount }}</text>
</view>
<view class="info-row">
<text class="info-label">运费</text>
<text class="info-value">¥{{ order.shipping_fee }}</text>
</view>
<view class="info-row total">
<text class="info-label">实付款</text>
<text class="info-value total-price">¥{{ order.total_amount }}</text>
</view>
</view>
<!-- 订单操作 -->
<view class="order-actions">
<view v-if="order.status === 1" class="action-buttons">
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
</view>
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click="remindShipping(order.id)">提醒发货</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">确认收货</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click="goReview(order)">评价</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
<view v-if="order.status === 5" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text>没有更多订单了</text>
</view>
<!-- 安全区域 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 底部导航 -->
<view class="tabbar-placeholder"></view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
// // import supa from '@/components/supadb/aksupainstance.uts'
// 响应式数据
const orders = ref<any[]>([])
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const refreshing = ref<boolean>(false)
const page = ref<number>(1)
const activeTab = ref<string>('all')
// 订单标签页
const orderTabs = reactive([
{ id: 'all', name: '全部', count: 12 },
{ id: 'pending', name: '待付款', count: 2 },
{ id: 'shipping', name: '待发货', count: 1 },
{ id: 'delivering', name: '待收货', count: 3 },
{ id: 'completed', name: '已完成', count: 5 },
{ id: 'cancelled', name: '已取消', count: 1 }
])
// Mock 订单数据
const mockOrders = [
{
id: '202311230001',
order_no: '202311230001',
status: 1, // 1:待付款 2:待发货 3:待收货 4:已完成 5:已取消
create_time: '2023-11-23 14:30:22',
product_amount: 378.00,
shipping_fee: 0.00,
total_amount: 378.00,
products: [
{
id: '1001',
name: '无线蓝牙耳机 降噪版',
price: 299.00,
image: 'https://picsum.photos/80/80?random=1',
spec: '白色',
quantity: 1
},
{
id: '1002',
name: '耳机保护套',
price: 29.00,
image: 'https://picsum.photos/80/80?random=2',
spec: '黑色',
quantity: 1
},
{
id: '1003',
name: '数据线',
price: 19.00,
image: 'https://picsum.photos/80/80?random=3',
spec: '1米',
quantity: 2
}
]
},
{
id: '202311220001',
order_no: '202311220001',
status: 2,
create_time: '2023-11-22 10:15:33',
product_amount: 199.00,
shipping_fee: 10.00,
total_amount: 209.00,
products: [
{
id: '2001',
name: '运动T恤 速干面料',
price: 79.00,
image: 'https://picsum.photos/80/80?random=4',
spec: '黑色 L',
quantity: 2
},
{
id: '2002',
name: '运动短裤',
price: 59.00,
image: 'https://picsum.photos/80/80?random=5',
spec: '黑色 M',
quantity: 1
}
]
},
{
id: '202311210001',
order_no: '202311210001',
status: 3,
create_time: '2023-11-21 16:45:12',
product_amount: 299.00,
shipping_fee: 0.00,
total_amount: 299.00,
products: [
{
id: '3001',
name: '智能手环 心率监测',
price: 199.00,
image: 'https://picsum.photos/80/80?random=6',
spec: '黑色',
quantity: 1
},
{
id: '3002',
name: '手环腕带',
price: 29.00,
image: 'https://picsum.photos/80/80?random=7',
spec: '蓝色',
quantity: 2
}
]
},
{
id: '202311200001',
order_no: '202311200001',
status: 4,
create_time: '2023-11-20 09:30:45',
product_amount: 99.00,
shipping_fee: 0.00,
total_amount: 99.00,
products: [
{
id: '4001',
name: '保温杯 500ml',
price: 49.00,
image: 'https://picsum.photos/80/80?random=8',
spec: '白色',
quantity: 2
}
]
},
{
id: '202311190001',
order_no: '202311190001',
status: 5,
create_time: '2023-11-19 14:20:18',
product_amount: 599.00,
shipping_fee: 0.00,
total_amount: 599.00,
products: [
{
id: '5001',
name: '蓝牙音箱 便携式',
price: 199.00,
image: 'https://picsum.photos/80/80?random=9',
spec: '黑色',
quantity: 3
}
]
}
]
// 计算属性:根据当前标签筛选订单
const filteredOrders = computed(() => {
if (activeTab.value === 'all') {
return orders.value
}
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
const targetStatus = statusMap[activeTab.value]
return orders.value.filter(order => order.status === targetStatus)
})
// 生命周期
onLoad((options) => {
if (options['status']) {
const status = options['status'] as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
if (options['type']) {
const type = options['type'] as string
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
}
})
onShow(() => {
loadOrders()
})
// 加载订单数据
const loadOrders = async () => {
loading.value = true
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
if (!userId) {
loading.value = false
return
}
try {
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// 过滤当前用户的订单
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 暂时显示所有订单用于测试
let userOrders = localOrders
// 根据标签页过滤
let filtered = userOrders
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value !== 'all') {
const targetStatus = statusMap[activeTab.value]
filtered = userOrders.filter((o: any) => o.status === targetStatus)
}
// 按时间倒序
filtered.sort((a: any, b: any) => {
const timeA = new Date(a.created_at || a.create_time).getTime()
const timeB = new Date(b.created_at || b.create_time).getTime()
return timeB - timeA
})
// 处理数据格式以适配当前页面
orders.value = filtered.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.status,
create_time: order.created_at || order.create_time,
product_amount: order.total_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.items || order.products || []).map((item: any) => ({
id: item.product_id || item.id,
name: item.product_name || item.name,
price: item.price,
image: item.product_image || item.image || '/static/default-product.png',
spec: item.sku_specifications ? formatSpec(item.sku_specifications) : (item.spec || ''),
quantity: item.quantity
}))
}))
// 更新统计数据
orderTabs[0].count = userOrders.length
orderTabs[1].count = userOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = userOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = userOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = userOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = userOrders.filter((o: any) => o.status === 5).length
} catch (err) {
console.error('加载订单异常:', err)
} finally {
loading.value = false
}
}
const formatDate = (isoString: string): string => {
if (!isoString) return ''
const date = new Date(isoString)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
const formatSpec = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs).map(key => `${key}:${specs[key]}`).join(' ')
}
return String(specs)
}
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
page.value = 1
orders.value = []
loadOrders()
}
// 获取状态文本
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
1: '待付款',
2: '待发货',
3: '待收货',
4: '已完成',
5: '已取消'
}
return statusMap[status] || '未知状态'
}
// 获取状态类名
const getStatusClass = (status: number): string => {
const classMap: Record<number, string> = {
1: 'status-pending',
2: 'status-shipping',
3: 'status-delivering',
4: 'status-completed',
5: 'status-cancelled'
}
return classMap[status] || 'status-unknown'
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadOrders()
refreshing.value = false
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1000)
}
// 上拉加载更多
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return
// 暂未实现分页,直接返回
hasMore.value = false
}
// 订单操作函数
const cancelOrder = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '订单已取消',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 5
orders.value = [...orders.value]
}
}
}
})
}
const payOrder = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId}`
})
}
const remindShipping = (orderId: string) => {
uni.showToast({
title: '已提醒卖家发货',
icon: 'success'
})
}
const viewLogistics = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${orderId}`
})
}
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
}
}
}
})
}
const goReview = (order: any) => {
const productIds = order.products.map((p: any) => p.id).join(',')
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}&productIds=${productIds}`
})
}
const repurchase = (order: any) => {
uni.showModal({
title: '再次购买',
content: '确定要将这些商品加入购物车吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '已加入购物车',
icon: 'success'
})
}
}
})
}
const viewOrderDetail = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${orderId}`
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })
}
const navigateToProduct = (product: any) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
}
const goShopping = () => {
uni.switchTab({ url: '/pages/mall/consumer/index' })
}
</script>
<style>
.orders-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
}
/* 头部 */
.orders-header {
background-color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 10;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.header-actions .search-icon {
font-size: 20px;
color: #666;
}
/* 标签页 */
.order-tabs {
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 50px;
z-index: 10;
}
.tab-scroll {
width: 100%;
white-space: nowrap;
}
.tab-container {
display: flex;
flex-direction: row;
padding: 0 10px;
min-width: 100%;
}
.tab-item {
/* 移除 flex: 1改为自适应宽度或固定最小宽度 */
padding: 15px 15px; /* 增加水平内边距 */
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap; /* 防止文字换行 */
flex-shrink: 0; /* 防止被压缩 */
}
.tab-item.active {
color: #ff5000;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ff5000;
}
.tab-name {
font-size: 14px;
}
.tab-count {
margin-left: 4px;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 1px 4px;
border-radius: 8px;
min-width: 12px;
text-align: center;
}
/* 内容区 */
.orders-content {
height: calc(100vh - 100px);
}
/* 空状态 */
.empty-orders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 10px 40px;
font-size: 16px;
}
/* 订单列表 */
.order-list {
padding: 10px;
}
.order-card {
background-color: white;
border-radius: 10px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 订单头部 */
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 14px;
color: #666;
}
.order-status {
font-size: 14px;
font-weight: bold;
}
.status-pending {
color: #ff5000;
}
.status-shipping {
color: #ff9500;
}
.status-delivering {
color: #007aff;
}
.status-completed {
color: #34c759;
}
.status-cancelled {
color: #999;
}
/* 订单商品 */
.order-products {
padding: 15px;
}
.order-product {
display: flex;
margin-bottom: 15px;
}
.order-product:last-child {
margin-bottom: 0;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 8px;
margin-right: 15px;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 15px;
color: #333;
margin-bottom: 5px;
display: block;
line-height: 1.4;
}
.product-spec {
font-size: 13px;
color: #999;
margin-bottom: 10px;
display: block;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 16px;
color: #ff5000;
font-weight: bold;
}
.product-quantity {
font-size: 14px;
color: #666;
}
/* 订单信息 */
.order-info {
padding: 15px;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row.total {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}
.info-label {
font-size: 14px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
}
/* 订单操作 */
.order-actions {
padding: 15px;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.action-btn {
padding: 6px 15px;
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background: none;
}
.action-btn.cancel {
color: #666;
border-color: #ccc;
}
.action-btn.pay {
color: #ff5000;
border-color: #ff5000;
}
.action-btn.remind {
color: #666;
border-color: #ccc;
}
.action-btn.view {
color: #666;
border-color: #ccc;
}
.action-btn.confirm {
color: #34c759;
border-color: #34c759;
}
.action-btn.review {
color: #ff9500;
border-color: #ff9500;
}
.action-btn.repurchase {
color: #ff5000;
border-color: #ff5000;
}
/* 加载更多 */
.loading-more {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #f0f5ff;
border-top-color: #ff5000;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-more {
text-align: center;
color: #999;
font-size: 13px;
padding: 20px 0;
}
/* 安全区域 */
.safe-area {
height: 20px;
}
/* 底部导航占位 */
.tabbar-placeholder {
height: 50px;
background-color: #f5f5f5;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.tab-item {
padding: 0 10px;
margin-right: 5px;
}
.action-btn {
padding: 6px 10px;
font-size: 12px;
}
}
@media screen and (min-width: 415px) {
.order-card {
margin-bottom: 15px;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -111,7 +111,7 @@
<script setup lang="uts">
import { ref, onMounted, watch, computed, onUnmounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
type PaymentMethodType = {
id: string
@@ -212,25 +212,25 @@ const updateOrderInStorage = (status: number) => {
// 加载订单信息
const loadOrderInfo = async () => {
try {
/* const { data, error } = await supa
.from('orders')
.select('order_no, actual_amount')
.eq('id', orderId.value)
.single()
if (!orderId.value) return
if (error !== null) {
console.error('加载订单信息失败:', error)
return
const order = await supabaseService.getOrderDetail(orderId.value)
if (order) {
orderNo.value = order.order_no
// Only update amount if not passed via options (options is priority for UI flow usually, but DB is source of truth)
// But checking consistency is good
const dbAmount = Number(order.total_amount)
if (dbAmount > 0) {
amount.value = dbAmount
}
if (order.items && order.items.length > 0) {
// Could update product name etc if displayed
}
} else {
// Fallback or error
console.warn('Order not found in DB', orderId.value)
if (!orderNo.value) orderNo.value = 'ORD_PENDING_' + Date.now()
}
if (data) {
orderNo.value = data.order_no
amount.value = data.actual_amount || amount.value
} */
// MOCK DATA
orderNo.value = 'ORD_MOCK_' + Date.now()
// Amount already set from options or default
} catch (err) {
console.error('加载订单信息异常:', err)
}
@@ -272,28 +272,12 @@ const loadPaymentMethods = () => {
// 加载用户余额
const loadUserBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
// 这里假设有用户钱包表
/* const { data, error } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载用户余额失败:', error)
return
}
userBalance.value = data?.balance || 0 */
// MOCK BALANCE
userBalance.value = 10000.00
const balance = await supabaseService.getUserBalance()
userBalance.value = balance
} catch (err) {
console.error('加载用户余额异常:', err)
userBalance.value = 0
}
}
@@ -346,44 +330,9 @@ const getPayButtonText = (): string => {
}
// 减少商品库存
const reduceStock = (orderId: string) => {
try {
// 读取订单
const ordersStr = uni.getStorageSync('orders')
if (!ordersStr) return
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId)
if (!order || !order.items) return
// 读取商品库(这里假设商品库也在本地,实际项目中通常在服务器端处理)
// 模拟:如果有本地商品缓存,则更新
/*
const productsStr = uni.getStorageSync('products')
if (productsStr) {
const products = JSON.parse(productsStr as string) as any[]
let hasChange = false
order.items.forEach((item: any) => {
const product = products.find((p: any) => p.id === item.product_id)
if (product && product.stock >= item.quantity) {
product.stock -= item.quantity
hasChange = true
console.log(`商品 ${product.name} 库存减少 ${item.quantity}, 剩余 ${product.stock}`)
}
})
if (hasChange) {
uni.setStorageSync('products', JSON.stringify(products))
}
}
*/
console.log('模拟扣减库存成功', order.items)
} catch (e) {
console.error('扣减库存失败', e)
}
}
// const reduceStock = (orderId: string) => {
// Update should happen on server side during payment processing
// }
// 确认支付
const confirmPayment = async () => {
@@ -416,34 +365,16 @@ const confirmPayment = async () => {
isPaying.value = true
try {
// 模拟支付过程
await new Promise(resolve => setTimeout(resolve, 2000))
// Call Supabase Service to handle payment
const success = await supabaseService.payOrder(orderId.value, selectedMethod.value, amount.value)
if (!success) {
throw new Error('Payment processing failed')
}
// 更新订单状态
updateOrderInStorage(2) // 2: 待发货(已支付)
// 扣减库存
reduceStock(orderId.value)
/* const { error } = await supa
.from('orders')
.update({
status: 2, // 待发货
payment_method: getPaymentMethodCode(selectedMethod.value),
payment_status: 1, // 已支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
throw error
}
// 余额支付需要扣减余额
if (selectedMethod.value === 'balance') {
await updateUserBalance(-amount.value)
} */
// 支付成功
uni.showToast({
title: '支付成功',
@@ -483,57 +414,6 @@ const getPaymentMethodCode = (methodId: string): number => {
return codes[methodId] || 0
}
// 更新用户余额
const updateUserBalance = async (change: number) => {
const userId = getCurrentUserId()
if (!userId) return
try {
/* const { data: wallet, error: walletError } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (walletError !== null) {
console.error('查询钱包失败:', walletError)
return
}
const newBalance = (wallet?.balance || 0) + change
const { error: updateError } = await supa
.from('user_wallets')
.update({ balance: newBalance })
.eq('user_id', userId)
if (updateError !== null) {
console.error('更新余额失败:', updateError)
return
}
// 记录余额变动
const { error: recordError } = await supa
.from('balance_records')
.insert({
user_id: userId,
change_amount: change,
current_balance: newBalance,
change_type: 'order_payment',
related_id: orderId.value,
remark: `订单支付: ${orderNo.value}`
})
if (recordError !== null) {
console.error('记录余额变动失败:', recordError)
}
userBalance.value = newBalance */
} catch (err) {
console.error('更新余额异常:', err)
}
}
// 输入密码
const inputPassword = (num: string) => {
if (password.value.length >= 6) return

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@
</view>
<!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal">
<view class="spec-section" @click="showSpecModal" v-if="productSkus.length > 0">
<text class="spec-title">规格</text>
<text class="spec-selected">{{ selectedSpec || '请选择规格' }}</text>
<text class="spec-arrow">></text>
@@ -276,6 +276,13 @@ export default {
},
methods: {
saveFootprint(productId: string) {
// 调用后端API记录足迹
supabaseService.addFootprint(productId).then(success => {
if (success) {
console.log('足迹已同步到服务器')
}
})
const footprintData = uni.getStorageSync('footprints')
let footprints: any[] = []
@@ -311,330 +318,158 @@ export default {
},
async loadProductDetail(productId: string, options: any = {}) {
// 尝试从数据库加载
let dbProductRaw = null
uni.showLoading({ title: '加载中...' })
try {
console.log('正在尝试从数据库加载商品详情:', productId)
dbProductRaw = await supabaseService.getProductById(productId)
console.log('数据库返回的商品详情 (原始数据):', dbProductRaw)
// 调试:打印数据库返回的所有字段
if (dbProductRaw) {
console.log('数据库返回字段详情:')
if (Array.isArray(dbProductRaw)) {
console.log('返回数据是数组,长度:', dbProductRaw.length)
if (dbProductRaw.length > 0) {
const firstItem = dbProductRaw[0]
console.log('数组第一个元素:', firstItem)
for (const key in firstItem) {
console.log(` ${key}:`, firstItem[key], typeof firstItem[key])
}
}
} else {
console.log('返回数据是对象')
for (const key in dbProductRaw) {
console.log(` ${key}:`, dbProductRaw[key], typeof dbProductRaw[key])
}
}
}
} catch (e) {
console.error('Failed to load product from DB', e)
}
// 处理数据库返回数据:可能是数组或对象
let dbProduct = null
if (dbProductRaw) {
if (Array.isArray(dbProductRaw)) {
if (dbProductRaw.length > 0) {
dbProduct = dbProductRaw[0] // 取数组第一个元素
} else {
console.warn('数据库返回空数组')
}
} else {
dbProduct = dbProductRaw // 已经是对象
}
const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: any = null
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
dbProduct = dbProductResponse[0]
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
dbProduct = dbProductResponse
}
if (dbProduct) {
console.log('使用数据库数据渲染页面')
// 调试打印dbProduct的详细结构和类型
console.log('dbProduct类型:', typeof dbProduct)
console.log('dbProduct原型:', Object.getPrototypeOf(dbProduct))
console.log('dbProduct的键:')
for (let key in dbProduct) {
console.log(' ', key, ':', dbProduct[key], '类型:', typeof dbProduct[key])
}
// 验证必要字段,如果关键字段缺失则使用模拟数据
// 注意数据库返回的字段可能与本地ProductType不完全匹配
console.log('验证必要字段dbProduct:', dbProduct)
// 尝试多种方式访问属性
const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined)
const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined)
// 价格字段兼容性处理:优先查找 price其次查找 base_price
let priceValue = dbProduct.price
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct.base_price
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['price']
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['base_price']
}
const hasId = idValue !== undefined && idValue !== null
const hasName = nameValue !== undefined && nameValue !== null
const hasPrice = priceValue !== undefined && priceValue !== null
const hasRequiredFields = dbProduct && hasId && hasName && hasPrice
console.log('字段检查 - id:', idValue, 'hasId:', hasId, 'name:', nameValue, 'hasName:', hasName, 'price:', priceValue, 'hasPrice:', hasPrice)
console.log('hasRequiredFields:', hasRequiredFields)
if (!hasRequiredFields) {
console.warn('数据库返回数据缺少必要字段,使用模拟数据')
// 继续执行会进入下面的else分支
dbProduct = null
} else {
// 更新dbProduct的字段为实际值确保后续使用正确的属性访问
if (dbProduct.id === undefined && idValue !== undefined) dbProduct.id = idValue
if (dbProduct.name === undefined && nameValue !== undefined) dbProduct.name = nameValue
if (dbProduct.price === undefined && priceValue !== undefined) dbProduct.price = priceValue
// 使用数据库数据 - 处理字段映射
// 数据库Product接口和本地ProductType接口字段可能不同
const images = [] as Array<string>
// 处理图片字段优先使用image_urls字段其次使用main_image_url
console.log('处理数据库图片字段')
// 尝试从数据库的image_urls字段获取图片JSON字符串或对象
if (dbProduct.image_urls) {
let imagesArray: any[] = []
if (typeof dbProduct.image_urls === 'string') {
try {
imagesArray = JSON.parse(dbProduct.image_urls)
} catch (e) {
console.error('解析image_urls字段失败:', e, dbProduct.image_urls)
// 尝试逗号分割
if (dbProduct.image_urls.includes(',')) {
imagesArray = dbProduct.image_urls.split(',').map((img: string) => img.trim())
}
}
} else if (Array.isArray(dbProduct.image_urls)) {
imagesArray = dbProduct.image_urls
}
if (imagesArray.length > 0) {
for (const img of imagesArray) {
if (typeof img === 'string' && img) {
images.push(img)
}
}
}
}
// 如果没有获取到相册图,但有主图,放入相册
if (dbProduct.main_image_url) {
// 如果相册里没有这张图,把它加到第一位
if (!images.includes(dbProduct.main_image_url)) {
images.unshift(dbProduct.main_image_url)
}
}
// 兼容旧字段 image
if (images.length === 0 && dbProduct.image) {
images.push(dbProduct.image)
}
// 如果仍然没有图片,使用传入的图片或默认图片
if (images.length === 0) {
if (options.image) {
images.push(decodeURIComponent(options.image as string))
} else {
images.push('/static/product1.jpg')
}
}
// 补充模拟图片如果图片数量不足3张
const needSupplementCount = 3 - images.length
if (needSupplementCount > 0) {
const supplementalImages = ['/static/product2.jpg', '/static/product3.jpg']
for (let i = 0; i < needSupplementCount && i < supplementalImages.length; i++) {
images.push(supplementalImages[i])
}
}
console.log('最终图片数组:', images)
// 映射字段数据库shop_id对应本地merchant_id
const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001'
// 确保数值字段有效
// 优先使用 price不存在则使用 base_price
let productPrice = 0
if (typeof dbProduct.price === 'number') {
productPrice = dbProduct.price
} else if (typeof dbProduct.base_price === 'number') {
productPrice = dbProduct.base_price
} else if (priceValue !== undefined) {
// 使用上面校验时获取到的 priceValue
productPrice = Number(priceValue)
}
const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : ((dbProduct.total_stock != null && !isNaN(Number(dbProduct.total_stock))) ? Math.floor(Number(dbProduct.total_stock)) : 100)
const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : ((dbProduct.sale_count != null && !isNaN(Number(dbProduct.sale_count))) ? Math.floor(Number(dbProduct.sale_count)) : 50)
// 解析 attributes
let attributes: any = {}
if (dbProduct.attributes) {
try {
if (typeof dbProduct.attributes === 'string') {
attributes = JSON.parse(dbProduct.attributes)
} else {
attributes = dbProduct.attributes
}
} catch (e) {
console.error('解析 attributes 失败', e)
}
}
// Map DB product to local product
this.product = {
id: dbProduct.id || productId,
merchant_id: merchantId,
category_id: dbProduct.category_id || 'cat_001',
name: dbProduct.name || '商品名称',
description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: images,
price: productPrice,
original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : ((dbProduct.market_price != null && !isNaN(Number(dbProduct.market_price))) ? Number(dbProduct.market_price) : null),
stock: stock,
sales: sales,
status: 1,
created_at: dbProduct.created_at || '2024-01-01',
// 药品相关字段
specification: attributes.specification || dbProduct.specification || null,
usage: attributes.usage || dbProduct.usage || null,
side_effects: attributes.side_effects || dbProduct.side_effects || null,
precautions: attributes.precautions || dbProduct.precautions || null,
expiry_date: attributes.expiry_date || dbProduct.expiry_date || null,
storage_conditions: attributes.storage_conditions || dbProduct.storage_conditions || null,
approval_number: attributes.approval_number || dbProduct.approval_number || null,
tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
id: dbProduct.id,
merchant_id: dbProduct.merchant_id || dbProduct.shop_id || '',
category_id: dbProduct.category_id || '',
name: dbProduct.name,
description: dbProduct.description || '',
images: [] as string[],
price: dbProduct.base_price || dbProduct.price || 0,
original_price: dbProduct.market_price || dbProduct.original_price || 0,
stock: dbProduct.available_stock || dbProduct.total_stock || dbProduct.stock || 0,
sales: dbProduct.sale_count || dbProduct.sales || 0,
status: dbProduct.status !== undefined ? dbProduct.status : 1,
created_at: dbProduct.created_at || new Date().toISOString(),
// Attributes
specification: dbProduct.specification || null,
usage: dbProduct.usage || null,
side_effects: dbProduct.side_effects || null,
precautions: dbProduct.precautions || null,
expiry_date: dbProduct.expiry_date || null,
storage_conditions: dbProduct.storage_conditions || null,
approval_number: dbProduct.approval_number || null,
tags: [] as string[]
} as ProductType
console.log('页面 product 对象已更新:', this.product)
console.log('商品图片数组:', this.product.images)
console.log('商品价格:', this.product.price, '库存:', this.product.stock, '销量:', this.product.sales)
}
} else {
console.log('数据库无数据或加载失败,使用模拟数据')
// 数据库无数据时,使用原有模拟逻辑
const generatePriceFromId = (id: string): number => {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0
}
const price = 50 + Math.abs(hash % 450)
return parseFloat(price.toFixed(2))
}
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
const productName = options.name ? decodeURIComponent(options.name) : (() => {
const productNames = ['高品质运动休闲鞋', '时尚简约双肩背包', '多功能智能手环', '便携式蓝牙音箱', '全自动雨伞', '抗菌防螨床上四件套', '不锈钢保温杯', '无线充电器', '高清行车记录仪', '智能体脂秤']
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
return productNames[nameIndex]
})()
const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [productImage, '/static/product2.jpg', '/static/product3.jpg'],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
}
// 尝试加载真实商户信息
let realMerchantLoaded = false
// 只有当 ID 是 UUID 格式(包含-)或者是真实数据时才尝试查询
if (this.product.merchant_id && (this.product.merchant_id.includes('-') || !this.product.merchant_id.startsWith('merchant_'))) {
console.log('尝试加载商户信息:', this.product.merchant_id)
// Handle Images
if (dbProduct.image_urls) {
try {
const shop = await supabaseService.getShopByMerchantId(this.product.merchant_id)
if (shop) {
console.log('加载到商户信息:', shop.shop_name)
const parsed = typeof dbProduct.image_urls === 'string' ? JSON.parse(dbProduct.image_urls) : dbProduct.image_urls
if (Array.isArray(parsed)) {
this.product.images = parsed.map((i: any) => String(i))
}
} catch (e) { console.error('Error parsing image_urls', e) }
}
// Fallback to main_image_url if no images found
if (this.product.images.length === 0 && dbProduct.main_image_url) {
this.product.images.push(dbProduct.main_image_url)
}
// Fallback to 'image' field (legacy)
if (this.product.images.length === 0 && dbProduct.image) {
this.product.images.push(dbProduct.image)
}
// Final fallback
if (this.product.images.length === 0) {
this.product.images.push('/static/default-product.png')
}
// 确保字段存在,避免 undefined 导致构造失败
// Handle Tags
if (dbProduct.tags) {
try {
const parsedTags = typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags
if (Array.isArray(parsedTags)) {
this.product.tags = parsedTags.map((t: any) => String(t))
}
} catch (e) {}
}
// Handle JSON attributes if present
if (dbProduct.attributes && typeof dbProduct.attributes === 'string') {
try {
const attrs = JSON.parse(dbProduct.attributes)
if (attrs) {
// Merge attributes into product if they match keys
if (attrs.specification) this.product.specification = attrs.specification
if (attrs.usage) this.product.usage = attrs.usage
// ... augment as needed
}
} catch(e) {}
}
// Load SKUs
// this.loadProductSkus(productId) // If SKU logic exists
} else {
throw new Error('No product found')
}
} catch (e) {
console.error('Failed to load product detail:', e)
// Fallback to options if available
this.product.id = productId
this.product.name = options.name ? decodeURIComponent(options.name) : '未知商品'
this.product.price = options.price ? parseFloat(options.price) : 0
this.product.images = options.image ? [decodeURIComponent(options.image)] : ['/static/default-product.png']
}
// Load Merchant and SKUs
if (this.product.merchant_id) {
await this.loadMerchantInfo(this.product.merchant_id)
}
if (this.product.id) {
this.loadProductSkus(this.product.id)
}
uni.hideLoading()
},
async loadMerchantInfo(merchantId: string) {
let realMerchantLoaded = false
if (merchantId.includes('-') || !merchantId.startsWith('merchant_')) {
try {
const shop = await supabaseService.getShopByMerchantId(merchantId)
if (shop) {
this.merchant = {
id: shop.id || '',
user_id: shop.merchant_id || '',
shop_name: shop.shop_name || '未命名店铺',
id: shop.id,
user_id: shop.merchant_id,
shop_name: shop.shop_name,
shop_logo: shop.shop_logo || '/static/default-shop.png',
shop_banner: shop.shop_banner || '/static/default-banner.png',
shop_description: shop.description || '',
contact_name: shop.contact_name || '店主',
contact_phone: shop.contact_phone || '',
shop_status: 1,
// 优先使用 avg_rating没有则使用默认值
rating: shop.rating_avg !== undefined && shop.rating_avg !== null ? shop.rating_avg : 4.8,
// 使用 order_count 或 product_count 作为销量/活跃度指标,如果没有则默认 0
total_sales: shop.total_sales !== undefined ? shop.total_sales : (shop.order_count !== undefined ? shop.order_count : 0),
rating: shop.rating_avg || 5.0,
total_sales: shop.total_sales || 0,
created_at: shop.created_at || new Date().toISOString()
} as MerchantType
realMerchantLoaded = true
}
} catch (e) {
console.error('加载商户信息失败', e)
console.error('Load shop failed', e)
}
}
if (!realMerchantLoaded) {
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const merchantIndex = Math.abs(merchantId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
id: merchantId,
user_id: 'user_mock_' + merchantIndex,
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_description: '优质服务,正品保障',
contact_name: '店主',
contact_phone: '',
shop_status: 1,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
rating: 4.8,
total_sales: 999,
created_at: '2023-01-01'
} as MerchantType
}
}
this.loadProductSkus(productId)
},
async loadProductSkus(productId: string) {
@@ -674,32 +509,9 @@ export default {
console.error('Fetch SKUs error', e)
}
// 模拟加载商品SKU数据
const basePrice = this.product.price
// 使用 productId 作为前缀生成唯一的 SKU ID防止不同商品的 SKU ID 冲突
this.productSkus = [
{
id: `${productId}_sku_001`,
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
price: basePrice,
stock: 50,
image_url: '/static/sku1.jpg',
status: 1
},
{
id: `${productId}_sku_002`,
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
price: parseFloat((basePrice * 1.1).toFixed(2)),
stock: 30,
image_url: '/static/sku2.jpg',
status: 1
}
]
// 如果没有从数据库加载到SKU则不显示规格选择直接作为无规格商品添加
// 移除之前的Mock逻辑因为Mock的ID不符合UUID格式会导致数据库错误
},
onSwiperChange(e: any) {
@@ -729,7 +541,7 @@ export default {
},
async addToCart() {
if (!this.selectedSkuId) {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -776,7 +588,7 @@ export default {
},
buyNow() {
if (!this.selectedSkuId) {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -784,7 +596,7 @@ export default {
return
}
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
const sku = this.selectedSkuId ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
// 调试:打印价格信息
console.log('立即购买 - 商品价格信息:')
@@ -829,56 +641,53 @@ export default {
},
checkFavoriteStatus(id: string) {
const storedFavorites = uni.getStorageSync('favorites')
if (storedFavorites) {
try {
const favorites = JSON.parse(storedFavorites as string) as any[]
this.isFavorite = favorites.some(item => item.id === id)
} catch (e) {
console.error('Failed to parse favorites', e)
}
}
// console.log('product-detail checkFavoriteStatus id:', id)
this.checkFavorite(id)
},
toggleFavorite() {
const storedFavorites = uni.getStorageSync('favorites')
let favorites: any[] = []
async checkFavorite(id: string) {
const isFav = await supabaseService.checkFavorite(id)
this.isFavorite = isFav
},
async toggleFavorite() {
if (!this.product.id) return
// 显示loading
uni.showLoading({ title: '处理中' })
if (storedFavorites) {
try {
favorites = JSON.parse(storedFavorites as string) as any[]
} catch (e) {
console.error('Failed to parse favorites', e)
}
}
// 记录操作前的状态
const wasFavorite = this.isFavorite
if (this.isFavorite) {
// 取消收藏
favorites = favorites.filter(item => item.id !== this.product.id)
// 执行切换返回的是最新的状态true=已收藏false=未收藏)
const isNowFavorite = await supabaseService.toggleFavorite(this.product.id)
uni.hideLoading()
if (isNowFavorite !== wasFavorite) {
// 状态发生了改变,说明操作成功
this.isFavorite = isNowFavorite
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
} else {
// 添加收藏
favorites.push({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price, // 保存原价
image: this.product.images[0],
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name
})
uni.showToast({
title: '收藏成功',
title: isNowFavorite ? '收藏成功' : '已取消收藏',
icon: 'success'
})
} else {
// 状态未改变,说明操作失败
uni.showToast({
title: '操作失败',
icon: 'none'
})
// 确保状态同步
this.checkFavoriteStatus(this.product.id)
}
} catch (e) {
uni.hideLoading()
console.error('Toggle favorite failed', e)
uni.showToast({
title: '操作异常',
icon: 'none'
})
}
uni.setStorageSync('favorites', JSON.stringify(favorites))
this.isFavorite = !this.isFavorite
},
goToHome() {

View File

@@ -1,909 +0,0 @@
<!-- 消费者端 - 个人中心 -->
<template>
<view class="consumer-profile">
<!-- 用户信息头部 -->
<view class="profile-header">
<image :src="userInfo.avatar_url || '/static/default-avatar.png'" class="user-avatar" @click="editProfile" />
<view class="user-info">
<text class="user-name">{{ userInfo.nickname || userInfo.phone }}</text>
<text class="user-level">{{ getUserLevel() }}</text>
<view class="user-stats">
<text class="stat-item">积分: {{ userStats.points }}</text>
<text class="stat-item">余额: ¥{{ userStats.balance }}</text>
</view>
</view>
<view class="settings-icon" @click="goToSettings">⚙️</view>
</view>
<!-- 订单状态快捷入口 -->
<view class="order-shortcuts">
<view class="section-title">我的订单</view>
<view class="order-tabs">
<view class="order-tab" @click="goToOrders('all')">
<text class="tab-icon">📋</text>
<text class="tab-text">全部订单</text>
<text v-if="orderCounts.total > 0" class="tab-badge">{{ orderCounts.total }}</text>
</view>
<view class="order-tab" @click="goToOrders('pending')">
<text class="tab-icon">💰</text>
<text class="tab-text">待支付</text>
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
</view>
<view class="order-tab" @click="goToOrders('shipped')">
<text class="tab-icon">🚚</text>
<text class="tab-text">待收货</text>
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
</view>
<view class="order-tab" @click="goToOrders('completed')">
<text class="tab-icon">⭐</text>
<text class="tab-text">待评价</text>
<text v-if="orderCounts.review > 0" class="tab-badge">{{ orderCounts.review }}</text>
</view>
</view>
</view>
<!-- 最近订单 -->
<view class="recent-orders">
<view class="section-header">
<text class="section-title">最近订单</text>
<text class="view-all" @click="goToOrders('all')">查看全部 ></text>
</view>
<view v-if="recentOrders.length === 0" class="empty-orders">
<text class="empty-text">暂无订单记录</text>
<button class="start-shopping" @click="goShopping">去逛逛</button>
</view>
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
<view class="order-header">
<text class="order-no">订单号: {{ order.order_no }}</text>
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
</view>
<view class="order-content">
<image :src="getOrderMainImage(order)" class="order-image" />
<view class="order-info">
<text class="order-title">{{ getOrderTitle(order) }}</text>
<text class="order-amount">¥{{ order.actual_amount }}</text>
<text class="order-time">{{ formatTime(order.created_at) }}</text>
</view>
</view>
<view class="order-actions">
<button v-if="order.status === 1" class="action-btn pay" @click.stop="payOrder(order)">立即支付</button>
<button v-if="order.status === 3" class="action-btn confirm" @click.stop="confirmReceive(order)">确认收货</button>
<button v-if="order.status === 4" class="action-btn review" @click.stop="reviewOrder(order)">评价</button>
</view>
</view>
</view>
<!-- 我的服务 -->
<view class="my-services">
<view class="section-title">我的服务</view>
<view class="service-grid">
<view class="service-item" @click="goToCoupons">
<text class="service-icon">🎫</text>
<text class="service-text">优惠券</text>
<text v-if="serviceCounts.coupons > 0" class="service-badge">{{ serviceCounts.coupons }}</text>
</view>
<view class="service-item" @click="goToAddress">
<text class="service-icon">📍</text>
<text class="service-text">收货地址</text>
</view>
<view class="service-item" @click="goToFavorites">
<text class="service-icon">❤️</text>
<text class="service-text">我的收藏</text>
<text v-if="serviceCounts.favorites > 0" class="service-badge">{{ serviceCounts.favorites }}</text>
</view>
<view class="service-item" @click="goToFootprint">
<text class="service-icon">👣</text>
<text class="service-text">浏览足迹</text>
</view>
<view class="service-item" @click="goToRefund">
<text class="service-icon">🔄</text>
<text class="service-text">退款/售后</text>
</view>
<view class="service-item" @click="contactService">
<text class="service-icon">💬</text>
<text class="service-text">在线客服</text>
</view>
<view class="service-item" @click="goToMySubscriptions">
<text class="service-icon">🧩</text>
<text class="service-text">我的订阅</text>
</view>
<view class="service-item" @click="goToSubscriptions">
<text class="service-icon">🧩</text>
<text class="service-text">软件订阅</text>
</view>
</view>
</view>
<!-- 消费统计 -->
<view class="consumption-stats">
<view class="section-title">消费统计</view>
<view class="stats-period">
<text v-for="period in statsPeriods" :key="period.key"
class="period-tab"
:class="{ active: activeStatsPeriod === period.key }"
@click="switchStatsPeriod(period.key)">{{ period.label }}</text>
</view>
<view class="stats-content">
<view class="stat-card">
<text class="stat-value">¥{{ currentStats.total_amount }}</text>
<text class="stat-label">总消费</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ currentStats.order_count }}</text>
<text class="stat-label">订单数</text>
</view>
<view class="stat-card">
<text class="stat-value">¥{{ currentStats.avg_amount }}</text>
<text class="stat-label">平均消费</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ currentStats.save_amount }}</text>
<text class="stat-label">节省金额</text>
</view>
</view>
</view>
<!-- 账户安全 -->
<view class="account-security">
<view class="section-title">账户安全</view>
<view class="security-items">
<view class="security-item" @click="changePassword">
<text class="security-icon">🔒</text>
<text class="security-text">修改密码</text>
<text class="security-arrow">></text>
</view>
<view class="security-item" @click="bindPhone">
<text class="security-icon">📱</text>
<text class="security-text">手机绑定</text>
<view class="security-status">
<text class="status-text" :class="{ bound: userInfo.phone }">{{ userInfo.phone ? '已绑定' : '未绑定' }}</text>
<text class="security-arrow">></text>
</view>
</view>
<view class="security-item" @click="bindEmail">
<text class="security-icon">📧</text>
<text class="security-text">邮箱绑定</text>
<view class="security-status">
<text class="status-text" :class="{ bound: userInfo.email }">{{ userInfo.email ? '已绑定' : '未绑定' }}</text>
<text class="security-arrow">></text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { UserType, OrderType } from '@/types/mall-types.uts'
type UserStatsType = {
points: number
balance: number
level: number
}
type OrderCountsType = {
total: number
pending: number
shipped: number
review: number
}
type ServiceCountsType = {
coupons: number
favorites: number
}
type ConsumptionStatsType = {
total_amount: number
order_count: number
avg_amount: number
save_amount: number
}
type StatsPeriodType = {
key: string
label: string
}
export default {
data() {
return {
userInfo: {
id: '',
phone: '',
email: '',
nickname: '',
avatar_url: '',
gender: 0,
user_type: 0,
status: 0,
created_at: ''
} as UserType,
userStats: {
points: 0,
balance: 0,
level: 1
} as UserStatsType,
orderCounts: {
total: 0,
pending: 0,
shipped: 0,
review: 0
} as OrderCountsType,
serviceCounts: {
coupons: 0,
favorites: 0
} as ServiceCountsType,
recentOrders: [] as Array<OrderType>,
statsPeriods: [
{ key: 'month', label: '本月' },
{ key: 'quarter', label: '本季度' },
{ key: 'year', label: '本年' },
{ key: 'all', label: '全部' }
] as Array<StatsPeriodType>,
activeStatsPeriod: 'month',
currentStats: {
total_amount: 0,
order_count: 0,
avg_amount: 0,
save_amount: 0
} as ConsumptionStatsType
}
},
onLoad() {
this.loadUserProfile()
},
onShow() {
this.refreshData()
},
methods: {
loadUserProfile() {
// 模拟加载用户信息
this.userInfo = {
id: 'user_001',
phone: '13800138000',
email: 'user@example.com',
nickname: '张三',
avatar_url: '/static/avatar1.jpg',
gender: 1,
user_type: 1,
status: 1,
created_at: '2023-06-15T10:30:00'
}
this.userStats = {
points: 1580,
balance: 268.50,
level: 3
}
this.orderCounts = {
total: 23,
pending: 2,
shipped: 1,
review: 3
}
this.serviceCounts = {
coupons: 5,
favorites: 12
}
this.recentOrders = [
{
id: 'order_001',
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3,
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-15T14:30:00'
},
{
id: 'order_002',
order_no: 'ORD202401140002',
user_id: 'user_001',
merchant_id: 'merchant_002',
status: 4,
total_amount: 158.00,
discount_amount: 0,
delivery_fee: 6.00,
actual_amount: 164.00,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-14T09:20:00'
}
]
this.loadConsumptionStats()
},
loadConsumptionStats() {
// 模拟加载消费统计数据
const statsData: Record<string, ConsumptionStatsType> = {
month: {
total_amount: 1280.50,
order_count: 8,
avg_amount: 160.06,
save_amount: 85.20
},
quarter: {
total_amount: 3680.80,
order_count: 18,
avg_amount: 204.49,
save_amount: 256.30
},
year: {
total_amount: 15680.90,
order_count: 56,
avg_amount: 280.02,
save_amount: 986.50
},
all: {
total_amount: 25680.50,
order_count: 89,
avg_amount: 288.55,
save_amount: 1580.20
}
}
this.currentStats = statsData[this.activeStatsPeriod]
},
refreshData() {
// 刷新页面数据
this.loadUserProfile()
},
getUserLevel(): string {
const levels = ['新手', '铜牌会员', '银牌会员', '金牌会员', '钻石会员']
return levels[this.userStats.level] || '新手'
},
getOrderStatusText(status: number): string {
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
return statusTexts[status] || '未知'
},
getOrderStatusClass(status: number): string {
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
return statusClasses[status] || 'error'
},
getOrderMainImage(order: OrderType): string {
// 模拟获取订单主图
return '/static/product1.jpg'
},
getOrderTitle(order: OrderType): string {
// 模拟获取订单标题
return '精选商品等多件商品'
},
formatTime(timeStr: string): string {
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) {
return '今天'
} else if (days === 1) {
return '昨天'
} else {
return `${days}天前`
}
},
switchStatsPeriod(period: string) {
this.activeStatsPeriod = period
this.loadConsumptionStats()
},
editProfile() {
uni.navigateTo({
url: '/pages/mall/consumer/edit-profile'
})
},
goToSettings() {
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
},
goToOrders(type: string) {
uni.navigateTo({
url: `/pages/mall/consumer/orders?type=${type}`
})
},
goShopping() {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
},
viewOrderDetail(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?orderId=${order.id}`
})
},
payOrder(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}`
})
},
confirmReceive(order: OrderType) {
uni.showModal({
title: '确认收货',
content: '确认已收到商品吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '确认收货成功',
icon: 'success'
})
this.refreshData()
}
}
})
},
reviewOrder(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}`
})
},
goToCoupons() {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
},
goToAddress() {
uni.navigateTo({
url: '/pages/mall/consumer/address'
})
},
goToFavorites() {
uni.navigateTo({
url: '/pages/mall/consumer/favorites'
})
},
goToFootprint() {
uni.navigateTo({
url: '/pages/mall/consumer/footprint'
})
},
goToRefund() {
uni.navigateTo({
url: '/pages/mall/consumer/refund'
})
},
contactService() {
uni.navigateTo({
url: '/pages/mall/service/chat'
})
},
goToMySubscriptions() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/my-subscriptions'
})
},
goToSubscriptions() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/plan-list'
})
},
changePassword() {
uni.navigateTo({
url: '/pages/mall/consumer/change-password'
})
},
bindPhone() {
uni.navigateTo({
url: '/pages/mall/consumer/bind-phone'
})
},
bindEmail() {
uni.navigateTo({
url: '/pages/mall/consumer/bind-email'
})
}
}
}
</script>
<style>
.consumer-profile {
background-color: #f5f5f5;
min-height: 100vh;
}
.profile-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 30rpx 40rpx;
display: flex;
align-items: center;
color: #fff;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
margin-right: 30rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.user-info {
flex: 1;
}
.user-name {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.user-level {
font-size: 24rpx;
background-color: rgba(255, 255, 255, 0.2);
padding: 6rpx 12rpx;
border-radius: 12rpx;
margin-bottom: 15rpx;
display: inline-block;
}
.user-stats {
display: flex;
gap: 30rpx;
}
.stat-item {
font-size: 24rpx;
opacity: 0.9;
}
.settings-icon {
font-size: 32rpx;
padding: 10rpx;
}
.order-shortcuts, .recent-orders, .my-services, .consumption-stats, .account-security {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.view-all {
font-size: 24rpx;
color: #007aff;
}
.order-tabs {
display: flex;
justify-content: space-between;
}
.order-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.tab-icon {
font-size: 40rpx;
margin-bottom: 10rpx;
}
.tab-text {
font-size: 24rpx;
color: #666;
}
.tab-badge {
position: absolute;
top: -8rpx;
right: 20rpx;
background-color: #ff4444;
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 10rpx;
min-width: 32rpx;
text-align: center;
}
.empty-orders {
text-align: center;
padding: 80rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 30rpx;
}
.start-shopping {
background-color: #007aff;
color: #fff;
padding: 20rpx 40rpx;
border-radius: 25rpx;
font-size: 26rpx;
border: none;
}
.order-item {
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.order-item:last-child {
border-bottom: none;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.order-no {
font-size: 26rpx;
color: #333;
}
.order-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 10rpx;
color: #fff;
}
.order-status.pending {
background-color: #ffa726;
}
.order-status.processing {
background-color: #2196f3;
}
.order-status.shipping {
background-color: #9c27b0;
}
.order-status.completed {
background-color: #4caf50;
}
.order-content {
display: flex;
align-items: center;
margin-bottom: 15rpx;
}
.order-image {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.order-info {
flex: 1;
}
.order-title {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
}
.order-amount {
font-size: 28rpx;
color: #ff4444;
font-weight: bold;
margin-bottom: 5rpx;
}
.order-time {
font-size: 22rpx;
color: #999;
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 15rpx;
}
.action-btn {
padding: 12rpx 25rpx;
border-radius: 20rpx;
font-size: 24rpx;
border: none;
}
.action-btn.pay {
background-color: #ff4444;
color: #fff;
}
.action-btn.confirm {
background-color: #4caf50;
color: #fff;
}
.action-btn.review {
background-color: #ffa726;
color: #fff;
}
.service-grid {
display: flex;
flex-wrap: wrap;
gap: 30rpx;
}
.service-item {
width: 30%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.service-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.service-text {
font-size: 24rpx;
color: #333;
}
.service-badge {
position: absolute;
top: -5rpx;
right: 10rpx;
background-color: #ff4444;
color: #fff;
font-size: 18rpx;
padding: 4rpx 6rpx;
border-radius: 8rpx;
min-width: 24rpx;
text-align: center;
}
.stats-period {
display: flex;
gap: 30rpx;
margin-bottom: 30rpx;
}
.period-tab {
font-size: 26rpx;
color: #666;
padding: 12rpx 24rpx;
border-radius: 20rpx;
background-color: #f0f0f0;
}
.period-tab.active {
background-color: #007aff;
color: #fff;
}
.stats-content {
display: flex;
gap: 20rpx;
}
.stat-card {
flex: 1;
text-align: center;
padding: 30rpx 0;
background-color: #f8f9fa;
border-radius: 10rpx;
}
.stat-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #666;
}
.security-items {
margin-top: 25rpx;
}
.security-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.security-item:last-child {
border-bottom: none;
}
.security-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.security-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.security-status {
display: flex;
align-items: center;
}
.status-text {
font-size: 24rpx;
color: #999;
margin-right: 10rpx;
}
.status-text.bound {
color: #4caf50;
}
.security-arrow {
font-size: 24rpx;
color: #999;
}
</style>

View File

@@ -70,9 +70,9 @@
<text class="service-icon">🔄</text>
<text class="service-text">退款/售后</text>
</view>
<view class="service-item" @click="contactService">
<text class="service-icon">💬</text>
<text class="service-text">在线客服</text>
<view class="service-item" @click="goToOrderReviews">
<text class="service-icon">📝</text>
<text class="service-text">评价</text>
</view>
<view class="service-item" @click="goToMySubscriptions">
<text class="service-icon">🧩</text>
@@ -248,7 +248,7 @@
<script>
import { UserType, OrderType } from '@/types/mall-types.uts'
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
import supabaseService from '@/utils/supabaseService.uts'
type UserStatsType = {
points: number
@@ -363,22 +363,27 @@ export default {
methods: {
// 加载订单数据
async loadOrders() {
const userStore = uni.getStorageSync('userInfo')
// const userId = userStore?.id
// if (!userId) return
try {
// 从本地存储加载订单数据
const storedOrders = uni.getStorageSync('orders')
let orders: any[] = []
if (storedOrders) {
orders = JSON.parse(storedOrders as string) as any[]
}
const orders = await supabaseService.getOrders()
this.allOrders = orders
// 按时间倒序
// 映射数据库字段到前端类型
this.allOrders = orders.map((o: any): OrderType => {
// 确保 status 字段存在
if (o['status'] == null && o['order_status'] != null) {
o['status'] = o['order_status']
}
// 确保 actual_amount 存在
if (o['actual_amount'] == null && o['total_amount'] != null) {
o['actual_amount'] = o['total_amount']
}
return o as OrderType
})
// 按时间倒序 (created_at)
this.allOrders.sort((a: any, b: any) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
const timeA = new Date(a.created_at || 0).getTime()
const timeB = new Date(b.created_at || 0).getTime()
return timeB - timeA
})
// 过滤最近的订单
@@ -388,8 +393,8 @@ export default {
this.orderCounts = {
total: this.allOrders.length,
pending: this.allOrders.filter((o: any) => o.status === 1).length,
toship: this.allOrders.filter((o: any) => o.status === 2).length, // 修复仅计算状态2为待发货
shipped: this.allOrders.filter((o: any) => o.status === 3).length, // 修复仅计算状态3为待收货
toship: this.allOrders.filter((o: any) => o.status === 2).length,
shipped: this.allOrders.filter((o: any) => o.status === 3).length,
review: this.allOrders.filter((o: any) => o.status === 4).length
}
} catch (e) {
@@ -437,11 +442,14 @@ export default {
level: 3
}
// orderCounts 将通过 loadOrders 从真实数据获取
// init with zeros
this.orderCounts = {
total: 23,
pending: 2,
shipped: 1,
review: 3
total: 0,
pending: 0,
toship: 0,
shipped: 0,
review: 0
}
this.serviceCounts = {
@@ -449,38 +457,8 @@ export default {
favorites: 12
}
this.recentOrders = [
{
id: 'order_001',
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3,
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-15T14:30:00'
},
{
id: 'order_002',
order_no: 'ORD202401140002',
user_id: 'user_001',
merchant_id: 'merchant_002',
status: 4,
total_amount: 158.00,
discount_amount: 0,
delivery_fee: 6.00,
actual_amount: 164.00,
payment_method: 1,
payment_status: 1,
delivery_address: {},
created_at: '2024-01-14T09:20:00'
}
]
// recentOrders 将通过 loadOrders 从真实数据获取
this.recentOrders = []
this.loadConsumptionStats()
},
@@ -546,14 +524,29 @@ export default {
return statusClasses[status] || 'error'
},
getOrderMainImage(order: OrderType): string {
// 模拟获取订单主
getOrderMainImage(order: any): string {
// 尝试从 ml_order_items 获取第一张
const items = order['ml_order_items'] as any[]
if (items && items.length > 0) {
const firstItem = items[0]
// 数据库字段通常是 image_url
const img = firstItem['image_url'] || firstItem['product_image']
if (img) return img as string
}
return '/static/product1.jpg'
},
getOrderTitle(order: OrderType): string {
// 模拟获取订单标题
return '精选商品等多件商品'
getOrderTitle(order: any): string {
const items = order['ml_order_items'] as any[]
if (items && items.length > 0) {
const firstItem = items[0]
const name = (firstItem['product_name'] || '商品') as string
if (items.length > 1) {
return `${name} 等${items.length}件商品`
}
return name
}
return '精选商品'
},
formatTime(timeStr: string): string {
@@ -669,7 +662,7 @@ export default {
goToRefund() {
uni.navigateTo({
url: '/pages/mall/consumer/refund'
url: '/pages/mall/consumer/orders?type=refund'
})
},
@@ -678,6 +671,11 @@ export default {
url: '/pages/mall/service/chat'
})
},
goToOrderReviews() {
uni.navigateTo({
url: '/pages/mall/consumer/orders?type=review'
})
},
goToMySubscriptions() {
uni.navigateTo({
url: '/pages/mall/consumer/subscription/my-subscriptions'

View File

@@ -101,7 +101,7 @@
<script setup lang="uts">
import { ref, onMounted, watch } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
type RefundType = {
id: string
@@ -187,40 +187,48 @@ const loadRefunds = async (loadMore: boolean = false) => {
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('refunds')
.select(`
*,
order:order_id(
order_no,
created_at,
order_items(
*,
product:product_id(images)
)
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 根据标签页过滤
let statusList: number[] = []
if (activeTab.value === 'processing') {
query = query.in('status', [1, 2]) // 待处理和处理中
statusList = [1, 2] // 待处理和处理中
} else if (activeTab.value === 'completed') {
query = query.in('status', [3, 4, 5]) // 已完成、已取消、已拒绝
statusList = [3, 4, 5] // 已完成、已取消、已拒绝
}
// 分页
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const rawData = await supabaseService.getRefunds(statusList, page, pageSize.value)
const { data, error } = await query
// Map data to UI structure (RefundType)
const newRefunds = rawData.map((item: any): RefundType => {
const orderObj = item.order || {}
const dbItems = orderObj.ml_order_items || []
const uiItems = dbItems.map((di: any) : any => ({
id: di.id || '',
product_name: di.product_name,
sku_specifications: di.specifications,
price: 0,
quantity: di.quantity || 1,
product: { images: [di.image_url || '/static/default-product.png'] }
}))
if (error !== null) {
console.error('加载售后记录失败:', error)
return
return {
id: item.id,
user_id: item.user_id,
order_id: item.order_id,
refund_no: item.refund_no,
refund_type: item.refund_type,
refund_reason: item.refund_reason,
refund_amount: Number(item.refund_amount),
status: item.status,
// Handle missing timeline by defaulting or leaving empty
status_history: item.status_history || [],
created_at: item.created_at,
order: {
id: item.order_id,
order_no: orderObj.order_no,
created_at: orderObj.created_at,
order_items: uiItems
}
const newRefunds = data || []
} as RefundType
})
if (loadMore) {
refunds.value.push(...newRefunds)

View File

@@ -200,7 +200,7 @@ onMounted(() => {
const loadOrderData = async () => {
try {
const { data: orderData, error: orderError } = await supa
.from('orders')
.from('ml_orders')
.select('*')
.eq('id', orderId.value)
.single()
@@ -214,7 +214,7 @@ const loadOrderData = async () => {
// 加载订单商品
const { data: itemsData, error: itemsError } = await supa
.from('order_items')
.from('ml_order_items')
.select(`
*,
product:product_id(images)
@@ -240,7 +240,7 @@ const loadOrderData = async () => {
// 加载商家信息
if (order.value.merchant_id) {
const { data: merchantData, error: merchantError } = await supa
.from('merchants')
.from('ml_shops')
.select('id, shop_name, rating')
.eq('id', order.value.merchant_id)
.single()
@@ -361,20 +361,18 @@ const submitReview = async () => {
}
// 提交商品评价
/* const productReviews = orderItems.value.map((item, index) => ({
const productReviews = orderItems.value.map((item, index) => ({
user_id: userId,
product_id: item.product_id,
order_id: orderId.value,
rating: ratings.value[index],
content: contents.value[index] || '',
images: images.value[index],
is_anonymous: anonymous.value,
is_valid: true,
created_at: new Date().toISOString()
is_anonymous: anonymous.value
}))
const { error: reviewsError } = await supa
.from('product_reviews')
.from('ml_product_reviews')
.insert(productReviews)
if (reviewsError !== null) {
@@ -385,22 +383,15 @@ const submitReview = async () => {
if (merchant.value) {
const merchantReview = {
user_id: userId,
merchant_id: merchant.value.id,
shop_id: merchant.value.id,
order_id: orderId.value,
description_rating: merchantRating.value.description,
logistics_rating: merchantRating.value.logistics,
service_rating: merchantRating.value.service,
average_rating: (
merchantRating.value.description +
merchantRating.value.logistics +
merchantRating.value.service
) / 3,
is_anonymous: anonymous.value,
created_at: new Date().toISOString()
service_rating: merchantRating.value.service
}
const { error: merchantError } = await supa
.from('merchant_reviews')
.from('ml_shop_reviews')
.insert(merchantReview)
if (merchantError !== null) {
@@ -408,18 +399,16 @@ const submitReview = async () => {
}
}
// 更新订单状态为已评价
// 更新订单状态为已评价 (如果需要标记为已评价,可以在这里处理,例如 status=5 implies Reviewed or keeping at 4)
// 这里保持为 4 (Completed)
const { error: orderError } = await supa
.from('orders')
.update({ status: 4 }) // 已完成
.from('ml_orders')
.update({ order_status: 4 })
.eq('id', orderId.value)
if (orderError !== null) {
console.error('更新订单状态失败:', orderError)
} */
// MOCK SUBMIT
await new Promise(resolve => setTimeout(resolve, 1000))
}
// 显示成功提示
uni.showToast({

View File

@@ -229,7 +229,7 @@
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
// 状态定义
@@ -248,28 +248,9 @@ const priceSortAsc = ref(false) // 价格排序是否为升序
const searchHistory = ref<string[]>([])
const hotSearchList = ref<any[]>([])
const guessList = ref<any[]>([])
const allGuessItems = ref<any[]>([]) // 缓存所有猜你喜欢商品
const searchResults = ref<any[]>([])
// 模拟数据库
const mockDatabase = {
hot: [
{ keyword: '感冒灵', hot: true },
{ keyword: '布洛芬', hot: true },
{ keyword: '口罩', hot: true },
{ keyword: '维生素C', hot: false },
{ keyword: '板蓝根', hot: false },
{ keyword: '创可贴', hot: false },
],
guess: [
{ id: 'g1', name: '医用外科口罩', price: 19.9, image: 'https://picsum.photos/200/200?random=1', sales: '1万+' },
{ id: 'g2', name: '酒精消毒液', price: 9.9, image: 'https://picsum.photos/200/200?random=2', sales: '5000+' },
{ id: 'g3', name: '电子体温计', price: 29.9, image: 'https://picsum.photos/200/200?random=3', sales: '2000+' },
{ id: 'g4', name: '碘伏消毒液', price: 5.5, image: 'https://picsum.photos/200/200?random=4', sales: '1000+' },
{ id: 'g5', name: '退热贴', price: 15.8, image: 'https://picsum.photos/200/200?random=5', sales: '3000+' },
{ id: 'g6', name: '棉签', price: 3.9, image: 'https://picsum.photos/200/200?random=6', sales: '8000+' },
]
}
// 搜索建议
const searchSuggestions = computed(() => {
if (!searchKeyword.value) return []
@@ -327,22 +308,35 @@ const initPage = () => {
}
// 加载基础数据
const loadData = () => {
// loading.value = true // 不使用全局loading避免影响搜索状态
const loadData = async () => {
isError.value = false
// 模拟网络请求
setTimeout(() => {
try {
loadSearchHistory()
hotSearchList.value = mockDatabase.hot
guessList.value = mockDatabase.guess
// loading.value = false // 不使用全局loading
// 获取热门商品作为热门搜索推荐和猜你喜欢
// 获取更多数据以便"换一批"
const hotProducts = await supabaseService.getHotProducts(30)
hotSearchList.value = hotProducts.slice(0, 10).map((p: any) => ({
keyword: p.name,
hot: true
}))
allGuessItems.value = hotProducts.map((p: any) => ({
id: p.id,
name: p.name,
price: p.base_price,
image: p.main_image_url || '/static/default.jpg',
sales: typeof p.sale_count === 'number' ? p.sale_count : 0
}))
// 初始显示随机6个
refreshGuessListItems()
} catch (e) {
console.error('Load data failed', e)
isError.value = true
// loading.value = false
}
}, 500)
}
// 点击重试
@@ -441,13 +435,12 @@ const selectSuggestion = (suggestion: string) => {
const currentPage = ref(1)
const performSearch = () => {
const performSearch = async () => {
// 再次强制设置状态,确保万无一失
showResults.value = true
loading.value = true
// 重置页码
currentPage.value = 1
// 保持旧数据直到新数据回来,或者依靠 loading 状态完全遮罩
// 使用 Supabase 搜索真实数据
const keyword = searchKeyword.value.trim()
@@ -462,25 +455,39 @@ const performSearch = () => {
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
} else if (activeSort.value === 'default') {
sortBy = 'default'
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value = response.data as any[]
try {
const response = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
searchResults.value = response.data.map((p: any) => {
let tag = ''
if (p.tags) {
try {
const tags = (typeof p.tags === 'string') ? JSON.parse(p.tags) : p.tags
if (Array.isArray(tags) && tags.length > 0) tag = String(tags[0])
} catch(e) {}
}
return {
id: p.id,
name: p.name,
image: p.main_image_url || '/static/default.jpg',
price: p.base_price,
specification: p.specification || '标准规格',
tag: tag,
sales: p.sale_count || 0
}
})
hasMore.value = response.hasmore
} catch(e) {
console.error('Search failed', e)
} finally {
loading.value = false
// 如果无结果,显示空状态
if (searchResults.value.length === 0) {
// empty-result 组件会自动显示
}
})
.catch((error) => {
console.error('搜索失败:', error)
loading.value = false
// 可以显示错误提示,但为了用户体验,先不显示
// 保持搜索结果为空让empty-result显示
})
}
// 切换排序
@@ -499,7 +506,7 @@ const switchSort = (type: string) => {
performSearch()
}
const loadMore = () => {
const loadMore = async () => {
if (loading.value || !hasMore.value || !searchKeyword.value.trim()) return
loading.value = true
@@ -513,79 +520,70 @@ const loadMore = () => {
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
} else if (activeSort.value === 'default') {
sortBy = 'default'
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value.push(...(response.data as any[]))
try {
const response = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
const newItems = response.data.map((p: any) => {
let tag = ''
if (p.tags) {
try {
const tags = (typeof p.tags === 'string') ? JSON.parse(p.tags) : p.tags
if (Array.isArray(tags) && tags.length > 0) tag = String(tags[0])
} catch(e) {}
}
return {
id: p.id,
name: p.name,
image: p.main_image_url || '/static/default.jpg',
price: p.base_price,
specification: p.specification || '标准规格',
tag: tag,
sales: p.sale_count || 0
}
})
searchResults.value.push(...newItems)
hasMore.value = response.hasmore
loading.value = false
})
.catch((error) => {
console.error('加载更多失败:', error)
loading.value = false
// 加载失败时,假设没有更多数据
} catch(e) {
console.error('Load more failed', e)
hasMore.value = false
})
} finally {
loading.value = false
}
}
const refreshGuessList = () => {
uni.showLoading({ title: '刷新中' })
setTimeout(() => {
guessList.value = guessList.value.sort(() => Math.random() - 0.5)
refreshGuessListItems()
uni.hideLoading()
}, 500)
}
const refreshGuessListItems = () => {
if (allGuessItems.value.length > 0) {
// 简单的随机乱序并取前6个
const shuffled = [...allGuessItems.value].sort(() => Math.random() - 0.5)
guessList.value = shuffled.slice(0, 6)
}
}
const viewProductDetail = (item: any) => {
// 跳转详情页逻辑
console.log('查看商品', item)
// 跳转详情页逻辑 - 传递必要的参数作为预加载/fallback
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}&name=${encodeURIComponent(item.name)}&image=${encodeURIComponent(item.image)}`
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&name=${encodeURIComponent(item.name)}`
})
}
// 添加到购物车
// 添加到购物车 - 搜索列表无法选择规格,跳转详情页
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_search_default',
shopName: product.shopName || (product.tag === '自营' ? '平台自营大药房' : '优质大药房'),
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
viewProductDetail(product)
}, 800)
}
const openCamera = () => {

View File

@@ -1,702 +0,0 @@
<!-- 设置页面 -->
<template>
<view class="settings-page">
<!-- 顶部栏 -->
<view class="settings-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">设置</text>
</view>
<scroll-view class="settings-content" scroll-y>
<!-- 账户设置 -->
<view class="settings-section">
<text class="section-title">账户设置</text>
<view class="section-list">
<view class="list-item" @click="goToProfile">
<text class="item-icon">👤</text>
<text class="item-text">个人资料</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="goToAddress">
<text class="item-icon">📍</text>
<text class="item-text">收货地址</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="changePassword">
<text class="item-icon">🔒</text>
<text class="item-text">修改密码</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="bindPhone">
<text class="item-icon">📱</text>
<text class="item-text">手机绑定</text>
<view class="item-right">
<text class="item-status" :class="{ bound: userInfo.phone }">
{{ userInfo.phone ? '已绑定' : '未绑定' }}
</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="bindEmail">
<text class="item-icon">📧</text>
<text class="item-text">邮箱绑定</text>
<view class="item-right">
<text class="item-status" :class="{ bound: userInfo.email }">
{{ userInfo.email ? '已绑定' : '未绑定' }}
</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 消息通知 -->
<view class="settings-section">
<text class="section-title">消息通知</text>
<view class="section-list">
<view class="list-item">
<text class="item-icon">🔔</text>
<text class="item-text">订单消息</text>
<switch :checked="notifications.order" @change="toggleNotification('order')" />
</view>
<view class="list-item">
<text class="item-icon">🎁</text>
<text class="item-text">促销活动</text>
<switch :checked="notifications.promotion" @change="toggleNotification('promotion')" />
</view>
<view class="list-item">
<text class="item-icon">⭐</text>
<text class="item-text">评价提醒</text>
<switch :checked="notifications.review" @change="toggleNotification('review')" />
</view>
</view>
</view>
<!-- 隐私设置 -->
<view class="settings-section">
<text class="section-title">隐私设置</text>
<view class="section-list">
<view class="list-item">
<text class="item-icon">👁️</text>
<text class="item-text">隐藏购物记录</text>
<switch :checked="privacy.hidePurchase" @change="togglePrivacy('hidePurchase')" />
</view>
<view class="list-item">
<text class="item-icon">🔍</text>
<text class="item-text">允许通过手机号找到我</text>
<switch :checked="privacy.allowSearchByPhone" @change="togglePrivacy('allowSearchByPhone')" />
</view>
<view class="list-item">
<text class="item-icon">💬</text>
<text class="item-text">接收商家消息</text>
<switch :checked="privacy.receiveMerchantMsg" @change="togglePrivacy('receiveMerchantMsg')" />
</view>
</view>
</view>
<!-- 通用设置 -->
<view class="settings-section">
<text class="section-title">通用设置</text>
<view class="section-list">
<view class="list-item" @click="clearCache">
<text class="item-icon">🗑️</text>
<text class="item-text">清除缓存</text>
<view class="item-right">
<text class="item-cache">{{ cacheSize }}</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="changeLanguage">
<text class="item-icon">🌐</text>
<text class="item-text">语言设置</text>
<view class="item-right">
<text class="item-status">{{ currentLanguage }}</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="changeTheme">
<text class="item-icon">🎨</text>
<text class="item-text">主题设置</text>
<view class="item-right">
<text class="item-status">{{ currentTheme }}</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 关于我们 -->
<view class="settings-section">
<text class="section-title">关于我们</text>
<view class="section-list">
<view class="list-item" @click="aboutUs">
<text class="item-icon"></text>
<text class="item-text">关于商城</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="userAgreement">
<text class="item-icon">📜</text>
<text class="item-text">用户协议</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="privacyPolicy">
<text class="item-icon">🛡️</text>
<text class="item-text">隐私政策</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="checkUpdate">
<text class="item-icon">🔄</text>
<text class="item-text">检查更新</text>
<view class="item-right">
<text class="item-status">{{ appVersion }}</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 客服与反馈 -->
<view class="settings-section">
<text class="section-title">客服与反馈</text>
<view class="section-list">
<view class="list-item" @click="contactService">
<text class="item-icon">💬</text>
<text class="item-text">联系客服</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="feedback">
<text class="item-icon">📝</text>
<text class="item-text">意见反馈</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="rateApp">
<text class="item-icon">⭐</text>
<text class="item-text">给个好评</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" @click="logout">退出登录</button>
</view>
<!-- 账号注销 -->
<view class="delete-account-section">
<text class="delete-account" @click="deleteAccount">注销账号</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts'
type UserType = {
id: string
phone: string | null
email: string | null
nickname: string | null
avatar_url: string | null
}
type NotificationType = {
order: boolean
promotion: boolean
review: boolean
}
type PrivacyType = {
hidePurchase: boolean
allowSearchByPhone: boolean
receiveMerchantMsg: boolean
}
const userInfo = ref<UserType>({
id: '',
phone: null,
email: null,
nickname: null,
avatar_url: null
})
const notifications = ref<NotificationType>({
order: true,
promotion: true,
review: true
})
const privacy = ref<PrivacyType>({
hidePurchase: false,
allowSearchByPhone: true,
receiveMerchantMsg: true
})
const cacheSize = ref<string>('0.0 MB')
const currentLanguage = ref<string>('简体中文')
const currentTheme = ref<string>('自动')
const appVersion = ref<string>('1.0.0')
// 生命周期
onMounted(() => {
loadUserInfo()
loadSettings()
})
// 加载用户信息
const loadUserInfo = () => {
const userStore = uni.getStorageSync('userInfo')
if (userStore) {
userInfo.value = userStore
}
}
// 加载设置
const loadSettings = () => {
// 从本地存储加载设置
const savedNotifications = uni.getStorageSync('userNotifications')
if (savedNotifications) {
notifications.value = savedNotifications
}
const savedPrivacy = uni.getStorageSync('userPrivacy')
if (savedPrivacy) {
privacy.value = savedPrivacy
}
// 计算缓存大小
calculateCacheSize()
// 获取应用版本
// @ts-ignore
const appInfo = uni.getAppBaseInfo()
if (appInfo?.appVersion) {
appVersion.value = appInfo.appVersion
}
}
// 计算缓存大小
const calculateCacheSize = () => {
// 这里应该计算实际缓存大小,这里使用模拟数据
cacheSize.value = '12.5 MB'
}
// 跳转到个人资料
const goToProfile = () => {
uni.navigateTo({
url: '/pages/mall/consumer/profile'
})
}
// 跳转到地址管理
const goToAddress = () => {
uni.navigateTo({
url: '/pages/mall/consumer/address'
})
}
// 修改密码
const changePassword = () => {
uni.navigateTo({
url: '/pages/user/change-password'
})
}
// 绑定手机
const bindPhone = () => {
uni.navigateTo({
url: '/pages/user/bind-phone'
})
}
// 绑定邮箱
const bindEmail = () => {
uni.navigateTo({
url: '/pages/user/bind-email'
})
}
// 切换通知设置
const toggleNotification = (type: keyof NotificationType) => {
notifications.value[type] = !notifications.value[type]
uni.setStorageSync('userNotifications', notifications.value)
}
// 切换隐私设置
const togglePrivacy = (type: keyof PrivacyType) => {
privacy.value[type] = !privacy.value[type]
uni.setStorageSync('userPrivacy', privacy.value)
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '清除缓存',
content: `确定要清除 ${cacheSize.value} 缓存吗?`,
success: (res) => {
if (res.confirm) {
// 这里应该清除实际缓存
uni.showLoading({
title: '清除中...'
})
setTimeout(() => {
cacheSize.value = '0.0 MB'
uni.hideLoading()
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}, 1000)
}
}
})
}
// 切换语言
const changeLanguage = () => {
uni.showActionSheet({
itemList: ['简体中文', 'English', '日本語'],
success: (res) => {
const languages = ['简体中文', 'English', '日本語']
currentLanguage.value = languages[res.tapIndex]
uni.setStorageSync('appLanguage', currentLanguage.value)
uni.showToast({
title: '语言已切换',
icon: 'success'
})
}
})
}
// 切换主题
const changeTheme = () => {
uni.showActionSheet({
itemList: ['自动', '浅色模式', '深色模式'],
success: (res) => {
const themes = ['自动', '浅色模式', '深色模式']
currentTheme.value = themes[res.tapIndex]
uni.setStorageSync('appTheme', currentTheme.value)
uni.showToast({
title: '主题已切换',
icon: 'success'
})
}
})
}
// 关于我们
const aboutUs = () => {
uni.navigateTo({
url: '/pages/info/about'
})
}
// 用户协议
const userAgreement = () => {
uni.navigateTo({
url: '/pages/user/terms'
})
}
// 隐私政策
const privacyPolicy = () => {
uni.navigateTo({
url: '/pages/info/privacy'
})
}
// 检查更新
const checkUpdate = () => {
uni.showLoading({
title: '检查更新中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showModal({
title: '检查更新',
content: '当前已是最新版本',
showCancel: false
})
}, 1000)
}
// 联系客服
const contactService = () => {
uni.navigateTo({
url: '/pages/mall/service/chat'
})
}
// 意见反馈
const feedback = () => {
uni.navigateTo({
url: '/pages/info/feedback'
})
}
// 给个好评
const rateApp = () => {
// 这里应该跳转到应用商店评分
uni.showModal({
title: '给个好评',
content: '如果喜欢我们的应用,请给个好评吧!',
confirmText: '去评分',
success: (res) => {
if (res.confirm) {
// 跳转到应用商店
// @ts-ignore
uni.navigateToMiniProgram({
appId: 'wx1234567890', // 示例AppID
fail: () => {
uni.showToast({
title: '跳转失败',
icon: 'none'
})
}
})
}
}
})
}
// 退出登录
const logout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
try {
// 调用登出接口
/*
const { error } = await supa.auth.signOut()
if (error !== null) {
console.error('登出失败:', error)
uni.showToast({
title: '登出失败',
icon: 'none'
})
return
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('userSettings')
// 跳转到登录页
uni.reLaunch({
url: '/pages/user/login'
})
} catch (err) {
console.error('登出异常:', err)
uni.showToast({
title: '登出失败',
icon: 'none'
})
}
}
}
})
}
// 注销账号
const deleteAccount = () => {
uni.showModal({
title: '注销账号',
content: '确定要注销账号吗?此操作不可恢复,所有数据将被删除!',
confirmText: '注销',
confirmColor: '#ff4757',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '处理中...'
})
try {
const userId = userInfo.value.id
// 这里应该调用注销账号的API
/*
const { error } = await supa
.from('users')
.update({ status: 0 }) // 标记为注销状态
.eq('id', userId)
if (error !== null) {
throw error
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
// 提示并跳转
uni.hideLoading()
uni.showToast({
title: '账号已注销',
icon: 'success',
duration: 2000
})
setTimeout(() => {
uni.reLaunch({
url: '/pages/user/login'
})
}, 1500)
} catch (err) {
uni.hideLoading()
console.error('注销账号失败:', err)
uni.showToast({
title: '注销失败',
icon: 'none'
})
}
}
}
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.settings-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.settings-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.settings-content {
flex: 1;
}
.settings-section {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.section-list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.list-item:last-child {
border-bottom: none;
}
.item-icon {
font-size: 20px;
margin-right: 15px;
}
.item-text {
flex: 1;
font-size: 14px;
color: #333333;
}
.item-arrow {
color: #999999;
font-size: 16px;
margin-left: 10px;
}
.item-right {
display: flex;
align-items: center;
}
.item-status {
font-size: 12px;
color: #999999;
margin-right: 10px;
}
.item-status.bound {
color: #4caf50;
}
.item-cache {
font-size: 12px;
color: #999999;
margin-right: 10px;
}
.logout-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.logout-btn {
background-color: #ffffff;
color: #ff4757;
height: 45px;
border: 1px solid #ff4757;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
}
.delete-account-section {
background-color: #ffffff;
padding: 20px 15px;
text-align: center;
}
.delete-account {
color: #999999;
font-size: 14px;
text-decoration: underline;
}
</style>

View File

@@ -17,11 +17,11 @@
<text class="item-text">个人资料</text>
<text class="item-arrow"></text>
</view>
<!--<view class="list-item" @click="goToAddressList">
<view class="list-item" @click="goToAddressList">
<text class="item-icon">📍</text>
<text class="item-text">收货地址</text>
<text class="item-arrow"></text>
</view>-->
</view>
<view class="list-item" @click="changePassword">
<text class="item-icon">🔒</text>
<text class="item-text">修改密码</text>
@@ -125,6 +125,18 @@
</view>
</view>
<!-- 我的服务 -->
<view class="settings-section">
<text class="section-title">我的服务</text>
<view class="section-list">
<view class="list-item" @click="goToMyReviews">
<text class="item-icon">📝</text>
<text class="item-text">我的评价</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
<!-- 关于我们 -->
<view class="settings-section">
<text class="section-title">关于我们</text>
@@ -193,7 +205,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onBackPress } from '@dcloudio/uni-app'
// import supa from '@/components/supadb/aksupainstance.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 拦截返回事件,强制跳转到个人中心页
onBackPress((options) => {
@@ -402,24 +414,32 @@ const changeTheme = () => {
})
}
// 我的评价
const goToMyReviews = () => {
// 跳转到订单列表的已完成或者是评价相关的页面
uni.navigateTo({
url: '/pages/mall/consumer/orders?status=completed'
})
}
// 关于我们
const aboutUs = () => {
uni.navigateTo({
url: '/pages/info/about'
url: '/pages/user/terms?type=about'
})
}
// 用户协议
const userAgreement = () => {
uni.navigateTo({
url: '/pages/user/terms'
url: '/pages/user/terms?type=agreement'
})
}
// 隐私政策
const privacyPolicy = () => {
uni.navigateTo({
url: '/pages/info/privacy'
url: '/pages/user/terms?type=privacy'
})
}
@@ -442,7 +462,7 @@ const checkUpdate = () => {
// 联系客服
const contactService = () => {
uni.navigateTo({
url: '/pages/mall/service/chat'
url: '/pages/mall/consumer/chat'
})
}
@@ -486,42 +506,51 @@ const logout = () => {
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: '正在退出...'
})
// 调用登出接口
/*
const { error } = await supa.auth.signOut()
if (error !== null) {
console.error('登出失败:', error)
uni.showToast({
title: '登出失败',
icon: 'none'
})
return
// 即使失败也继续清除本地状态
}
*/
// 清除本地存储
// 清除本地存储的用户信息
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('userSettings')
uni.removeStorageSync('user_id')
uni.removeStorageSync('access_token')
// 跳转到登录页
uni.hideLoading()
uni.showToast({
title: '已退出登录',
icon: 'success'
})
setTimeout(() => {
uni.reLaunch({
url: '/pages/user/login'
})
} catch (err) {
console.error('登出异常:', err)
}, 1000)
} catch (e) {
uni.hideLoading()
console.error('Logout Exception:', e)
uni.showToast({
title: '登出失败',
title: '退出异常',
icon: 'none'
})
// 强制退出
uni.removeStorageSync('userInfo')
uni.reLaunch({
url: '/pages/user/login'
})
}
}
}
})
}
// 注销账号
const deleteAccount = () => {
uni.showModal({
@@ -531,28 +560,32 @@ const deleteAccount = () => {
confirmColor: '#ff4757',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: '处理中...'
title: '注销中...'
})
const userId = userInfo.value.id || uni.getStorageSync('user_id')
if (userId) {
try {
const userId = userInfo.value.id
// 这里应该调用注销账号的API
/*
const { error } = await supa
.from('users')
.update({ status: 0 }) // 标记为注销状态
.eq('id', userId)
if (error !== null) {
throw error
// 标记用户状态为注销 (status=3)
await supa
.from('ml_user_profiles')
.update({ status: 3 })
.eq('user_id', userId)
} catch(e) {
console.error('Update status failed', e)
}
// 登出
await supa.auth.signOut()
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('user_id')
uni.removeStorageSync('access_token')
// 提示并跳转
uni.hideLoading()

View File

@@ -0,0 +1,231 @@
# Supabase 消费者端集成指南 (Consumer Frontend Integration Guide)
本文档基于 `complete_mall_database.sql` 数据库设计,为消费者端 (Consumer App) 前端开发提供对接指引。
## 1. 核心表结构概览
所有商城相关表均以 `ml_` 开头。
| 功能模块 | 核心表 | 视图 (推荐使用) | 说明 |
| :--- | :--- | :--- | :--- |
| **用户** | `ml_user_profiles`, `ml_user_addresses` | `ml_users_view` | 用户扩展信息、收货地址 |
| **商品** | `ml_products`, `ml_categories`, `ml_product_skus` | `ml_products_detail_view` | 商品、分类、SKU库存 |
| **店铺** | `ml_shops` | - | 店铺基础信息 |
| **购物车** | `ml_shopping_cart` | - | 购物车数据 |
| **订单** | `ml_orders`, `ml_order_items` | `ml_orders_detail_view` | 订单主表及明细 |
| **营销** | `ml_user_coupons`, `ml_coupon_templates` | - | 优惠券 |
| **互动** | `ml_user_favorites`, `ml_product_reviews` | - | 收藏、评价 |
---
## 2. 关键业务场景与查询示例
### 2.1 首页展示
#### 获取一级分类
```typescript
const { data, error } = await supabase
.from('ml_categories')
.select('id, name, icon_url')
.eq('level', 1)
.eq('is_active', true)
.order('sort_order', { ascending: true });
```
#### 获取热销/推荐商品
```typescript
const { data, error } = await supabase
.from('ml_products')
.select('id, name, main_image_url, base_price, sale_count')
.eq('status', 1) // 上架状态
.eq('is_hot', true) // 热销标记
.limit(10);
```
### 2.2 商品详情页
建议优先使用 `ml_products` 表配合关联查询,或者使用视图。
#### 获取商品基础信息
```typescript
const { data, error } = await supabase
.from('ml_products')
.select(`
*,
category:ml_categories(id, name),
brand:ml_brands(id, name),
shop:ml_shops(id, shop_name, shop_logo),
skus:ml_product_skus(*),
specs:ml_product_specs(*)
`)
.eq('id', productId)
.single();
```
*注意:`skus` 和 `specs` 是通过外键关联获取的,确保前端处理好 `1:N` 的关系。*
### 2.3 购物车管理
前端需维护购物车逻辑,数据均存储在 `ml_shopping_cart`
#### 获取我的购物车 (含商品详情)
**重要**:务必使用内联查询 (`!inner` 或关联) 获取商品最新价格和图片。
```typescript
const { data, error } = await supabase
.from('ml_shopping_cart')
.select(`
id,
quantity,
selected,
sku_id,
product:ml_products!inner (
id,
name,
main_image_url,
base_price, -- 基础价格
status, -- 检查是否下架
merchant_id -- 用于店铺分组
),
sku:ml_product_skus (
id,
sku_code,
price, -- SKU价格如果有
specifications,
stock,
image_url
)
`)
.eq('user_id', currentUserId)
.order('created_at', { ascending: false });
```
#### 店铺分组逻辑 (前端处理)
前端获取数据后,应根据 `product.merchant_id` 进行分组,并聚合显示店铺名称 (需另外查询或关联 `ml_shops`)。
### 2.4 收货地址
#### 获取地址列表
```typescript
const { data, error } = await supabase
.from('ml_user_addresses')
.select('*')
.eq('user_id', currentUserId)
.eq('status', 1) // 1: 正常
.order('is_default', { ascending: false }) // 默认地址排最前
.order('updated_at', { ascending: false });
```
### 2.5 订单列表
#### 查询我的订单
```typescript
const { data, error } = await supabase
.from('ml_orders')
.select(`
id,
order_no,
total_amount,
order_status,
created_at,
items:ml_order_items (
id,
product_name,
image_url,
quantity,
price,
specifications
),
shop:ml_shops (
shop_name
)
`)
.eq('user_id', currentUserId)
.order('created_at', { ascending: false });
```
---
## 3. RLS (行级安全) 注意事项
数据库已配置 RLS 策略,前端直接调用 Supabase Client 即可,**无需在查询时手动增加 `user_id` 过滤** (除了显式需要对业务逻辑进行过滤的地方RLS 会自动兜底)。
- **`ml_shopping_cart`**: 用户只能查/改/删自己的购物车记录。
- **`ml_user_addresses`**: 用户只能查/改/删自己的地址。
- **`ml_orders`**: 用户只能查看自己的订单。
- **`ml_products`**: 设置为 `status = 1` 的商品所有人可读。
*确保在 App 启动时正确初始化 Supabase Auth 并处于登录状态。*
## 4. 推荐使用的数据库函数 (RPC)
可以直接通过 ` supabase.rpc('function_name', params)` 调用以下函数:
| 函数名 | 参数 | 描述 |
| :--- | :--- | :--- |
| `calculate_cart_total` | `p_user_id` | 计算当前用户购物车选中商品的总金额 (服务端计算更安全) |
| `get_product_available_stock` | `p_product_id`, `p_sku_id` | 获取特定商品或SKU的实时可用库存 |
| `get_user_default_address` | `p_user_id` | 快速获取用户的默认收货地址 |
### 调用示例
```typescript
// 计算购物车总价
const { data: total, error } = await supabase
.rpc('calculate_cart_total', {
p_user_id: currentUserId
});
```
## 5. 类型定义参考 (TypeScript)
为方便前端开发,以下是核心表对应的推荐接口定义:
```typescript
// 购物车项 (结合了关联查询的结果)
export interface CartItem {
id: string;
quantity: number;
selected: boolean;
product: {
id: string;
name: string;
main_image_url: string;
base_price: number;
merchant_id: string;
};
sku?: {
id: string;
price: number;
specifications: string; // JSON string
stock: number;
};
shop_name?: string; // 前端处理后注入
}
// 订单结构
export interface Order {
id: string;
order_no: string;
total_amount: number;
order_status: number; // 1:待付款 2:待发货 3:待收货 4:已完成 ...
items: Array<{
product_name: string;
image_url: string;
quantity: number;
price: number;
}>;
}
```
## 6. 特殊字段说明
- **Product Images**:
- `main_image_url`: 列表页和购物车主图。
- `image_urls`: JSONB 数组,用于商品详情轮播图。
- **Specifications**:
-`ml_product_skus` 表中 `specifications` 为 JSONB 格式 (例如 `{"color": "红色", "size": "L"}`),前端需解析展示。
- **Money**:
- 数据库使用 `DECIMAL`API 返回为 `number`,建议前端统一处理为两位小数展示。
---
*文档生成日期: 2026-02-02*

View File

@@ -18,13 +18,14 @@
<script setup lang="uts">
import { ref } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
const email = ref('')
const code = ref('')
const counting = ref(false)
const count = ref(60)
const sendCode = () => {
const sendCode = async () => {
if (counting.value) return
if (!email.value || !email.value.includes('@')) {
uni.showToast({
@@ -34,6 +35,23 @@ const sendCode = () => {
return
}
uni.showLoading({ title: '发送中...' })
try {
const { error } = await supa.auth.updateUser({
email: email.value
})
uni.hideLoading()
if (error != null) {
uni.showToast({
title: '发送失败: ' + error.message,
icon: 'none'
})
return
}
counting.value = true
count.value = 60
@@ -49,9 +67,14 @@ const sendCode = () => {
title: '验证码已发送',
icon: 'none'
})
} catch(e) {
uni.hideLoading()
console.error(e)
uni.showToast({ title: '发送异常', icon: 'none' })
}
}
const handleSubmit = () => {
const handleSubmit = async () => {
if (!email.value || !code.value) {
uni.showToast({
title: '请填写完整信息',
@@ -60,27 +83,48 @@ const handleSubmit = () => {
return
}
// TODO: Call API to bind email
uni.showLoading({ title: '提交中...' })
setTimeout(() => {
uni.showLoading({ title: '绑定中...' })
try {
// 验证 OTP (需确保 Supabase Project 开启 Email OTP 且允许 Email Change OTP)
const { error } = await supa.auth.verifyOtp({
email: email.value,
token: code.value,
type: 'email_change'
})
uni.hideLoading()
if (error != null) {
uni.showToast({
title: '绑定失败: ' + error.message,
icon: 'none'
})
return
}
uni.showToast({
title: '绑定成功',
icon: 'success'
})
// 更新本地存储的用户信息
// 更新本地存储
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
// @ts-ignore
userInfo.email = email.value
uni.setStorageSync('userInfo', userInfo)
let u = userInfo as any
u['email'] = email.value
uni.setStorageSync('userInfo', u)
}
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
} catch(e) {
uni.hideLoading()
console.error(e)
uni.showToast({ title: '系统错误', icon: 'none' })
}
}
</script>

View File

@@ -18,13 +18,14 @@
<script setup lang="uts">
import { ref } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
const phone = ref('')
const code = ref('')
const counting = ref(false)
const count = ref(60)
const sendCode = () => {
const sendCode = async () => {
if (counting.value) return
if (!phone.value || phone.value.length !== 11) {
uni.showToast({
@@ -34,6 +35,24 @@ const sendCode = () => {
return
}
uni.showLoading({ title: '发送中...' })
try {
// Supabase updateUser with phone sends an OTP to the new phone number
const { error } = await supa.auth.updateUser({
phone: phone.value
})
uni.hideLoading()
if (error != null) {
uni.showToast({
title: '发送失败: ' + error.message,
icon: 'none'
})
return
}
counting.value = true
count.value = 60
@@ -49,9 +68,14 @@ const sendCode = () => {
title: '验证码已发送',
icon: 'none'
})
} catch(e) {
uni.hideLoading()
console.error(e)
uni.showToast({ title: '发送异常', icon: 'none' })
}
}
const handleSubmit = () => {
const handleSubmit = async () => {
if (!phone.value || !code.value) {
uni.showToast({
title: '请填写完整信息',
@@ -60,10 +84,26 @@ const handleSubmit = () => {
return
}
// TODO: Call API to bind phone
uni.showLoading({ title: '提交中...' })
setTimeout(() => {
uni.showLoading({ title: '绑定中...' })
try {
// 验证 OTP
const { error } = await supa.auth.verifyOtp({
phone: phone.value,
token: code.value,
type: 'phone_change'
})
uni.hideLoading()
if (error != null) {
uni.showToast({
title: '绑定失败: ' + error.message,
icon: 'none'
})
return
}
uni.showToast({
title: '绑定成功',
icon: 'success'
@@ -73,14 +113,19 @@ const handleSubmit = () => {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
// @ts-ignore
userInfo.phone = phone.value
uni.setStorageSync('userInfo', userInfo)
let u = userInfo as any
u['phone'] = phone.value
uni.setStorageSync('userInfo', u)
}
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
} catch(e) {
uni.hideLoading()
console.error(e)
uni.showToast({ title: '系统错误', icon: 'none' })
}
}
</script>

View File

@@ -21,12 +21,13 @@
<script setup lang="uts">
import { ref } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
const oldPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const handleSubmit = () => {
const handleSubmit = async () => {
if (!oldPassword.value || !newPassword.value || !confirmPassword.value) {
uni.showToast({
title: '请填写完整信息',
@@ -43,10 +44,27 @@ const handleSubmit = () => {
return
}
// TODO: Call API to change password
uni.showLoading({ title: '提交中...' })
setTimeout(() => {
try {
// 注意Supabase Auth updatePassword 不需要由于已经是登录状态不需要验证旧密码
// 如果严谨流程,应该先用旧密码尝试登录一次(Verified)
// 这里简化流程直接修改
const { error } = await supa.auth.updateUser({
password: newPassword.value
})
uni.hideLoading()
if (error !== null) {
console.error(error)
uni.showToast({
title: '修改失败: ' + error.message,
icon: 'none'
})
return
}
uni.showToast({
title: '修改成功',
icon: 'success'
@@ -54,7 +72,14 @@ const handleSubmit = () => {
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
} catch (e) {
uni.hideLoading()
console.error(e)
uni.showToast({
title: '请求异常',
icon: 'none'
})
}
}
</script>

File diff suppressed because it is too large Load Diff