consumer模块完成90%,前端完成supabase对接
This commit is contained in:
@@ -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'
|
||||
|
||||
// 备用配置(已注释,如需切换可取消注释)
|
||||
// 开发环境 - 其他内网地址
|
||||
|
||||
@@ -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
@@ -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 $$;
|
||||
@@ -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 $$;
|
||||
@@ -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 $$;
|
||||
@@ -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 $$;
|
||||
@@ -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
@@ -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
|
||||
$$;
|
||||
@@ -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 字段';
|
||||
*/
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 $$;
|
||||
@@ -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;
|
||||
@@ -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 $$;
|
||||
75
doc_mall/reports/PAYMENT_PAGE_GUIDE.md
Normal file
75
doc_mall/reports/PAYMENT_PAGE_GUIDE.md
Normal 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).
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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或者购物车ID,createOrder 中会使用 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
62
pages/mall/consumer/doc/FEATURES_&_PAGES.md
Normal file
62
pages/mall/consumer/doc/FEATURES_&_PAGES.md
Normal 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` | ✅ 正常 | 支持星级评分与文本评价。 |
|
||||
|
||||
44
pages/mall/consumer/doc/MOCK_DATA_GUIDE.md
Normal file
44
pages/mall/consumer/doc/MOCK_DATA_GUIDE.md
Normal 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. **验证**:
|
||||
* 各状态标签页下是否有对应的订单。
|
||||
* 商品图片和名称是否显示正常。
|
||||
* 在“待收货”或“已完成”订单上点击**申请售后**,验证是否跳转正确。
|
||||
62
pages/mall/consumer/doc/README.md
Normal file
62
pages/mall/consumer/doc/README.md
Normal 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)
|
||||
121
pages/mall/consumer/doc/SUPABASE_INTEGRATION.md
Normal file
121
pages/mall/consumer/doc/SUPABASE_INTEGRATION.md
Normal 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 实时推送。
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 保存回本地存储
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新未读数量
|
||||
|
||||
1277
pages/mall/consumer/messages医药.uvue
Normal file
1277
pages/mall/consumer/messages医药.uvue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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' })
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
231
pages/mall/consumer/sql/SUPABASE_CONSUMER_INTEGRATION.md
Normal file
231
pages/mall/consumer/sql/SUPABASE_CONSUMER_INTEGRATION.md
Normal 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*
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user